BeagleBone-家庭自动化蓝图-全-

BeagleBone 家庭自动化蓝图(全)

原文:annas-archive.org/md5/6158a53ac3a986ec2f5eb20219880185

译者:飞龙

协议:CC BY-NC-SA 4.0

序言

BeagleBone Black 是一款嵌入式系统,能够运行嵌入式 GNU/Linux 发行版以及像 Debian 或 Ubuntu 这样的普通(和强大的)发行版,并且用户可以通过两个专用扩展连接器连接多个外部外设。

因为它具有强大的分发能力,并且是一款易于扩展的嵌入式板,BeagleBone Black 系统是一款先进的设备,允许用户构建强大而多功能的监控和控制应用程序。

本书展示了几个家庭自动化原型,包括硬件和软件,以便向您解释如何使用连接了多个设备的 BeagleBone Black 来控制您的家。

每个原型都在其各自的硬件和软件章节中讨论,解释所有连接和管理多个外设所需的软件包。然后,详细解释了将所有内容粘合在一起的代码,直至每个项目的最终测试。

本书所用的硬件设备已经选择,以涵盖我们在使用 BeagleBone Black 开发板时可能遇到的所有连接类型,因此您会找到 I2C、SPI、USB、1-Wire、串行、数字和模拟设备。

本书选择的编程语言是根据找到解决当前问题的最快最简单方法的规则进行选择的;特别是,您可以在 Bash、C、PHP、Python、HTML 甚至 JavaScript 的示例代码中找到例子。

注意

警告!本书中所有项目均为原型,不能用作最终应用。

本书的作者和 Packt Publishing 都不建议或支持单独使用或作为任何应用程序组件的这些产品,除非进行必要的修改将这些原型转换为最终产品。

本书的作者和 Packt Publishing 不会对这些原型的未经授权使用负责。用户可以自行承担使用这些设备的硬件和软件的风险!

在我们需要使用守护程序或内核模块的章节中,或者我们需要重新编译整个内核的章节中,我已经添加了关于读者应该做什么以及他们可以在哪里获取关于所使用工具更多信息的简短描述;然而,一些管理 GNU/Linux 系统、内核模块或内核本身的基本技能是必需的(读者可以查看由本书作者编写的书籍《BeagleBone Essentials》,Packt Publishing,以获取有关这些主题的更多信息)。

本书内容概述

第一章, 危险气体传感器,将展示如何使用 BeagleBone Black 监控房间内的危险气体,如一氧化碳、甲烷、液化石油气等,并在危险情况下启用声光报警系统。此外,通过使用 GSM 模块,用户可以向预定的电话号码发送短信,提醒例如亲戚等。

第二章, 超声波停车助手,将展示如何使用 BeagleBone Black 实现一个停车助手。我们将使用超声波传感器检测汽车与车库墙壁之间的距离,并使用 LED 灯向驾驶员反馈汽车的位置,避免碰撞。

第三章, 水族箱监控器,将展示如何制作一个水族箱监控器,借此我们可以通过 PC、智能手机或平板电脑上的网页面板记录所有环境数据,并控制我们心爱的鱼类的生活。

第四章, Google Docs 气象站,将介绍一个简单的气象站,该气象站也可以作为物联网设备使用。这一次,我们的 BeagleBone Black 将收集环境数据并将其发送到远程数据库(Google Docs 电子表格),以便重新处理并呈现在共享环境中。

第五章, WhatsApp 洗衣房监控器,将展示一个洗衣房监控器的实现,该监控器配有多个传感器,能够在特定事件发生时直接通过 WhatsApp 帐户提醒用户。

第六章, 婴儿房警卫,将展示如何实现一个能够监控婴儿房的婴儿房警卫,检测婴儿是否在哭泣,或者婴儿在睡觉时是否有呼吸。此外,作为一项特殊功能,该系统还能够使用无接触温度传感器测量婴儿的体温。

第七章, Facebook 植物监控器,将展示如何实现一个植物监控器,该监控器能够测量光照、土壤湿度和土壤温度(包括土壤内外),并通过网络摄像头在特定时间间隔拍摄一些照片,然后将其发布到 Facebook 时间线上。

第八章, 入侵检测系统,将展示如何利用 BeagleBone Black 和两个(或更多)网络摄像头实现一个低成本、合理质量的入侵检测系统。该系统将能够通过发送带有入侵者照片的电子邮件来提醒用户。

第九章,带智能卡和 RFID 的 Twitter 访问控制系统,将展示如何使用智能卡读卡器以及两种 RFID 读卡器(低频和超高频),以展示不同的方法来实现一个最简单的身份识别系统,用于访问控制,并向 Twitter 账户发送警报信息。

第十章,使用电视遥控器的灯光管理器,将展示如何通过电视遥控器或任何红外设备来管理连接到 BeagleBone Black 的简单开关设备。

第十一章,使用 Z-Wave 的无线家庭控制器,将展示如何通过连接到 BeagleBone Black 的 Z-Wave 控制器以及两个 Z-Wave 设备(一个墙壁插座和一个多功能传感器设备)来实现一个小型的无线家庭控制器。

本书所需内容

为了从本书中获得最大收益,您需要具备以下先决条件。

软件先决条件

关于软件,读者应对非图形文本编辑器(如 vi、emacs 或 nano)有一些基本了解。即使读者能够将 LCD 显示器、键盘和鼠标直接连接到 BeagleBone Black,并使用图形界面,本书仍假设读者能够通过仅支持文本的编辑器对文本文件进行一些修改。

主机计算机,即读者用来交叉编译代码和/或管理 BeagleBone Black 的计算机,假定运行的是基于 GNU/Linux 的发行版。我的主机 PC 运行的是 Ubuntu 14.04 LTS,但读者也可以使用基于 Debian 的系统,稍作修改后即可使用,或者使用其他 GNU/Linux 发行版,但需要进行一些努力,主要是为了安装交叉编译工具。由于我们不应使用低技术系统来为高技术系统开发代码,因此本书涉及 Windows、Mac OS 或类似系统!

了解 C 编译器如何工作以及如何管理 Makefile 可能会有所帮助,但不用担心,所有示例都从最基础开始,即使是没有经验的开发者也能完成任务。

本书将介绍一些内核编程技巧,但不应将其视为 内核编程课程。对于这种主题,您需要一本专门的书籍!然而,每个示例都有很好的文档说明,读者将找到若干建议的资源。

关于内核,我想说明一下,默认情况下我使用的是板载内核,即版本 3.8.13。然而,在某些章节中,我使用了自编译的内核,版本 3.13.11;在这种情况下,我会提供一个小教程,教读者如何完成这项工作。

如果你使用的是较新的内核版本,可能会遇到一些小问题,但你应该能够毫无问题地移植我所做的工作。如果你使用的是非常新的内核,请注意,cape 管理器文件 /sys/devices/bone_capemgr.9/slots 已经被移到 /sys/devices/platform/bone_capemgr/slots,因此你需要相应地更改所有相关命令。

最后需要说明的是,我假设读者知道如何将 BeagleBone Black 板连接到互联网,以便下载包或通用文件。

硬件先决条件

本书中的所有代码都是为 BeagleBone Black 板的 C 版本开发的,但读者也可以使用较旧的版本,且不会遇到任何问题;事实上,代码是可移植的,应该也能在其他系统上运行!

关于本书中使用的计算机外设,我在每章中都有注明硬件的来源和购买地点,当然,读者也可以选择上网寻找更好、更便宜的购买选择。还会有一条说明,告诉读者哪里可以找到数据手册。

读者在将本书中介绍的硬件连接到 BeagleBone Black 时应该不会遇到任何困难,因为连接非常少且文档齐全。读者无需具备特殊的硬件技能(只需知道如何使用烙铁);然而,具备一定的电子学知识会有所帮助。

在阅读过程中,我将提到 BeagleBone Black 的引脚,尤其是扩展连接器上的引脚。所有使用的引脚都会有说明,但是如果需要,你可以在 elinux.org/Beagleboard:Cape_Expansion_Headers 上找到 BeagleBone Black 连接器的完整描述。

本书的目标读者

如果你是一位开发者,想要学习如何使用嵌入式机器学习功能,并获取访问 GNU/Linux 设备驱动程序的权限以收集外设数据或控制设备,那么这本书就是为你准备的。

如果你希望通过实施不同种类的智能家居设备来管理你的家庭,这些设备可以与智能手机、平板电脑或 PC 进行交互,或者如果你只是独立工作,具备一定的硬件或电气工程经验,那么本书适合你。你还需要了解 C、Bash、Python、PHP 和 JavaScript 编程基础,尤其是在 UNIX 环境下的编程。

约定

本书中有许多不同的文本样式,用以区分不同类型的信息。以下是一些样式示例及其含义的解释。

代码和命令行

文本中的代码词、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入以及 Twitter 用户名均如下所示:“为了获取之前的内核消息,我们可以使用 dmesgtail -f /var/log/kern.log 命令。”

代码块如下所示:

CREATE TABLE status (
   n VARCHAR(64) NOT NULL,
   v VARCHAR(64) NOT NULL,
   PRIMARY KEY (n)
) ENGINE=MEMORY;

所有在 BeagleBone Black 上给出的命令行输入或输出如下所示:

root@beaglebone:~# make CFLAGS="-Wall -O2" helloworldcc -Wall -O2 helloworld.c -o helloworld

任何在我的主机计算机上作为非特权用户给出的命令行输入或输出都会写成如下:

$ tail -f /var/log/kern.log

当我需要以特权用户(root)的身份在我的主机计算机上给出命令时,命令行的输入或输出将写成如下:

# /etc/init.d/apache2 restart

读者应该注意,所有特权命令也可以通过正常用户使用sudo命令来执行,格式如下:

$ sudo <command>

因此,前面的命令可以由普通用户执行,如下所示:

$ sudo /etc/init.d/apache2 restart

内核和日志信息

在多个 GNU/Linux 发行版中,内核信息具有以下形式:

Oct 27 10:41:56 hulk kernel: [46692.664196] usb 2-1.1: new high-speed USB device number 12 using ehci-pci

这对于本书来说是一个相当长的行,因此从行的开头直到实际信息开始的部分都会被省略。所以,在前面的示例中,这行的输出将被报告如下:

usb 2-1.1: new high-speed USB device number 12 using ehci-pci

长输出和终端中的重复或不太重要的行会被替换为三个点(...),如下所示:

output begin
output line 1
output line 2
...
output line 10
output end

文件修改

当读者需要修改文本文件时,我将使用统一的上下文diff格式,因为这是表示文本修改的非常高效和紧凑的方式。该格式可以通过使用diff命令并添加-u选项参数来获得。

作为一个简单的例子,假设我们考虑file1.old中的以下文本:

This is first line
This is the second line
This is the third line
...
...
This is the last line

假设我们需要修改第三行,如下片段所示:

This is first line
This is the second line
This is the new third line modified by me
...
...
This is the last line

读者可以轻松理解,每次对一个简单的修改都报告整个文件是相当晦涩且浪费空间的;然而,通过使用统一的上下文diff格式,前面的修改可以写成如下:

$ diff -u file1.old file1.new
--- file1.old 2015-03-23 14:49:04.354377460 +0100
+++ file1.new 2015-03-23 14:51:57.450373836 +0100
@@ -1,6 +1,6 @@
 This is first line
 This is the second line
-This is the third line
+This is the new third line modified by me
 ...
 ...
 This is the last line

现在,修改非常清晰,并且以紧凑的形式写出!它以一个两行的标题开始,原文件前面是---,新文件前面是+++,然后是一个或多个变化块,包含文件中的行差异。前面的示例只有一个变化块,其中未改变的行前面有一个空格字符,而要添加的行前面有一个+字符,要删除的行前面有一个-字符。

串行和网络连接

在本书中,我将主要使用两种不同的方式与 BeagleBone Black 开发板进行交互:串行控制台和 SSH 终端。前者可以通过连接器 J1 直接访问(在本书中从未使用)或通过与用于供电的同一 USB 连接进行模拟访问,而后者则可以通过上面的 USB 连接或通过以太网连接来使用。

串行控制台主要用于从命令行管理系统。它在很大程度上用于监控系统,特别是用于控制内核消息。

SSH 终端与串行控制台非常相似,尽管它并不完全相同(例如,内核信息不会自动出现在终端上);然而,它可以像串行控制台一样使用,执行命令并从命令行编辑文件。

在接下来的章节中,我将通过串行控制台或 SSH 连接不加区分地使用终端,给出实现本书中所有原型所需的大多数命令和配置设置。

要从主机 PC 访问 USB 仿真串行控制台,可以使用以下minicon命令:

$ minicom -o -D /dev/ttyACM0 

请注意,在某些系统上,你可能需要 root 权限才能访问/dev/ttyACM0设备(在这种情况下,你可以使用sudo命令来覆盖它)。

如上所述,要访问 SSH 终端,你可以通过与串行控制台相同的 USB 电缆使用仿真以太网连接。实际上,如果你的主机 PC 配置正确,当你插入 USB 电缆为 BeagleBone Black 开发板供电时,过一会儿,你应该会看到一个新连接,IP 地址为192.168.7.1。然后,你可以使用这个新连接通过以下命令访问 BeagleBone Black:

$ ssh root@192.168.7.2 

最后一个可用的通信通道是以太网连接。它主要用于从主机 PC 或互联网下载文件,可以通过将以太网电缆连接到 BeagleBone Black 的以太网端口并根据你的局域网设置配置端口来建立连接。

然而,需要特别指出的是,你还可以通过之前提到的仿真以太网连接来连接互联网。实际上,通过在主机 PC(显然是基于 GNU/Linux 的)上使用以下命令,你将能够将其用作路由器,让你的 BeagleBone Black 开发板像连接到真实以太网端口一样上网:

# iptables --table nat --append POSTROUTING --out- interface eth1 -j MASQUERADE
# iptables --append FORWARD --in-interface eth4 -j ACCEPT
# echo 1 >> /proc/sys/net/ipv4/ip_forward

然后,在 BeagleBone Black 上,我们应该通过 USB 电缆使用以下命令设置网关:

root@beaglebone:~# route add default gw 192.168.7.1

请注意,eth1设备是我主机系统首选的互联网连接,而eth4设备是 BeagleBone Black 在我主机系统上显示的设备,因此你需要相应修改命令以满足你的需求。

其他约定

新术语重要单词以粗体显示。在屏幕上看到的单词,例如在菜单或对话框中,文本中会这样显示:“点击下一步按钮将你带到下一个屏幕。”

注意

警告或重要说明会出现在像这样的框中。

提示

提示和技巧会以这种方式显示。

读者反馈

我们欢迎读者的反馈。请告诉我们你对本书的看法——你喜欢或不喜欢什么。读者反馈对我们非常重要,因为它帮助我们开发出你真正能从中受益的书籍。

要向我们发送一般反馈,请简单地发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个领域有专业知识,并且有兴趣编写或为书籍做贡献,请查看我们的作者指南:www.packtpub.com/authors

客户支持

既然您已经成为 Packt 书籍的骄傲拥有者,我们提供了许多帮助您充分利用购买内容的资源。

下载示例代码

您可以从www.packtpub.com的账户中下载所有您购买的 Packt 出版书籍的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packtpub.com/support,并注册以便直接通过电子邮件收到文件。

本书的示例代码也可以从作者的 GitHub 库中下载,地址为github.com/giometti/beaglebone_home_automation_blueprints

只需使用以下命令一次性获取它:

$ git clone https://github.com/giometti/beaglebone_home_automation_blueprints

示例按章节名称分组,因此您可以在阅读书籍时轻松找到代码。

下载本书的彩色图像

我们还为您提供了一份 PDF 文件,其中包含本书中使用的截图/图表的彩色图像。这些彩色图像将帮助您更好地理解输出的变化。您可以从www.packtpub.com/sites/default/files/downloads/BeagleBoneHomeAutomationBlueprints_ColoredImages.pdf下载此文件。

勘误

虽然我们已尽一切努力确保内容的准确性,但错误难免发生。如果您在我们的书籍中发现错误——可能是文本或代码中的错误——我们将非常感激您向我们报告。通过这样做,您可以帮助其他读者避免困扰,并帮助我们改进后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata报告,选择您的书籍,点击Errata Submission Form链接,并输入您的勘误详情。您的勘误一旦验证后,将被接受,并上传到我们的网站或添加到该书籍标题下的现有勘误列表中。

要查看先前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需信息将显示在Errata部分。

盗版

网络上的版权盗版问题在所有媒体中普遍存在。Packt 公司非常重视我们版权和许可证的保护。如果您在互联网上遇到我们作品的非法复制品,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们采取相应的补救措施。

如发现涉嫌盗版的资料,请通过<copyright@packtpub.com>与我们联系,并提供相关链接。

感谢您在保护我们作者权益和帮助我们提供有价值内容方面的支持。

问题

如果您对本书的任何方面有问题,可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章:危险气体传感器

在本章中,我们将学习如何使用BeagleBone Black来监测房间中的一些危险气体,如一氧化碳甲烷液化石油气等,并在危险发生时启用声光报警。此外,通过使用 GSM 模块,用户还可以向预定义的电话号码发送短信,例如通知亲人。

此外,用户还可以通过命令行界面在系统控制台/终端中控制、记录和显示测量的浓度(这样可以保持代码的简洁)。

我们将看到如何构建电路来管理传感器,并从中获取气体浓度。接着,我们将了解如何管理 GSM 模块,以便发送短信。

基本功能

在本项目中,我们的 BeagleBone Black 将定期读取传感器的环境数据,比较用户可选择的范围,如果某个(或多个)数据读数超出该范围,则触发报警。

传感器将通过专用电路连接到 BeagleBone Black 的 ADC,而报警器将通过专用 GPIO 引脚启用。然后,GSM 模块将连接到 BeagleBone Black 的串口,用于通过短信发送其他报警信息。

设置硬件

如前所述,所有设备都与 BeagleBone Black 连接,BeagleBone Black 是系统的核心,如下图所示:

设置硬件

数据流从传感器流向报警执行器(LED、蜂鸣器和 GSM 模块),用户可以通过系统控制台发送命令或查看系统状态和采集到的数据。

连接气体传感器

气体传感器用于监测环境,我们可以选择不同种类的设备。我决定使用以下截图中所示的传感器,因为它们根据气体浓度充当可变电阻,因此可以通过普通 ADC 轻松读取:

连接气体传感器

在这里展示的原型中,气体传感器实际上有四个,但名为MQ-2烟雾探测器)、MQ-4甲烷探测器)和MQ-7液化石油气探测器)的传感器看起来非常相似(除了每个传感器的标签),因此我只在前述截图中展示了其中一个,而一氧化碳探测器是带有 MQ-7 标签的红色设备。

注意

这些设备可以通过以下链接购买(或在互联网上查找):

以下是我们可以获取每个气体传感器说明书的 URL:

仔细查看气体传感器的说明书,我们可以看到这些传感器的类别如何根据气体浓度变化其内部电阻(实际上,它还取决于环境的湿度和温度;但对于室内使用,我们可以将这些值视为常数)。因此,如果我们将其与一个电阻串联并施加一个恒定电压,我们可以得到一个与实际气体浓度成正比的输出电压。

下图展示了一个可能的电路图,其中气体传感器连接到5V电源,RL电阻由两个电阻(R1R2)组成,因为我们不能在 BeagleBone Black 的 ADC 引脚上施加超过 1.8V 的电压。因此,通过选择这两个电阻,使得R1 ≥ 2R2,我们可以确保在任何可能的工作条件下,ADC 输入引脚上的电压不超过5.0V/3 ≈ 1.67V*,即使传感器的内部电阻被短接。然而,为了完全确保安全,我们可以添加一个反向阈值为 1.8V 的齐纳二极管Z),但我在我的原型中并没有使用它。

下图展示了我用于连接每个传感器的电路:

连接气体传感器

小贴士

请注意,气体传感器有六个引脚,成对标记为ABH;其中AB成对的引脚被短接,而标记为H的引脚一端必须连接到输入电压(在我们这里是 5V),另一端连接到地(有关更多信息,请参阅说明书)。

关于这些传感器的另一个重要问题是我们在使用它们之前应进行的校准。这个最后的调整非常重要;正如 MQ-2 的说明书中所报告的,我们读到以下建议:

我们建议您将探测器校准为 1000 ppm 液化石油气LPG)或 1000 ppm 异丁烷i-C[4]H[10])浓度的空气,并使用大约负载电阻RL)为 20K(5K 至 47K)的值。

这一步可以通过将电阻R1R2替换为可变电阻,然后微调其电阻来完成。然而,我决定使用普通电阻(R1 = 15KΩR2 = 6.8KΩ,使得 RL = R1 + R2 ≈ 20KΩ,如数据手册所示),然后在软件中实现了一些小的转换(见下节),即我们可以将 ADC 的原始数据转换为 ppm百万分之一)值,以便用户可以处理物理数据。

该翻译可以通过以下公式中的 增益偏移 值为每个传感器进行:

  • ppm = raw * 增益 + 偏移

在校准过程中,我们只需要使用两个已知点(ppm1ppm2),读取相应的原始数据(raw1raw2),然后应用以下公式:

  • 增益 = (ppm1 – ppm2) / (raw1 – raw2)

  • 偏移 = ppm1 – raw1 * 增益

当然,我们需要四组增益/偏移配对,每个传感器一组(校准过程相当长!)

一旦我们修正了输入电路,我们只需要将每个Vout连接到 BeagleBone Black 的 ADC 输入引脚。我们的板子有 8 个 ADC 输入,因此我们可以使用以下连接:

引脚 气体传感器
P9.39 - AIN0 Vout @MQ-2
P9.37 - AIN2 Vout @MQ-4
P9.35 - AIN6 Vout @MQ-5
P9.33 - AIN4 Vout @MQ-7

要启用它们,我们使用以下命令:

root@beaglebone:~# echo cape-bone-iio > /sys/devices/bone_capemgr.9/slots

如果一切正常,我们应该会看到以下内核信息:

part_number 'cape-bone-iio', version 'N/A'
slot #7: generic override
bone: Using override eeprom data at slot 7
slot #7: 'Override Board Name,00A0,Override Manuf,cape-bone-iio'
slot #7: Requesting part number/version based 'cape-bone-iio-00A0.dtbo
slot #7: Requesting firmware 'cape-bone-iio-00A0.dtbo' for board-name 'Override Board Name', version '00A0'
slot #7: dtbo 'cape-bone-iio-00A0.dtbo' loaded; converting to live tree
slot #7: #1 overlays
helper.12: ready
slot #7: Applied #1 overlays.

然后,AIN0AIN1、…、AIN7 文件应该会变得可用,如下所示:

root@beaglebone:~# find /sys -name '*AIN*'
/sys/devices/ocp.3/helper.12/AIN0
/sys/devices/ocp.3/helper.12/AIN1
/sys/devices/ocp.3/helper.12/AIN2
/sys/devices/ocp.3/helper.12/AIN3
/sys/devices/ocp.3/helper.12/AIN4
/sys/devices/ocp.3/helper.12/AIN5
/sys/devices/ocp.3/helper.12/AIN6
/sys/devices/ocp.3/helper.12/AIN7

注意

这些设置可以通过书中示例代码仓库中的bin/load_firmware.sh脚本来完成,如下所示:

root@beaglebone:~# ./load_firmware.sh adc

然后,我们可以使用 cat 命令读取输入数据:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
1716

提示

请注意,ADC 也可以通过其他文件在 sysfs 文件系统中读取。例如,以下命令从 AIN0 输入引脚读取:

root@beaglebone:~# cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

连接报警执行器

现在,我们需要以一种方式连接报警执行器,使得用户可以通过视觉和听觉反馈任何可能的危险气体浓度。此外,我们还需要将 GSM 模块连接到串口以进行通信。

LED 和蜂鸣器

LED 和蜂鸣器的连接非常简单。LED 可以直接(通过电阻)与 BeagleBone Black 的 GPIO 引脚连接,没有问题,而蜂鸣器则需要更多的工作,因为它需要比 LED 更高的电流才能工作。然而,我们可以通过使用晶体管来解决这个问题,正如下图所示,通过晶体管以更高的电流管理蜂鸣器。

提示

请注意,蜂鸣器不能是没有内部振荡器的简单压电器,否则必须使用外部振荡器电路或 PWM 信号!

LED 和蜂鸣器

在我的电路中,我为LEDL)使用了一个R(470Ω)电阻,为蜂鸣器使用了R(2KΩ)和Rd(4.7KΩ)电阻,并使用了一个BC546 晶体管T)。请注意,关于 LED,R = 100Ω的电阻会导致更高的亮度,因此可以根据 LED 的颜色进行更改,以获得不同的效果。

另请注意,蜂鸣器电路中的电阻Rd用于在启动过程中将 GPIO 拉低。实际上,在此阶段它被设置为输入,即使在这种配置下,从引脚流出的电流也可能会启动蜂鸣器!

BeagleBone Black 有很多 GPIO 线,因此我们可以使用以下连接:

引脚 执行器
P8.9 - GPIO69 R @LED
P8.10 - GPIO68 R @蜂鸣器

现在,为了测试连接,我们可以通过导出 GPIO 并使用以下命令将这些线路设置为输出,以此来设置 GPIO:

root@beaglebone:~# echo 68 > /sys/class/gpio/export
root@beaglebone:~# echo out > /sys/class/gpio/gpio68/direction
root@beaglebone:~# echo 0 > /sys/class/gpio/gpio68/value 
root@beaglebone:~# echo 69 > /sys/class/gpio/export
root@beaglebone:~# echo out > /sys/class/gpio/gpio69/direction
root@beaglebone:~# echo 0 > /sys/class/gpio/gpio69/value

注意

请注意,使用闪烁的 LED 来执行此任务是个不错的主意。然而,在本章中,我将使用普通的 GPIO 线,留待后续章节再讨论这个话题。

现在,要打开和关闭 LED 和蜂鸣器,我们只需将10写入相应的文件,如下所示:

root@beaglebone:~# echo 1 > /sys/class/gpio/gpio68/value 
root@beaglebone:~# echo 0 > /sys/class/gpio/gpio68/value
root@beaglebone:~# echo 1 > /sys/class/gpio/gpio69/value
root@beaglebone:~# echo 0 > /sys/class/gpio/gpio69/value

注意

这些设置可以通过使用本书示例代码库中的bin/gpio_set.sh脚本来完成,如下所示:

root@beaglebone:~# ./gpio_set 68 out
root@beaglebone:~# ./gpio_set 69 out

GSM 模块

如本章简介所述,我们希望添加一个 GSM 模块,以便能够远程提醒用户。为此,我们可以将此设备与 TTL 级信号的常规串口连接。在这种情况下,我们只需选择 BeagleBone Black 上的一个串口即可。

以下截图显示了我决定使用的 GSM 模块:

GSM 模块

注意

该设备可以通过以下链接购买(或通过网络搜索):

www.cosino.io/product/serial-gsmgprs-module

用户手册可以通过www.mikroe.com/downloads/get/1921/gsm_click_manual_v101c.pdf获取。

BeagleBone Black 有四个可用的串口。通过选择使用设备/dev/ttyO1,我们可以使用以下连接:

引脚 GSM 模块
P9.24 - TX-O1 RX
P9.26 - RX-O1 TX
P9.1 - GND GND
P9.3 - 3.3V 3.3V
P9.5 - 3.3V 5V

要启用串口,我们必须使用以下命令:

root@beaglebone:~# echo BB-UART1 > /sys/devices/bone_capemgr.9/slots

如果一切正常,我们应该会看到以下内核消息:

part_number 'BB-UART1', version 'N/A'
slot #8: generic override
bone: Using override eeprom data at slot 8
slot #8: 'Override Board Name,00A0,Override Manuf,BB-UART1'
slot #8: Requesting part number/version based 'BB-UART1-00A0.dtbo
slot #8: Requesting firmware 'BB-UART1-00A0.dtbo' for board-name 'Override Board Name', version '00A0'
slot #8: dtbo 'BB-UART1-00A0.dtbo' loaded; converting to live tree
slot #8: #2 overlays
48022000.serial: ttyO1 at MMIO 0x48022000 (irq = 73) is a OMAP UART1
slot #8: Applied #2 overlays.

设备文件/dev/ttyO1现在应该可用。

注意

这些设置可以通过使用本书示例代码库中的bin/load_firmware.sh脚本来完成,如下所示:

root@beaglebone:~# ./load_firmware.sh ttyO1

为了验证新设备是否已准备好,我们可以使用以下ls命令:

root@beaglebone:~# ls -l /dev/ttyO1
crw-rw---T 1 root dialout 248, 1 Apr 23 22:25 /dev/ttyO1

注意

读者可以参考本书作者所写的BeagleBone EssentialsPackt Publishing)一书,以获取更多关于如何激活和使用系统中可用的 GPIO 线和串口的信息。

现在,我们可以通过使用screen命令来测试我们是否能够与调制解调器通信,具体命令如下:

root@beaglebone:~# screen /dev/ttyO1 115200

注意

可以通过使用aptitude命令来安装screen命令,具体命令如下:

root@beaglebone:~# aptitude install screen

按下ENTER键后,你应该会看到一个空白终端,在那里如果输入ATZ字符串,你应该会得到字符串OK作为回应,代码如下所示:

ATZ
OK

是 GSM 模块在回应表示其正常工作。要退出screen命令,你必须按下CTRL + A + *键组合,然后当程序提示你Really quit and kill all your windows [y/n]时,按y*键确认。

最终图像

好的,现在我们需要将所有内容整合在一起!下面的图片展示了我制作的原型,用来实现这个项目并测试软件:

最终图像

请注意,我们需要外部电源供应,因为外部电路(尤其是 GSM 模块)需要 5V 电源。

设置软件

现在是时候思考实现所需功能的软件了,即检查气体浓度、记录数据,并最终激活警报。我们需要以下内容:

  1. 一个周期性程序(read_sensors.php),定期扫描所有传感器并将其数据记录到数据库中。

  2. 一个周期性程序(monitor.php),读取传感器的数据,将其与预设的阈值进行比较,然后设置一些内部状态。

  3. 一个周期性程序(write_actuators.php),根据之前保存的状态启用警报。

以下图表展示了这一情况:

设置软件

系统的核心是数据库,我们将所有希望记录的数据以及系统状态存储在其中。这样,所有的周期性功能可以作为独立任务实现,它们通过数据库本身进行交互。同时,我们可以通过在运行时修改config表来控制所有任务。

我使用MySQL实现了数据库系统,之前的配置可以通过使用my_init.sh脚本创建,在其中我们定义了适当的表格。

提示

可以通过使用aptitude命令来安装 MySQL 守护进程,具体命令如下:

root@beaglebone:~# aptitude install mysql-client mysql-server

下面是脚本的片段:

CREATE TABLE status (
   n VARCHAR(64) NOT NULL,
   v VARCHAR(64) NOT NULL,
   PRIMARY KEY (n)
) ENGINE=MEMORY;

# Setup default values
INSERT INTO status (n, v) VALUES('alarm', 'off');

#
# Create the system configuration table
#

CREATE TABLE config (
   n VARCHAR(64) NOT NULL,
   v VARCHAR(64) NOT NULL,
   PRIMARY KEY (n)
);

# Setup default values
INSERT INTO config (n, v) VALUES('sms_delay_s', '300');

INSERT INTO config (n, v) VALUES('mq2_gain', '1');
INSERT INTO config (n, v) VALUES('mq4_gain', '1');
INSERT INTO config (n, v) VALUES('mq5_gain', '1');
INSERT INTO config (n, v) VALUES('mq7_gain', '1');
INSERT INTO config (n, v) VALUES('mq2_off', '0');
INSERT INTO config (n, v) VALUES('mq4_off', '0');
INSERT INTO config (n, v) VALUES('mq5_off', '0');
INSERT INTO config (n, v) VALUES('mq7_off', '0');

INSERT INTO config (n, v) VALUES('mq2_th_ppm', '2000');
INSERT INTO config (n, v) VALUES('mq4_th_ppm', '2000');
INSERT INTO config (n, v) VALUES('mq5_th_ppm', '2000');
INSERT INTO config (n, v) VALUES('mq7_th_ppm', '2000');

#
# Create one table per sensor data
#

CREATE TABLE MQ2_log (
   t DATETIME NOT NULL,
   d float,
   PRIMARY KEY (t)
);

CREATE TABLE MQ4_log (
   t DATETIME NOT NULL,
   d float,
   PRIMARY KEY (t)
);

CREATE TABLE MQ5_log (
   t DATETIME NOT NULL,
   d float,
   PRIMARY KEY (t)
);

CREATE TABLE MQ7_log (
   t DATETIME NOT NULL,
   d float,
   PRIMARY KEY (t)
);

注意

my_init.sh脚本存储在本书示例代码库中的chapter_01/my_init.sh文件中。

读者应该注意,我们定义了一个status表,使用MEMORY存储引擎,因为我们不需要在重启时保留它,但需要良好的访问性能,而config表和每个传感器的日志表(MQ2_logMQ4_logMQ5_logMQ7_log)则定义为普通表,因为我们需要在完全重启时仍然保存这些数据。请注意,我们为每个变量定义了一个表,以便轻松访问日志数据;然而,即使我们决定将日志数据保存在一个全局日志表中,也不会改变什么。

还需要注意的是,在数据库初始化过程中,我们可以通过简单地使用INSERT命令记录这些值来定义一些默认设置。对于status表,我们只需要将alarm变量设置为off,而在config表中,我们可以设置等待重新发送新的短信报警前的最小延迟时间(秒)(sms_delay_s),增益/偏移量转换变量(mq2_gain/mq2_off等),以及每个传感器的阈值变量(mq2_th_ppm等),这些变量用于触发报警。

管理 ADC

现在,要从ADC获取数据并将其保存到数据库中,我们需要编写一个定期任务。这非常简单,以下代码片段展示了文件read_sensors.php的主要功能实现,该功能完成此操作:

function daemon_body()
{
   global $loop_time;
   global $sensors;

   # The main loop
   dbg("start main loop (loop_time=${loop_time}s)");
   while (sleep($loop_time) == 0) {
      dbg("loop start");

      # Read sensors
      foreach ($sensors as $s) {
         $name = $s['name'];
         $file = $s['file'];
         $var = $s['var'];
         $log = $s['log'];

         # Get the converting values
         $gain = db_get_config($var . "_gain");
         $off = db_get_config($var . "_off");

         dbg("gain[$var]=$gain off[$var]=$off");

         # Read the ADC file
         $val = file_get_data($file);
         if ($val === false) {
            err("unable to read sensor $name");
            continue;
         }

      # Do the translation
      $ppm = $val * $gain + $off;

      dbg("file=$file val=$val ppm=$ppm");

      # Store the result into the status table
      $ret = db_set_status($var, $ppm);
      if (!$ret) {
         err("unable to save $name status db_err=%s",
             mysql_error());
         continue;
      }

      # Store the result into the proper log table
      $ret = db_log_var($log, $ppm);
      if (!$ret)
         err("unable to save $name log db_err=%s",
             mysql_error());
      }

      dbg("loop end");
   }
}

注意

完整的脚本存储在本书示例代码仓库中的chapter_01/read_sensors.php文件中。

该函数非常简单。它启动主循环,定期读取 ADC 数据,获取当前变量所需的增益偏移量转换值,将其转换为相应的ppm值,然后修改当前的status变量,并将新值添加到读取传感器的日志表中。

如果我们执行脚本并启用所有调试命令行选项,我们将获得:

root@beaglebone:~# ./read_sensors.php -d -f -l -T 5
read_sensors.php[5388]: signals traps installed
read_sensors.php[5388]: start main loop (loop_time=5s)
read_sensors.php[5388]: loop start
read_sensors.php[5388]: gain[mq2]=0.125 off[mq2]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN0 val=810 ppm=101.25
read_sensors.php[5388]: gain[mq4]=1 off[mq4]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN2 val=1477 ppm=1477
read_sensors.php[5388]: gain[mq5]=1 off[mq5]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN6 val=816 ppm=816
read_sensors.php[5388]: gain[mq7]=1 off[mq7]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN4 val=572 ppm=572
read_sensors.php[5388]: loop end
read_sensors.php[5388]: loop start
read_sensors.php[5388]: gain[mq2]=0.125 off[mq2]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN0 val=677 ppm=84.625
read_sensors.php[5388]: gain[mq4]=1 off[mq4]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN2 val=1456 ppm=1456
read_sensors.php[5388]: gain[mq5]=1 off[mq5]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN6 val=847 ppm=847
read_sensors.php[5388]: gain[mq7]=1 off[mq7]=0
read_sensors.php[5388]: file=/sys/devices/ocp.3/helper.12/AIN4 val=569 ppm=569
read_sensors.php[5388]: loop end
...

提示

请注意,只有第一个传感器已经(或多或少)被校准!

该过程可以像往常一样通过CTRL + C组合键停止。

现在,我们可以通过使用my_dump.sh脚本来读取系统状态(在这种情况下,最后读取的传感器数据),方法如下:

root@beaglebone:~# ./my_dump.sh status
n   v
alarm   off
mq2   84.625
mq4   1456
mq5   815
mq7   569

注意

my_dump.sh脚本存储在本书示例代码仓库中的chapter_01/my_dump.sh文件中。

同一个脚本也可以用于转储日志表。例如,如果我们希望查看 MQ-2 的日志数据,可以使用以下命令:

root@beaglebone:~# ./my_dump.sh mq2_log
t   v
2015-05-15 17:39:36	101.25
2015-05-15 17:39:41	84.625
2015-05-15 17:39:46	84.625

管理执行器

当传感器检测到危险的气体浓度时,alarm状态变量将被设置为开启状态。因此,当发生这种情况时,我们必须同时开启 LED 和蜂鸣器,并且必须向用户预定义的号码发送短信。

要执行这些操作,我们必须像之前展示的那样正确设置管理 LED 和蜂鸣器的 GPIO 线,并通过串口与GSM模块通信以发送短信。为了完成最后一步,我们需要安装gsm-utils软件包,在其中可以找到gsmsendsms命令,用于实际发送短信。要安装该软件包,可以使用以下命令:

root@beaglebone:~# aptitude install gsm-utils

然后,在将可用的 SIM 卡放入模块后,我们可以验证通过以下代码与 GSM 模块通信:

root@beaglebone:~# gsmctl -d /dev/ttyO1 me 
<ME0>  Manufacturer: Telit
<ME1>  Model: GL865-QUAD
<ME2>  Revision: 10.00.144
<ME3>  Serial Number: 356308042878501

然后,我们可以使用以下命令验证当前 PIN 状态:

root@beaglebone:~# gsmctl -d /dev/ttyO1 pin
<PIN0> READY

上述消息显示了 GSM 模块已正确配置,并且其中的 SIM 卡已准备好运行;然而,如果我们收到以下消息,则必须通过插入正确的 PIN 码来启用 SIM 卡:

gsmsendsms[ERROR]: ME/TA error 'SIM PIN required' (code 311)

在这种情况下,我们必须使用以下命令:

root@beaglebone:~# gsmctl -d /dev/ttyO1 -I "+cpin=NNNN"

在上述命令中,NNNN是你的 SIM 卡的 PIN 码。如果命令完全没有输出而挂起,意味着连接有问题。

现在我们已经检查了连接并且 SIM 卡已启用,我们可以通过以下命令开始发送短信:

root@beaglebone:~# gsmsendsms -d /dev/ttyO1 "+NNNNNNNNNNNN" 'Hello world!'

在上述命令中,NNNNNNNNNNNN字符串是必须发送短信的号码。

提示

如果模块的回答如下,意味着短信服务中心地址SCA)即接受短信以进行投递的中心电话号码未正确设置在你的手机中:

gsmsendsms[ERROR]: ME/TA error 'Unidentified subscriber' (code 28)

在这种情况下,你应该向你的 GSM 运营商询问,然后尝试以下命令:

root@beaglebone:~# gsmctl -o setsca "+SSSSSSSSSSSS"

在上述命令中,SSSSSSSSSSSS字符串是你的服务中心号码。

现在我们已经获得了所有控制执行器所需的信息。管理任务的主要功能可能的实现如下:

function daemon_body()
{
 global $loop_time;
 global $actuators;

 $sms_delay = db_get_config("sms_delay_s");

 $old_alarm = 0;
 $sms_time = strtotime("1970");

 # The main loop
 dbg("start main loop (loop_time=${loop_time}s)");
 while (sleep($loop_time) == 0) {
 dbg("loop start");

 # Get the "alarm" status and set all alarms properly
 $alarm = db_get_status("alarm");
 foreach ($actuators as $a) {
 $name = $a['name'];
 $file = $a['file'];

 dbg("file=$file alarm=$alarm");
 $ret = gpio_set($file, $alarm);
 if (!$ret)
 err("unable to write actuator $name");
 }

 # Send the SMS only during off->on transition
 if ($alarm == "on" && $old_alarm == "off" &&
 strtotime("-$sms_time seconds") > $sms_delay) {
 do_send_sms();
 $sms_time = strtotime("now");
 }

 $old_alarm = $alarm;

 dbg("loop end");
 }
}

注意

完整的脚本存储在本书示例代码仓库中的chapter_01/write_actuators.php文件中。

再次强调,这个函数非常简单——我们只需从数据库中读取当前alarm变量的状态,然后根据它设置执行器。请注意,必须为短信管理做特殊处理;事实上,系统必须每次只发送一条短信,并且仅在从关到开的转换中,并且必须在sms_delay秒后才能发送。为了实现这一点,我们使用old_alarmsms_time变量来保存上次循环的状态。

要测试代码,我们可以使用以下命令通过my_set.sh命令控制alarm变量:

root@beaglebone:~# ./my_set.sh status alarm on
root@beaglebone:~# ./my_set.sh status alarm off

注意

脚本存储在本书示例代码仓库中的chapter_01/my_set.sh文件中。

因此,让我们使用以下命令启动脚本:

root@beaglebone:~# ./write_actuators.php -d -f -l -T 5
write_actuators.php[5474]: signals traps installed
write_actuators.php[5474]: start main loop (loop_time=5s)
write_actuators.php[5474]: loop start
write_actuators.php[5474]: file=/sys/class/gpio/gpio68 alarm=off
write_actuators.php[5474]: file=/sys/class/gpio/gpio69 alarm=off
write_actuators.php[5474]: loop end
write_actuators.php[5474]: loop start
write_actuators.php[5474]: file=/sys/class/gpio/gpio68 alarm=off
write_actuators.php[5474]: file=/sys/class/gpio/gpio69 alarm=off
write_actuators.php[5474]: loop end

在另一个终端上,我们可以通过以下命令更改alarm变量,正如已经提到的那样:

root@beaglebone:~# ./my_set.sh status alarm on

之后,我们注意到脚本正在正常工作:

write_actuators.php[5474]: loop start
write_actuators.php[5474]: file=/sys/class/gpio/gpio68 alarm=on
write_actuators.php[5474]: file=/sys/class/gpio/gpio69 alarm=on
write_actuators.php[5474]: send SMS...
write_actuators.php[5474]: loop end

关于如何在 PHP 中发送短信消息,我只是简单地使用了以下代码:

function do_send_sms()
{
   dbg("send SMS...");
   system('gsmsendsms -d /dev/ttyO1 "' . PHONE_NUM . '" "GAS alarm!"');
}

基本上,在这里我们使用system()函数调用gsmsendsms命令。

注意

你可能会注意到,gsmsendsms发送短信需要一些时间。这是正常现象。

控制环境

现在,我们只需要传感器和执行器之间的粘合剂,即一个周期性函数,根据用户输入定期检查是否根据读取的信息需要激活报警器。

monitor.php脚本主函数的可能实现如下:

function daemon_body()
{
   global $loop_time;
   global $actuators;

   # The main loop
   dbg("start main loop (loop_time=${loop_time}s)");
   while (sleep($loop_time) == 0) {
      dbg("loop start");

      # Get the gas concentrations and set the "alarm" variable
      $mq2 = db_get_status("mq2");
      $mq2_th_ppm = db_get_config("mq2_th_ppm");
      dbg("mq2/mq2_th_ppm=$mq2/$mq2_th_ppm");
      $mq4 = db_get_status("mq4");
      $mq4_th_ppm = db_get_config("mq4_th_ppm");
      dbg("mq4/mq4_th_ppm=$mq4/$mq4_th_ppm");
      $mq5 = db_get_status("mq5");
      $mq5_th_ppm = db_get_config("mq5_th_ppm");
      dbg("mq5/mq5_th_ppm=$mq5/$mq5_th_ppm");
      $mq7 = db_get_status("mq7");
      $mq7_th_ppm = db_get_config("mq7_th_ppm");
      dbg("mq7/mq7_th_ppm=$mq7/$mq7_th_ppm");

      $alarm = $mq2 >= $mq2_th_ppm ||
         $mq2 >= $mq2_th_ppm ||
         $mq2 >= $mq2_th_ppm ||
         $mq2 >= $mq2_th_ppm ? 1 : 0;

      db_set_status("alarm", $alarm);
      dbg("alarm=$alarm");

      dbg("loop end");
   }
}

注意

完整脚本存储在书籍示例代码库中的chapter_01/monitor.php文件中。

该函数启动了main循环,在获取传感器阈值后,它仅获取最后一个传感器的值,并相应地设置alarm变量。

同样,我们可以通过使用my_set.sh命令来更改气体浓度阈值,具体如下:

root@beaglebone:~# ./my_set.sh config mq2_th_ppm 5000

我们可以通过以下方式以与之前两次相同的方式执行脚本进行测试:

root@beaglebone:~# ./monitor.php -d -f -l -T 5
monitor.php[5819]: signals traps installed 
monitor.php[5819]: start main loop (loop_time=5s)
monitor.php[5819]: loop start
monitor.php[5819]: mq2/mq2_th_ppm=84.625/5000
monitor.php[5819]: mq4/mq4_th_ppm=1456/2000
monitor.php[5819]: mq5/mq5_th_ppm=815/2000
monitor.php[5819]: mq7/mq7_th_ppm=569/2000
monitor.php[5819]: alarm=0
monitor.php[5819]: loop end
monitor.php[5819]: loop start
monitor.php[5819]: mq2/mq2_th_ppm=84.625/5000
monitor.php[5819]: mq4/mq4_th_ppm=1456/2000
monitor.php[5819]: mq5/mq5_th_ppm=815/2000
monitor.php[5819]: mq7/mq7_th_ppm=569/2000
monitor.php[5819]: alarm=0
monitor.php[5819]: loop end
...

要停止测试,只需使用CTRL + C组合键。你应该能得到如下输出:

^Cmonitor.php[5819]: signal trapped!

最终测试

一旦所有设备都连接好并且软件准备就绪,就可以对我们的新系统进行一个小测试。这个演示可以通过使用打火机来完成。事实上,我们的系统对打火机中的气体非常敏感!

首先,我们需要检查系统配置:

root@beaglebone:~# ./my_dump.sh config
n   v
mq2_gain   0.125
mq2_off   0
mq2_th_ppm   150
mq4_gain   0.125
mq4_off   0
mq4_th_ppm   150
mq5_gain   0.125
mq5_off   0
mq5_th_ppm   150
mq7_gain   0.125
mq7_off   0
mq7_th_ppm   150
sms_delay_s   300

提示

请注意,我使用了一个非常弱的校准设置;然而,这些设置适用于演示。

然后,我们可以查看系统的当前状态:

root@beaglebone:~# ./my_dump.sh status
n   v
mq2   73.5
mq4   121.75
mq5   53
mq7   80.5
alarm   0

然后,我们可以通过使用chapter_01/SYSINIT.sh脚本一次性进行所有硬件设置,脚本可以在书中的示例代码库中找到,具体如下:

root@beaglebone:~# ./SYSINIT.sh
done!

好的,现在让我们启动所有必需的进程守护程序:

root@beaglebone:~# ./read_sensors.php -d -T 2
root@beaglebone:~# ./write_actuators.php -d -T 2
root@beaglebone:~# ./monitor.php -d -T 2

提示

请注意,所有守护程序都在后台运行;然而,调试消息已启用,可以通过以下命令在系统日志中查看:

# tail -f /var/log/syslog

现在,我们需要将打火机靠近传感器,并按下打火机上的按钮,使传感器能够检测到气体。一段时间后,报警器应该会启动,查看系统状态时,我们应该能看到以下内容:

root@beaglebone:~# ./my_dump.sh status
n   v
mq2   203.875
mq4   166.5
mq5   52.5
mq7   122.625
alarm   1

此外,如果我们已设置了电话号码,我们应该会收到一条短信!

最后一步,让我们通过绘制图表来显示记录的数据。我们可以使用以下命令从数据库中提取数据:

root@beaglebone:~# ./my_dump.sh mq2_log | awk '{ print $2 " " $3 }' > mq2.log

mq2.log文件中,我们应该能找到如下内容:

root@beaglebone:~# cat mq2.log
15:02:07 75.25
15:02:10 74.25
15:02:12 74.25
15:02:14 74.375
15:02:16 74.25
...

现在,使用下一个命令,我们将创建一个包含我们数据图表的 PNG 图像:

$ gnuplot mq2.plot

提示

请注意,要执行此命令,你需要安装gnuplot命令,安装方法如下:

# aptitude install gnuplot

此外,还需要mq2.logmq2.plot文件。前者由前述命令行创建,后者可以在书籍示例代码库中的chapter_01/mq2.plot文件中找到。它包含了有效绘制图表的gnuplot指令。

我测试的 MQ-2 数据的图表如下图所示:

最终测试

如你所见,传感器对气体非常敏感;当我打开打火机并且气体接触到它们时,ppm 浓度迅速上升到很高的值。

要停止测试,我们可以使用以下命令:

root@beaglebone:~# killall read_sensors.php
root@beaglebone:~# killall write_actuators.php
root@beaglebone:~# killall monitor.php

总结

在这一章中,我们学习了如何管理 ADC、GPIO 线、GSM 模块和串口。我们还了解了如何实现一个简单的监控程序,该程序可以通过数据库服务器与传感器读取任务进行通信,收集输入数据,并与执行器管理器配合,在紧急情况下提醒用户。

在下一章中,我们将看到如何管理超声波距离传感器,以实现一个停车辅助系统,该系统能够向驾驶员传达汽车与车库墙壁之间的距离。然而,下一章真正有趣的部分是关于如何在两种不同的设置中管理距离传感器:一种是所有外设都靠近 BeagleBone Black,另一种是通过 USB 电缆远程连接传感器。

第二章。超声波停车助手

在本章中,我们将学习如何使用 BeagleBone Black 来实现停车辅助系统。我们将使用超声波传感器来检测汽车与车库墙壁之间的距离,并通过一些 LED 向驾驶员反馈汽车的位置,以避免碰撞。

我们将看到如何通过两种不同的方式设置超声波传感器,使用不同的接口来获取数据,以便用两种不同的方法解决问题并获得两种不同的系统配置。

基本工作原理

这个项目非常简单,尽管它需要一些电子技能来管理传感器输出。基本上,我们的 BeagleBone Black 只需要定期轮询超声波传感器的输出,然后根据与墙壁的距离点亮 LED:距离越短,点亮的 LED 就越多。

设置硬件

正如前面所说,在这个项目中,我们尝试实现两种不同的设置:第一种使用超声波传感器的模拟输出并实现电路设计,所有设备都直接连接到 BeagleBone Black(所有外设都靠近主板);而第二种设置则允许我们通过 USB 连接远程管理超声波传感器,因此可以将传感器安装在离 BeagleBone Black 主板较远的地方。

简单来说,我们可以将传感器放在一个地方,而 LED 灯则放在另一个地方,可能放在一个更显眼的位置,如下图所示:

设置硬件

如您所见,表示驱动视角的虚线箭头,如果 LED 灯位于距离传感器的上方,更容易看清,而传感器应放置在离地面较近的位置,以便更好地捕捉到汽车的前部。

首次设置 – 所有设备放置在 BeagleBone Black 附近

在这个设置中,我们将使用 BeagleBone Black 的一个 ADC 引脚来读取超声波传感器的模拟输出。

使用距离传感器的模拟输出

以下图像展示了我在原型中使用的超声波传感器:

使用距离传感器的模拟输出

注意

设备可以通过以下链接购买(或通过上网查找):

www.cosino.io/product/ultrasonic-distance-sensor

该设备的数据手册可以在 www.maxbotix.com/documents/XL-MaxSonar-EZ_Datasheet.pdf 上找到。

该设备非常有趣,因为它具有多个输出通道,可以用于获取测量的距离。特别是,它可以通过模拟电压通道和串口给出测量值;前者用于这个设置,后者将在第二个设置中讨论。

查阅数据手册后,我们发现模拟输出的分辨率为 Vcc/1024 每厘米,在 5V 下最大有效范围约为 700 毫米,在 3.3V 下约为 600 厘米。在这个设置中,我们使用 Vcc 设置为 3.3V,因此最大输出电压(VoutMAX)将为:

  • VoutMAX = 3.3V / 1024 * 600 ≈ 1.93V

记住 BeagleBone Black 的 ADC 最大输入电压为 1.8V,我们必须找到一种方法来缩小这个值。一个 快速而简单 的技巧是使用经典的电压分压器,如下图所示:

使用距离传感器的模拟输出

使用前述电路,我们只需将传感器输出除以 2。ADC 输入引脚的电压可以通过以下公式计算:

  • V[ADCin] = R / (R + R) * Vout = R / 2 R * Vout = 1 / 2 * Vout

所以,唯一需要做的就是为两个电阻(R)选择一个合适的值。在我的原型中,我将这个值设置为 R=6.8KΩ,这是一个合理的值,可以从传感器获得适当的电流。

在这种情况下,我们的分辨率变为约 1.61mV/厘米,连接到 BeagleBone Black 的接线如下面的表格所示:

引脚 距离传感器引脚(标签)
P9.1 - GND 7
P9.3 - 3.3V 6 (Vcc)
P9.39 - AIN0 3 (AN)

现在,为了启用 BeagleBone Black 的 ADC 引脚,我们可以像在第一章中一样使用以下命令,危险气体传感器

root@beaglebone:~# echo cape-bone-iio > /sys/devices/bone_capemgr.9/slots

如果一切正常,我们应该看到以下内核信息:

part_number 'cape-bone-iio', version 'N/A'
slot #7: generic override
bone: Using override eeprom data at slot 7
slot #7: 'Override Board Name,00A0,Override Manuf,cape-bone-iio'
slot #7: Requesting part number/version based 'cape-bone-iio-00A0.dtbo
slot #7: Requesting firmware 'cape-bone-iio-00A0.dtbo' for board-name 'Override Board Name', version '00A0'
slot #7: dtbo 'cape-bone-iio-00A0.dtbo' loaded; converting to live tree
slot #7: #1 overlays
bone-iio-helper helper.12: ready
slot #7: Applied #1 overlays.

然后,AIN0AIN1、…、AIN7 文件应该可以访问,如下所示:

root@beaglebone:~# find /sys -name '*AIN*'
/sys/devices/ocp.3/helper.12/AIN0
/sys/devices/ocp.3/helper.12/AIN1
/sys/devices/ocp.3/helper.12/AIN2
/sys/devices/ocp.3/helper.12/AIN3
/sys/devices/ocp.3/helper.12/AIN4
/sys/devices/ocp.3/helper.12/AIN5
/sys/devices/ocp.3/helper.12/AIN6
/sys/devices/ocp.3/helper.12/AIN7

注意

这些设置可以通过使用书中示例代码仓库中的 bin/load_firmware.sh 脚本完成,如下所示:

root@beaglebone:~# ./load_firmware.sh adc

然后,我们可以使用 cat 命令读取输入数据,如下所示:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
1716

小贴士

正如在第一章中所述,危险气体传感器,ADC 也可以通过使用另一个文件仍然位于 sysfs 文件系统中来读取,使用以下命令:

root@beaglebone:~# cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

现在,我们必须找到一种方法,将从 ADC 读取的值转换为以米为单位的距离,这样我们就能决定如何管理 LED 来给驾驶员提供反馈。回想一下前面提到的,分辨率约为 1.61mV/厘米,并且考虑到 ADC 的分辨率为 12 位,最大电压为 3.3V,距离d)与汽车和墙壁之间的厘米数可以通过以下公式给出(其中 n 是从 ADC 读取的数据):

  • d = 3.3V * n / 4095 / 0.00161V/厘米

请注意,这些是估计值,因此最好对传感器进行校准,以便至少在我们希望测量的最低值附近得到正确的读数(在我们的例子中,这个值是 0.20 米)。为此,我们可以将物体放置在距离传感器 20 厘米的位置,测量 ADC 输出值,然后计算补偿值 K,使得以下公式能够精确返回 20:

  • d[calib] = K * 3.3V * n/4095 / 0.00161V/cm

请注意,在没有校准的情况下,K 可以设置为 1(在这种情况下,我们再次得到原始公式,d = d[calib]。)

在我的原型中,将物体放置在距离传感器 20 厘米的位置时,我得到了以下值:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
29

所以,K 应设置为 1.38

第一次设置中的 LED 连接

LED 的连接非常简单,因为它们可以直接连接到 BeagleBone Black 的 GPIO 引脚,以下是电路图,显示了单个 LED 连接的示意图,可以为每个 LED 复制:

第一次设置中的 LED 连接

注意

我使用了一个 R = 470Ω 的电阻来连接 LEDL)。同样,如前一章所述,我们需要记住,电阻值 R 应根据 LED 的颜色进行调整,如果我们希望达到更亮的效果。

我们有 5 个 LED,因此需要 5 根 GPIO 引脚。我们可以使用以下连接:

引脚 LED 颜色 当距离低于时激活
P8.45-GPIO44 白色 5.00 米
P8.46-GPIO67 黄色 2.00 米
P8.7-GPIO69 红色 1.00 米
P8.8-GPIO68 红色 0.50 米
P8.9-GPIO45 红色 0.20 米

白色 LED 用来提示用户距离墙壁小于 5 米;黄色 LED 用来提示距离墙壁小于 2 米;红色 LED 用来提示车库墙壁距离小于 1 米、0.50 米和 0.20 米。

为了测试 LED 的连接,我们可以使用与第一章中相同的命令,危险气体传感器。例如,我们可以通过以下命令测试 GPIO68 上的 LED,首先设置 GPIO,然后将其关闭再打开:

root@beaglebone:~# echo 68 > /sys/class/gpio/export
root@beaglebone:~# echo out > /sys/class/gpio/gpio68/direction
root@beaglebone:~# echo 0 > /sys/class/gpio/gpio68/value 
root@beaglebone:~# echo 1 > /sys/class/gpio/gpio68/value

第二次设置 – 距离传感器远程化

在此设置中,我们将使用 BeagleBone Black 的串行端口读取超声波传感器测得的距离。

使用距离传感器的串行输出

这次,我们关注的是数据表中描述传感器串行输出能力的部分。特别是,我们读到:

第 5 引脚输出异步串行 RS232 格式,除了电压是 0-Vcc。输出是 ASCII 大写字母 "R",后跟三个 ASCII 数字字符,表示最大 765 厘米的测量范围,最后是回车符(ASCII 13)。波特率为 9600,8 位,无奇偶校验,1 个停止位。虽然 0-Vcc 的电压超出了 RS232 标准,但大多数 RS232 设备有足够的裕度来读取 0-Vcc 串行数据。如果需要标准电压级别的 RS232,请反转并连接 RS232 转换器。

这非常有趣,主要有两个原因:

  1. 由于传感器以数字格式提供数据,而非模拟格式(因此测量对干扰更具免疫力),测量非常精确。

  2. 信息可以通过 RS-232 线路发送(即使需要一些即将介绍的电子修正),这将允许我们将系统核心放置在与传感器不同的位置,从而提高整个系统的可用性。

所以,通过使用这种新的设置,LED 仍然安装在 BeagleBone Black 上,而距离传感器则通过 RS-232 线路远程连接。然而,由于我们仍然需要为传感器供电,并且标准的 RS-232 电缆无法传输电源,因此我们无法使用经典的 RS-232 线路!

解决方案是通过 USB 电缆使用 RS-232 连接。实际上,使用标准的 USB 电缆,我们能够发送/接收 RS-232 数据并提供所需的电源。

然而,仍然存在一些问题:

  1. USB 的电源电压为 5V,因此我们需要一个能够默认管理此电压级别的 USB-to-serial 转换器,或者至少能够耐受 5V。

  2. 认真阅读数据手册中的前一段,我们发现输出电平是 TTL 并且是反向的!因此,在将 TX 信号发送到 USB-to-serial 转换器(连接到 RX 引脚)之前,我们必须将其电气反向。(别担心,我会详细解释的。)

第一个问题的解决方案是使用以下 USB-to-serial 转换器,该转换器不仅支持 3.3V 工作,还能耐受 5V。

使用距离传感器的串行输出

注意

这些设备可以通过以下链接(或在互联网上搜索)购买:

www.cosino.io/product/usb-to-serial-converter

该设备的数据手册可以在 www.silabs.com/Support%20Documents/TechnicalDocs/cp2104.pdf 获取。

为了解决第二个问题,我们可以使用以下电路来反转传感器 TX 信号的 TTL 电平:

使用距离传感器的串行输出

提示

我使用了电阻值R1=2.2KΩ,R2=10KΩ,以及BC546 晶体管T)。Vin与传感器的第 5 引脚(TX)连接,而VoutRS232转换器的RX引脚连接。

工作原理非常简单——它是一个逻辑非端口与电平转换器。当逻辑 0(接近 0V 的电压)施加到Vin时,晶体管T)不工作,因此没有电流通过它,电阻R2上没有电压损失,Vout为 5V(逻辑 1)。另一方面,当逻辑 1(接近 3.3V 的电压)施加到Vin时,晶体管T)被打开,电流可以通过它流动,Vout降到接近 0V(逻辑 0)。下表清晰地显示了电路的工作原理,你可以看到它的工作与我们预期的完全一致!

Vin (V)/逻辑 Vout (V)/逻辑
0/0 5/1
3.3/1 0/0

在这种情况下,连接到 BeagleBone Black 的过程非常简单。事实上,我们只需将普通 USB 电缆连接到USB-to-serial转换器,再连接到距离传感器,具体连接方式见下表:

USB-to-serial 引脚 距离传感器引脚(标签)
GND 7
VBUS 6 (Vcc)
RX 5 (/TX)

提示

请注意,在表格中,我使用了/TX电子符号表示超声波传感器的 TX 引脚(在C中,我们可以写作!TX),因为如前所述,其输出信号必须反转,所以实际上,距离传感器的 TX 引脚必须连接到 TTL 反相器的Vin引脚,而Vout是有效信号/TX,必须连接到 USB 串口的 RX 引脚!

如果我们决定使用这个设置来连接距离传感器,从软件的角度来看,工作会更简单,因为不需要任何校准,因为传感器将以数字格式返回给我们距离,也就是说,不会出现由于模拟到数字转换或电压缩放造成的任何错误,正如在前一部分所看到的那样。事实上,我们只需通过 USB 连接读取串口数据即可获取距离;因此,如果一切正常,一旦我们连接 USB 电缆,应该会看到以下内核消息:

hub 1-0:1.0: state 7 ports 1 chg 0000 evt 0002
usb 1-1: reset full-speed USB device number 2 using musb-hdrc
hub 1-0:1.0: state 7 ports 1 chg 0000 evt 0002
usb 1-1: cp210x converter now attached to ttyUSB0

/dev/ttyUSB0设备现在可用:

root@beaglebone:~/chapter_02# ls -l /dev/ttyUSB0
crw-rw---T 1 root dialout 188, 0 Apr 23 20:28 /dev/ttyUSB0

现在,为了读取测量值,我们需要根据数据手册要求配置串口,使用以下命令:

root@beaglebone:~# stty -F /dev/ttyUSB0 9600

然后,可以使用以下命令实时显示数据:

root@beaglebone:~# cat /dev/ttyUSB0
126

提示

你可以通过按CTRL + C键来停止读取。

连接第二个设置中的 LED

在这个第二个设置中,由于连接与第一个设置基本相同,因此没有特别需要说明的 LED 事项。

请记住,LED 与 USB 连接无关,USB 连接仅用于远程连接距离传感器!

最终图片

以下截图显示了我实现这个项目并测试软件的原型。

请注意,我实现了两种配置:面包板的左半部分是超声波传感器及其相关电路(即可以远程操作的部分);右半部分是 LED 电路;而上中部是反向电压转换器;下中部是实现电压分压器的两个电阻。

还要注意截图中间的 USB 转串口转换器,我将 USB 电缆连接到 BeagleBone Black 的 USB 主机端口:

最终图片

由于外部电路和 BeagleBone Black 可能需要的电力超过了 PC 的 USB 端口提供的电量,我还使用了外部电源。

设置软件

在这个项目中,软件非常简单,因为我们只需要一个过程来定期读取距离,然后相应地打开和关闭 LED;然而,仍然有一些问题需要指出,尤其是关于如何管理 LED 以及两种超声波传感器配置之间的差异。

管理 LED

尽管前一章已介绍 GPIO 的管理,但需要指出的是,Linux 内核有多种设备,每种设备都有特定的用途,其中一种特殊设备是 LED 设备,这是一种可以用于管理 LED 并具有不同触发器的设备。触发器是 LED 的某种管理器,可以编程使其以特定方式工作。好了,还是做个示例更好,胜过空泛的解释!

首先,我们需要使用专用的设备树来定义 LED 设备,正如在书籍示例代码库中的chapter_02/BB-LEDS-C2-00A0.dts文件中所述。以下是该文件的代码片段:

   fragment@1 {
      target = <&ocp>;

      __overlay__ {
         c2_leds {
            compatible      = "gpio-leds";
            pinctrl-names   = "default";
            pinctrl-0       = <&bb_led_pins>;

            white_led {
               label   = "c2:white";
               gpios   = <&gpio3 6 0>;
               linux,default-trigger = "none";
               default-state = "on";
            };

            yellow_led {
               label   = "c2:yellow";
               gpios   = <&gpio3 7 0>;
               linux,default-trigger = "none";
               default-state = "on";
            };

            red_far_led {
               label   = "c2:red_far";
               gpios   = <&gpio3 2 0>;
               linux,default-trigger = "none";
               default-state = "on";
            };

            red_mid_led {
               label   = "c2:red_mid";
               gpios   = <&gpio3 3 0>;
               linux,default-trigger = "none";
               default-state = "on";
            };

            red_near__led {
               label   = "c2:red_near";
               gpios   = <&gpio3 5 0>;
               linux,default-trigger = "none";
               default-state = "on";
            };
         };
      };
   };

提示

有关如何定义 Linux LED 设备的更多信息,可以在 Linux 的源代码树中找到linux/Documentation/devicetree/bindings/leds/leds-gpio.txt文件,或者在线访问www.kernel.org/doc/Documentation/devicetree/bindings/leds/leds-gpio.txt

如你所见,每个 GPIO 都被启用,并通过gpio-leds驱动程序定义为 LED 设备。代码非常易于理解,可以很清楚地看到每个 GPIO 定义都有一个预定义触发器(即默认触发器为none),并且预定义状态设置为on

为了启用此设置,我们必须使用dtc命令将其编译成二进制形式,命令如下:

root@beaglebone:~# dtc -O dtb -o /lib/firmware/BB-LEDS-C2-00A0.dtbo -b 0 -@ BB-LEDS-C2-00A0.dts

然后,我们可以使用以下命令将其加载到内核中:

root@beaglebone:~# echo BB-LEDS-C2 > /sys/devices/bone_capemgr.9/slots

如果一切正常,我们应该能看到以下内核活动:

part_number 'BB-LEDS-C2', version 'N/A'
slot #7: generic override
bone: Using override eeprom data at slot 7
slot #7: 'Override Board Name,00A0,Override Manuf,BB-LEDS-C2'
slot #7: Requesting part number/version based 'BB-LEDS-C2-00A0.dtbo
slot #7: Requesting firmware 'BB-LEDS-C2-00A0.dtbo' for board-name 'Override Board Name', version '00A0'
slot #7: dtbo 'BB-LEDS-C2-00A0.dtbo' loaded; converting to live tree
slot #7: #2 overlays
...
slot #7: Applied #2 overlays.

提示

如果出现以下错误,我们需要禁用HDMI支持:

-bash: echo: write error: File exists

这可以通过编辑/boot/uboot/uEnv.txt文件中的 uboot 设置来完成,然后通过取消注释启用以下行:

optargs=capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN

请注意,在某些 BeagleBone Black 版本中,您可能会在/boot目录下找到uEnv.txt文件,您需要修改的uboot设置如下:

cape_disable=capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN

然后,我们只需要重新启动系统。如果一切设置正确,我们应该能够无错误地执行前述命令。

请注意,现在所有 LED 都已打开。现在,为了管理这些新的 LED 设备,我们可以使用以下目录下的 sysfs 条目:

root@beaglebone:~# ls -d /sys/class/leds/c2*
/sys/class/leds/c2:red_far   /sys/class/leds/c2:white
/sys/class/leds/c2:red_mid   /sys/class/leds/c2:yellow
/sys/class/leds/c2:red_near

如您所见,所有在 DTS 文件中使用的名称都已经存在,我们还会在每个目录中找到以下文件:

root@beaglebone:~# ls /sys/class/leds/c2\:white
brightness  device  max_brightness  power  subsystem  trigger  uevent

相关的文件有triggerbrightnessmax_brightnesstrigger文件用于查找当前的触发器,并在必要时更改它。实际上,通过读取该文件,我们可以看到以下内容:

root@beaglebone:~# cat /sys/class/leds/c2\:white/trigger
[none] nand-disk mmc0 mmc1 timer oneshot heartbeat backlight gpio cpu0 default-on transient

正如我们所期望的,当前触发器是none(方括号中的部分),我们可以通过将新名称写入相同的文件来简单地更改它(参见前面的示例)。

brightnessmax_brightness文件是当前触发器特有的,可以用来将 LED 的亮度从0值设置到max_brightness文件中存储的最大值。为了测试它,我们可以读取这些文件中的当前值,以验证当前状态是否已达到最大亮度:

root@beaglebone:~# cat /sys/class/leds/c2\:white/max_brightness
255
root@beaglebone:~# cat /sys/class/leds/c2\:white/brightness
255

要关闭 LED,我们可以使用以下命令:

root@beaglebone:~/# echo 0 > /sys/class/leds/c2\:white/brightness

提示

请注意,我们的 LED 只有两个功能值,即0255,这是因为我们使用的 LED 只有两个有效状态。

然而,在我们的项目中,当汽车靠近墙壁并且距离逐渐缩小时,具有闪烁功能,特别是当距离小于 0.10 米时,红色 LED 会亮起并停止闪烁,保持开启状态,这将为日益增加的危险提供更好的警告,可能非常有趣。具体来说,我们可以以这种方式做到:当根据本章中《第一次设置 LED 连接》部分的内容,需要打开红色 LED 时,闪烁的频率将随着距离的减少而不断增加,当距离小于 0.10 米时,闪烁将停止,LED 将保持开启状态。

要以所需的频率闪烁 LED,我们可以使用timer触发器。为了展示其工作原理,尝试通过以下命令在名为red_far的 LED 上启用它:

root@beaglebone:~# echo timer > /sys/class/leds/c2\:red_far/trigger

执行此命令后,LED 应该开始闪烁;然后再次查看目录,我们会看到现在有了新的文件:

root@beaglebone:~# ls /sys/class/leds/c2\:red_far
brightness  delay_on  max_brightness  subsystem  uevent
delay_off   device    power        trigger

新的有趣文件是delay_ondelay_off,它们可以用于定义 LED 必须开启和关闭的时长。显而易见,LED 的闪烁频率(F)现在可以通过以下公式设置:

F = 1 / T,其中T = T[delay_on] + T[delay_off]

因此,例如,如果我们希望 LED 以 10Hz 的频率闪烁,我们可以使用以下命令:

root@beaglebone:~# echo 50 > /sys/class/leds/c2\:red_far/delay_on 
root@beaglebone:~# echo 50 > /sys/class/leds/c2\:red_far/delay_off

值 50 表示:50ms 的状态和 50ms 的状态。所以,我们有T[delay_on]=50msT[delay_off] =50ms,所以 T=100ms,然后 F=10Hz

考虑到人眼在最大约 25Hz 频率下仍然敏感,且允许的最小频率为 1Hz,写入上述两个文件的可能值从 500(毫秒)用于 1Hz 的闪烁频率,到 20(毫秒)用于 25Hz 的闪烁频率。

控制 LED 的程序实现可以在书中示例代码库中的chapter_02/led_set.sh文件中找到。以下代码是相关代码的片段:

case $mode in
-1)
   # Turn on the LED
   echo none > /sys/class/leds/c2\:$name/trigger
   echo 255 > /sys/class/leds/c2\:$name/brightness
   ;;

0)
   # Turn off the LED
   echo none > /sys/class/leds/c2\:$name/trigger
   echo 0 > /sys/class/leds/c2\:$name/brightness
   ;;

*)
   # Flash the LED
   t=$((1000 / $mode / 2))

   echo timer > /sys/class/leds/c2\:$name/trigger
   echo $t > /sys/class/leds/c2\:$name/delay_on
   echo $t > /sys/class/leds/c2\:$name/delay_off
   ;;
esac

在这里,当mode变量设置为-1时,代码将打开name变量指定的 LED,而当mode设置为0时,它会关闭同一个 LED。此外,代码会在mode变量的值在125(Hz)之间时,启用具有适当设置的timer触发器。

以下是一个示例用法:

root@beaglebone:~# ./led_set.sh red_far -1
root@beaglebone:~# ./led_set.sh red_far 0
root@beaglebone:~# ./led_set.sh red_far 10

距离监视器

现在是时候看看我们的停车助手如何在实践中工作了。代码的一个可能实现可以在书中的示例代码库中的chapter_02/distance_mon.sh脚本中找到。以下代码片段展示了主要代码:

# Ok, do the job
while sleep .1 ; do
   # Read the current distance from the sensor
   d=$($d_fun)
   dbg "d=$d"

   # Manage the LEDs
   leds_man $d
done

功能很简单——代码周期性地使用d_fun变量指向的函数读取传感器的距离,然后根据距离d(以厘米为单位)使用leds_man函数打开和关闭 LED。

d_fun变量保存应该通过使用 ADC 读取距离的函数的名称,即read_adc,或者使用串口的函数名称,即read_tty。以下是这两个函数:

function read_adc () {
   n=$(cat $ADC_DEV)

   d=$(bc -l <<< "$k * 3.3 * $n/4095 / 0.00161")
   printf "%.0f\n" $d
}

function read_tty () {
   while read d < $TTY_DEV ; do
      [[ "$d" =~ R[0-9]{2,3} ]] && break
   done

   # Drop the "R" character
   d=${d#R}

   # Drop the leading "0"
   echo ${d#0}
}

请注意,read_adc文件使用bc程序来计算之前讨论的转换公式,而read_tty使用 Bash 的readwhile命令来读取完整的数据行(数据行格式为Rxxx\r,如数据手册所述)。

提示

bc命令可能不会默认安装在 BeagleBone Black 的发行版中,因此你可以通过以下命令安装它:

root@beaglebone:~# aptitude install bc

leds_man函数如下:

function leds_man () {
   d=$1

   # Calculate the blinking frequency with the following
   # fixed values:
   #    f=1Hz  if d=100cm
   #    f=25Hz if d=25cm
   f=$((25 - 21 * ( d - 25 ) / 75))
   [ $f -gt 25 ] && f=25
   [ $f -lt 1 ] && f=1

   if [ "$d" -gt 500 ] ; then
      ./led_set.sh white     0
      ./led_set.sh yellow    0
      ./led_set.sh red_far   0
      ./led_set.sh red_mid   0
      ./led_set.sh red_near  0

      return
   fi

   if [ "$d" -le 500 -a "$d" -gt 200 ] ; then
      ./led_set.sh white    -1
      ./led_set.sh yellow    0
      ./led_set.sh red_far   0
      ./led_set.sh red_mid   0
      ./led_set.sh red_near  0

      return
   fi

   if [ "$d" -le 200 -a "$d" -gt 100 ] ; then
      ./led_set.sh white    -1
      ./led_set.sh yellow   -1
      ./led_set.sh red_far   0
      ./led_set.sh red_mid   0
      ./led_set.sh red_near  0

      return
   fi

   if [ "$d" -le 100 -a "$d" -gt 50 ] ; then
      ./led_set.sh white    -1
      ./led_set.sh yellow   -1
      ./led_set.sh red_far  $f
      ./led_set.sh red_mid   0
      ./led_set.sh red_near  0

      return
   fi

   if [ "$d" -le 50 -a "$d" -gt 20 ] ; then
      ./led_set.sh white    -1
      ./led_set.sh yellow   -1
      ./led_set.sh red_far  -1
      ./led_set.sh red_mid  $f
      ./led_set.sh red_near  0

      return
   fi

   # if -le 20
   ./led_set.sh white    -1
   ./led_set.sh yellow   -1
   ./led_set.sh red_far  -1
   ./led_set.sh red_mid  -1
   ./led_set.sh red_near -1
}

该函数首先计算闪烁频率,以遵循前述部分的要求,然后使用一个大的 case 语句决定应使用哪种 LED 配置来通知驱动程序。

最终测试

要测试原型,我们首先必须选择一个设置并进行所需的连接,如之前所述。然后我们需要打开电路板。

登录后,我们必须使用之前讨论过的命令来设置系统,或者直接使用书中示例代码库中的chapter_02/SYSINIT.sh命令。然后,我们必须相应地执行distance_mon.sh命令。

注意

注意查看SYSINIT.sh文件时,你可以看到:

# Uncomment the following in case of buggy kernel in USB host management
# cat /dev/bus/usb/001/001 > /dev/null ; sleep 1

这是指在插入 USB 电缆后,设备识别/dev/ttyUSB0时出现错误的情况。

为了使用第一种设置测试我的原型,我使用了以下命令:

root@beaglebone:~# ./distance_mon.sh -d -k 1.38 adc
distance_mon.sh: d_fun=read_adc k=1.38
distance_mon.sh: d=176
distance_mon.sh: d=175
distance_mon.sh: d=175
distance_mon.sh: d=175
distance_mon.sh: d=175
...

另一方面,为了测试第二种设置,我使用了这个命令:

root@beaglebone:~# ./distance_mon.sh -d serial
distance_mon.sh: d_fun=read_tty k=1
distance_mon.sh: d=151
distance_mon.sh: d=152
distance_mon.sh: d=151
distance_mon.sh: d=152
distance_mon.sh: d=152
...

你可以通过按下CTRL + C键来停止程序。

总结

在本章中,我们探讨了如何通过两种不同的方式管理超声波传感器,一种是使用 ADC,另一种是通过 USB 电缆进行串行连接,从而实现同一设备的两种不同设置:一种是所有外设都连接到 BeagleBone Black 上,另一种是通过 USB 连接将传感器远程化。

此外,我们还学习了如何管理 Linux 的 LED 设备,这使我们能够通过内核功能对简单的 GPIO 线路进行不同的使用。

在下一章,我们将看到如何实现一个水族箱监控系统,在这个系统中,我们能够记录所有环境数据,然后我们将学习如何通过网页面板控制我们心爱的鱼类的生活。

第三章:水族箱监控器

在本章中,我们将看到如何实现一个水族箱监控器,通过这个监控器,我们能够记录所有的环境数据,然后通过网页面板控制我们亲爱的鱼的生活。

通过使用特定的传感器,你将学会如何监控你的水族箱,设置警报,记录水族箱数据(如水温),并执行一些操作,如冷却水和喂鱼。

简单来说,我们将实现一个简单的水族箱网页监控器,包含实时视频直播、故障时的警报,并简单的温度数据记录,使我们能够从标准 PC 以及智能手机或平板电脑上监控系统,无需使用专门的移动应用程序,只需使用板载的标准浏览器。

功能基础

这个水族箱监控器是一个很好的(尽管非常简单)示例,展示了一个网页监控系统应如何实现,给读者一些关于一个中等复杂系统如何工作的基本想法,以及我们如何与它互动,以修改一些系统设置,显示故障时的警报,并在 PC、智能手机或平板上绘制数据记录。

尽管存在这些方面的问题,这个项目的基本功能和我们之前章节中做的类似:我们有一个定时任务来收集数据并决定要做什么。然而,这次我们有一个用户界面(网页面板)来管理,还有一个视频流需要重定向到网页中。

还需要注意的是,在这个项目中,我们需要一个额外的电源来为 12V 设备(如水泵、灯和冷却器)供电并管理它们,而 BeagleBone Black 本身是 5V 供电的。

注意

请注意,我不会在一个真实的水族箱上测试这个原型(因为我没有水族箱),而是使用一个普通的茶杯来装水!所以,你应该仅将这个项目视为教育用途,尽管经过一些改进后,它也可以用于真实的水族箱!

设置硬件

关于硬件,至少有两个主要问题需要指出:

  • 电源:由于水泵、灯和冷却器是 12V 供电,而其他设备是 5V/3.3V 供电,所以我们需要使用两种不同的电压。因此,我们必须使用一个双输出电源(或两个不同的电源)来为我们的原型供电。

  • 接口:第二个问题是关于在 12V 世界和 5V 世界之间使用适当的接口电路,以确保不会损坏 BeagleBone Black 或其他设备。我要指出的是,BeagleBone Black 的每个 GPIO 引脚只能管理 3.3V 电压,因此我们需要一个合适的电路来管理 12V 设备。

设置 12V 设备

如前所述,这些设备需要特别的关注,并且需要一条专用的 12V 电源线,这条线当然不能是我们用来为 BeagleBone Black 供电的那条线。在我的原型中,我使用了一款最大输出 1A 的 12V 电源。这些特性足以驱动单个水泵、灯泡和风扇。

获取适当的电源后,我们可以展示如何使用电路来控制 12V 设备。由于这些设备都是简单的开/关设备,我们可以使用继电器来控制它们。我使用了以下图片中的设备,其中包含 8 个继电器:

设置 12V 设备

注意

这些设备可以在以下链接处购买(或通过浏览互联网找到):www.cosino.io/product/5v-relays-array

然后,连接单个 12V 设备的电路图如下所示:

设置 12V 设备

简单来说,对于每个设备,我们可以通过移动 BeagleBone Black 的特定 GPIO 来简单地打开和关闭电源。注意,每个继电器阵列板的继电器可以通过选择合适的连接来实现正逻辑或反逻辑控制,具体情况可以参考板上的标识。也就是说,我们可以决定,将 GPIO 设置为逻辑 0 状态时,激活继电器,从而打开连接的设备;而将 GPIO 设置为逻辑 1 状态时,关闭继电器,从而关闭连接的设备。

提示

使用以下逻辑时,当继电器的 LED 灯亮起时,对应的设备将被通电。

BeagleBone Black 的 GPIO 和我与 12V 设备一起使用的继电器阵列的引脚如下表所示:

引脚 继电器阵列引脚 12V 设备
P8.10 - GPIO66 3 灯泡
P8.9 - GPIO69 2 风扇
P8.12 - GPIO68 1 水泵
P9.1 - GND GND
P9.5 - 5V Vcc

为了测试每个 GPIO 引脚的功能,我们可以使用以下命令将 GPIO 设置为高电平输出引脚:

root@arm:~# ./bin/gpio_set.sh 68 out 1

提示

请注意,继电器的 状态是 1,而 状态是 0

然后,我们只需向 /sys/class/gpio/gpio68/value 文件写入 01 来控制继电器的开关,如下所示:

root@arm:~# echo 0 > /sys/class/gpio/gpio68/value
root@arm:~# echo 1 > /sys/class/gpio/gpio68/value

设置网络摄像头

我在原型中使用的网络摄像头是普通的基于 UVC 的摄像头,但你可以安全地使用其他受 mjpg-streamer 工具支持的摄像头。

注意

查看 mjpg-streamer 项目的主页以获取更多信息:sourceforge.net/projects/mjpg-streamer/

一旦连接到 BeagleBone Black USB 主机端口,我得到了以下内核活动:

usb 1-1: New USB device found, idVendor=045e, idProduct=0766
usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
usb 1-1: Product: Microsoft LifeCam VX-800
usb 1-1: Manufacturer: Microsoft
...
uvcvideo 1-1:1.0: usb_probe_interface
uvcvideo 1-1:1.0: usb_probe_interface - got id
uvcvideo: Found UVC 1.00 device Microsoft LifeCam VX-800 (045e:0766)

现在,一个名为 uvcvideo 的新驱动已加载到内核中:

root@beaglebone:~# lsmod
Module                  Size  Used by
snd_usb_audio          95766  0
snd_hwdep               4818  1 snd_usb_audio
snd_usbmidi_lib        14457  1 snd_usb_audio
uvcvideo               53354  0
videobuf2_vmalloc       2418  1 uvcvideo
...

好的,现在,为了建立一个流媒体服务器,我们需要下载 mjpg-streamer 源代码并编译它。我们可以在 BeagleBone Black 上直接执行以下命令来完成:

root@beaglebone:~# svn checkout svn://svn.code.sf.net/p/mjpg-streamer/code/ mjpg-streamer-code

提示

svn命令是subversion包的一部分,可以通过以下命令安装:

root@beaglebone:~# aptitude install subversion

下载完成后,我们可以通过以下命令行编译并安装代码:

root@beaglebone:~# cd mjpg-streamer-code/mjpg-streamer/ && make && make install

注意

你可以在书籍示例代码仓库中的chapter_03/mjpg-streamer-code.tgz文件里找到该程序的压缩档案副本。

如果没有错误报告,你现在应该能够按照如下执行新的命令,在这里我们请求帮助信息:

root@beaglebone:~# mjpg_streamer --help
-----------------------------------------------------------------------
Usage: mjpg_streamer
 -i | --input "<input-plugin.so> [parameters]"
 -o | --output "<output-plugin.so> [parameters]"
 [-h | --help ]........: display this help
 [-v | --version ].....: display version information
 [-b | --background]...: fork to the background, daemon mode
...

提示

如果你遇到以下错误,这意味着你的系统缺少convert工具:

make[1]: Entering directory `/root/mjpg-streamer-code/mjpg-streamer/plugins/input_testpicture'
convert pictures/960x720_1.jpg -resize 640x480!
pictures/640x480_1.jpg
/bin/sh: 1: convert: not found
make[1]: *** [pictures/640x480_1.jpg] Error 127

你可以通过使用常见的aptitude命令来安装它:

root@beaglebone:~# aptitude install imagemagick

好的,现在我们准备测试网络摄像头。只需运行以下命令行,然后在网页浏览器中输入地址http://192.168.32.46:8080/?action=stream(在这里你需要将我的 IP 地址192.168.32.46替换为你 BeagleBone Black 的 IP 地址),即可获取来自摄像头的实时视频:

root@beaglebone:~# LD_LIBRARY_PATH=/usr/local/lib/ mjpg_streamer -i "input_uvc.so -y -f 10 -r QVGA" -o "output_http.so -w /var/www/"

提示

注意,如果你没有使用 BeagleBone Black 的以太网端口,你也可以使用 USB 以太网地址192.168.7.2

如果一切正常,你应该看到类似以下截图的内容:

设置网络摄像头

提示

如果你遇到以下错误,这意味着另一个进程占用了8080端口,很可能是被Bone101服务占用:

bind: Address already in use

要禁用它,可以使用以下命令:

root@BeagleBone:~# systemctl stop bonescript.socket
root@BeagleBone:~# systemctl disable bonescript.socket
rm '/etc/systemd/system/sockets.target.wants/bonescript.socket'

或者,你可以简单地使用另一个端口,也许是8090端口,使用以下命令行:

root@beaglebone:~# LD_LIBRARY_PATH=/usr/local/lib/ mjpg_streamer -i "input_uvc.so -y -f 10 -r QVGA" -o "output_http.so -p 8090 -w /var/www/"

连接温度传感器

我在原型中使用的温度传感器如下图所示:

连接温度传感器

注意

这些设备可以通过以下链接(或通过上网搜索)购买:www.cosino.io/product/waterproof-temperature-sensor

该设备的数据表可以在datasheets.maximintegrated.com/en/ds/DS18B20.pdf找到。

如你所见,它是一个防水设备,因此我们可以安全地将其放入水中以获取温度。

该设备是一个1-Wire设备,我们可以通过使用w1-gpio驱动程序来访问它,该驱动程序通过使用标准的 BeagleBone Black GPIO 引脚模拟 1-Wire 控制器。电气连接必须按照以下表格进行,记住传感器有三根颜色不同的连接电缆:

引脚 电缆颜色
P9.4 - Vcc 红色
P8.11 - GPIO1_13 白色
P9.2 - GND 黑色

注意

感兴趣的读者可以访问以下 URL,了解更多关于 1-Wire 工作原理的信息:en.wikipedia.org/wiki/1-Wire

请记住,由于我们的 1-Wire 控制器是通过软件实现的,我们必须在红色和白色电缆之间添加一个 4.7KΩ的上拉电阻,以确保其正常工作!

一旦所有连接就位,我们可以使用书中示例代码库中的chapter_03/BB-W1-GPIO-00A0.dts文件,在 BeagleBone Black 的扩展连接器的P8.11引脚上启用 1-Wire 控制器。以下代码片段显示了我们启用w1-gpio驱动程序并为其分配适当 GPIO 的相关代码:

   fragment@1 {
      target = <&ocp>;

      __overlay__ {
         #address-cells  = <1>;
         #size-cell      = <0>;
         status          = "okay";

         /* Setup the pins */
         pinctrl-names   = "default";
         pinctrl-0       = <&bb_w1_pins>;

         /* Define the new one-wire master as based on w1-gpio
         * and using GPIO1_13
         */
         onewire@0 {
            compatible      = "w1-gpio";
            gpios           = <&gpio2 13 0>;
         };
      };
   };

要启用它,我们必须使用dtc程序按如下方式进行编译:

root@beaglebone:~# dtc -O dtb -o /lib/firmware/BB-W1-GPIO-00A0.dtbo -b 0 -@ BB-W1-GPIO-00A0.dts

然后,我们需要使用以下命令将其加载到内核中:

root@beaglebone:~# echo BB-W1-GPIO > /sys/devices/bone_capemgr.9/slots

如果一切正常,我们应该能在/sys/bus/w1/devices/目录下看到一个新的 1-Wire 设备,如下所示:

root@beaglebone:~# ls /sys/bus/w1/devices/
28-000004b541e9  w1_bus_master1

新的温度传感器由名为28-000004b541e9的目录表示。要读取当前温度,我们可以使用cat命令读取w1_slave文件,如下所示:

root@beaglebone:~# cat /sys/bus/w1/devices/28-000004b541e9/w1_slave
d8 01 00 04 1f ff 08 10 1c : crc=1c YES
d8 01 00 04 1f ff 08 10 1c t=29500

提示

请注意,您的传感器可能有不同的 ID,因此在您的系统中,您将在/sys/bus/w1/devices/28-NNNNNNNNNNNN/w1_slave路径下看到不同的路径名称。

在前面的示例中,当前温度为t=29500,单位是毫摄氏度m°C),所以它相当于 29.5°C。

提示

读者可以查看本书作者撰写的《BeagleBone Essentials》,Packt Publishing,以获取有关在 BeagleBone Black 上管理 1-Wire 设备的更多信息。

连接喂食器

鱼饲料器是一种可以通过移动电机释放一些饲料的设备。其工作原理在下图中表示:

连接喂食器

在关闭位置时,电机处于水平位置,因此饲料无法掉落,而在开启位置时,电机处于垂直位置,饲料才能掉落。我没有真正的鱼饲料器,但从前面的工作原理来看,我们可以通过使用下图所示的伺服电机来模拟它:

连接喂食器

注意

该设备可以通过以下链接购买(或通过网络浏览):www.cosino.io/product/nano-servo-motor

该设备的数据手册可以在hitecrcd.com/files/Servomanual.pdf上找到。

该设备可以进行位置控制,且通过适当的PWM信号输入,它可以旋转 90 度。事实上,查看数据表后,我们发现该伺服电机可以通过使用周期性方波来管理,其周期T)为 20ms,高电平时间t[high])介于 0.9ms 和 2.1ms 之间,1.5ms 为中心值(大致)。因此,我们可以认为电机在开启位置时* t[high] =1ms,在关闭位置时 t[high] =2ms*(当然,这些值在喂食器真正搭建完成后应进行仔细校准!)。

我们可以根据下表连接伺服电机:

引脚 电缆颜色
P9.3 - Vcc 红色
P9.22 - PWM 黄色
P9.1 - GND 黑色

提示

感兴趣的读者可以在en.wikipedia.org/wiki/Pulse-width_modulation了解更多关于 PWM 的信息。

为了测试连接,我们需要启用 BeagleBone Black 的一个 PWM 生成器。为了保持前面的连接,我们需要启用一个输出线路位于扩展连接器P9.22引脚的 PWM 生成器。为此,我们可以使用以下命令:

root@beaglebone:~# echo am33xx_pwm > /sys/devices/bone_capemgr.9/slots
root@beaglebone:~# echo bone_pwm_P9_22 > /sys/devices/bone_capemgr.9/slots

然后,在/sys/devices/ocp.3目录下,我们应该能找到与新启用的 PWM 设备相关的新条目,如下所示:

root@beaglebone:~# ls -d /sys/devices/ocp.3/pwm_*
/sys/devices/ocp.3/pwm_test_P9_22.12

查看/sys/devices/ocp.3/pwm_test_P9_22.12目录,我们可以看到可以用来管理新 PWM 设备的文件:

root@beaglebone:~# ls /sys/devices/ocp.3/pwm_test_P9_22.12/
driver	duty   modalias   period   polarity   power   run   subsystem   uevent

如我们从之前的文件名中可以推断,我们需要正确地在名为polarityperiodduty的文件中设置值。例如,可以通过以下命令实现伺服电机的中心位置:

root@beaglebone:~# echo 0 > /sys/devices/ocp.3/pwm_test_P9_22.12/polarity
root@beaglebone:~# echo 20000000 > /sys/devices/ocp.3/pwm_test_P9_22.12/period
root@beaglebone:~# echo 1500000 > /sys/devices/ocp.3/pwm_test_P9_22.12/duty

极性设置为0以进行反转,而其他文件中写入的值是以纳秒表示的时间值,设置为 20ms 的周期和 1.5ms 的占空比,如数据表要求(时间值均为纳秒)。现在,要将齿轮完全顺时针转动,我们可以使用以下命令:

root@beaglebone:~# echo 2100000 > /sys/devices/ocp.3/pwm_test_P9_22.12/duty

另一方面,以下命令是将其完全逆时针转动:

root@beaglebone:~# echo 900000 > /sys/devices/ocp.3/pwm_test_P9_22.12/duty

因此,通过使用以下命令序列,我们可以打开并随后关闭(延迟1秒)饲料器的闸门:

echo 1000000 > /sys/devices/ocp.3/pwm_test_P9_22.12/duty
sleep 1
echo 2000000 > /sys/devices/ocp.3/pwm_test_P9_22.12/duty

请注意,通过简单地修改延迟,我们可以控制每次启动饲料器时掉落的饲料量。

注意

实现饲料控制机制的脚本可以在书籍示例代码仓库中的chapter_03/feeder.sh文件中找到。

水传感器

我使用的水传感器如以下截图所示:

水传感器

注意

该设备可以通过以下链接(或通过上网搜索)购买:www.cosino.io/product/water_sensor

这是一个非常简单的设备,实现在以下截图中所示,其中添加了电阻R)来限制水分闭合电路时的电流:

水传感器

当一滴水接触原理图中梳子的两个或更多的齿时,电路闭合,输出电压Vout)从Vcc降至 0V。

因此,如果我们想检查水族箱中的水位,换句话说,如果我们想检查是否有水泄漏,我们可以将水族箱放入某种托盘中,然后将此设备放入其中。如果发生水泄漏,水会被托盘收集,传感器的输出电压应从Vcc降至 GND。

此设备使用的 GPIO 如以下表格所示:

引脚 电缆颜色
P9.3 - 3.3V 红色
P8.16 - GPIO67 黄色
P9.1 - GND 黑色

为了测试连接,我们必须使用以下命令将 GPIO 67 定义为输入行:

root@beaglebone:~# ../bin/gpio_set.sh 67 in

然后,我们可以通过使用以下两条命令来尝试读取 GPIO 状态,看看传感器在水中和不在水中的状态:

root@beaglebone:~# cat /sys/class/gpio/gpio67/value
0
root@beaglebone:~# cat /sys/class/gpio/gpio67/value
1

最终图片

以下截图展示了我为实现该项目并测试软件而实现的原型。正如你所看到的,水族箱已被一杯水替代:

最终图片

请注意,我们有两个外部电源:一个是常规的 5V 电源供电给 BeagleBone Black,另一个是输出电压为 12V 的电源供电给其他设备(你可以在右上角看到其连接器,位于网络摄像头右侧)。

设置软件

关于软件,这次的主要部分由 Web 界面覆盖,它是项目的核心,负责获取水族箱数据并管理执行器的控制过程。接下来,一个专门的监控脚本将用于实现 Web 界面与内部数据库之间的通信。

管理系统状态和配置

为了管理所有设备的状态并进行数据记录,我们可以再次使用数据库来存储所有相关数据,就像我们在第一章中所做的那样,危险气体传感器。因此,我们可以使用书中示例代码仓库中的chapter_03/my_init.sh文件来设置数据库。以下片段显示了定义项目中使用的表格的相关代码:

# Select database
USE aquarium_mon;

#
# Create the system status table
#

CREATE TABLE status (
        n VARCHAR(64) NOT NULL,
        v VARCHAR(64) NOT NULL,
        PRIMARY KEY (n)
) ENGINE=MEMORY;

# Setup default values
INSERT INTO status (n, v) VALUES('alarm_sys', '0');
INSERT INTO status (n, v) VALUES('alarm_level', '0');
INSERT INTO status (n, v) VALUES('alarm_temp', '0');
INSERT INTO status (n, v) VALUES('water', '21');
INSERT INTO status (n, v) VALUES('cooler', '0');
INSERT INTO status (n, v) VALUES('pump', '0');
INSERT INTO status (n, v) VALUES('lamp', '0');
INSERT INTO status (n, v) VALUES('force_cooler', '0');
INSERT INTO status (n, v) VALUES('force_pump', '0');
INSERT INTO status (n, v) VALUES('force_lamp', '0');
INSERT INTO status (n, v) VALUES('force_feeder', '0');

#
# Create the system configuration table
#

CREATE TABLE config (
   n VARCHAR(64) NOT NULL,
   v VARCHAR(64) NOT NULL,
   PRIMARY KEY (n)
);

# Setup default values
INSERT INTO config (n, v) VALUES('pump_t_on', '20');
INSERT INTO config (n, v) VALUES('pump_t_off', '60');
INSERT INTO config (n, v) VALUES('feeder_interval', '60');
INSERT INTO config (n, v) VALUES('water_temp_max', '27');
INSERT INTO config (n, v) VALUES('water_temp_min_alarm', '18');
INSERT INTO config (n, v) VALUES('water_temp_max_alarm', '29');

#
# Create one table per sensor data
#

CREATE TABLE temp_log (
   t DATETIME NOT NULL,
   v FLOAT,
   PRIMARY KEY (t)
);

status表格包含系统状态变量,含义如下:

变量名称 描述
alarm_sys 一般系统报警(I/O 和通信错误等)。
alarm_level 检测到水泄漏。
alarm_temp 水温超过water_temp_max_alarm值或低于water_temp_min_alarm值(单位:℃)。
Water 当前水温(单位:℃)。
Cooler 当前冷却器状态(0 = 关闭,1 = 开启)。
Pump 当前水泵状态(0 = 关闭,1 = 开启)。
Lamp 当前灯光状态(0 = 关闭,1 = 开启)。
force_cooler 用户请求开启冷却器。
force_pump 用户请求开启水泵。
force_lamp 用户请求开启灯光。
force_feeder 用户请求启用喂料器。

请注意,喂料器没有当前状态变量,因为它不能保持开启或关闭状态,而是具有脉冲开启的功能;也就是说,当启用时,它会打开然后关闭内部门。

另一方面,在config表格中,有系统配置变量,含义如下:

变量名称 描述
pump_t_on 水泵必须开启的时间(单位:秒)。
pump_t_off 水泵必须关闭的时间(单位:秒)。
feeder_interval 两次连续“午餐时间”之间的时间间隔(单位:秒)。
water_temp_max 如果水温超过此值(单位:°C),则启动冷却器。
water_temp_min_alarm 如果水温低于此值(单位:°C),则启用水温报警。
water_temp_max_alarm 如果水温超过此值(单位:°C),则启用水温报警。

提示

请注意,由于我的项目没有加热器来在水温过低时提高水温,因此缺少 water_temp_min 配置变量。然而,读者应该能够通过阅读本章获得填补此空白所需的所有信息!

最后,temp_log 表用于存储所有水温测量数据,这些数据对于在用户控制面板中展示小图表非常有用(请参阅下一个章节)。

构建 Web 控制面板

Web 控制面板是用 PHPJavaScript 编写的。PHP 用于实现数据采集和控制过程以及主页面,而 JavaScript 用于实现图形小部件。特别是,最后一部分是通过使用一个名为 Drinks 的有趣工具包实现的 (www.goincompany.com/drinks.php)。

使用该工具包实现的小部件非常简单。为了安装它,我们只需从项目主页下载 zip 压缩包,解压缩它,然后将扩展名为 .js 的文件移动到 Web 服务器的根目录。在我的 BeagleBone Black 上,我使用的是 Apache Web 服务器,其根目录在 /var/www 目录下。因此,为了安装 Drinks 工具包,我将文件移动如下:

root@beaglebone:~/chapter_03# ls /var/www/
Display.js  Knob.js  Slider.js   Drinks.js   Led.js   Switch.js

现在,我们需要添加代码以构建我们的 Web 控制面板并在该目录中进行管理。主脚本可以在书中的示例代码库中的 chapter_03/aquarium.php 文件中找到。接下来我将把它的所有相关代码分成几个代码片段进行展示。

在接下来的第一个代码片段中,是获取输入小部件初始状态的 PHP 代码。也就是说,这些小部件是用户直接管理的,用来向系统发送命令。在此页面首次加载时,用户将看到当前这些小部件的状态,它们存储在内部数据库中:

# Open the DB
db_open("aquarium_mon");

# Set initial statuses for input widgets
$force_cooler = db_get_status("force_cooler");
$force_pump = db_get_status("force_pump");
$force_lamp = db_get_status("force_lamp");
$force_feeder = db_get_status("force_feeder");

接下来是 HTML 页面的头部内容,如下所示:

<html>
   <head>
      <link href="aquarium.css" rel="stylesheet" type="text/css">

      <script type="text/javascript" src="img/Drinks.js"></script>

      <script>
         var man_in = Drinks.createManager();
         man_in.href = 'handler.php';
         man_in.input = new Array("force_cooler", "force_pump", "force_lamp", "force_feeder");
         man_in.refresh = 1;
         man_in.start();

         var man_out = Drinks.createManager();
         man_out.href = 'handler.php';
         man_out.refresh = 1;
         man_out.start();
      </script>
   </head>

通过使用这段代码,我们指示 Drinks 工具包生成两个管理器——一个管理输入小部件(man_in),另一个管理所有其他输出小部件(man_out)。与输入小部件不同,输出小部件是所有不受用户直接控制的小部件,它们由系统更新以向用户显示系统状态。

两个管理器每秒刷新一次其内部状态(refresh=1),并且都将使用名为handler.php的外部处理程序来完成这一操作。这个处理程序的代码(将在下一节中呈现)是定期执行的,用于获取输入小部件的状态,并将输出小部件的状态设置到控制面板中。

然后,控制面板分为三个主要小节。第一个小节用于放置实时视频和报警。可以通过以下代码片段实现:

   <table>
      <tr>
         <th><h3>Live video</h3></th>
         <th><h3>Alarms</h3></th>
      </tr>
      <tr>
         <td>
            <img src="img/<?=$_SERVER["SERVER_ADDR"]?>:8080/?action=stream" alt="real-time video feed" />
         </td>
         <td>
            <table class="widget">
               <tr>
                  <th>system</th>
                  <th>Water level</th>
                  <th>Water temperature</th>
               </tr>
               <tr>
                  <td><led id="alarm_sys" type="round" radius="25" color="red"></led></td>
                  <td><led id="alarm_level" type="round" radius="25" color="red"></led></td>
                  <td><led id="alarm_temp" type="round" radius="25" color="red"></led></td>
               </tr>
            </table>
         </td>
      </tr>
   </table>

在这里,以下行用于启用来自摄像头的实时视频:

<img src="img/<?=$_SERVER["SERVER_ADDR"]?>:8080/?action=stream" alt="real-time video feed" />

最后,以下三行用于定义与相应报警变量相关的报警 LED:

               <td><led id="alarm_sys" type="round" radius="25" color="red"></led></td>
               <td><led id="alarm_level" type="round" radius="25" color="red"></led></td>
               <td><led id="alarm_temp" type="round" radius="25" color="red"></led></td>

第二个小节包含控制小部件,即水温计、灯、冷却器、泵和进料器的 LED 和开关。由于所有输入小部件都在这里,代码使用一个大的 HTML 表单,将这些项目放置其中:

   <form method="post">
      <table class="widget">
         <tr>
            <th>Water temp (C)</th>
            <th>Lamp</th>
            <th>Cooler</th>
            <th>Pump</th>
            <th>Feeder</th>
         </tr>
         <tr>
            <td>
               <display id="water" type="thermo" max_range="50" range_from="10" range_to="50" autoscale="true"></display>
            </td>
            <td>
               <led id="lamp" type="round" radius="25"></led>
               <switch id="force_lamp" type="circle" value="<?=$force_lamp?>"></switch>
            </td>
            <td>
               <led id="cooler" type="round" radius="25"></led>
               <switch id="force_cooler" type="circle" value="<?=$force_cooler?>"></switch>
            </td>
            <td>
               <led id="pump" type="round" radius="25"></led>
               <switch id="force_pump" type="circle" value="<?=$force_pump?>"></switch>
            </td>
            <td>
               <led id="feeder" type="round" radius="25"></led>
               <switch id="force_feeder" type="toggle" width="80" value="<?=$force_feeder?>"></switch>
            </td>
         </tr>
      </table>
      <input type="hidden">
   </form>

以下行用于显示报告水温的温度计:

<display id="water" type="thermo" max_range="50" range_from="10" range_to="50" autoscale="true"></display>

以下两行用于显示灯、冷却器和泵的开关及相应的 LED 指示设备状态:

               <led id="lamp" type="round" radius="25"></led>
               <switch id="force_lamp" type="circle" value="<?=$force_lamp?>"></switch>

当 LED 打开时,相应的设备会被打开,而当 LED 关闭时,设备会被关闭。另一方面,用户通过切换前面的开关之一,可以强制系统在下一个周期内打开相应的设备。(我将在下一节中解释下一个周期的含义。)

进料器必须申请特殊的通知。如前所述,它可以通过脉冲启用,而不仅仅是开或关。为了突出这一点,这次我使用了不同类型的开关小部件。因此,LED 用于通知用户进料器将在下一个周期启用,而 LED 将保持点亮,直到进料器真正启用后才会关闭。

显示进料器控制的代码如下所示:

               <led id="feeder" type="round" radius="25"></led>
               <switch id="force_feeder" type="toggle" width="80" value="<?=$force_feeder?>"></switch>

这里,开关的类型是toggle,而不是circle

最后一小节是温度日志图形,用于向用户显示过去 20 个周期内水温的变化情况。实现这一部分的代码如下:

   <display id="temp_graph" type="graph" scale="range" autoscale="true" mode="ch1" power_onload="true">
      <channel href="log_temp.php" refresh="60" sweep="0.005" frequency="20"></channel>
   </display>

请注意,在这种情况下,我们需要一个特殊的处理程序来生成表示水温的图形点。这个处理程序被称为log_temp.php,它在channel条目的href参数中指定,而其他参数定义了刷新时间(以秒为单位,refresh="60")和图形的缩放(sweepfrequency)。有关这些参数的更多信息,请参阅Drinks文档页面。

在每次刷新时,log_temp.php脚本会被调用,它将返回一个点序列,以便显示小部件显示。为了了解它是如何发生的,我们需要移步到下一部分。但在此之前,让我向你展示一下我们刚才展示的 Web 控制面板在我的 PC 上是如何显示的:

构建 Web 控制面板

控制面板的处理

在前一部分中,我们发现网络控制面板需要一些处理程序来与底层系统发送/接收数据。特别是,我们提到输入/输出管理器与handler.php脚本进行通信,而温度日志则需要log_temp.php脚本来获取图形数据。接下来让我们看看这些脚本是如何编写的。

handler.php脚本中的代码如下:

<?php

require("db.php");

# Open the DB
db_open("aquarium_mon");

if (count($_GET) > 0) {
   # Input section
       db_set_status("force_cooler", $_GET["force_cooler"]);
       db_set_status("force_pump", $_GET["force_pump"]);
       db_set_status("force_lamp", $_GET["force_lamp"]);

   if ($_GET["force_feeder"])
      db_set_status("force_feeder", 1);
}

# Output section
$values["alarm_sys"] = db_get_status("alarm_sys");
$values["alarm_level"] = db_get_status("alarm_level");
$values["alarm_temp"] = db_get_status("alarm_temp");

$values["water"] = db_get_status("water");
$values["cooler"] = db_get_status("cooler");
$values["pump"] = db_get_status("pump");
$values["lamp"] = db_get_status("lamp");
$values["feeder"] = db_get_status("force_feeder");

$values["force_feeder"] = 0;

echo json_encode($values);
?>

注意

该脚本可以在书籍示例代码库中的chapter_03/handler.php文件中找到。

该脚本有一个输入部分来管理输入小部件,还有一个输出部分来管理输出小部件。在输入部分,我们只需获取每个输入小部件的状态,然后将其存储到status表中。唯一的例外是force_feeder变量,它只在状态为1时被记录,因为当馈料器在下一个周期启用时,它的状态会被清除(再次提到,下一个周期的含义将在稍后解释)。

在输出部分,我们只是从数据库中获取每个状态变量的状态,然后将其存储到一个数组中,该数组将通过json_encode()函数返回给Drinks工具包。这里需要特别注意的是,一旦force_feeder开关被移动到高电平状态,它的状态会被记录,然后会被清除,只是为了模拟它不是一个普通的开/关开关,而是一个脉冲开开关。

另一方面,正如刚才所说,log_temp.php脚本只需返回一个点的列表。以下是代码:

<?php

require("db.php");

# Open the DB
db_open("aquarium_mon");

# Get the last 20 points
$query = "SELECT v FROM temp_log ORDER BY t DESC LIMIT 20";
$ret = mysql_query($query);
if (!$ret)
  die();

$data = array();
$n = 0;
while ($row = mysql_fetch_array($ret)) {
  array_unshift($data, $row["v"]);
  $n++;
}

if ($n < 20)
  echo json_encode(array_merge(array_fill(0, 20 - $n, 0), $data));
else
  echo json_encode($data);
?>

注意

该脚本可以在书籍示例代码库中的chapter_03/log_temp.php文件中找到。

该脚本简单地从temp_log表中选择最后 20 个记录点,然后将它们存储到data[]数组中,并在开头添加一些零,以防存在少于 20 个存储的温度值。array_unshift()函数用于将每个新提取的值放到数组的开头,因为SELECT语句返回的数据是反向顺序的。

现在,最后一步是将所有这些脚本放到 Web 浏览器的根目录中。我的 BeagleBone Black 上的/var/www目录如下:

root@beaglebone:~/chapter_03# ls /var/www/
Display.js  Knob.js  Slider.js  aquarium.css  db.php   log_temp.php
Drinks.js   Led.js   Switch.js  aquarium.php  handler.php

提示

CSS 文件可以在书籍示例代码库中的chapter_03/aquarium.css文件中找到。由于它非常简单,并且对于理解项目并非严格必要,因此这里没有展示代码。

了解内部状态机

现在控制面板已经正确设置,我们需要查看内部的状态机,也就是在每个周期中收集所有环境数据,然后根据其内部状态和新的环境状态决定做什么的过程。

我们的机器实现位于书籍示例代码库中的chapter_03/aquarium_mon.php文件中。以下是其daemon_body()函数的几个代码片段(这是程序的真正核心)。

在开始时,该函数如下所示:

function daemon_body()
{
   global $loop_time;
   global $sensors;

   $pump_time = strtotime("now");
   $feeder_time = strtotime("now");

   # The main loop
   dbg("start main loop (loop_time=${loop_time}s)");
   while (sleep($loop_time) == 0) {
   dbg("loop start");

      $alarm_sys = 0;

在开始时,该函数初始化一些变量,然后启动主循环,第一步是获取水温,因为根据这个值需要执行许多任务!

还要注意,while()语句每次执行sleep($loop_time)函数,即每loop_time秒开始一个新的机器周期,并且所有变量根据读取的环境数据和用户输入进行修改。

然后,代码继续按如下方式读取温度:

   #
   # Temperature management
   #

   $ret = temp_get();
   if ($ret === false) {
      err("unable to get temperature!");
      $alarm_sys = 1;
   }
   $temp = $ret;
   dbg("t=$temp");

   # Save status
   db_set_status("water", $temp);

   #
   # Check alarms
   #

   $water_temp_min = db_get_config("water_temp_min_alarm");
   $water_temp_max = db_get_config("water_temp_max_alarm");
   $val = ($temp < $water_temp_min ||
      $temp > $water_temp_max) ? 1 : 0;
   db_set_status("alarm_temp", $val);

   # Store the result into the proper log table
   db_log_var("temp_log", $temp);

   $water_level = get_water_level();
   db_set_status("alarm_level", $water_level);

temp_get()函数通过读取相应的w1_slave文件来读取水温。它将此值存储在temp变量中,然后根据此新值检查一些警报。还要注意,在这种情况下,alarm_sys变量可以设置为1,以指示是否发生了 I/O 错误。

get_water_level()函数用于读取与水温传感器连接的 GPIO,其主体如下:

function get_water_level()
{
   global $gpios;

   return gpio_get($gpios["water"]) == 0 ? 1 : 0;
}

提示

请注意,如前面的代码所示,水温传感器具有反向逻辑。

现在轮到灯光的部分:

   #
   # Lamp management
   #

   # The lamp is directly managed by the force_lamp switch

   $lamp = db_get_status("force_lamp");

   # Set the new status
   set_lamp($lamp);
   db_set_status("lamp", $lamp);
   dbg("lamp %sactivated", $lamp ? "" : "de");

在前面的代码片段中,我们看到灯光根据用户输入开关,而没有任何系统的自动机制。

这对于冷却器来说并不成立。它的管理代码如下所示:

   #
   # Cooler management
   #

   # The cooler must be enabled if temp > water_temp_max in order
   # to try to reduce the temperature of the water...
   $water_temp_max = db_get_config("water_temp_max");
   $cooler = $temp > $water_temp_max ? 1 : 0;

   # We must force on?
   $force_cooler = db_get_status("force_cooler");
   $cooler = $force_cooler ? 1 : $cooler;

   # Set the new status
   set_cooler($cooler);
   db_set_status("cooler", $cooler);
   dbg("cooler %sactivated", $cooler ? "" : "de");

冷却器的状态根据temp值和water_temp_max设置来确定,最后如果force_cooler变量被设置为1,用户也可以强制开启。

对泵也适用类似的功能:

   #
   # Pump management
   #

   # The pump must be on for pump_t_on delay time and off for
      # pump_t_off delay time (if not forced of course...)
      $force_pump = db_get_status("force_pump");
      $pump = db_get_status("pump");
      $pump_interval = $pump ? db_get_config("pump_t_on") :
         db_get_config("pump_t_off");
      if ($force_pump ||
         strtotime("-$pump_time seconds") > $pump_interval) {
            $pump_time = strtotime("now");

            $pump = $force_pump ? 1 : !$pump;
         }

         # Set the new status
         set_pump($pump);
         db_set_status("pump", $pump);
         dbg("pump %sactivated", $pump ? "" : "de");

这次,开关状态是通过超时来设置的,同样,设备可以通过用户输入强制开启,即如果force_pump变量被设置为1

所有前三段代码都调用了适当的函数来打开或关闭相应的 GPIO;例如,最后一段调用了set_pump()函数来设置泵的状态。该函数的主体如下:

function set_pump($status)
{
   global $gpios;

   gpio_set($gpios["pump"], $status ? 0 : 1);
}

另外两个函数也类似。

最后的提示是关于喂食器的。这次代码如下:

   #
   # Feeder management
   #

   $force_feeder = db_get_status("force_feeder");
   $feeder_interval = db_get_config("feeder_interval");
   if ($force_feeder || (strtotime("-$feeder_time seconds") > $feeder_interval)) {
      $feeder_time = strtotime("now");

      do_feeder();
      db_set_status("force_feeder", 0);
      dbg("feeder activated");
   }

喂食器可以根据超时或用户输入来激活;但是与前面的示例不同,代码调用了do_feeder()函数来调用前面展示的feeder.sh脚本,然后它必须清除force_feeder状态变量,告知用户喂食器已被激活。do_feeder()函数的主体如下:

function do_feeder()
{
   system("feeder.sh &");
}

提示

system()函数中的字符&是为了创建一个专门的进程来执行feeder.sh脚本。

现在,是时候执行脚本了。在我的系统上,我使用了以下命令行以调试模式执行它:

root@beaglebone:~# ./aquarium_mon.php -d -f -l
aquarium_mon.php[3882]: signals traps installed
aquarium_mon.php[3882]: start main loop (loop_time=15s)
aquarium_mon.php[3882]: loop start
aquarium_mon.php[3882]: t=28.5
aquarium_mon.php[3882]: lamp deactivated
aquarium_mon.php[3882]: cooler activated
aquarium_mon.php[3882]: pump deactivated
aquarium_mon.php[3882]: feeder activated
aquarium_mon.php[3882]: loop end
...

提示

请注意,在你的系统上,可能没有安装PHP支持。在这种情况下,你可以通过使用以下命令来解决:

root@beaglebone:~# apt-get install php5 libapache2-mod-php5

每 15 秒,脚本会唤醒并执行前面所有步骤,进入新的循环,属于状态机的一部分。请注意,要使其工作,你必须按本节中介绍的方式设置所有硬件。

最终测试

为了测试原型,我开启了开发板,并在登录后,通过使用之前讨论过的命令,或者使用书中示例代码库中的chapter_03/SYSINIT.sh脚本来设置系统,方法如下:

root@beaglebone:~# ./SYSINIT.sh
done!

然后,我按照如下方式执行了aquarium_mon.php命令:

root@beaglebone:~# ./aquarium_mon.php -d -f -l

此外,我用以下命令执行了视频流传输器:

root@beaglebone:~# LD_LIBRARY_PATH=/usr/local/lib/ mjpg_streamer -i "input_uvc.so -y -f 10 -r QVGA" -o "output_http.so -w /var/www/"

然后,我将浏览器指向 BeagleBone Black 的 IP 地址上的aquarium.php文件(即 URL http://192.168.7.2/aquarium.php),游戏就完成了!

请注意,此时,我们可以尝试强制设置一些参数,或者尝试通过使用书中示例代码库中的chapter_03/my_dump.shchapter_03/my_set.sh脚本来更改一些配置变量,方法如下:

root@beaglebone:~# ./my_dump.sh config
n   v
feeder_interval   60
pump_t_off   60
pump_t_on   20
water_temp_max   27
water_temp_max_alarm   29
water_temp_min   20
water_temp_min_alarm   18
root@beaglebone:~# ./my_set.sh config water_temp_max_alarm 30.5
root@beaglebone:~# ./my_dump.sh config
n   v
feeder_interval   60
pump_t_off   60
pump_t_on   20
water_temp_max   27
water_temp_max_alarm   30.5
water_temp_min   20
water_temp_min_alarm   18

在上述设置中,我仅作为示例更改了water_temp_max_alarm的限制值,你可以根据需要在你的系统上进行所有更改。

在本章结束之前,让我展示一下这个控制面板在我的智能手机上的样子:

最终测试

提示

读者应该注意到,在温度日志中有三个尖峰,这是因为在温度读取过程中,传感器返回了一个错误。这个问题可以通过在返回错误之前重复读取两到三次来解决。

总结

在本章中,我们了解了如何将我们的 BeagleBone Black 与多个电压不同的设备接口连接,并且如何管理 1-Wire 设备和 PWM 设备。同时,我们介绍了Drinks工具包来实现一个可以在 PC、智能手机或平板上使用的网页控制面板。

在下一章中,我们将看到如何实现一个气象站,该气象站可以将收集到的数据本地存储,不仅可以在网页浏览器上以漂亮的方式显示数据,还可以将数据发送到 Google Docs 文档!

简单来说,我们要实现一个简单的物联网IoT)机器。

第四章. Google Docs 气象站

在本章中,我们将查看一个简单的气象站,它也可以作为一个物联网设备使用。这次,我们的 BeagleBone Black 将收集环境数据,并将其发送到远程数据库,以便重新处理并展示到共享环境中。

本地和远程数据都可以在我们喜欢的浏览器中查看,因为这是一个本地系统,我们将使用 wfrog 工具,作为远程系统,我们将使用 Google Docs 表格。

功能基础

在这个项目中,我们的 BeagleBone Black 将通过两个传感器收集天气数据。但这一次,我们将使用现成的气象站软件,而不是编写专门的软件,在 BeagleBone Black 板上完成工作。远程端,我们将使用著名的 Google Docs 云系统来存储数据,并将其展示给用户。

通过这种方式,我们可以用较少的努力实现一个()专业的结果!

在这种情况下,我们的工作是连接传感器,将气象站软件调整到我们的硬件上,以便读取传感器数据,然后添加适当的代码,将数据发送到 Google Docs 表格。

硬件设置

这次,硬件设置并不复杂,因为我们只需要两个 I²C 芯片就可以获取气象站的基本环境数据,而所有的复杂性都在软件设置中,因为我们需要至少 3.13 版本的内核来管理传感器,并且需要完整的软件工具链来与 Google Docs 系统进行通信!

也许这对你来说不是问题,但我的 BeagleBone Black 运行的是 3.8 版本的内核,缺少一些驱动程序。这就是为什么我决定在一个外部 microSD 上安装基于 3.13 内核版本的新发行版,这样我就不必修改默认的板载 eMMC 设置。

在任何情况下,单纯设置硬件时,我可以使用当前运行的内核,在该内核中我可以通过以下命令启用名为 I2C1 的 I²C 总线:

root@beaglebone:~# echo BB-I2C1 > /sys/devices/bone_capemgr.9/slots

如果一切正常,你应该在板上看到以下内核活动:

part_number 'BB-I2C1', version 'N/A'
slot #7: generic override
bone: Using override eeprom data at slot 7
slot #7: 'Override Board Name,00A0,Override Manuf,BB-I2C1'
slot #7: Requesting part number/version based 'BB-I2C1-00A0.dtbo
slot #7: Requesting firmware 'BB-I2C1-00A0.dtbo' for board-name 'Override Board Name', version '00A0'
slot #7: dtbo 'BB-I2C1-00A0.dtbo' loaded; converting to live tree
slot #7: #2 overlays
omap_i2c 4802a000.i2c: bus 2 rev0.11 at 100 kHz
omap_i2c 4802a000.i2c: unable to select pin group
slot #7: Applied #2 overlays.

现在应该可以使用新的设备 /dev/i2c-2

root@beaglebone:~# ls -l /dev/i2c-2
crw-rw---T 1 root i2c 89, 2 Apr 23 20:23 /dev/i2c-2

好的,现在我们可以开始将硬件添加到 BeagleBone Black,并测试与当前内核的连接。

注意

读者还可以查看本书作者所写的《BeagleBone Essentials》,由 Packt Publishing 出版,获取更多关于如何管理 BeagleBone Black 上的 I²C 总线的信息,这些总线是与传感器通信所必需的。

设置温湿度传感器

作为温湿度传感器,我决定使用下图所示的设备:

设置温湿度传感器

注意

该设备可以通过以下链接购买(或者通过网络搜索):www.cosino.io/product/humidity-sensor

该设备的数据手册可以在 dlnmh9ip6v2uc.cloudfront.net/datasheets/BreakoutBoards/HTU21D.pdf 找到。

该设备非常简单。I²C 连接如下:

引脚 温湿度传感器引脚
P9.4 - Vcc +
P8.17 - CLK CL
P8.18 - SDA DA
P9.2 - GND -

提示

如果您想进一步了解 I²C 总线的工作原理,可以从维基百科的文章 en.wikipedia.org/wiki/I%C2%B2C 开始阅读。

现在,为了验证连接,我们可以使用 i2cdetect 命令,如下所示:

root@arm:~# i2cdetect -y -r 2
 0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: UU -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- 

提示

请注意,即使系统中的 I²C 总线被命名为 I2C1,它也必须使用 2 作为 ID 编号进行寻址!

0x40 地址处的字符串 UU(或 40)表示设备已连接!然而,由于某些硬件问题,您可能根本无法获取 UU 字符串。在这种情况下,我们可以使用 i2cget 命令,如下所示,强制设备进行 I²C 活动:

root@beaglebone:~# i2cget -y 2 0x40 0xe7 0x02

好的,设备已连接。但如果您收到以下输出,必须重新检查连接:

root@beaglebone:~# i2cget -y 2 0x40 0xe7
Error: Read failed

提示

请注意,您可能需要通过清除传感器上的焊接跳线来禁用板载上拉电阻。实际上,BeagleBone Black 的 I²C 控制器具有 I²C 总线规范所需的内部上拉电阻,在某些情况下,传感器板上的上拉电阻可能会干扰它。

设置气压传感器

作为气压传感器,我决定使用下图所示的设备:

设置气压传感器

注意

该设备可以通过以下链接购买(或通过上网搜索):www.cosino.io/product/barometric_sensor

该设备的数据手册可以在 www.epcos.com/inf/57/ds/T5400.pdf 找到,另附有有用的应用说明:www.epcos.com/inf/57/ds/T5400.pdf

该设备有两个接口:I²C 和 SPI。然而,由于之前的设备是 I²C 接口,我决定使用相同的接口。因此,连接必须按照以下表格中的方式进行,其他引脚不连接:

引脚 气压传感器引脚
P9.4 - Vcc VCC
P8.17 - CLK SCL/SCLK
P8.18 - SDA SDA/MOSI
P9.2 - GND GND

提示

请注意,我们将把两个设备连接到同一 I²C 总线。暂时,您可以断开之前的传感器,然后连接这个传感器。但请记住,在最终配置中,所有传感器设备都将连接到同一总线。

现在,为了验证连接,我们可以使用 i2cdetect 命令,如下所示:

root@arm:~# i2cdetect -y -r 2
 0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77 

如前面的命令所示,0x77 地址处的字符串 77(或 UU)表示设备已连接!这时,设备应该能顺利被检测到。所以,如果你没有看到前面的输出,请考虑重新检查硬件连接。

最终图片

以下截图展示了我实现该项目并测试软件的原型。如你所见,这次的连接非常复杂。

最终图片

设置软件

现在,是时候全力以赴了!我们必须安装一个带有特定补丁的新内核,以添加所需的驱动程序。然后,我们需要设置我们的 Google 账户,以启用 Google Docs API 来管理云端的电子表格。最后,我们必须安装并正确配置我们选择的天气站软件,用于收集天气数据。

安装新内核

要安装新内核,我们必须使用主机 PC,通过以下命令下载源代码:

$ git clone git://github.com/RobertCNelson/bb-kernel.git

完成后,我们必须进入 bb-kernel 目录,然后检出内核版本 3.13:

$ git checkout am33x-v3.13

现在,我们应该通过从示例文件生成一个名为 system.sh 的配置文件来配置编译套件,操作如下:

$ cp system.sh.sample system.sh

在我的系统上,我使用以下设置修改了新创建的 system.sh 文件:

CC=/usr/bin/arm-linux-gnueabihf-
MMC=/dev/sdd

MMC 变量由安装工具(名为 install_kernel.sh)使用,并指向安装了 BeagleBone 系统的 microSD 所对应的设备。

提示

警告!你必须小心并确保 MMC 定义设置正确,否则主机可能会受到损坏。

现在,我们必须将补丁应用到书中的示例代码库中的 chapter_04/0001-Add-support-for-I2C1-bus-and-the-connected-devices.patch 文件,以启用名为 I2C1 的 I²C 总线以及前述传感器的驱动程序,并为气压传感器添加缺失的驱动程序。命令如下:

$ git am --whitespace=nowarn  0001-Add-support-for-I2C1-bus-and-the-connected-devices.patch

提示

请注意,--whitespace=nowarn 命令行选项是必要的,以防你的 git 系统配置为自动修复空格错误,而在这种情况下这是错误的。

如果一切正常,以下命令应该显示以下文本:

$ git log -1
commit 50949bd3a5c53d915dfdce8f790e3cfdd9ae702a
Author:     Rodolfo Giometti <giometti@hce-engineering.com>
AuthorDate: Wed Jun 24 21:58:50 2015 +0200
Commit:     Rodolfo Giometti <giometti@hce-engineering.com>
CommitDate: Wed Jun 24 22:06:06 2015 +0200

 Add support for I2C1 bus and the connected devices

 Signed-off-by: Rodolfo Giometti <giometti@hce-engineering.com>

在开始内核编译之前,让我简单介绍一下这个补丁。它仅添加了以下两个补丁:

$ ls patches/bbb-habp/
0001-iio-Add-t5403-barometric-pressure-sensor-driver.patch
0100-arm-am335x-bone-common.dtsi-enable-bus-I2C1-on-pins-.patch

第一个补丁是添加气压传感器的驱动程序,第二个补丁是启用标记为 I2C1 的 I²C 总线并定义已连接的设备。特别地,第二个补丁完成了以下代码片段中的步骤:

diff --git a/arch/arm/boot/dts/am335x-bone-common.dtsi b/arch/arm/boot/dts/am335x-bone-common.dtsi
index 5270d18..ba891ce 100644
--- a/arch/arm/boot/dts/am335x-bone-common.dtsi
+++ b/arch/arm/boot/dts/am335x-bone-common.dtsi
@@ -84,6 +84,13 @@
 >;
 };

+   i2c1_pins: pinmux_i2c1_pins {
+      pinctrl-single,pins = <
+         0x158 (PIN_INPUT_PULLUP | MUX_MODE2)    /* i2c1_sda.i2c1_sda */
+         0x15c (PIN_INPUT_PULLUP | MUX_MODE2)    /* i2c1_scl.i2c1_scl */
+      >;
+   };
+
 i2c2_pins: pinmux_i2c2_pins {
 pinctrl-single,pins = <
 0x178 0x73 /* (SLEWCTRL_SLOW | PIN_INPUT_PULLUP | MUX_MODE3) uart1_ctsn.i2c2_sda */
@@ -295,6 +302,24 @@
 };
 };

+&i2c1 {
+   pinctrl-names = "default";
+   pinctrl-0 = <&i2c1_pins>;
+
+   status = "okay";
+   clock-frequency = <400000>;
+
+   htu21: htu21@40 {
+      compatible = "htu21";
+      reg = <0x40>;
+   };
+
+   t5403: t5403@77 {
+      compatible = "t5403";
+      reg = <0x77>;
+   };
+};

首先,前面的代码通过选择合适的 pinmux 设置来定义 i2c1_pins 组,然后它启用 I2C1 总线,设置正确的总线频率,并为附加的传感器设备定义了合适的驱动程序。

然后,补丁添加了启用其应用所需的代码,如下所示:

$ git whatchanged -p -1 patch.sh 
...
diff --git a/patch.sh b/patch.sh
index 83787f7..ed3a886 100644
--- a/patch.sh
+++ b/patch.sh
@@ -191,6 +191,12 @@ saucy () {
 ${git} "${DIR}/patches/saucy/0003-saucy-disable-stack-protector.patch"
 }

+bbb_habp () {
+       echo "dir: bbb-habp"
+       ${git} "${DIR}/patches/bbb-habp/0001-iio-Add-t5403-barometric-pressure-s
+       ${git} "${DIR}/patches/bbb-habp/0100-arm-am335x-bone-common.dtsi-enable-
+}
+
 ###
 #arm
 deassert_hard_reset
@@ -211,4 +217,6 @@ boards

 saucy

+bbb_habp
+
 echo "patch.sh ran successful"

此外,作为最后一步,它将新添加的驱动程序启用到默认内核的配置中:

$ git whatchanged -p -1 patches/defconfig
commit b9b954d37ed2722f7e85e9192d697bb79544ca78
Author:     Rodolfo Giometti <giometti@linux.it>
AuthorDate: Wed Jun 24 21:58:50 2015 +0200
Commit:     Rodolfo Giometti <giometti@linux.it>
CommitDate: Wed Jun 24 22:31:32 2015 +0200

 Add support for I2C1 bus and the connected devices

 Signed-off-by: Rodolfo Giometti <giometti@hce-engineering.com>

diff --git a/patches/defconfig b/patches/defconfig
index 7be0172..464301d 100644
--- a/patches/defconfig
+++ b/patches/defconfig
@@ -4529,6 +4529,7 @@ CONFIG_IIO_SYSFS_TRIGGER=m
 CONFIG_IIO_ST_PRESS=m
 CONFIG_IIO_ST_PRESS_I2C=m
 CONFIG_IIO_ST_PRESS_SPI=m
+CONFIG_T5403=m

 #
 # Temperature sensors

好的,所有修改都已说明,现在我们可以使用以下命令开始编译内核:

$ ./build_kernel.sh

提示

这一步以及后续步骤需要较长时间,并且需要耐心,因此您应该拿一杯自己喜欢的茶或咖啡,耐心等待。

一段时间后,程序将显示标准的内核配置面板,我们应该验证所需的驱动程序是否已启用。您应该导航至设备驱动程序 | 硬件监控支持,并在Measurement Specialties HTU21D 湿度/温度传感器项中选择模块();在设备驱动程序 | 工业 I/O 支持 | 压力传感器中,应该选择EPCOS T5403 数字气压传感器驱动程序作为模块。

然后,退出菜单,内核编译将开始。当编译结束时,新的内核映像已准备好,应该会出现以下信息:

-----------------------------
Script Complete
eewiki.net: [user@localhost:~$ export kernel_version=3.13.10-bone12]
-----------------------------

提示

请注意,当执行build_kernel.sh文件时,可能会出现以下错误信息:

$ ./build_kernel.sh
+ Detected build host [Ubuntu 14.04.3 LTS]
+ host: [x86_64]
+ git HEAD commit: [b00737d02a5b3567169a6c87311fec76a694fea6]
Debian/Ubuntu/Mint: missing dependencies, please install:
-----------------------------
sudo apt-get update
sudo apt-get install device-tree-compiler lzma lzop u-boot-tools libncurses5:i386 libstdc++6:i386
-----------------------------
* Failed dependency check

在这种情况下,您可以通过执行前面两个建议的apt-get(或aptitude)命令来解决问题。

现在,我们可以使用安装工具将其安装到 microSD 上:

$ ./tools/install_kernel.sh

在更新内核之前,工具会询问用户是否确定选择要安装内核的设备。例如,在我的系统中,我得到以下输出:

I see...
fdisk -l:
Disk /dev/sda: 500.1 GB, 500107862016 bytes
...
sdd       8:48   1   3.7G  0 disk
|-sdd1    8:49   1    12M  0 part  /media/giometti/BOOT
`-sdd2    8:50   1   3.7G  0 part  /media/giometti/rootfs
-----------------------------
Are you 100% sure, on selecting [/dev/sdd] (y/n)?

我的MMC变量设置为/dev/sdd;所以,如果我仔细查看相应的行,就可以验证这些是 BeagleBone Black 文件系统的正确名称。这样,我可以放心地输入y字符,选择“是”。

提示

请注意,microSD 应为class 10,并且至少为 4GB 大小。

在命令执行结束时,我们应该得到如下输出:

This script has finished...
For verification, always test this media with your end device...

现在,只需从主机上取下 microSD,并将其放入 BeagleBone Black 中。按住用户按钮开机,强制从 microSD 启动。如果一切正常,我们可以在正常登录后使用以下命令验证新的内核是否正在运行:

# uname -a
Linux arm 3.13.10-bone9 #1 SMP Fri Nov 7 23:25:59 CET 2014 armv7l GNU/ Linux

注意

读者还可以参考这本书《BeagleBone Essentials》,Packt Publishing出版,由本书的作者撰写,以获取更多关于如何在外部 microSD 上安装更新内核的信息。

好的,新内核已经准备好!现在我们可以验证所需的驱动程序是否也正确加载:

root@arm:~# lsmod | egrep '(t5403|htu21)'
t5403                   3072  0
htu21                   2385  0
industrialio           46516  3 t5403,ti_am335x_adc,kfifo_buf

新设备现在可以通过sysfs接口进行访问。要获取当前的气压值,可以使用以下命令:

root@arm:~# cat /sys/bus/iio/devices/iio\:device1/in_pressure_input
101.926000

数据以千帕kPa)为单位给出。

可以使用以下命令访问温湿度传感器:

root@arm:~# cat /sys/class/hwmon/hwmon0/device/humidity1_input
42988
root@arm:~# cat /sys/class/hwmon/hwmon0/device/temp1_input
27882

湿度以相对湿度百分比(m%RH)给出,温度以千分之一摄氏度(m°C)给出,因此我们需要将这两个数值分别除以 1000,以得到相对湿度百分比(%RH)和摄氏度(°C)。

运行气象站软件

现在是设置我们的气象站的时候了。为了做到这一点,正如之前所说,我们决定使用一个现成的项目,而不是编写一个新的。这是因为现有很多非常优秀的气象站软件,我们可以使用它们来以更好的方式在本地显示收集到的数据。

这个软件就是 wfrog 项目。

注意

项目的主页在 code.google.com/p/wfrog/

要安装它,我们可以通过以下命令从在线仓库获取源代码:

root@arm:~# svn checkout http://wfrog.googlecode.com/svn/trunk/ wfrog-read-only

提示

svn 命令位于 subversion 包中,可以通过以下命令安装:

root@arm:~# aptitude install subversion

请注意,程序的压缩档案可以在书本示例代码库中的 chapter_04/wfrog/wfrog-read-only.tgz 文件中找到。

下载后,我们应该进入新创建的目录 wfrog-read-only,并通过以下命令生成 debian 包:

root@arm:~# cd wfrog-read-only/
root@arm:~/wfrog-read-only# ./debian/rules binary

提示

请注意,您的系统可能缺少某些必要的软件包,以便能够生成新的 debian 包。通过以下命令,您应该可以安装所需的一切,以完成这项工作:

root@arm:~# aptitude install debhelper

如果一切顺利,rules 命令应该显示以下信息:

dpkg-deb: building package `wfrog' in `../wfrog_0.8.2-1_all.deb'.

然后,为了安装新的软件包,我们可以使用 gdebi 命令,它将为我们下载所有包的依赖项,如下所示:

root@arm:~/wfrog-read-only# gdebi ../wfrog_0.8.2-1_all.deb
Reading package lists... Done
Building dependency tree 
Reading state information... Done
Building data structures... Done
Building data structures... Done

Requires the installation of the following packages:
libxslt1.1  libyaml-0-2  python-cheetah  python-lxml  python-pygooglechart  python-serial  python-support  python-usb  python-yaml  python2.6  python2.6-minimal
Web-based customizable weather station software
 wfrog is a software for logging weather station data and statistics,
 viewing them graphically on the web and sending them to a remote FTP site.
 The layout and behaviour is fully customizable through an advanced configuration system.
 It is written in python with an extensible architecture allowing new station drivers to be written very easily.
 wfrog supports many weather stations and is compliant with the WESTEP protocol.
 Supported stations:
 * Ambient Weather WS1080
 * Davis VantagePro, VantagePro2
 * Elecsa AstroTouch 6975
 * Fine Offset Electronics WH1080, WH1081, WH1090, WH1091, WH2080, WH2081
 * Freetec PX1117
 * LaCrosse 2300 series
 * Oregon Scientific WMR100N, WMR200, WMRS200, WMR928X
 * PCE FWS20
 * Scientific Sales Pro Touch Screen Weather Station
 * Topcom National Geographic 265NE
 * Watson W8681
Do you want to install the software package? [y/N]:
...
Selecting previously unselected package wfrog.
(Reading database ... 40720 files and directories currently installed.)
Unpacking wfrog (from ../wfrog_0.8.2-1_all.deb) ...
Setting up wfrog (0.8.2-1) …

提示

可以通过以下命令安装 gdebi 命令:

root@arm:~# aptitude install gdebi

好的,现在软件已经安装完毕,但我们仍然需要完成一些步骤才能继续。第一个步骤是通过一个特殊的模拟器配置系统,以验证网页界面和数据采集系统是否正常工作。为此,我们应按如下方式执行 wfrog 命令:

root@arm:~# wfrog -S

提示

请注意,如果在软件第一次运行时遇到如下所示的错误,您可能需要以下补丁来修复问题:

Traceback (most recent call last):
 File "/usr/bin/wfrog", line 132, in <module>
 settings = wflogger.setup.SetupClient().setup_settings(SETTINGS_DEF, settings, settings_file)
 File "/usr/lib/wfrog/wflogger/setup.py", line 40, in setup_settings
 if source == None:
UnboundLocalError: local variable 'source' referenced before assignment

补丁如下:

root@arm:~/wfrog-read-only# svn diff
Index: wflogger/setup.py
===================================================================
--- wflogger/setup.py   (revision 973)
+++ wflogger/setup.py   (working copy)
@@ -35,6 +35,7 @@
 self.logger.debug('Current settings file: '+str(source_file))
 self.logger.debug('New settings file:'+target_file)
 defs = yaml.load( file(settings_def_file, 'r') )
+        source = None
 if source_file is not None:
 source = yaml.load( file(source_file, 'r') )
 if source == None:

然后,您只需要重新构建软件包。

上述补丁可以在书本示例代码库中的 chapter_04/wfrog/0001-fix-setup.diff 文件中找到。

现在,您应该仔细回答所有问题,并在系统要求您输入气象站模型的驱动程序时选择 1) random-simulator 选项,如下所示:

Please enter the driver for your station model:
 1) random-simulator - Station Simulator
 2) vantagepro2 - Davis VantagePro
 3) wh1080 - Fine Offset WH1080 and compatibles
 4) wh3080 - Fine Offset WH3080 and compatibles
 5) wmr200 - Oregon Scientific WMR200
 6) wmr928nx - Oregon Scientific WMR928NX
 7) wmrs200 - Oregon Scientific WMRS200
 8) ws2300 - LaCrosse WS2300
 9) ws28xx - LaCrosse WS28xx
> 1

配置完成后,您可以通过执行以下两个命令启动气象站系统:

root@beaglebone:~# /etc/init.d/wflogger start
root@beaglebone:~# /etc/init.d/wfrender start

然后,网页界面可以通过 BeagleBone Black 的 IP 地址(通常是 192.168.7.2)的 7680 端口访问,如下所示的截图所示:

运行气象站软件

好的,它已经工作了;但是系统现在使用的是模拟器,我们希望它使用我们刚刚安装的气象传感器的数据!因此,我们必须向新的气象站添加一个新驱动程序。为此,我们必须修改刚下载的源代码。

wfrog-read-only/wfdriver/station/目录下,我们需要添加一个名为bbb_habp.py的新文件。以下显示了它的代码的第一部分,其中定义了一个与我们新站点相关的新类:

import time
import logging
from wfcommon import units

class BBBhabpStation(object):

    '''
    Station driver for BeagleBone Black Home Automation Blueprints.

    [Properties]

    period [numeric] (optional):
        Polling interval in seconds. Defaults to 60\.    
    '''

    period=60

    logger = logging.getLogger('station.bbb_habp')

    name = 'BeagleBone Home Automation Blueprints weather station'

然后,代码定义了读取环境数据的函数:get_press()函数读取压力,get_temp()函数读取温度,get_hum()函数读取湿度,如下所示:

   def get_press(self):
    f = open("/sys/bus/iio/devices/iio:device1/in_pressure_input", "r")
    v = f.read()
    f.close()

        return float(v) * 10.0

   def get_temp(self):
    f = open("/sys/class/hwmon/hwmon0/device/temp1_input", "r")
    v = f.read()
    f.close()

        return int(v) / 1000.0

   def get_hum(self):
    f = open("/sys/class/hwmon/hwmon0/device/humidity1_input", "r")
    v = f.read()
    f.close()

        return int(v) / 1000.0

完成此操作后,代码定义了核心功能,该功能通过调用generate_event()函数并传入适当的参数来生成所有天气事件。刚生成的事件存储在e变量中,我们只需填写其字段并通过send_event()函数将数据发送到气象站,如下所示的代码片段:

    def run(self, generate_event, send_event, context={}):
        while True:
            try:
                e = generate_event('press')
                e.value = self.get_press()
                send_event(e)
                self.logger.debug("press=%fhPa" % e.value)

            except Exception, e:
                self.logger.error(e)

            try:
                e = generate_event('temp')
                e.sensor = 0
                e.value = self.get_temp()
                send_event(e)
                self.logger.debug("temp=%fC" % e.value)

            except Exception, e:
                self.logger.error(e)

            try:
                e = generate_event('hum')
                e.sensor = 0
                e.value = self.get_hum()
                send_event(e)
                self.logger.debug("hum=%f%%RH" % e.value)

            except Exception, e:
                self.logger.error(e)

            try:
                e = generate_event('temp')
                e.sensor = 1
                e.value = self.get_temp()
                send_event(e)
                self.logger.debug("temp=%fC" % e.value)

            except Exception, e:
                self.logger.error(e)

            try:
                e = generate_event('hum')
                e.sensor = 1
                e.value = self.get_hum()
                send_event(e)
                self.logger.debug("hum=%f%%RH" % e.value)

            except Exception, e:
                self.logger.error(e)

最后一行用于安排下一个周期:

            # pause until next update time
            next_update = self.period - (time.time() % self.period)
            time.sleep(next_update)                

注意

前面的代码在书中示例代码库的chapter_04/wfrog/bbb_habp.py文件中报告。

现在,为了完成任务,我们需要按照以下方式修补wfrog-read-only/wfdriver/station/__init__.py文件:

root@arm:~/wfrog-read-only# svn diff wfdriver/station/__init__.py
Index: wfdriver/station/__init__.py
===================================================================
--- wfdriver/station/__init__.py   (revision 973)
+++ wfdriver/station/__init__.py   (working copy)
@@ -19,6 +19,7 @@
 import yaml

 import simulator
+import bbb_habp
 import wmrs200
 import wmr928nx
 import wmr200
@@ -66,6 +67,10 @@
 yaml_tag = u'!ws28xx'
 auto.stations.append(ws28xx)

+class YamlWS28xxStation(bbb_habp.BBBhabpStation, yaml.YAMLObject):
+    yaml_tag = u'!bbb_habp'
+auto.stations.append(bbb_habp)
+
 class YamlRandomSimulator(simulator.RandomSimulator, yaml.YAMLObject):
 yaml_tag = u'!random-simulator'
 auto.stations.append(simulator)

通过这种方式,我们告诉wfrog系统已经添加了一个新站点。

注意

补丁保存在书中示例代码库的chapter_04/wfrog/0002-add-bbb_habp-station.diff文件中。

在进行所有前面的修改后,我们必须按照刚才展示的方式重新生成软件包,并通过重新运行配置程序并选择新的驱动程序来重新配置它,具体如下:

root@arm:~# wfrog -S
This is the setup of wfrog 0.8.2-svn user settings that will be written in /etc/wfrog/settings.yaml

Please enter the driver for your station model:
 1) bbb_habp - BeagleBone Home Automation Blueprints weather station
 2) random-simulator - Station Simulator
 3) vantagepro2 - Davis VantagePro
 4) wh1080 - Fine Offset WH1080 and compatibles
 5) wh3080 - Fine Offset WH3080 and compatibles
 6) wmr200 - Oregon Scientific WMR200
 7) wmr928nx - Oregon Scientific WMR928NX
 8) wmrs200 - Oregon Scientific WMRS200
 9) ws2300 - LaCrosse WS2300
[random-simulator] > 1

提示

让我提醒你,为了重新生成软件包,你必须执行以下两个命令:

root@arm:~/wfrog-read-only# ./debian/rules binary
root@arm:~/wfrog-read-only# gdebi ../wfrog_0.8.2-1_all.deb

请注意,这一次,添加了一个名为bbb_habp的新条目,因此只需选择它并根据需要重新配置系统。

当所有修改完成后,我们必须停止正在运行的wfrog任务:

root@arm:~# /etc/init.d/wflogger stop
[ ok ] Stopping wfrog logger - Weather Station Software : wfrog.
root@arm:~# /etc/init.d/wfrender stop
[ ok ] Stopping wfrog renderer - Weather Station Software : wfrender.

然后,我们可以安全地使用以下命令清除wfrog保存天气数据的文件:

root@arm:~# rm /var/lib/wfrog/wfrog-current.xml /var/lib/wfrog/wfrog.csv

然后,我们可以按照以下方式重新启动wfrog任务:

[....] Starting wfrog logger - Weather Station Software : wfrogStarting /usr/lib/wfrog/bin/wfrog...
Detaching to start /usr/lib/wfrog/bin/wfrog...done.
. ok
root@arm:~/chapter_04# /etc/init.d/wfrender start
[ ok ] Starting wfrog renderer - Weather Station Software : wfrender.

现在,如果你控制/var/lib/wfrog目录中的文件,你应该会看到它们将会被传感器的新数据重新填充。

添加 Google Docs API

我们的气象站现在已经完全正常工作,但正如本章一开始所述,我们还希望更进一步——我们希望一个能够通过网络将数据保存到云系统的气象站。为了做到这一点,我们决定使用 Google Docs 表格。

这个想法是获取一个包含当前天气数据和保存的历史数据的工作表,所有数据都会定期更新。让我们看看如何做到这一点。

执行此任务的 API 由gspread工具实现,我们可以通过以下命令将其安装到我们的 BeagleBone Black 上:

root@arm:~# aptitude install python-pip python2.7-dev libffi-dev
root@arm:~# pip install --upgrade cffi cryptography PyOpenSSL oauth2client gspread

注意

项目的主页地址是 github.com/burnash/gspread

在安装所有前述包之后,我们需要准备一个 Google 账户。在这个项目中,我使用了自己的账户,所以我只是访问了我的 Google Docs 页面。然后,我创建了一个名为bbb_weather的新电子表格。

注意

请参阅 Google Docs 文档 support.google.com/docs,了解有关 Google Docs 使用的更多信息。

一旦创建,我们必须以一种可以从远程计算机访问的方式发布电子表格。为此,我们必须按照gspread.readthedocs.org/en/latest/oauth2.html中的说明进行操作,那里解释了 OAuth2 授权系统。以下是直接来自该页面的所需步骤的小列表:

  1. 转到 Google 开发者控制台 (console.developers.google.com/project) 并创建一个新项目(或选择您已有的项目)。

  2. 在 API 中的API & auth下,启用Drive API

  3. 转到凭证,然后点击创建新的客户端 ID

  4. 选择服务账户。点击创建客户端 ID将生成一对新的公钥和私钥。您将自动下载一个 JSON 文件,里面包含以下数据:

    {
        "private_key_id": "2cd ... ba4",
        "private_key": "-----BEGIN PRIVATE KEY-----\nNrDyLw ...
                           jINQh/9\n-----END PRIVATE KEY-----\n",
        "client_email": "473 ... hd@developer.gserviceaccount.com",
        "client_id": "473 ... hd.apps.googleusercontent.com",
        "type": "service_account"
    }
    
  5. 转到 Google Sheets,并与您在json_key['client_email']中拥有的电子邮件共享电子表格。否则,当您尝试打开它时,您将遇到SpreadsheetNotFound异常。

    注意

    在接下来的代码示例中,我的 Google 凭据存储在Project-9a372e9e20e6.json文件中,出于安全原因,该文件没有在书籍的示例代码库中提供。

现在,为了测试一切是否正确设置,您可以使用以下命令在新创建的电子表格中创建一个空白工作表:

root@arm:~# ./create_new.py

注意

您可以在书籍示例代码库中的chapter_04/wfrog/create_new.py文件中找到前面的文件。

如果没有错误,您应该能找到一个名为BBB weather的新工作表,如下图所示:

添加 Google Docs API

提示

请注意,在执行前述命令时,您可能会遇到以下错误:

ImportError: No module named httplib2

在这种情况下,您可以通过以下命令解决安装缺失的python-httplib2包的问题:

root@arm:~# aptitude install python-httplib2

或者,您可能会遇到以下错误:

oauth2client.client.CryptoUnavailableError: No crypto library available

在这种情况下,解决方法是使用以下命令安装缺失的python-crypto包:

root@arm:~# aptitude install python-crypto

create_new.py文件中的代码非常简单,如下所示:

import gspread
import json
from oauth2client.client import SignedJwtAssertionCredentials

# Load the credentials
json_key = json.load(open('Project-9a372e9e20e6.json'))
scope = ['https://spreadsheets.google.com/feeds']
credentials = SignedJwtAssertionCredentials(json_key['client_email'], json_key['private_key'], scope)

# Ask for authorization
gc = gspread.authorize(credentials)

# Open the "bbb_weather" spreadsheet
sh = gc.open("bbb_weather")

# Add a new worksheet named "BBB weather" with size of 7x4 cells
wks = sh.add_worksheet(title="BBB weather", rows="7", cols="4")

# Setup the "current status" part
wks.update_acell('A1', 'Current status')

wks.update_acell('A2', 'Time (D h)')
wks.update_acell('B2', 'Pressure (hPa)')
wks.update_acell('C2', 'Temperature (C)')
wks.update_acell('D2', 'Humidity (%)')

# Setup the "old statuses" part
wks.update_acell('A5', 'Old statuses')

wks.update_acell('A6', 'Time (D h)')
wks.update_acell('B6', 'Pressure (hPa)')
wks.update_acell('C6', 'Temperature (C)')
wks.update_acell('D6', 'Humidity (%)')

wks.update_acell('A7', 'LAST')

首先,您需要注意,必须向 json.load() 函数提供您自己的凭证(即刚刚获得的 Project-xxxxxxxxxxxx.json 文件)。然后,注意在打开 bbb_weather 电子表格后,我们通过使用 gc.open() 函数简单地添加了一个名为 sh.add_worksheet() 的新工作表。接着,我们使用 wks.update_acell() 方法设置单元格内容。

现在,读者可能会好奇为什么 A7 单元格包含 LAST 字符串。耐心点,这将在稍后解释!

好的,现在我们需要一种方法将收集到的数据发送到我们的新工作表。如前所述,wfrog 程序将其数据存储在两个文件中:wfrog-current.xml 存储当前值,wfrog.csv 存储历史数据。为此,我们可以使用存储在书籍示例代码库中的 chapter_04/send_data.py 文件中的代码。以下是一些相关的代码片段。该文件的第一部分与 create_new.py 命令相同,因此我们可以跳过开头部分,直到打开 bbb_weather 电子表格为止:

# Open the "bbb_weather" spreadsheet
sh = gc.open("bbb_weather")

# Select the worksheet named "BBB weather"
wks = sh.worksheet("BBB weather")
Then we can parse the XML file and extract the current status data to be send over the network:
#
# Send data to Google Docs
#

# Parse the XML file holding the current weather status
xmldoc = minidom.parse('/var/lib/wfrog/wfrog-current.xml')

# Extract the data
time_obj = xmldoc.getElementsByTagName('time')
time = time_obj[0].firstChild.nodeValue
press_obj = xmldoc.getElementsByTagName('pressure')
press = float(press_obj[0].firstChild.nodeValue)
temp_obj = xmldoc.getElementsByTagName('temp')
temp = float(temp_obj[0].firstChild.nodeValue)
hum_obj = xmldoc.getElementsByTagName('humidity')
hum = float(hum_obj[0].firstChild.nodeValue)
print "current: %s press=%f temp=%f hum=%f" % (time, press, temp, hum)

一旦提取,这些数据可以发送到相应的单元格:

# Update the current status
wks.update_acell('A3', time)
wks.update_acell('B3', press)
wks.update_acell('C3', temp)
wks.update_acell('D3', hum)

现在是保存历史数据的时候了。这时,LAST 字符串帮助我们解决了问题!首先,我们需要解析 .csv 文件:

# Parse the CSV file holding the old weather statuses
csvfile = open('/var/lib/wfrog/wfrog.csv', 'rb')
reader = csv.reader(csvfile, delimiter=',')

# Skip the headers
headers = reader.next()

然后,我们使用 wks.find("LAST").row 方法请求包含 LAST 字符串的行号。由于 LAST 字符串最初位于第 7 行,我们可以找到在 wfrog.csv 文件中跳过多少行,从而找到要存储的新数据,如下所示:

# Find the "LAST" string where to insert data to
last = wks.find("LAST").row - 7
print "last saved row was %d" % last

# Skip already read row
for i in range(0, last):
   dummy = reader.next()

现在,我们可以通过使用 wks.insert_row(data, n) 函数将数据提取并保存到工作表中,将它们存储在 第 n 行

# Start saving not yet saved data
for row in reader:
   time = row[1]
   press = float(row[11])
   temp = float(row[2])
   hum = float(row[3])
   print "old: %s press=%f temp=%f hum=%f" % (time, press, temp, hum)

   # Add a new line with an old status
   wks.insert_row([time, press, temp, hum], 7 + last)
   last += 1

要测试代码,我们可以执行以下 send_data.py 命令:

root@arm:~# ./send_data.py
current: 2015-06-27 13:56:00 press=1026.354367 temp=29.083000 hum=44.537000
last saved row was 0
old: 2015-06-27 12:24:32 press=1026.700000 temp=29.200000 hum=49.600000
old: 2015-06-27 12:35:00 press=1026.700000 temp=29.500000 hum=50.100000
old: 2015-06-27 12:45:00 press=1026.600000 temp=29.500000 hum=48.800000
old: 2015-06-27 12:55:00 press=1026.700000 temp=29.400000 hum=48.400000
old: 2015-06-27 13:05:00 press=1026.700000 temp=29.300000 hum=47.500000
old: 2015-06-27 13:15:00 press=1026.600000 temp=29.200000 hum=48.100000
old: 2015-06-27 13:25:00 press=1026.500000 temp=29.100000 hum=45.900000
old: 2015-06-27 13:35:00 press=1026.500000 temp=28.700000 hum=47.100000

提示

请注意,在前面的文件中,您必须根据您的 JSON 文件名修改以下行!

json_key = json.load(open('Project-9a372e9e20e6.json'))

程序正确地检测到之前没有保存历史数据,并从头开始保存新数据。我的工作表现在看起来像下面的截图所示:

添加 Google Docs API

现在,LAST 字符串指向第 15 行,因此如果我们等待新数据并重新运行命令,我们会得到如下结果:

root@arm:~# ./send_data.py
current: 2015-06-27 13:51:00 press=1026.334273 temp=29.276000 hum=46.871000
last saved row was 8
old: 2015-06-27 13:46:00 press=1026.500000 temp=28.900000 hum=48.200000

如下截图所示,我们的程序已经将新数据保存在了正确的位置:

添加 Google Docs API

为了自动化这些步骤,我们可以使用 cron 守护进程,按照预定的延迟安排执行 send_data.py 程序。

最终测试

这次,最终测试比其他项目需要更多的时间,因为我们必须收集多项数据才能得到合适的图形。因此,我们按照前面的截图所示执行 wfrog 任务。然后,我们让它们运行两天或更长时间。对于我的测试,结果如下两个截图所示:

最终测试最终测试

然后,我们可以通过查看我们的 Google 账户来验证前面的天气数据是否已存储在 Google Docs 表格中。以下截图显示了我的测试结果:

最终测试

概要

在本章中,我们了解了如何通过将现成的天气站软件安装到我们的 BeagleBone Black 上,适配硬件传感器,并安装带有适当驱动程序的新内核版本。接着,我们查看了如何将数据存储到 Google Docs 表格中,以便日后处理。

在下一章,我们将继续与远程系统进行交互,以便管理洗衣监控系统。我们将使用 WhatsApp 系统来了解洗衣机何时完成工作。

第五章. WhatsApp 洗衣房监控器

在本章中,我们将学习如何实现一个洗衣房监控系统,使用多个传感器,当特定事件发生时,能够通过 WhatsApp 账号直接提醒用户。

我们将学习如何将声音传感器和光线传感器连接到 BeagleBone Black,并通过它们监控我们的洗衣机。此外,我们还将看到如何通过使用 WhatsApp 账号直接与用户的智能手机进行交互,以便在某些事件发生时通知他们。

基本工作原理

假设我们的洗衣房配备了一台洗衣机和一盏灯,用户在取衣服时会用到这盏灯。在这种情况下,BeagleBone Black 可以配备一些特殊传感器,用于检测洗衣机何时开始或完成工作,以及何时有人进入洗衣房取走洗好的衣物。

在这种情况下,BeagleBone Black 应该能够检测到用户何时启动洗衣机,并在工作完成后等待。此时,系统可以生成一条 WhatsApp 消息,提醒用户取走衣服。当用户进入房间时,灯会亮起,当他们离开房间时,灯会熄灭。通过这种方式,我们的 BeagleBone Black 可以检测到用户是否已完成任务,并重新启动循环。

设置硬件

如前所述,在本项目中,我们需要两种不同类型的传感器:一种用于检测洗衣机的启动/停止,另一种用于检测是否有人进出洗衣房。前者的任务可以通过使用声音传感器来实现,即能够测量环境声音水平的设备;而后者的任务可以通过使用光线传感器来实现,即能够测量环境光线的设备。我们可以将这两种信号与阈值进行比较,以便检测相关的事件。

当洗衣机运行时,我们应当测得较高的声音水平,且持续一段较长的时间;而当洗衣机未运行时,环境声音应接近零并持续一段较长的时间。另一方面,我们可以假设,负责取衣服的人需要在洗衣房内打开灯,而当房间里没有人的时候,灯通常是关闭的。

为了帮助用户理解系统内部发生了什么,我们可以添加两颗 LED 灯,它们可以被打开/关闭或设置为闪烁模式,并赋予特定的含义(在下一节中,我将详细解释这些含义)。

设置声音传感器

本项目中用于检测环境声音的设备如下图所示:

设置声音传感器

注意

这些设备可以通过以下链接购买(或在网上搜索):www.cosino.io/product/sound-detector

该板基于放大器 LMV324,数据手册可在dlnmh9ip6v2uc.cloudfront.net/datasheets/Sensors/Sound/LMV324.pdf上查看,而该板的原理图可在dlnmh9ip6v2uc.cloudfront.net/datasheets/Sensors/Sound/sound-detector.pdf上查看。

该设备非常简单,因为它提供了三个输出:标记为AUDIO的输出可以直接获取捕获的音频,而标记为ENVELOPE的输出则可以通过简单地读取模拟电压来轻松读取声音的幅度。最后一个标记为GATE的输出则是通过使用固定的阈值(尽管你可以通过改变板载电阻来改变阈值)来指示声音的存在。

对于我们的原型,我们可以使用ENVELOPE输出,因为我们可以读取模拟电压。不仅如此,它还允许我们设置自己的软件阈值。因此,让我们看看下面表格中的连接:

引脚 声音传感器引脚
P9.4 - Vcc VCC
P9.39 - AIN0 R @ENVELOPE
P9.3 - GND GND

如第二章中所述,超声波停车助手,ADC 的输入必须限制在 1.8V,而 Vcc 电平是 3.3V,因此我们可以使用那里提到的电压分压器来将输出电压按 2 倍的比例缩放。请确保最大输入电平不超过 1.8V。因此,读者不应直接将P9.39引脚与声音传感器连接;他们应该使用第二章中所使用的电阻连接方式,来保护 BeagleBone Black 的ADC

现在,为了验证所有连接是否正常,我们可以使用以下命令启用 BeagleBone Black 的 ADC:

root@beaglebone:~# echo cape-bone-iio > /sys/devices/bone_capemgr.9/slots

注意

这些设置可以通过使用书中示例代码库中的bin/load_firmware.sh脚本来完成,具体如下:

root@beaglebone:~# ./load_firmware.sh adc

然后,我们可以使用以下命令读取捕获的声音包络:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
24

如果我们在重新运行命令时尝试说话,我们应该得到更高的值,如下所示:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
201

因此,环境声音越大,返回的值也就越高。

提示

再次提醒,如第一章中所述,危险气体传感器,也可以通过以下命令使用sysfs文件系统中另一个文件来读取 ADC:

root@beaglebone:~# cat /sys/bus/iio/devices/iio:device0/in_voltage0_raw

设置光传感器

光传感器是下图所示的设备:

设置光传感器

注意

这些设备可以通过以下链接购买(或通过上网搜索):www.cosino.io/product/light-sensor

本设备的用户指南可在www.phidgets.com/docs/1143_User_Guide中找到。

至于声音传感器,这个设备有一个模拟输出,可以用来测量环境光照度。根据用户指南,光照度可以通过以下公式获得:

光照度(lux) = e^(m传感器输出 + b)*

在这里,传感器输出是传感器的原始值,mb是用来获取粗略近似值的已定义常数。不过,由于我们只关心测量光的存在,而不是其精确强度,我们可以使用我们自己的值,或者为了简单起见,直接使用传感器输出值。

在用户指南中,我们还读到,即使设备需要 5V 的 Vcc 来工作,它的输出值不会超过 2.5V。因此,考虑到我们的 BeagleBone Black 的 ADC 最大输入值为 1.8V,我们可以使用上面提到的电压分压器将输出值缩小 2 倍,从而确保满足 1.8V 的阈值。

连接方式如以下表格所示:

引脚 光传感器电缆
P9.6 - Vcc 红色
P9.40 - AIN1 红色 @white
P9.1 - GND 黑色

现在,就像在上一节中对声音传感器的操作一样,我们可以通过以下命令来测试设备:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
386

然而,如果我将设备放在光线下,我得到:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
528

而当我用一杯咖啡覆盖传感器时,我得到:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
79

因此,关于声音传感器,同样的规则适用:环境光强度越高,传感器返回的值越高。

连接 LED

为了连接 LED,我们可以使用与第一章中相同的电路,危险气体传感器。连接方式见下表:

引脚 LED 颜色
P8.9 - GPIO69 红色 @red
P8.10 - GPIO68 红色 @yellow

为了测试连接,我们可以使用以下命令将线路设置为输出,然后将其设置为高电平状态:

root@beaglebone:~# ../bin/gpio_set.sh 68 out
root@beaglebone:~# ../bin/gpio_set.sh 69 out
root@beaglebone:~# echo 1 > /sys/class/gpio/gpio68/value
root@beaglebone:~# echo 1 > /sys/class/gpio/gpio69/value

如果一切顺利,你应该看到两个 LED 都亮起。

最终图片

以下图片展示了我为实现这个项目并测试软件所做的原型:

最终图片

没什么特别要说的,除了你必须为你的 BeagleBone Black 提供网络连接,否则 WhatsApp 警报服务将无法工作!如你所见,我使用了普通的以太网电缆,但让我提醒你,你也可以使用 USB 连接到主机,正如在前言中提到的那样。

设置软件

这次,为了实现这个原型的软件,我们可以使用一个状态机,包含以下状态及其相应的交易:

状态 描述 操作 交易条件
IDLE 空闲状态;洗衣机未工作。
  • LED 黄色关闭

  • LED 红色关闭

|

  • 如果检测到声音,将t0=t并将状态切换为SOUND

|

SOUND 检测到声音!请继续监控环境一段时间。
  • LED 黄灯闪烁

  • LED 红灯熄灭

|

  • 如果检测到声音且 t-t0 > timeout,则切换到 RUNNING 状态。

|

RUNNING 检测到持续声音,因此洗衣机开始工作。
  • LED 黄灯亮起

  • LED 红灯熄灭

  • 提醒用户

|

  • 如果未检测到声音,设置 t0=t 并切换到 NO_SOUND 状态。

|

NO_SOUND 不再检测到声音!请继续监控环境一段时间。
  • LED 黄灯亮起

  • LED 红灯闪烁

|

  • 如果未检测到声音且 t-t0 > timeout,则切换到 DONE 状态。

  • 如果检测到声音,切换到 RUNNING 状态。

|

DONE 长时间没有声音;洗衣机已完成工作。
  • LED 黄灯亮起

  • LED 红灯亮起

  • 提醒用户

|

  • 如果检测到光线,设置 t0=t 并将状态切换到 LIGHT

|

LIGHT 检测到光线!请继续监控环境一段时间。
  • LED 黄灯闪烁

  • LED 红灯亮起

|

  • 如果检测到光线且 t-t0 > timeout,则切换到 ROOM 状态。

  • 如果未检测到光线,切换到 DONE 状态。

|

ROOM 灯光持续亮起;有人进入洗衣房。
  • LED 黄灯熄灭

  • LED 红灯亮起

|

  • 如果未检测到光线,设置 t0=t 并将状态切换到 NO_LIGHT

|

NO_LIGHT 不再检测到光线!请继续监控环境一段时间。
  • LED 黄灯熄灭

  • LED 红灯闪烁

|

  • 如果未检测到光线且 t-t0 > timeout,则切换到 IDLE 状态。

|

起始状态为 IDLE,变量 t 存储当前时间。t0 用来表示开始时间,而 timeout 值可以设置为适当的时间,以避免误判(因此,您应该尝试不同的值来适应您的需求)。

对于每个状态,如果任何交易条件未满足,状态机假定没有执行任何转换,并保持在原始状态。

上述表格的另一种表示方式如下所示,其中我们机器的所有状态通过圆圈表示,状态转换通过箭头和相应的标签表示,标签中包含状态转换条件(方块只是表示在状态之间转换前需要执行的动作)。这种表示方式更加清晰地展示了我们从一个状态到另一个状态所需的条件,以及状态之间的连接方式。

设置软件

声音检测器管理器

好的,现在我们应该尝试理解如何检测洗衣机在运行和停止时的状态。如前所述,声音检测器可以帮助我们区分这两种状态。实际上,通过使用书中示例代码库中 chapter_05/sample.sh 文件中的脚本,我们可以绘制从 ADC 输入采样得到的图表。该脚本很简单,相关代码片段如下:

# Install the signals traps
trap sig_handler SIGTERM SIGINT

# Start sampling the data till a signal occours
echo "$NAME: collecting data into file sample.log..."

do_exit=false
t0=$(date '+%s.%N')
( while ! $do_exit ; do
        t=$(date '+%s.%N')
        v=$(cat $AIN_PATH/$dev)

        echo "$(bc -l <<< "$t - $t0") $v"

        # Sleep till the next period
        sleep $(bc -l <<< ".5 - $(date '+%s.%N') + $t")
done ) | tee sample.log

# Plot the data
echo "$NAME: done. Now generate the plot..."

gnuplot <<EOF
set terminal png size 800,600 enhanced font "Helvetica,20"
set output 'sample.png'
set autoscale
set nokey
set grid lw 1
show grid
set xlabel "\nTime"
set ylabel 'sample'
set xtics rotate
plot "sample.log" using 1:2 with lines
EOF

echo "$NAME: done. Data plotted into file sample.png"

脚本的第一部分仅仅是一个while循环,用于每 500 毫秒左右读取 ADC 数据(脚本是 Bash 编写的,所以不要期望太高的精度)。当用户按下CTRL + C键时,它们会生成一个信号,该信号被sig_handler信号处理程序捕获,该处理程序简单地将do_exit变量设置为true,如下所示:

function sig_handler () {
        do_exit=true
}

tee命令用于将采样数据显示到终端并同时保存到sample.log文件中。一旦数据被收集,我们使用gnuplot工具生成图表的方法与第一章 危险气体传感器 中所做的类似。

以下是我在原型机上做的一个样品演示。在测试的中间,我讨论了字母A以产生可检测的声音水平:

root@beaglebone:~# ./sample.sh AIN0
sample.sh: collecting data into file sample.log (press CTRL+C to stop)...
.046822125 29
.607965667 36
1.168452792 27
1.728863042 37
2.290465209 31
2.851453792 22
3.417320167 25
3.980918459 26
4.541227334 324
5.101803001 439
5.662116709 452
6.223465293 466
6.783610585 631
7.346517043 670
7.910204543 600
8.471078668 569
9.032048168 677
9.592383627 728
10.153342335 708
10.714916752 736
11.275682627 769
11.836266085 672
12.396825252 308
12.958963794 267
13.520244377 20
14.081049085 19
14.641610585 20
15.202588419 20
15.762929794 19
16.324602752 19
16.885479836 25
17.450904252 19
^Csample.sh: done. Now generate the plot...

 Rectangular grid drawn at x y tics
 Major grid drawn with linetype 0 linewidth 1.000
 Minor grid drawn with linetype 0 linewidth 1.000
 Grid drawn at default layer

sample.sh: done. Data plotted into file sample.png

正如您在前面的输出中所看到的,我只是用了我的声音,就可以轻松区分声音的存在或不存在。然而,以下屏幕截图是从前述脚本生成的sample.png文件中获取的,更具说明性:

声音检测管理器

很明显,只需使用阈值为 200,我们就可以搞定。

光传感器管理器

光传感器的功能与声音传感器非常相似,因此我们可以使用相同的sample.sh脚本来从中获取一些样本。这次,我通过简单地用小杯子盖住光传感器来模拟光的缺失/存在。

使用的命令如下所示:

root@beaglebone:~# ./sample.sh AIN1
sample.sh: collecting data into file sample.log (press CTRL+C to stop)...
.046757875 78
.609375334 78
1.169878875 78
1.730529209 78
2.291640417 78
2.852100834 78
3.412606126 78
3.973172542 79
4.534430834 77
5.094665667 78
5.655394084 463
6.216107209 477
6.777377459 484
7.337617209 486
7.898274043 486
8.458853793 487
9.023789835 486
9.590632751 486
10.154009085 486
10.715376668 479
11.275998293 479
11.836406710 476
12.397433502 403
12.958216252 92
13.519537710 79
14.080658377 79
14.641473210 79
15.202038044 78
15.762509877 78
16.323857252 79
16.884874669 78
17.445492127 77
18.006021794 78
^Csample.sh: done. Now generate the plot...

 Rectangular grid drawn at x y tics
 Major grid drawn with linetype 0 linewidth 1.000
 Minor grid drawn with linetype 0 linewidth 1.000
 Grid drawn at default layer

sample.sh: done. Data plotted into file sample.png

相应的绘图如下所示:

光传感器管理器

即使在这种情况下,我们可以使用阈值 200 来区分这两种状态。

控制 LED

如在第一章 危险气体传感器 或第二章 超声波停车助手 中已经显示的,Linux 系统中管理 LED 有两种不同的方式。第一种是使用 GPIO,第二种是使用 LED 设备;但是,由于我们的状态机要求 LED 应该闪烁,我们应该使用允许我们使用触发器获取闪烁状态的 LED 管理方法。

类似地,如同在第二章 超声波停车助手 中所做的那样,我们需要一个合适的.dts文件,读者可以在书中示例代码库的chapter_05/BB-LEDS-C5-00A0.dts文件中找到。找到后,我们必须使用以下命令行编译它:

root@beaglebone:~# dtc -O dtb -o /lib/firmware/BB-LEDS-C5-00A0.dtbo -b 0 -@ BB-LEDS-C5-00A0.dts

现在,我们可以使用以下命令启用它:

root@beaglebone:~# echo BB-LEDS-C5 > /sys/devices/bone_capemgr.9/slots

然后,系统中现在有两个新的 LED,如下所示:

root@beaglebone:~# ls -d /sys/class/leds/c5*
/sys/class/leds/c5:yellow  /sys/class/leds/c5:red

设置 WhatsApp API

现在是时候向您展示如何与WhatsApp服务进行交互了。在这个项目中,我们只需要向用户的账户发送消息,但即使是这么简单的任务,也需要我们完成几个步骤。

首先,我们必须在 BeagleBone Black 上安装一些前提软件包,如下所示:

root@beaglebone:~# aptitude install python python-dateutil python-argparse

接下来,我们需要安装一个名为yowsup的软件包,它可以用来通过 WhatsApp 发送消息:

root@beaglebone:~# pip install yowsup

注意

yowsup工具的维基页面在github.com/tgalal/yowsup/wiki

安装完成后,我们可以使用以下命令来获取一个示例配置文件:

root@beaglebone:~# yowsup-cli demos --help-config > yowsup-cli.config

新的文件yowsup-cli.config现在应该包含以下几行:

root@beaglebone:~# cat yowsup-cli.config

############# Yowsup Configuration Sample ###########
#
# ====================
# The file contains info about your WhatsApp account. This is used during # registration and login.
# You can define or override all fields in the command line args as well.
#
# Country code. See http://www.ipipi.com/help/telephone-country-codes.htm.
 # This is now required.
cc=49
#
# Your full phone number including the country code you defined in 'cc', # without preceding '+' or '00'
phone=491234567890
#
# You obtain this password when you register using Yowsup.
password=NDkxNTIyNTI1NjAyMkBzLndoYXRzYXBwLm5ldA==
#######################################################

以#字符开头的行是注释,可以删除,重要的行如下:

cc=39
phone=39XXXXXXXXXX
id=
password=

提示

请注意,id= 行可能不存在。

在前面的示例中,为了保护隐私,我将我的电话号码替换成了X字符,但你必须在此处输入你的电话号码,以便访问系统。

提示

请注意,当你已经在使用 WhatsApp 时,无法使用该电话号码,否则会与你在智能手机上使用的 WhatsApp 客户端产生冲突。这就是为什么我在没有激活 WhatsApp 服务时使用了一个电话号码。

简单来说,你不需要在接收短信的手机上运行 WhatsApp 客户端!

一旦你添加了一个电话号码,你可以将其放入前面的yowsup-cli.config配置文件中,并保持idpassword变量的行未分配。然后,必须执行以下命令:

root@beaglebone:~# yowsup-cli registration -r sms -c  yowsup-cli.config

过了一会儿,命令应该会回应如下信息:

INFO:yowsup.common.http.warequest:{"status":"sent","length":6,"method":"sms","retry_after":1805}

status: sent
retry_after: 1805
length: 6
method: sms

然后,你应该会收到一条短信,短信内容会显示你号码的信息;你只需要短信中的信息即可;实际上,短信内容应该类似于WhatsApp code 633-170,因此你需要使用以下命令完成注册:

root@beaglebone:~# yowsup-cli registration -R 633-170 -c yowsup-cli.config

如果一切顺利,前面的命令应该会输出如下信息:

{"status":"ok","login":"39XXXXXXXXXX","pw":"Kwf07sjuSz2J0Qwm3sBEtVNeBIk=","type":"new","expiration":1467142355,"kind":"free","price":"\u20ac0,89","cost":"0.89","currency":"EUR","price_expiration":1438319298}

status: ok
kind: free
pw: Kwf07sjuSz2J0Qwm3sBEtVNeBIk=
price: € 0,89
price_expiration: 1438319298
currency: EUR
cost: 0.89
expiration: 1467142355
login: 393292571400
type: new

这里的重要信息是我们必须使用的密码,以便正确登录我们的新 WhatsApp 账户。密码在pw字段中,因此在将此信息填入配置文件中的password字段后,yowsup-cli.config文件的新样式应如下所示:

root@beaglebone:~# cat yowsup-cli.config
cc=39
phone=39XXXXXXXXXX
id=
password=Kwf07sjuSz2J0Qwm3sBEtVNeBIk=

现在我们可以登录到我们的新账户,并从中发送消息了!例如,可以使用以下命令行从命令行发送消息:

root@beaglebone:~# yowsup-cli demos -c yowsup-cli.config -s 39YYYYYYYYYY "Hello, it's your BeagleBone Black writing! :)"
WARNING:yowsup.stacks.yowstack:Implicit declaration of parallel layers in a tuple is deprecated, pass a YowParallelLayer instead
INFO:yowsup.demos.sendclient.layer:Message sent

Yowsdown

请注意,我使用了另一个电话号码作为目标号码,隐藏为39YYYYYYYYYY,以便与之前使用的发送号码区分开。

提示

警告信息可以安全地忽略。

此外,也可能发生第一次执行命令时,未显示“Message Sent”输出。在这种情况下,请尝试重新执行该命令。

好的,现在一切都准备好了,我们只需要看看如何实现状态机。接下来我们进入下一节。

状态机

现在,每个子系统都已设置完成,是时候看看之前描述的状态机的可能实现了。完整的代码非常简单,已用 Bash 开发,并可以在书中的示例代码库中的chapter_05/state_machine.sh文件中找到。以下是相关代码的几个片段。

第一个片段是关于配置文件读取的,如下所示:

SOUND_DEV="/sys/devices/ocp.3/helper.12/AIN0"
LIGHT_DEV="/sys/devices/ocp.3/helper.12/AIN1"

source ../lib/logging.sh
source ./config.sh

# Check the configuration settings. If not specified use default values
[ -z "$TIMEOUT" ] && TIMEOUT=60
[ -z "$SOUND_TH" ] && SOUND_TH=500
[ -z "$LIGHT_TH" ] && LIGHT_TH=500
if [ -z "$WHATSAPP_USER" ] ; then
        err "you must define WHATSAPP_USER!"
        exit 1
fi

在进行一些初始设置后,代码会加载包含系统设置的config.sh文件(有关该文件的示例,请参见最后一节),然后检查设置变量。接着,代码继续定义传感器的读取函数。在以下代码片段中,我只展示了其中一个函数,因为它们非常相似:

function read_sound () {
        ret=0

        while [ -z "$v" ] ; do
                v=$(cat $SOUND_DEV)
        done
        [ "$v" -gt $SOUND_TH ] && ret=1

        echo -n $ret
}

该函数简单地读取 ADC 并将数据与指定的阈值进行比较。返回的值是01,取决于声音或光的有无。请注意,如果读取数据时发生错误,函数会重试操作直到成功读取。

提示

在这里,我们应该添加一个重试次数限制,以避免进入无限循环。但为了简化,我决定不实现它。

LED 管理部分如下所示:

function set_led () {
        name=$1
        val=$2

        case $val in
        on)
                echo none > /sys/class/leds/c5\:$name/trigger
                echo 255 > /sys/class/leds/c5\:$name/brightness
                ;;

        off)
                echo none > /sys/class/leds/c5\:$name/trigger
                echo 0 > /sys/class/leds/c5\:$name/brightness
                ;;

        blink)
                t=$((1000 / 2))

                echo timer > /sys/class/leds/c5\:$name/trigger
                echo $t > /sys/class/leds/c5\:$name/delay_on
                echo $t > /sys/class/leds/c5\:$name/delay_off
                ;;

        *)
                err "invalid LED status! Abort"
                exit 1
                ;;
        esac
}

function signal_status () {
        s=$1

        case $s in
        IDLE)
                set_led yellow off
                set_led red off
                ;;

        SOUND)
                set_led yellow blink
                set_led red off
                ;;

        RUNNING)
                set_led yellow on
                set_led red off
                ;;

        NO_SOUND)
                set_led yellow on
                set_led red blink
                ;;

        DONE)
                set_led yellow on
                set_led red on
                ;;

        LIGHT)
                set_led yellow blink
                set_led red on
                ;;

        ROOM)
                set_led yellow off
                set_led red on
                ;;

        NO_LIGHT)
                set_led yellow off
                set_led red blink
                ;;
        esac

        return
}

set_led函数根据signal_status函数传递的系统状态简单地设置 LED 状态。

提示

请注意,signal_status函数可以更紧凑地实现(可能通过使用关联数组),但这种形式更具可读性。

然后,通过 WhatsApp 系统发送警报消息的函数代码如下:

function send_alert () {
        msg=$1

        dbg "user=$WHATSAPP_USER msg=\"$1\""
        yowsup-cli demos -c yowsup-cli.config -s $WHATSAPP_USER "$msg"

        return
}

现在,整个项目的核心是change_status函数。这个函数实现了状态机。它根据当前状态和系统输入决定新的状态:

function change_status () {
     status=$1
     sound=$2
     light=$3
     t0=$4

     t=$(date "+%s")

     dbg "status=$status sound=$sound light=$light t-t0=$(($t - $t0))"

     case $status in
     IDLE)
             if [ $sound -eq 1 ] ; then
                     echo SOUND
                     return
             fi
             ;;

     SOUND)
             if [ $sound -eq 1 -a $(($t - $t0)) -gt $TIMEOUT ] ; then
                     echo RUNNING
                     return
             fi
             if [ $sound -eq 0 ] ; then
                     echo IDLE
                     return
             fi
             ;;

     RUNNING)
             if [ $sound -eq 0 ] ; then
                     echo NO_SOUND
                     return
             fi
             ;;

     NO_SOUND)
             if [ $sound -eq 0 -a $(($t - $t0)) -gt $TIMEOUT ] ; then
                     echo DONE
                     return
             fi
             if [ $sound -eq 1 ] ; then
                     echo RUNNING
                     return
             fi
             ;;

     DONE)
             if [ $light -eq 1 ] ; then
                     echo LIGHT
                     return
             fi
             ;;

     LIGHT)
             if [ $light -eq 1 -a $(($t - $t0)) -gt $TIMEOUT ] ; then
                     echo ROOM
                     return
             fi
             if [ $light -eq 0 ] ; then
                     echo DONE
                     return
             fi
             ;;

     ROOM)
             if [ $light -eq 0 ] ; then
                     echo NO_LIGHT
                     return
             fi
             ;;

     NO_LIGHT)
             if [ $light -eq 0 -a $(($t - $t0)) -gt $TIMEOUT ] ; then
                     echo IDLE
                     return
             fi
             if [ $light -eq 1 ] ; then
                     echo NO_LIGHT
                     return
             fi
             ;;

     *)
             err "invalid status! Abort"
             exit 1
             ;;
     esac

     # No status change!
     echo $status
}

你可以验证这个函数是否正确地实现了本章之前展示的状态机表(或图)。

此时,主函数的核心看起来如下所示:

# Ok, do the job
dbg "using TIMEOUT=$TIMEOUT SOUND_TH=$SOUND_TH LIGHT_TH=$LIGHT_TH"

status="IDLE"
t0=0

signal_status $status
while sleep 1 ; do
        dbg "old-status=$status"

        # Read the sensors
        sound=$(read_sound)
        light=$(read_light)

        # Change status?
        new_status=$(change_status $status $sound $light $t0)
        if [ "$new_status" != "$status" ] ; then
                t0=$(date "+%s")

                # Set the leds status
                signal_status $new_status

                # We have to send any alert?
                case $new_status in
                RUNNING)
                        # Send the message during SOUND->RUNNING # transaction
                        # only
                        [ "$status" == SOUND ] && send_alert "washing machine is started!"
                        ;;

                DONE)
                        # Send the message during NO_SOUND->DONE # transaction
                        # only
                        [ "$status" == NO_SOUND ] && send_alert "washing machine has finished!"
                        ;;

                *)
                        # Nop
                        ;;
                esac
        fi
        dbg "new-status=$new_status"

        status=$new_status
done

如你所见,main函数只是一个大循环,它定期读取传感器输入,然后根据输入更改系统的内部状态,在需要时发送一些警报,并相应地设置 LED 的状态。

最终测试

为了测试原型,我使用了一些技巧来模拟洗衣机和房间里的灯。洗衣机可以通过在主机 PC 上播放音频/视频并调节合理的音量来轻松模拟,而房间灯的开/关状态可以通过用小杯子遮住光传感器来模拟。

为了设置所有外设和驱动程序,我们可以使用所有前面的命令或SYSINIT.sh脚本,如下所示:

root@beaglebone:~# ./SYSINIT.sh
done!

注意

此命令可以在书中示例代码库中的chapter_05/SYSINIT.sh文件中找到

作为初始状态(IDLE),我们应该覆盖光传感器(以模拟灯关了)并停止视频/音频播放器(以模拟洗衣机关了)。然后,我们需要在配置文件中设置音频和光检测的低阈值,并设置一个非常短的超时(5秒)以加速测试。以下是我的配置文件:

root@beaglebone:~# cat config.sh
# Set the timeout value
TIMEOUT=5

# Set the sound threshold
SOUND_TH=200

# Set the light threshold
LIGHT_TH=200

# Set the Whatsapp account
WHATSAPP_USER=39YYYYYYYYYY

然后,我启动了系统,并通过以下命令在终端启用了所有调试消息:

root@beaglebone:~# ./state_machine.sh -d -l
state_machine.sh: using TIMEOUT=5 SOUND_TH=200 LIGHT_TH=200
state_machine.sh: old-status=IDLE
state_machine.sh: status=IDLE sound=0 light=0 t-t0=1398295377
state_machine.sh: new-status=IDLE
state_machine.sh: old-status=IDLE

注意

请注意,初始状态是IDLE,在没有检测到新事件之前,状态不会发生变化。

在接下来的输出列表中,我将使用...字符跳过不相关的行:

state_machine.sh: status=IDLE sound=0 light=0 t-t0=1398295379
...
state_machine.sh: status=IDLE sound=1 light=0 t-t0=1398295381
state_machine.sh: new-status=SOUND
state_machine.sh: old-status=SOUND

现在,我将模拟以下情况:首先,我打开洗衣机并等待它完成工作。然后,我去洗衣房拿我的洗净衣物。如前所述,我将通过视频/音频播放器来模拟运行中的洗衣机,而开/关灯则通过揭开/覆盖光传感器来模拟。

好的,测试开始了。过了一会儿,我启动了视频/音频播放器。所以,检测到了声音,新的状态变为SOUND

state_machine.sh: status=SOUND sound=1 light=0 t-t0=1
...
state_machine.sh: status=SOUND sound=0 light=0 t-t0=4
state_machine.sh: new-status=IDLE
state_machine.sh: old-status=IDLE

哎呀!有一瞬间,声音水平低于阈值,所以我们又切换回了IDLE状态!这是正确的,因为洗衣机可能会暂时停下来。这里就是timeout起作用的地方,也就是说,我们必须选择比所有可能的洗衣机暂停时间更长的超时:

state_machine.sh: status=IDLE sound=1 light=0 t-t0=1
...
state_machine.sh: old-status=SOUND
cat: /sys/devices/ocp.3/helper.12/AIN0: Resource temporarily unavailable

这是读取 ADC 输入时的一个错误,但软件已编写为在没有问题的情况下重试故障操作:

state_machine.sh: status=SOUND sound=1 light=0 t-t0=4
...
state_machine.sh: status=SOUND sound=1 light=0 t-t0=6
state_machine.sh: user=393492432127 msg="washing machine is started!"
WARNING:yowsup.stacks.yowstack:Implicit declaration of parallel layers in a tuple is deprecated, pass a YowParallelLayer instead
INFO:yowsup.demos.sendclient.layer:Message sent

Yowsdown
state_machine.sh: new-status=RUNNING
state_machine.sh: old-status=RUNNING

好的!当超时在SOUND状态下过期时,意味着已经检测到持续的声音,这意味着洗衣机已经开始工作。

提示

请注意,更可靠的实现应该使用不同的超时来标识特定的事务。

以下片段展示了这一点:

state_machine.sh: status=RUNNING sound=1 light=0 t-t0=2
...
state_machine.sh: new-status=RUNNING
state_machine.sh: old-status=RUNNING
state_machine.sh: status=RUNNING sound=0 light=0 t-t0=8
state_machine.sh: new-status=NO_SOUND
state_machine.sh: old-status=NO_SOUND

现在,我已停止了视频/音频播放器,并且没有检测到声音,因此我们切换到NO_SOUND状态:

state_machine.sh: status=NO_SOUND sound=0 light=0 t-t0=1
...
state_machine.sh: status=NO_SOUND sound=0 light=0 t-t0=6
state_machine.sh: user=393492432127 msg="washing machine has finished!"
WARNING:yowsup.stacks.yowstack:Implicit declaration of parallel layers in a tuple is deprecated, pass a YowParallelLayer instead
INFO:yowsup.demos.sendclient.layer:Message sent

Yowsdown
state_machine.sh: new-status=DONE
state_machine.sh: old-status=DONE

好的,当我们在NO_SOUND状态下超时过期时,我们切换到DONE状态,表示洗衣机已经完成了工作:

state_machine.sh: status=DONE sound=0 light=0 t-t0=1
...
state_machine.sh: status=DONE sound=0 light=1 t-t0=10
state_machine.sh: new-status=LIGHT
state_machine.sh: old-status=LIGHT

现在,我已经揭开了光传感器,以模拟有人在洗衣房里打开了灯:

state_machine.sh: status=LIGHT sound=0 light=1 t-t0=1
...
state_machine.sh: status=LIGHT sound=0 light=1 t-t0=6
state_machine.sh: new-status=ROOM
state_machine.sh: old-status=ROOM

再次,由于超时已过,我们可以认为灯已经开了很长时间,这意味着用户已经收到了我们的 WhatsApp 消息并且进入了洗衣房拿取洗好的衣物:

state_machine.sh: status=ROOM sound=0 light=1 t-t0=1
...
state_machine.sh: status=ROOM sound=0 light=0 t-t0=8
state_machine.sh: new-status=NO_LIGHT
state_machine.sh: old-status=NO_LIGHT

现在,我再次覆盖了光传感器,以模拟洗衣房里的灯已经关闭:

state_machine.sh: status=NO_LIGHT sound=0 light=0 t-t0=1
...
state_machine.sh: status=NO_LIGHT sound=0 light=0 t-t0=6
state_machine.sh: new-status=IDLE
state_machine.sh: old-status=IDLE
state_machine.sh: status=IDLE sound=0 light=0 t-t0=1
state_machine.sh: new-status=IDLE
state_machine.sh: old-status=IDLE

最后,在timeout超时后,我们可以返回到IDLE状态,等待新一轮循环的开始。

以下是我的智能手机显示 WhatsApp 活动的截图:

最终测试

总结

在本章中,我们了解了如何使用特定的传感器检测声音和光线的强度,并学习了如何编写一个简单的 Bash 脚本来实现一个状态机,用以管理我们的洗衣房。此外,我们还了解了如何通过 WhatsApp 服务向智能手机发送一些警报信息。

在下一章中,我们将尝试实现一个婴儿房间守卫来控制我们的小宝宝的情况!我们将能够监控房间温度,检测宝宝是否在哭泣,或者她是否正在呼吸,还有更多功能。

第六章:宝宝房间哨兵

在本章中,我们将展示一个可能的宝宝房间哨兵实现,它可以通过检测宝宝是否在哭泣或宝宝在睡觉时是否在呼吸来监控房间。另外,作为一个特别功能,该系统还能够通过非接触式温度传感器测量宝宝的体温。

我们将看到几种不同的传感器,如压力传感器、声音传感器和温度传感器。另外,关于温度传感器,我们还将看到一个有趣的红外版本,它可以在不接触物体的情况下测量表面温度。此外,我们还将展示一个小巧精致的 LCD 屏幕原型,以便观察宝宝房间的情况。

功能基础

当我们有了宝宝后,购买各种设备来检测宝宝是否在哭泣、发烧,或者在睡觉时是否仍在呼吸是很常见的。因此,在本章中,我们将尝试使用我们的 BeagleBone Black 和一些特殊传感器来实现几种智能传感器,以检测这些危险状态。

小贴士

警告!请让我提醒您,这个项目只是一个原型,不能作为个人安全应用使用! 它仅仅是一个关于可能实现的宝宝房间哨兵设备的研究。

本书作者和 Packt 出版社都不建议或支持将此产品单独使用或作为任何个人安全应用的组件。 读者需要注意,这些传感器和控制器不包括用于此类用途的自检冗余电路。

本书作者和 Packt 出版社对未经授权使用此原型不承担任何责任。用户使用此设备需自担风险!

为了检测宝宝是否在哭泣,我们可以使用声音检测器,就像我们在上一章中做的那样;但这一次,我们应该稍微处理一下输入信号,以便有效地检测宝宝是否真的在哭泣。通过查看以下截图,我们可以看到一个简单的 40 秒的哭泣宝宝音频信号图(采样时间为Ts=0.01 s=10ms):

功能基础

红色部分是原始音频信号,黄色部分则是当前时刻 5 秒窗口内该信号的平均值,也就是说,黄色信号是过去 5 秒内所有采集到的音频样本的平均值。

如前所述,采样时间Ts10ms,这对于音频录制来说并不适用,但足以满足我们的需求。事实上,我们可以看到,通过使用黄色线表示的平均值,我们只需使用合适的阈值,就能检测到宝宝是否在哭泣。

关于呼吸,问题类似;事实上,我们可以假设,正常的睡眠呼吸频率大约在每分钟 12 到 16 次之间,也就是 0.26 Hz 到 0.2 Hz 的频率范围。然而,这次信号的平均水平没有用处,我们可以在一个合适的时间窗口内使用其幅度来替代。为了更好地解释这个概念,请看下面的截图:

功能原理

红色表示原始压力信号,而黄色表示该信号在当前时间之前 5 秒窗口内的平均值。如前所述,黄色信号仅是过去 5 秒内所有采集音频样本的平均值(采样时间仍为Ts=10 ms)。蓝色表示压力信号的幅度,它是通过计算所考虑时间窗口内最大值和最小值之间的差异得出的,即这次,我们每次都会计算过去 5 秒内采集音频样本的最大值Vmax)和最小值Vmin),并计算 Vmax 与 Vmin 之间的差值。

在前面图表的前 8 秒钟,输出接近 0,因为传感器上没有任何物体。然后,约在T = 8秒时,一个宝宝被放置到传感器上,传感器开始返回较高的值,正如预期的那样。在这种情况下,输入信号的平均值和幅度都增加,并且达到了(或多或少)一些稳定值。然而,重要的是要强调的是,当宝宝停止呼吸时(不用担心!在这个测试中没有宝宝停止呼吸!这只是一个模拟)。当宝宝停止呼吸时(大约发生在 T = 38秒附近),压力传感器仍然探测到某些信号,且平均值和幅度都下降了;但最明显的变化是压力的幅度发生了最大的跳跃!正如你在上面的截图中看到的,当平均值从700降到450时,幅度从700降到10

对于我们的原型,我们可以使用压力幅度来检测宝宝的呼吸,而使用压力平均值来检测是否有宝宝在场。用户应注意,平均值和幅度的计算可以通过使用如下所示的 C 函数同时进行:

void extract(int arr[], size_t n, int *avg, int *min, int *max)
{
        int i;
        float sum = 0;

        if (min)
                *min = 4096;
        if (max)
                *max = 0;
        for (i = 0; i < n; i++) {
                sum += ((float) arr[i]) / ((float) n);
                if (min)
                        *min = min(*min, arr[i]);
                if (max)
                        *max = max(*max, arr[i]);
        }
        *avg = (int) sum;
}

extract()函数获取包含压力数据的arr数组,并通过使用一个单一的for循环,可以并行执行这两个计算。

关于压力传感器,我们必须考虑到如果没有正确地将其放入一个具有特殊机制的盒子中,它无法正常工作,而该机制适用于检测呼吸。在下面的截图中,我展示了一个可能的这种盒子的实现:

功能原理

盒子应放在婴儿背部附近,注意确保上面的位置正确,以捕捉肺部的运动。盒子的顶部(移动表面)可以上下移动,得益于弹簧,它可以通过婴儿肺部运动时的压力感应器探测到压力,并通过针脚传递压力。

现在,最后需要处理的是用于测量体温的数字温度计。为此,我们可以使用普通的温度传感器,但由于我们讨论的是婴儿,我们希望使用非接触式温度传感器。这些特殊的传感器能够通过使用物体表面发出的红外线,并且这些红外线位于传感器的视野范围内,从而在不接触物体的情况下测量物体的温度。所以,当传感器前面没有物体时,我们可以检测到环境温度,但当靠近一个表面时,我们可以在不接触它的情况下检测该表面的温度!

以下截图展示了一个合适的区域,用于对准传感器以测量婴儿的体温:

功能基础

设置硬件

在这个项目中,我们将使用两个模拟传感器、一个数字传感器和一个小型 LCD 来实现一个简单的 GUI。模拟传感器连接到两个不同的 ADC,而数字传感器(即非接触式温度传感器)通过 I²C 总线与 BeagleBone Black 通信。最后,小型 LCD 通过 SPI 总线和一些 GPIO 连接到我们的 BeagleBone Black 板。

关于用来提醒父母的报警设备,我们可以使用普通的蜂鸣器,或者更先进的短信网关,甚至两者结合。但无论如何,这些设备的连接方式可以参考前面的章节,因此由于篇幅限制,本章不再添加相关内容。读者可以尝试自行实现硬件和软件作为练习。

设置非接触式温度传感器

本原型中使用的非接触式温度传感器如下面的截图所示:

设置非接触式温度传感器

注意

这些设备可以从www.cosino.io/product/contactless-temperature-sensor购买,或者通过互联网查找。

该设备的用户手册可以在www.sparkfun.com/datasheets/Sensors/Temperature/SEN-09570-datasheet-3901090614M005.pdf找到。

这款设备非常有趣,因为它能够在不接触物体的情况下测量物体的温度!实际上,它是一个红外线温度计,具有 17 位分辨率,覆盖广泛的温度范围:环境温度为–40°C 至 85°C,物体温度为–70°C 至 382.2°C。

测量的值是传感器视野内所有物体的平均温度,因此显然我们可以用它来测量环境温度以及人体温度。只需将传感器靠近身体,结果就出来了!

该传感器的另一个重要特点是它是数字设备,也就是说,可以通过数字连接获取数据,这种连接对环境干扰免疫,甚至可以在(相对)较长的距离上使用。因此,我们可以考虑将其安装在手持设备上以实现更方便的使用。

这个设备使用的总线是 I²C 总线,所需的连接在下表中给出:

引脚 温度传感器引脚
P9.4 - VCC 3 - VDD
P9.17 - SCL 1 - SCL
P9.18 - SDA 2 - SDA
P9.2 - GND 3 - Vss

为了完整起见,设备的引脚映射显示在下图中:

设置无接触温度传感器

现在,如果一切连接正确,我们可以使用以下命令激活 I²C 总线:

root@beaglebone:~# echo BB-I2C1 > /sys/devices/bone_capemgr.9/slots

然后,通过使用i2cdetect命令,我们应该能看到类似以下的输出:

root@beaglebone:~# i2cdetect -y -r 2
 0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- 5a -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- -- 

在这里,我们可以看到一个地址为0x5a的设备已做出响应。

提示

请注意,您可能会获得不同的地址。在这种情况下,所有后续命令必须相应地进行修改。

通过查看数据表,我们发现温度可以通过读取设备位置0x07来获取。因此,使用i2cget命令,我们可以执行以下操作:

root@beaglebone:~# i2cget -y 2 0x5a 0x07 wp
0x3bab

现在,可以通过将其转换为十进制后,再乘以 0.02,来将输出值转换为摄氏度°C)。因此,我们可以使用以下命令:

root@beaglebone:~# echo "$(printf "ibase=16; %X\n" $(i2cget -y 2 0x5a 0x07 wp) | bc) * 0.02 - 273.15" | bc
32.11

为了更好地理解我们在前面的命令中所做的事情,下面我将通过一系列等效(且更易读)的命令来解释,从而展示如何通过使用i2cget从传感器获取数据,并将其存储到v_hex变量中:

v_hex=$(i2cget -y 2 0x5a 0x07 wp)

然后,我们使用bc命令将十六进制值转换为十进制,并将其存储在v_dec变量中,如下所示:

v_dec=$(printf "ibase=16; %X\n" $v_hex | bc)

最后,我们只需将v_dec变量中保存的十进制值乘以 0.02,就可以得到开尔文温度°K)。然后,再减去 273.15 就能得到摄氏度(°C):

echo "$v_dec * 0.02 - 273.15" | bc

现在,要测量体温,我们只需将传感器对准我们的头部,靠近太阳穴,并执行以下命令。我的输出如下:

root@beaglebone:~# echo "$(printf "ibase=16; %X\n" $(i2cget -y 2 0x5a 0x07 wp) | bc) * 0.02 - 273.15" | bc
34.97

太好了,我没有生病!

注意

读者可以参考《BeagleBone Essentials》一书,该书由本书的作者撰写,出版商为Packt Publishing,以获得更多关于如何激活和使用系统中可用的 I²C 总线的信息。

设置压力传感器

以下是压力传感器的示意图:

设置压力传感器

注意

这些设备可以从www.cosino.io/product/pressure-sensor购买,或者通过互联网搜索购买。

该设备的用户手册可在www.pololu.com/file/download/fsr_datasheet.pdf?file_id=0J383获得。

该设备可以检测(并测量)作用在其表面上的力。简单来说,它可以通过变化其内部电阻来报告压力强度。从数据手册中可以看到,当没有施加力时,该电阻可能超过 1 MΩ,而当施加力时,则可能下降到几百欧姆。

通过记住 BeagleBone Black 的 ADC 输入必须限制在 1.8V,我们可以使用以下电路安全地读取此传感器的数据(请参阅第二章,超声波停车助手):

设置压力传感器

在上图中,R=6.8 KΩRp 是压力传感器的内部电阻,它们由可变电阻表示。

提示

请注意,上述电路不仅将传感器连接到 BeagleBone Black 的 ADC 输入引脚,还防止该引脚的输入电压低于临界值 1.8V!V[ADCin]电压由以下公式给出:

V[ADCin] = R / (R + Rp + R) * Vcc = R / (2R + Rp) * Vcc

现在,我们知道Vcc是 3.3V,所以即使Rp值降到 0 Ω,V[ADCin]也等于 Vcc/2,即 1.65V,这是 BeagleBone Black 的 ADC 安全值。

该传感器必须连接到 BeagleBone Black 的AIN1输入引脚,该引脚标记为P9.40,另一端则必须连接到电阻 R,如前述电路图所示。

现在,为了检查所有连接,我们可以使用以下命令启用 BeagleBone Black 的 ADC:

root@beaglebone:~# echo cape-bone-iio > /sys/devices/bone_capemgr.9/slots

注意

可以通过使用书中的示例代码仓库中的bin/load_firmware.sh脚本来完成这些设置,具体如下:

root@beaglebone:~# ./load_firmware.sh adc

然后,我们可以使用以下命令读取传感器上的压力:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
2

上述值是因为传感器上没有任何物体;但是,如果我们只是尝试用手指触碰它,然后重新读取传感器,我们会得到以下结果:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
982

因此,传感器上的压力越高,返回的值也越高。

设置声音检测器

声音检测器与第五章中使用的相同,即WhatsApp 洗衣房监控,因此你可以查看同一章节中的硬件设置部分,了解如何设置和测试该设备。不过,为了完整性,关于它的一些基本信息再次提供,并且展示在下面的图像中:

设置声音检测器

注意

这些设备可以从 www.cosino.io/product/sound-detector 购买,或通过互联网浏览获得。

该板基于放大器 LMV324,数据手册可在 dlnmh9ip6v2uc.cloudfront.net/datasheets/Sensors/Sound/LMV324.pdf 上找到,板的原理图可在 dlnmh9ip6v2uc.cloudfront.net/datasheets/Sensors/Sound/sound-detector.pdf 上找到。

连接信息如下表所示:

引脚 声音传感器
P9.4 - VCC VCC
P9.39 - AIN0 R @ENVELOPE
P9.3 - GND GND

提示

请记住,ADC 输入必须限制在 1.8V,因此我们必须将传感器的输出电压缩小两倍,正如前一章所述。

现在,要检查所有连接,我们可以使用以下命令:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
24

如果你在重新运行命令时试图发声,你应该会得到更高的值,如下所示:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
201

所以,环境声音越大,返回的值就越高。

连接微型 LCD

本章使用的微型 LCD 如下图所示:

连接微型 LCD

注意

该设备可以从 www.cosino.io/product/color-tft-lcd-1-8-160x128 购买,或通过互联网浏览获取。

该 LCD 基于 ST7735R 芯片,数据手册可以在 www.adafruit.com/datasheets/ST7735R_V0.2.pdf 上找到。

如前所述,要连接 LCD,我们必须使用 SPI 总线和 BeagleBone Black 扩展连接器中可用的一些 GPIO。下表显示了 BeagleBone Black 引脚与 LCD 引脚之间的电气连接:

引脚 LCD 引脚
P9.4 - Vcc 9 - Vcc
P9.29 - MISO 未连接
P9.30 - MOSI 4 - MOSI
P9.31 - SCLK 3 - SCK
P9.28 - SS0 5 - TFT_CS
P9.25 7 - D/C
P9.27 8 - RESET
P8.19 1 - LITE
P9.2 - GND 10 - GND

要启用设备,我们可以使用一个驱动程序,该驱动程序应该已经在系统中可用。要验证它,只需使用以下命令:

root@beaglebone:~# zcat /proc/config.gz | grep -i st7735
CONFIG_FB_ST7735=y

在我的内核配置中,驱动程序是静态链接到内核中的,但将其作为模块也没问题。在这种情况下,输出应类似于以下输出:

CONFIG_FB_ST7735=m

检查驱动程序后,我们还需要一个合适的 DTS 文件来设置内核。为了避免从头开始编写,我通过 wget 命令从以下 URL 获取了一个合适的 DTS 文件:

root@beaglebone:~# wget https://raw.githubusercontent.com/beagleboard/devicetree-source/master/arch/arm/boot/dts/cape-bone-adafruit-lcd-00A0.dts

下载后,我们只需要使用以下命令编译前面的 DTS 文件:

root@beaglebone:~# dtc -O dtb -o /lib/firmware/cape-bone-lcd-00A0.dtbo -b 0 -@ cape-bone-adafruit-lcd-00A0.dts

现在,我们可以使用常规的 echo 命令启用 LCD:

root@beaglebone:~# echo cape-bone-lcd > /sys/devices/bone_capemgr.9/slots

提示

如果我们遇到以下错误,则需要禁用 HDMI 支持:

-bash: echo: write error: File exists

提示

这可以通过编辑 /boot/uboot/uEnv.txt 文件中的 u-boot 设置,然后启用以下行,方法是取消注释:

optargs=capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN

请注意,在某些 BeagleBone Black 版本中,uEnv.txt 文件可能位于 /boot 目录下,修改它的 u-boot 设置如下:

cape_disable=capemgr.disable_partno=BB-BONELT-HDMI,BB-BONELT-HDMIN

然后,我们只需重启系统。如果一切正确,我们应该能够无错误地执行前述命令。

如果一切正常,BeagleBone Black 应该启用一个彩色帧缓冲设备,宽度为 32 x 26 字符,并通过 /dev/fb0 设备在用户空间中表示。

注意

读者可以查看本书作者所写的《BeagleBone Essentials》,Packt Publishing,以获取更多关于如何激活和使用系统上可用的 SPI 总线、如何重新编译内核驱动程序以及 DTS 文件简要描述的信息。

最后,读者应记得,我们可以通过以下命令在 LCD 上打印字符串:

root@beaglebone:~# echo "Testing string" > /dev/tty0

这里,/dev/tty0 设备是连接到在 /dev/fb0 帧缓冲上运行的终端的设备。

最终图片

以下图片展示了我为实施这个项目并测试软件所制作的原型:

最终图片

请注意,非接触式温度传感器已经通过平带电缆连接到板上,方便将其移动到不同的物体上进行温度测量。

设置软件

在这个项目中,我们将展示一个在两进程之间交换数据的简单方法。在本章开始时提到,ADC 必须以 100Hz 的频率进行采样,但我们并不需要这么快就能在外部 LCD 上呈现一个简单的界面。实际上,用户界面的合理更新频率可以是 1Hz(每秒一次)。因此,为了保持代码简洁,我们通过使用两个不同频率运行的进程来实现我们的设备,它们相互交换数据,而不是使用单一进程。

简单来说,如果我们实现一个名为 adc 的程序,它以 100Hz 的频率从 ADC 读取数据,并以 1Hz 的频率将输出打印到 stdout 流(标准输出),我们可以将此输出重定向到另一个名为 lcd.sh 的程序,它以 1Hz 的频率从 stdin 流(标准输入)读取数据,并相应地绘制用户界面。

数据流是单向的。程序 adc 从 ADC 读取数据,并在经过处理阶段后,将其输出发送到 lcd.sh,该程序管理 LCD。这个特殊的功能可以通过 Unix 的 管道 很好地表示,如下所示:

root@beaglebone:~# ./adc | ./lcd.sh

读者还应注意,接口的定时是由 adc 程序生成的,它只是以预定的间隔打印输出,没有任何其他定时机制,直接输出到 lcd.sh 程序。所以,让我们看看如何做到这一点。

ADC 管理器

如前所述,要正确管理和分析 ADC 的输入信号,我们需要低 jitter 和细粒度的采样时间。已经提到,使用 Ts=10 ms 作为采样时间对于我们的目的足够了,那么让我们看看如何实现它!

在第五章《WhatsApp 洗衣房监视器》中,我们使用了一个简单的 Bash 脚本从 ADC 读取数据;但是对于那个原型,信号频率如此之低,以至于实现的解决方案非常可靠。现在,我们需要做一些更复杂的事情。这次,我们将使用 C 程序从 ADC 读取数据,如以下代码片段所示:

#define SYSFS_PRESSURE  "/sys/devices/ocp.3/helper.12/AIN1"
#define HZ              100
#define DELAY_US        (1000000 / HZ)

   /* Start sampling the ADC */
   while (1) {
      ret = clock_gettime(CLOCK_MONOTONIC_RAW, &t0);
      EXIT_ON(ret < 0);

      /* Read the ADC */
      fd = open(SYSFS_PRESSURE, O_RDONLY);
      EXIT_ON(fd < 0);
      ret = read(fd, ch, 5);
      EXIT_ON(ret < 1);
      close(fd);
      ret = sscanf(ch, "%d", &val);
      EXIT_ON(ret != 1);

      printf("%ld.%06ld %d\n", t0.tv_sec, t0.tv_nsec / 1000, val);

      /* Calculate the delay to sleep to the next period */
      ret = clock_gettime(CLOCK_MONOTONIC_RAW, &t);
      EXIT_ON(ret < 0);
      delay_us = DELAY_US - difftime_us(&t0, &t);
      EXIT_ON(delay_us < 0);
      usleep(delay_us);
   }

注意

完整代码可以在书籍示例代码库中的 chapter_06/adc_simple.c 文件中找到。

代码可以通过 make 命令直接在 BeagleBone Black 上编译。

代码的功能很简单。首先,我们使用 clock_gettime() 函数获取当前时间。然后,通过 sysfs 接口读取来自 ADC 的数据。最后,我们计算出在到达新的活动周期前需要休眠的时间。

运行前面的代码后,我们得到以下输出:

root@beaglebone:~/chapter_06# ./adc_simple
317330.142227 0
317330.153381 10
317330.163604 7
317330.174134 10
317330.184298 5
317330.194473 10
317330.204696 7
317330.214955 7
317330.225119 13
317330.235331 10
317330.245558 1
317330.255714 10
317330.265858 10
317330.276034 10
317330.286186 7
317330.296346 7
317330.306500 9
317330.316646 8
317330.326924 0
...

正如我们从前面的输出中看到的,程序相当精确;但是,如果我们使用一个简单的 awk 脚本来计算 1,000 个样本中的最小值、最大值和平均 jitter 值,我们会发现程序并不是那么精确:

root@beaglebone:~# ./adc_simple | awk -v T=0.01 -v N=1000 -f jitter.awk
avg=0.000255 min=0.000078 max=0.012252

注意

awk 脚本文件 jitter.awk 可以在书籍示例代码库中的 chapter_06/jitter.awk 文件中找到。

平均值和最小值是可以接受的,但最大值确实很高。此外,有时可能会发生以下情况:

root@beaglebone:~/chapter_06# ./adc_simple
319111.158747 0
319111.168981 8
319111.179131 9
319111.189269 10
319111.199439 10
319111.209586 11
…
319113.140526 0
adc_simple.c[  65]: main: fatal error in main() at line 65

当以下行中计算出的延迟变为负值时,会发生此错误:

        delay_us = DELAY_US - difftime_us(&t0, &t);
        EXIT_ON(delay_us < 0);

如果系统在周期开始时调度进程太慢,可能会发生我们没有足够的时间完成任务!这是因为我们没有使用实时系统,并且没有保证正确调度。

然而,我们可以尝试通过一些技巧来解决这个问题。Linux 内核并非实时系统,但它有一些可以帮助我们实现可接受折衷的功能。实际上,系统允许我们使用不同的调度器来管理 BeagleBone Black 上运行的进程。特别是,我们可以使用 chrt 命令来操作进程的实时调度属性,然后设置 FIFO 调度器,这可能有助于减少 jitter 值和调度延迟错误。如果我们使用 chrt 命令重新运行前面的测试,如下所示,我们将获得不同的结果:

root@beaglebone:~# chrt -f 99 ./adc_simple | awk -v T=0.01 -v N=1000 -f jitter.awk
avg=0.000102 min=0.000022 max=0.000781

此外,调度延迟错误消失了!

提示

请注意,即使使用 chrt 命令,Linux 内核仍然不是实时的,因此没有人能保证一切会永远顺利!为了使系统可靠,我们需要添加一些恢复代码,以防出现问题。

考虑到刚才所解释的,以下代码片段展示了可能的 ADC 管理器实现:

   /* Set stdout line buffered */
   setlinebuf(stdout);

   /* Do a dummy read to init the data buffers */
   c = read_adc(SYSFS_SOUND);
   for (snd_idx = 0; snd_idx < ARRAY_SIZE(snd); snd_idx++)
      snd[snd_idx] = c;
   c = read_adc(SYSFS_PRESSURE);
   for (prs_idx = 0; prs_idx < ARRAY_SIZE(prs); prs_idx++)
      prs[prs_idx] = c;

   /* Set FIFO scheduling */
   param.sched_priority = 99;
   ret = sched_setscheduler(getpid(), SCHED_FIFO, &param);
   EXIT_ON(ret < 0);

   /* Start sampling the ADC */
   snd_idx = prs_idx = 0;
   ret = clock_gettime(CLOCK_MONOTONIC_RAW, &t);
   EXIT_ON(ret < 0);
   while (1) {
      ret = clock_gettime(CLOCK_MONOTONIC_RAW, &t0);
      EXIT_ON(ret < 0);

      /* Read the data from the ADCs */
      snd[snd_idx] = read_adc(SYSFS_SOUND);
      prs[prs_idx] = read_adc(SYSFS_PRESSURE);

      /* Extract informations from buffered data */
      extract(snd, ARRAY_SIZE(snd), &snd_avg, NULL, NULL);
      extract(prs, ARRAY_SIZE(prs), &prs_avg, &prs_min, &prs_max);
      dbg("%ld.%06ld prs:%d min=%d max=%d snd:%d", t0.tv_sec, t0.tv_nsec / 1000, prs[prs_idx], prs_min, prs_max, snd[snd_idx]);

      /* We have to output the pressure data each second,
      * that is every HZ ticks.
      * Also we have to read the sound level...
      */
      if (ticks++ == 0)
         printf("%d %d %d\n", prs_avg, prs_max - prs_min, snd_avg);
         ticks %= HZ;

         /* Calculate the delay to sleep to the next period */
         ret = clock_gettime(CLOCK_MONOTONIC_RAW, &t);
         EXIT_ON(ret < 0);
         delay_us = DELAY_US - difftime_us(&t0, &t);
         EXIT_ON(delay_us < 0);
         usleep(delay_us);

         /* Move the index */
         prs_idx++;
         prs_idx %= ARRAY_SIZE(prs);
         snd_idx++;
         snd_idx %= ARRAY_SIZE(snd);
   }

注意

完整代码可以在书籍的示例代码库中的chapter_06/adc.c文件找到。

setlinebuf()函数用于强制在每一行输出时刷新,而sched_setscheduler()函数则用于启用 FIFO 调度器(就像chrt命令一样)。这段代码与之前的代码非常相似,唯一不同的是我们使用了extract()函数(在本章开头提到过)来计算输入数据的平均值、最小值和最大值。请注意,程序每秒打印一次输出,这要归功于ticks变量。

如果执行该程序,它将每秒打印几行,报告压力的平均值、压力信号的幅度以及声音的平均值,内容如下:

root@beaglebone:~# ./adc
0 16 20
0 19 21
1 21 21
2 22 23
3 22 23
4 22 24
...

所有这些数据都作为输入由lcd.sh进程获取,该进程将在下一节中描述。

注意

请注意,BeagleBone Black 的 ADC 具有连续模式功能,可以用来提高采样率,但由于该功能在所有内核中并非严格必要且并非所有内核都支持,因此我在这个项目中没有使用它。

好奇的读者可以通过processors.wiki.ti.com/index.php/AM335x_ADC_Driver's_Guide获取更多关于此主题的信息。

LCD 管理器

管理 LCD 的程序是一个简单的 Bash 脚本,它使用一些技巧来实现对采集数据的精美渲染。

如前所述,该程序每秒运行一次,感谢 ADC 管理器定期将其输出发送到lcd.sh程序。该程序的简单功能可以通过以下元代码表示:

while true ; do
   wait_for_data_from_ADC
   render_data_to_LCD
done

就这些!其他复杂性仅与我们希望如何实现用户界面相关。

关于这个问题,我决定使用一个非常简单的解决方案:一些终端转义序列来管理颜色,以及figlet程序来绘制大字体。转义序列通过echo命令轻松地在屏幕上打印带有指定颜色的字符,示例如下:

root@beaglebone:~# echo -e "\e[31mRED TEXT\e[39m"
RED TEXT

\e[31m序列设置红色,而\e[39m序列将颜色重置为默认值。

注意

欲了解有关这些序列的更多信息,一个很好的起点是en.wikipedia.org/wiki/ANSI_escape_code

figlet程序是一个可以在终端模拟打印大字体的工具,一种 ASCII 艺术。要安装它,可以使用以下命令:

root@beaglebone:~# aptitude install figlet

然后,使用方法非常简单,如以下示例所示:

root@beaglebone:~# figlet "simple string"
 _                 _            _        _ 
 ___(_)_ __ ___  _ __ | | ___   ___| |_ _ __(_)_ __   __ _
/ __| | '_ ` _ \| '_ \| |/ _ \ / __| __| '__| | '_ \ / _` |
\__ \ | | | | | | |_) | |  __/ \__ \ |_| |  | | | | | (_| |
|___/_|_| |_| |_| .__/|_|\___| |___/\__|_|  |_|_| |_|\__, |
 |_|                                  |___/

对于我们的用户界面实现,我使用了一些特殊的选项参数,由于篇幅限制,我不打算在这里解释,但有兴趣的读者可以查看figlet的 man 页面了解更多信息。

在这段简短的介绍之后,接下来是展示 lcd.sh 程序的主要代码片段:

# Ok, do the job
clear_scr

tick=1
while true ; do
   # Read the temperature from the sensor and convert it in C
   t=$(i2cget -y 2 0x5a 0x07 wp)
   t=$(hex2dec $t)
   t=$(echo "$t * 0.02 - 273.15" | bc)

   # Read the pressure and sound data from the " adc" tool
   read -u 0 v b s

   # Draw the GUI

   # Check for a minimum pressure, otherwise drop to 0 sound and
   # pressure data in order to not enable any alarm
   if [ $v -lt $PRS_AVG ] ; then
      s=0
      b=0
      enabled="false"
   else
      enabled="true"
   fi

   # Rewrite the screen
   goto_xy 0 0

   echo -en "[${CH_PULSE:$tick:1}] "
   echo -e "${FC_LIGHT_MAGENTA}BBB - BABY SENTINEL${FC_DEFAULT}\n"

   echo -en "TEMPERATURE (C):"
   if (( $(bc <<< "$t > 37.00") == 1 )) ; then
      echo -e "$FC_RED"
      t_alrm="true"
   else
      echo -e "$FC_GREEN"
      t_alrm="false"
   fi
   figlet -f small -W -r -w 32 "$t"
   echo -e "$FC_DEFAULT"

   echo -en "SOUND LEVEL:"
   if $enabled && [ $s -gt $SND_AVG ] ; then
      echo -e "$FC_RED"
      s_alrm="true"
   else
      echo -e "$FC_DEFAULT"
      s_alrm="false"
   fi
   figlet -f small -W -r -w 32 "$s"

   echo -en "BREATH LEVEL:"
   if $enabled && [ $b -lt $PRS_AMP ] ; then
      echo -e "$FC_RED"
      b_alrm="true"
   else
      echo -e "$FC_DEFAULT"
      b_alrm="false"
   fi
   figlet -f small -W -r -w 32 "$b"
   echo -en "${ES_CLEAR_LN}${FC_LIGHT_RED}ALARMS: ${FC_DEFAULT}"
   $t_alrm && echo -en "${BC_RED}TEMP. "
   $s_alrm && echo -en "${BC_RED}SOUND "
   $b_alrm && echo -en "${BC_RED}BREATH "
   echo -e "${BC_DEFAULT}"

   # Print some debugging messages if requested
   dbg "$(printf "t=%0.2f v=% 4d b=% 4d s=% 4d" $t $v $b $s)"
   dbg "PRS_AVG=$PRS_AVG PRS_AMP=$PRS_AMP SND_AVG=$SND_AVG"

   tick=$(( ($tick + 1) % ${#CH_PULSE} ))
done

注意

完整的代码可以在书籍示例代码库中的 chapter_06/lcd.sh 文件找到。

while 循环的开始到 read 语句,我们只是收集数据。然后,Draw the GUI 注释之后的代码只是为了渲染用户界面。请注意,带有 read 命令的那一行会等待直到来自 stdin 流的输入行到达,也就是来自 adc 程序的输入。

要通过 SSH 在我们主机系统的普通终端上测试它,并模拟 LCD,我们可以执行以下命令,将终端窗口的大小缩小为 32x26 个字符,这是 LCD 上终端的大小:

root@beaglebone:~# echo -e '\e8;26;32t'

提示

请注意,上面的命令只是另一个转义序列。

然后,我们可以按照如下方式执行程序:

root@beaglebone:~# ./adc | ./lcd.sh

输出如下面的截图所示:

![LCD 管理器

最终测试

为了测试原型,我使用了一些技巧来模拟婴儿:我在互联网上找到了哭声,并用音频播放器简单地播放出来。至于呼吸,我用一个娃娃,手动按压其胸部,配合我的呼吸。我承认这不是最好的测试方法,但我的孩子们太大了,无法帮我做这些实验!

要设置所有外设和驱动程序,我们可以使用 SYSINIT.sh,如以下命令所示:

root@beaglebone:~# ./SYSINIT.sh
done!

注意

这个命令可以在书籍示例代码库中的 chapter_06/SYSINIT.sh 文件找到。

然后,我使用以下命令行执行了 adclcd.sh 程序,以便将所有输出发送到运行在小型 LCD 上的终端:

root@beaglebone:~# ./adc | ./lcd.sh > /dev/tty0

提示

请注意,在第一个帧缓冲设备上,默认至少定义了一个终端,通过 /dev/tty0 设备引用它。

总结

在这一章中,我们发现了一种更可靠、更精确的方式来访问 BeagleBone Black 的 ADC,并学习了如何通过直接访问总线来访问 I²C 设备。这样做是为了能够管理压力传感器和无接触温度传感器。此外,我们还发现了如何通过 SPI 总线将一个小型 LCD 连接到我们的 BeagleBone Black 主板,以便添加一些用户界面。

在下一章中,我们将尝试实现一个植物监测器,来测量我们心爱的植物发生了什么!此外,我们还将发现如何定期拍摄一些照片,然后将其发布到 Facebook 账户上。

第七章:Facebook 植物监控

社交网络如今非常普遍,拥有一个与社交网络互动的监控(或控制)系统已成为必需,尤其对于消费系统来说。

在本章中,我们将学习如何实现一个植物监控器,能够测量太阳光、土壤湿度、土壤温度(包括内部和外部温度),并且通过网络摄像头在特定时间间隔拍摄照片。

用户可以通过 Web 界面控制监控器,并且可以决定是否将植物的照片发布到他们的 Facebook 时间线。

功能原理

在这个项目中,我将展示一个简单的植物监控系统实现,其中包含以下两个特殊功能:

  • 第一个功能是能够根据专用传感器直接测量的湿度,以及通过植物所在花园花盆内外土壤温度差异来估算土壤湿度。这是因为湿度传感器的内部电阻可能会随温度变化;实际上,当阳光照射到土壤上,土壤温度升高时,电阻会发生变化,这种效应会导致出现错误的读数。因此,我们使用两个不同的温度探头,以便判断土壤是否相对于内部土壤温度过热,从而调整湿度水平。

  • 第二个功能是能够添加一个网络摄像头,在规定的时间间隔拍摄我们心爱的植物的照片,并根据用户的要求将这些照片发布到我们的 Facebook 时间线,向朋友们展示我们有多么的绿色大拇指!

以下是传感器位置的简单示意图,以便正确实现第一个功能:

功能原理

假设 Te 是外部土壤温度,Ti 是内部土壤温度。如果我们将湿度传感器返回的值命名为 M,则可以通过以下公式给出湿度的合理估计值 (Me):

Me = M + K * (Te – Ti) 当 Te > Ti

Me = M 当 Te ≤ Ti

其中 K 是一个合适的(且经验性的)湿度系数,用户可以在运行时设置,以满足他们的需求。请注意,如果 K0,系统将直接采用测得的湿度水平,不进行任何补偿。

关于第二个功能,可以拍摄被监控植物的照片并将其发布到 Facebook。为了做到这一点,我们必须使用 Facebook API 来与 Facebook 帐户进行交互。这个步骤将在下一节中详细解释。

设置硬件

在本项目中,我们将使用两个模拟传感器、两个数字传感器和一个网络摄像头来拍照。模拟传感器连接到两个不同的 ADC 输入通道(如前一章所述)。非接触式外部温度传感器通过 I²C 总线与 BeagleBone Black 通信,而防水内部温度传感器使用 1-Wire 总线。最后,网络摄像头通过USB 总线连接。

如前一章所述,我可以添加一些执行器来给植物灌水等;但由于空间不足,我决定将这些任务留给读者作为练习。

连接湿度传感器

湿度传感器就是下图所示的设备:

连接湿度传感器

注意

该设备可以从www.cosino.io/product/moisture-sensor购买,或者通过浏览互联网来获取。

该设备的用户指南可以在seeedstudio.com/wiki/Grove_-_Moisture_Sensor获取。

该设备与第三章中的水传感器水族馆监控非常相似,因为它的工作原理仍然基于水的电导率;然而,由于我们关注的是土壤湿度水平,而非水的存在与否,其形状有所不同。

连接到 BeagleBone Black 的方式如下表所示:

引脚 电缆颜色
P9.3 - Vcc 红色
P9.39 - AIN0 黄色
P9.1 - GND 黑色

使用万用表,我们可以验证当传感器处于空气中时输出电压接近 0V,当传感器处于水中时输出电压约为 1.5V,而其他所有输出值都保持在此范围内。

请注意,由于最大输出电压约为 1.5V,我们可以安全地将该设备的输出引脚直接连接到 BeagleBone Black 的 ADC 输入。然而,我们可以在AIN0引脚和 GND 之间使用一个 1.8V 的齐纳二极管,以确保输出电压不会超过 1.8V 的临界值。(记得前几章中多次提到过的这个问题,尤其是从第二章,超声波停车助手开始。)

现在,为了验证设备输出,我们可以通过以下命令启用 BeagleBone Black 的 ADC:

root@beaglebone:~# echo cape-bone-iio > /sys/devices/bone_capemgr.9/slots

注意

这些设置可以通过使用书中示例代码库中的bin/load_firmware.sh脚本来完成,如下所示:

root@beaglebone:~# ./load_firmware.sh adc

然后,我们可以将传感器放入水杯中读取水中的湿度水平,并执行以下命令:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
1745

然后,我们可以将传感器从水中取出,并重新运行以下命令来读取空气中的湿度水平:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN0
1

所以,湿度水平越高,返回的值也就越高。

连接光传感器

下图展示了一个环境光传感器:

连接光传感器

注意

该设备可以从 www.cosino.io/product/photoresitor 购买,或者通过上网查找。

该设备的用户指南可以在 www.sparkfun.com/datasheets/Sensors/Imaging/SEN-09088-datasheet.pdf 查阅。

该设备的功能与 第五章 中介绍的光传感器非常相似,WhatsApp 洗衣房监控器;然而,这是一个低功耗的单个光敏电阻,输出功能与 第六章 中介绍的压力传感器非常相似,宝宝房间守卫,因此,即使在这种情况下,我们也可以使用相同的电路进行管理,如下图所示:

连接光传感器

在上面的示意图中,R=6.8 KΩRp 是光传感器的内部电阻,由一个可调电阻表示。

小贴士

请注意,上面的电路不仅将传感器连接到 BeagleBone Black 的 ADC 输入引脚,还确保该引脚上的输入电压低于 1.8V 的临界值!实际上,V[ADCin] 电压由以下公式给出:

  • V[ADCin] = R / (R + Rp + R) * Vcc = R / (2R + Rp) * Vcc

现在我们知道 Vcc 为 3.3V,因此,即使 Rp 的值降到 0Ω,V[ADCin] 也等于 Vcc/2,即 1.65V,这是 BeagleBone Black 的 ADC 安全值。

该传感器必须连接到 BeagleBone Black 的 AIN1 输入引脚,该引脚标记为 P9.40,而另一端必须连接到电阻 R,如上面的电路图所示。

现在对于所有连接,我们可以使用以下命令:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
317

现在,正如我们在前几章中对其他光传感器所做的那样,我们可以用一个杯子将其覆盖,然后重新读取 ADC:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
15

另一方面,如果我们在传感器上方打开一盏灯,我们将得到以下结果:

root@beaglebone:~# cat /sys/devices/ocp.3/helper.12/AIN1
954

因此,环境光越亮,返回的数值越高。

设置非接触温度传感器

非接触温度传感器与 第六章 中使用的相同,宝宝房间守卫,因此你可以查看 设置非接触温度传感器 部分,了解如何设置和测试该设备;但是,为了完整性,以下再次提供一些基本信息:

设置非接触温度传感器

注意

该设备可以从 www.cosino.io/product/contactless-temperature-sensor 购买,或者通过上网查找。

此设备的用户手册可在www.sparkfun.com/datasheets/Sensors/Temperature/SEN-09570-datasheet-3901090614M005.pdf中找到。

连接信息如下表所示:

引脚 温度传感器引脚
P9.4 - Vcc 3 - Vdd
P9.17 - SCL 1 - SCL
P9.18 - SDA 2 - SDA
P9.2 - GND 3 - Vss

现在,如果一切连接正确,我们可以使用以下命令激活 I²C 总线:

root@beaglebone:~# echo BB-I2C1 > /sys/devices/bone_capemgr.9/slots

温度可以通过读取设备位置0x07来获取,因此使用i2cget命令,我们可以执行如下操作:

root@beaglebone:~# i2cget -y 2 0x5a 0x07 wp
0x3bab

输出值必须通过乘以 0.02 并转换为十进制值后转化为摄氏度,因此我们可以使用以下命令:

root@beaglebone:~# echo "$(printf "ibase=16; %X\n" $(i2cget -y 2 0x5a 0x07 wp) | bc) * 0.02 - 273.15" | bc

设置防水温度传感器

关于内部温度传感器,我使用的是与第三章,水族箱监控器中使用的相同传感器,因此您可以查看连接温度传感器部分,了解如何设置和测试此设备;不过,为了完整起见,以下是一些基本信息:

设置防水温度传感器

注意

该设备可以从www.cosino.io/product/waterproof-temperature-sensor购买,或者通过上网搜索找到。

该设备的数据手册可在datasheets.maximintegrated.com/en/ds/DS18B20.pdf中找到。

连接信息如下表所示:

引脚 电缆颜色
P9.4 - Vcc 红色
P8.11 - GPIO1_13 白色
P9.2 - GND 黑色

要启用它,我们必须通过以下命令将适当的 DTS 文件加载到内核中:

root@beaglebone:~# echo BB-W1-GPIO > /sys/devices/bone_capemgr.9/slots

注意

请参见第三章,水族箱监控器,了解如何获取 DTS 文件。

如果一切正常,我们应该在/sys/bus/w1/devices/目录下看到一个新的 1-Wire 设备,如下所示:

root@beaglebone:~# ls /sys/bus/w1/devices/
28-000004b541e9  w1_bus_master1

新的温度传感器由名为28-000004b541e9的目录表示,若要读取当前温度,我们可以使用cat命令访问w1_slave文件,如下所示:

root@beaglebone:~# cat /sys/bus/w1/devices/28-000004b541e9/w1_slave
d8 01 00 04 1f ff 08 10 1c : crc=1c YES
d8 01 00 04 1f ff 08 10 1c t=29500

在前面的示例中,当前温度为t=29500,表示为毫度摄氏度m°C),因此它相当于 29.5°C。

提示

请注意,您的传感器有不同的 ID,因此,在您的系统中,您将得到不同的路径名称,形式为/sys/bus/w1/devices/28-NNNNNNNNNNNN/w1_slave

设置摄像头

在第三章中,水族馆监视器,我解释了如何使用网络摄像头与 BeagleBone Black,你可以查看该章节来完成这项任务。然而,在那一章中,我要求使用基于 UVC 的网络摄像头(或至少是mjpg-streamer工具支持的其他摄像头)。但这次,你可以使用任何 Video4Linux 驱动类支持的摄像头。

注意

好奇的读者可以从www.linuxtv.org/wiki/index.php/Main_Page获取关于 Video4Linux 驱动程序的更多信息。

要知道我们的网络摄像头是否受 Video4Linux 系统支持,只需将其连接到 BeagleBone Black 的 USB 主机端口,然后检查是否添加了新的/dev/video X设备(其中X可以是 0、1、2 等)。

例如,在我的系统上,使用与第三章中相同的网络摄像头,水族馆监视器,我得到了以下输出:

root@beaglebone:~# ls -l /dev/video*
crw-rw---T 1 root video 81, 0 Jan  1  2000 /dev/video0

所以,我的网络摄像头由 Video4Linux 子系统支持,可以通过fswebcam程序来拍照。要安装该程序,可以使用以下命令:

root@beaglebone:~# aptitude install fswebcam

然后,我们可以通过以下命令拍摄简单的图片:

root@beaglebone:~# fswebcam webcam-shot.jpg
--- Opening /dev/video0...
Trying source module v4l2...
/dev/video0 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 352x288.
--- Capturing frame...
Captured frame in 0.00 seconds.
--- Processing captured image...
Writing JPEG image to 'webcam-shot.jpg'.

提示

当执行fswebcam程序时,可能会收到以下消息:

--- Opening /dev/video0...
Trying source module v4l2...
/dev/video0 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 352x288.
--- Capturing frame...
gd-jpeg: JPEG library reports unrecoverable error: Not a JPEG file: starts with 0x11 0x80
Captured frame in 0.00 seconds.
--- Processing captured image...
Writing JPEG image to 'webcam-shot.jpg'.

然后,当你尝试显示图片时,图片是黑色的。要解决这个问题,诀窍是使用-S选项参数跳过前几帧,如下所示:

root@beaglebone:~# fswebcam -S 10 webcam-shot.jpg

然而,这个程序真正有趣的地方在于可以在图片的不同位置添加文本。例如,通过以下命令,我们可以拍摄一张图片,在右下角显示文本图片标题,在左下角显示文本信息文本,并带有时间戳,使用更小的字体:

root@beaglebone:~# fswebcam --title "Picture's title" --info "Information text" --jpeg 85 webcam-shot.jpg
--- Opening /dev/video0...
Trying source module v4l2...
/dev/video0 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 352x288.
--- Capturing frame...
Captured frame in 0.00 seconds.
--- Processing captured image...
Setting title "Picture's title".
Setting info text "Information text".
Setting output format to JPEG, quality 85
Writing JPEG image to 'webcam-shot.jpg'.

输出图片如下截图所示:

设置网络摄像头

正如你所见,这是一个用来带有描述信息拍照的优秀软件!

添加水泵

由于我们知道地板上是否存在水,当检测到缺水时,我们还可以决定实施一个自动灌溉系统来为我们的植物浇水。一个可能的解决方案是使用水泵,就像在第三章中已经做过的那样。然后,我们可以通过使用 BeagleBone Black 的 GPIO 控制一个或多个继电器来控制几个水泵,但是,正如已经提到的,由于空间不足,我不打算在这里做这件事,因此,将此问题留给读者作为一个有用的练习。

最终图片

以下图片展示了我制作的原型,用于实施这个项目并测试软件:

最终图片

这次连接非常简单。还要注意,所有传感器应该用长电缆连接到 BeagleBone Black,以便轻松放置在植物花盆中。

设置软件

这一次,我们监控(并可能控制)一个非常慢的系统(一个生长中的植物)。因此,使用一个简单的 Bash 脚本是非常合适的,而 Web 界面可以通过两个小的 HTML(带 JavaScript)和 PHP 脚本来实现。除了访问外围设备的难度外,真正的难点是使用 Facebook API 来访问一个账户。

关于监控循环和 Web 界面,有一个主要问题,即用于交换数据的进程间通信IPC)系统。在第一章《危险气体传感器》中,我们主要使用了 MySQL 服务器来存储系统的数据和配置设置,但我们也利用它在构成系统的不同任务之间交换数据!然而,这一次,我们要存储的数据非常少,使用数据库可能非常昂贵。因此,我决定使用一种简单的方法来解决这个问题:我打算使用一个文件!是的,通过使用一个具有良好定义的内部结构和适当锁定功能的普通文件来序列化对其的访问,我们可以用极少的系统资源解决这个问题。这个解决方案将在后续部分详细解释。

Facebook API

developers.facebook.com/docs/reference/php/5.0.0上,有一本用户手册,介绍了我用来访问 Facebook 账户的 PHP API。

注意

实际上,有不同的 API 可以访问 Facebook,关于这些 API 的更多信息,读者可以参考developers.facebook.com/docs/apis-and-sdks

阅读的内容很多,因此在接下来的部分,我列出了我安装和配置它时所遵循的所有步骤。

下载代码

首先,我们必须下载源代码。有两种方法可以做到这一点:通过Composer安装和手动安装。我决定使用手动安装方法,因为我的 BeagleBone Black 上没有 Composer 支持(而且我也不想为了这么一个简单的任务去安装它)。

注意

Composer 工具是一个 PHP 依赖管理工具;好奇的读者可以参考getcomposer.org/以获取更多信息。

代码可以通过在主机 PC 上的浏览器中点击Download the SDK for PHP v5.0按钮,或者直接在 BeagleBone Black 上使用以下命令下载:

root@beaglebone:~# wget https://github.com/facebook/facebook-php-sdk-v4/archive/5.0-dev.zip

注意

该归档的副本可以在书籍示例代码仓库中的chapter_07/5.0-dev.zip文件中找到。

下载完成后,将归档文件放置在 BeagleBone Black 上(如果尚未存在),然后使用以下命令解压:

root@beaglebone
:~# unzip 5.0-dev.zip

提示

如果未安装,可以通过常用的aptitude命令获取unzip命令。

现在,要使用该 API,我们只需在所有脚本中使用以下两行:

define('FACEBOOK_SDK_V4_SRC_DIR', __DIR__ . '/facebook-php-sdk-v4-5.0-dev/src/Facebook/');
require_once __DIR__ . '/facebook-php-sdk-v4-5.0-dev/src/Facebook/autoload.php';

为了简化操作,我创建了一个名为setup.php的文件,在其中放入了这些行,这样我就可以在所有脚本中简单地包含它,如下所示:

require_once "setup.php";

注意

前面的文件可以在书籍示例代码库中的chapter_07/setup.php文件中找到。

创建新的 Facebook 应用

现在,我们需要一个新的应用来使用 API。事实上,所有对 Facebook 的访问都必须通过一个专用的应用程序进行,以下是创建新应用所需的步骤:

  1. 访问 Facebook 开发者页面:developers.facebook.com/apps/?action=create,然后点击添加新应用按钮。

  2. 选择网站选项。

  3. 为你的应用输入一个名称(我使用的是BBB Plant Monitor),然后点击创建新的 Facebook 应用 ID按钮。

  4. 选择一个类别(我使用的是娱乐),然后点击创建应用 ID

  5. 站点 URL中填写http://localhost,然后点击下一步

    提示

    请注意,为了能够按照前面的步骤操作,你需要一个预先存在的 Facebook 账户,否则系统会要求你先注册再继续。

你的应用现在已经创建。接下来,在下一步部分,点击跳转到开发者仪表板,然后应该出现如下截图所示的面板:

创建新的 Facebook 应用

应用 ID默认是可见的,但应用密钥是隐藏的。点击显示,再次输入密码(如果系统要求),以显示应用密钥,然后复制并保留此信息。

现在,为了完成工作,点击设置,将http://localhost添加到应用域名中,将有效的电子邮件地址添加到联系电子邮件中,然后点击保存更改

获取 Facebook 账户访问权限

现在我们的应用程序已经准备好,我们可以开始在其上尝试一些代码。然而,在上线之前,我们可以创建一个具有不同权限的测试用户账户,这样它就不会干扰我们的真实 Facebook 账户,以确保一切设置正确(在角色菜单下,然后是测试用户部分)。

以下截图显示了测试用户面板:

获取 Facebook 账户访问权限

点击编辑按钮,并从显示的列表中选择为此测试用户获取访问令牌选项,接受默认设置,然后复制并保留该访问令牌。

现在是测试我们第一个脚本的时候了!以下是一个用于获取 Facebook 账户基本信息的简单脚本代码片段:

# Define the Facebook session
$fb = new Facebook\Facebook([
   'app_id'                  => APP_ID,
   'app_secret'              => APP_SECRET,
   'default_graph_version'   => 'v2.4',
   'default_access_token'    => DEF_TOKEN,
   'fileUpload'              => true,
   'cookie'                  => true,
]);

# Print user's information
try {
   $res = $fb->get('/me');
} catch(Exception $e) {
   err("error!\n");
   dbg("=========================================================================\n");
   dbg($e);
   dbg("=========================================================================\n");
   die();
}
$node = $res->getGraphObject();
info("name is \"%s\" (%s)\n",
   $node->getProperty('name'), $node->getProperty('id'));

# Print user's permissions
$res = $fb->get("/me/permissions");
$node = $res->getDecodedBody();
info("permissions are:\n");
foreach ($node['data'] as $perm)
   info("\t%s is %s\n", $perm['permission'], $perm['status']);

注意

完整代码可以在书籍示例代码库中的chapter_07/get_info.php文件中找到。

这个脚本非常简单。作为第一步,我们需要使用Facebook\Facebook类定义一个新的 Facebook 会话,其中APP_IDAPP_SECRET的值来自前面的应用信息,而DEF_TOKEN是之前生成的用户测试令牌。所有这些信息都存储在一个名为config.php的专用文件中,代码如下:

<?php

define('APP_ID', '000000000000000');
define('APP_SECRET', '00000000000000000000000000000000');
define('DEF_TOKEN', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX');

?>

提示

请注意,所有信息已被替换为零或X字符,因此您需要将其替换为自定义值。

然后,文件可以像前面的命令一样通过以下 PHP 命令加载:

require_once "config.php";

一旦创建了 Facebook 会话,我们可以通过使用get()方法并传入不同的参数来开始从中获取数据。例如,通过使用/me字符串,我们可以获取用户的姓名和 ID,而使用/me/permissions字符串,我们可以获取用户的权限。

注意

Facebook 的权限列表和解释可以在developers.facebook.com/docs/facebook-login/permissions/v2.2查看。

一旦收集完成,用户的信息将显示如下:

root@beaglebone:~# ./get_info.php
get_info.php[  62]: name is "Open Graph Test User" (119617175051891)
get_info.php[  67]: permissions are:
get_info.php[  69]:    user_friends is granted
get_info.php[  69]:    publish_actions is granted
get_info.php[  69]:    public_profile is granted

API 正常,访问信息已经正确写入!现在,让我们看看如何在测试用户的时间线上发布一张图片。为了实现这个功能,我们必须使用post()方法,如下所示的代码片段:

# Define the Facebook session
$fb = new Facebook\Facebook([
   'app_id'                  => APP_ID,
   'app_secret'              => APP_SECRET,
   'default_graph_version'   => 'v2.4',
   'default_access_token'    => DEF_TOKEN,
   'fileUpload'              => true,
   'cookie'                  => true,
]);

# Print user's information
try {
   $res = $fb->get('/me');
} catch(Exception $e) {
   err("error!\n");
   dbg("=========================================================================\n");
   dbg($e);
   dbg("=========================================================================\n");
   die();
}
$node = $res->getGraphObject();
info("name is \"%s\" (%s)\n",
   $node->getProperty('name'), $node->getProperty('id'));

# Publish to user's timeline
try {
   $ret = $fb->post('/me/photos', array(
      'message'   => 'MyPlant message',
      'source'   => $fb->videoToUpload(realpath('web-cam-shot.jpg')),
   ));
} catch(Exception $e) {
   err("error!\n");
   dbg("=========================================================================\n");
   dbg($e);
   dbg("=========================================================================\n");
   die();
}

info("done\n");

在创建 Facebook 会话后,如前所述,我们必须调用post()方法,使用/me/photos字符串和适当的参数。特别是,您应该注意videoToUpload()方法,它用于指定要发布的图片。

现在,假设图片保存在webcam-shot.jpg文件中,我们可以使用以下命令进行发布:

root@beaglebone:~# ./post_pic.php
post_pic.php[  62]: name is "Open Graph Test User" (119617175051891)
post_pic.php[  78]: done

为了验证发布内容,我们可以通过选择作为此测试用户登录选项,使用之前生成的测试用户账户登录。以下截图显示了我在测试中得到的结果:

获取 Facebook 账户的访问权限

完成后,您必须从此测试账户登出,然后使用您的账户重新登录。

现在我们准备好了。在您是否希望将此应用及其所有实时功能向公众开放?部分点击状态与审核菜单,然后点击。接着,我们需要一个 Facebook 配置文件的访问令牌,因此我们必须进入工具与支持顶端菜单,然后选择Graph API Explorer选项。在新页面中,点击Graph API Explorer下拉列表,然后选择我们的新应用BBB Plant Monitor。接着,点击获取令牌下拉列表,选择获取访问令牌

在新窗口中,您可以添加任何权限;然而,我们只需要扩展权限publish_actions,因此请启用它,然后点击获取访问令牌按钮。

此时,应该会弹出一个授权窗口,如下图所示(抱歉,窗口中有非英文文本):

获取 Facebook 账户访问权限

然后,批准并选择新应用的受众(仅限你自己、只有你的朋友,或公开。别担心——这个设置可以稍后从你的个人资料设置中更改)。

现在,获取新的访问令牌并将其复制到config.php文件中,然后重新运行get_info.php脚本。如果一切正常,你应该看到类似下面的内容:

root@beaglebone:~# ./get_info.php
get_info.php[  62]: name is "Rodolfo Giometti" (10206138948545992)
get_info.php[  67]: permissions are:
get_info.php[  69]:    publish_actions is granted
get_info.php[  69]:    public_profile is granted

很好,现在我可以尝试通过使用post_pic.php脚本将一张图片发布到我的 Facebook 时间线,如下所示:

root@beaglebone:~# ./post_pic.php
post_pic.php[  62]: name is "Rodolfo Giometti" (10206138948545992)
post_pic.php[  78]: done

以下截图显示了我的 Facebook 时间线的一部分,包含了新的帖子:

获取 Facebook 账户访问权限

好的,看起来似乎工作得不错!但是,有一个问题。如果你点击蓝色圆圈中的i(在新访问令牌的开始处),一个窗口会弹出,显示令牌的过期时间(也就是令牌将有效的时间),如你所见,这个时间非常短!通常只有 1 到 2 个小时。

参见以下截图作为示例:

获取 Facebook 账户访问权限

为了增加这个时间,我们可以点击在访问令牌工具中打开按钮,然后点击延长访问令牌按钮请求一个扩展版。接着,将发布一个新的扩展令牌,如下图所示(注意,系统可能会要求你再次输入个人资料密码才能为你提供扩展令牌;如果是这样,请再次输入密码):

获取 Facebook 账户访问权限

新令牌现在有效期为 60 天。

不幸的是,我还没有找到合适的方式来自动化此过程,或者在当前令牌过期时更新令牌。当发生这种情况时,你需要重复此过程以获取一个新的有效令牌,然后相应地更新config.php文件。

提示

可能会遇到执行命令时出现错误的情况。在这种情况下,你可以通过使用-d选项参数启用调试信息,然后再次尝试执行命令,如下所示:

root@beaglebone:~# ./get_info.php -d
get_info.php[  54]: error!
get_info.php[  55]: ==========================================================================
get_info.php[  56]: exception 'Facebook\Exceptions\FacebookSDKException' with message 'SSL certificate problem: certificate is not yet valid' in /root/chapter_07/facebook-php-sdk-v4-5.0-dev/src/Facebook/HttpClients/FacebookCurlHttpClient.php:83
Stack trace:
#0 /root/chapter_07/facebook-php-sdk-v4-5.0-dev/src/Facebook/FacebookClient.php(216): Facebook\HttpClients\FacebookCurlHttpClient->send('https://graph.f...', 'GET', '', Array, 60)
#1 /root/chapter_07/facebook-php-sdk-v4-5.0-dev/src/Facebook/Facebook.php(504): Facebook\FacebookClient->sendRequest(Object(Facebook\FacebookRequest))
#2 /root/chapter_07/facebook-php-sdk-v4-5.0-dev/src/Facebook/Facebook.php(377): Facebook\Facebook->sendRequest('GET', '/me', Array, NULL, NULL, NULL)
#3 /root/chapter_07/get_info.php(52): Facebook\Facebook->get('/me')
#4 {main}get_info.php[  57]: ==========================================================================

这与 Facebook API 本身无关,而是与 SSL 证书的身份验证阶段有关。一个可能的解决方法是通过使用以下补丁实现的。然而,这会降低整个 API 的安全级别。请注意!

root@beaglebone# diff -u facebook-php-sdk-v4-5.0-dev/src/Facebook/HttpClients/FacebookCurlHttpClient.php.orig facebook-php-sdk-v4-5.0-dev/src/Facebook/HttpClients/FacebookCurlHttpClient.php
--- facebook-php-sdk-v4-5.0-dev/src/Facebook/HttpClients/FacebookCurlHttpClient.php.orig   2014-04-26 01:34:08.187500961 +0000
+++ facebook-php-sdk-v4-5.0-dev/src/Facebook/HttpClients/FacebookCurlHttpClient.php   2014-04-26 01:34:37.582032215 +0000
@@ -111,7 +111,7 @@
 CURLOPT_RETURNTRANSFER => true, // Follow 301 redirects
 CURLOPT_HEADER => true, // Enable header processing
 CURLOPT_SSL_VERIFYHOST => 2,
-            CURLOPT_SSL_VERIFYPEER => true,
+            CURLOPT_SSL_VERIFYPEER => false,
 CURLOPT_CAINFO => __DIR__ . '/certs/DigiCertHighAssuranceEVRootCA.pem',
 ];

监控循环

现在 Facebook API 已经正常运行,我们可以开始编写代码来实现我们的植物监控器。

如前所述,监控系统非常慢,因此使用 Bash 脚本可能是解决问题的最快捷粗暴方法。实际上,我们只需要从所有已安装的传感器读取植物数据,然后执行一些简单的操作。最困难的部分是创建一个包含所有测量数据的状态文件,该文件将与网页接口进行交换(有关这一部分,请参见下一节)。

监控循环位于书中的示例代码仓库中的chapter_07/plant_mon.sh文件中,相关代码显示如下:

function daemon_body () {
   # Read plant data and take the first picture
   read_sensors
   next_date=$(do_picture)

   # The main loop
   dbg "start main loop"
   while sleep 1 ; do
      # Read plant data from all sensors
      read_sensors

      ( # Wait for lock on LOCK_FILE (fd 99) for 10 seconds
      flock -w 10 -x 99 || exit 1

      # Read the user parameters
      cff_mois=$(cat $STATUS_FILE | json_decode cff_mois)
      [ -z "$cff_mois" ] && cff_mois=1
      dbg "cff_mois=$cff_mois"

      # Compute the moisture level
      est_mois=$msr_mois
      if (( $(bc <<< "$int_temp < $ext_temp") == 1 )) ; then
         est_mois=$(bc -l <<< "$msr_mois + $cff_mois * ( $ext_temp - $int_temp )")
         fi
         dbg "est_mois=$est_mois"

         # Write back the plant parameters
         json_encode lig_levl $lig_levl \
            int_temp $int_temp \
            ext_temp $ext_temp \
            msr_mois $msr_mois \
            cff_mois $cff_mois \
            est_mois $est_mois > $STATUS_FILE

         # Release the lock
         ) 99>$LOCK_FILE

         # Have to take a new picture?
         [ $(date "+%H%M") == "$next_date" ] && next_date=$(do_picture)
     done
}

如你所见,执行时有三个主要步骤:

  1. 从所有传感器读取植物数据。

  2. 通过管理由STATUS_FILE变量指向的文件来交换数据。

  3. 根据用户输入拍摄新照片。

第一步通过read_sensors函数实现,代码如下:

function read_sensors ( ) {
   lig_levl=$(adc_read $LIGHT_DEV)
   int_temp=$(w1_read $INT_TEMP_DEV)
   ext_temp=$(i2c_read $EXT_TEMP_DEV)
   msr_mois=$(adc_read $MOISTURE_DEV)
   dbg "lig_levl=$lig_levl int_temp=$int_temp ext_temp=$ext_temp msr_mois=$msr_mois"
   dbg "curr_date=$(date "+%H%M") next_date=$next_date"
}

最后一步由do_picture函数实现,代码如下:

function do_picture ( ) {
   # Compute the light level
   ll="mid"
   [ $lig_levl -le $LIGHT_LOW ] && ll="low"
   [ $lig_levl -ge $LIGHT_HIGH ] && ll="high"

   # Take the picture
   fswebcam -q --title "My lovely plant" \
      --info "Temp: $ext_temp/$int_temp°C - Light: $ll" \
      --jpeg 85 $IMG_FILE

   # Compute the next picture time
   date -d "now +$1 minutes" "+%H%M"
}

第二个操作需要一些解释。如前所述,我们必须将植物的数据发送到网页接口。从网页接口中,我们需要读取一个输入值(湿度系数)。为了做到这一点,我决定使用普通文件,因为所有操作执行得非常慢。

使用文件交换数据的唯一困难在于我们必须确保对该文件具有独占访问权限。为此,我们可以使用flock()系统调用向系统请求对文件的独占访问,从而排除并发读取或写入操作。

要在 Bash 脚本中使用flock(),我们有flock命令,当它在 shell 脚本中使用时,必须按照flock的 man 页面中的建议进行使用。代码如下:

   (
      flock -n 9 || exit 1
      # ... commands executed under lock ...
   ) 9>/var/lock/mylockfile

提示

请参阅man flock命令获取更多信息。

获取锁后,我们可以开始读取文件。该文件以 JSON 格式保存系统状态,因为文件内容必须与一个 PHP 应用共享,而该应用具有预定义的函数来管理此格式。因此,首先我们通过json_decode函数读取用户输入,然后使用本章开始时描述的公式计算预估的湿度水平,最后,使用json_encode函数将状态文件以 JSON 格式写回。

要执行程序并启用所有调试信息,我们可以使用以下命令行:

root@beaglebone:~# ./plant_mon.sh -d -l -f -k 1
plant_mon.sh: min=1
plant_mon.sh: signals traps installed
plant_mon.sh: lig_levl=295 int_temp=29.50 ext_temp=31.01 msr_mois=0
plant_mon.sh: curr_date=1109 next_date=
plant_mon.sh: start main loop
plant_mon.sh: lig_levl=304 int_temp=29.50 ext_temp=31.11 msr_mois=0
plant_mon.sh: curr_date=1109 next_date=1110
plant_mon.sh: cff_mois=50
plant_mon.sh: est_mois=80.50
...

然后,可以通过按下CTRL + C键停止程序。

提示

请记得修改定义INT_TEMP_DEV变量的那一行,该变量存储了 1-Wire 温度传感器的 ID,根据你传感器的 ID 进行修改,否则在执行程序时会出现读取错误。

请注意,作为第一步,程序读取所有传感器的数据,然后拍照,以便网页接口获取它所需要的任何内容,以便向用户显示当前系统状态,具体细节见下一节。

网页接口

这一次,我将使用 HTML、PHP 和 JavaScript 语言实现一个简单的网页界面。我想实现的功能非常简单,允许用户查看植物的数据以及系统中存储的植物最后一张图片。然后,用户应该能够将这张图片发布到 Facebook 时间线上。

网页界面的核心在书中示例代码库的 chapter_07/plant.html 文件中,相关代码显示在下面的代码片段中:

<body>
 <h1>Plant monitor status</h1>

 <h2>Internal variables</h2>

 <table class="status">
  <tr class="d0">
    <td>Light level</td>
    <td><b id="lig_levl">0</b></td>
    <td></td>
  </tr>
  <tr class="d1">
    <td>Internal temperature[C]</td>
    <td><b id="int_temp">0</b></td>
    <td></td>
  </tr>
  <tr class="d0">
    <td>External temperature[C]</td>
    <td><b id="ext_temp">0</b></td>
    <td></td>
  </tr>
  <tr class="d1">
    <td>Measured moisture</td>
    <td><b id="msr_mois">0</b></td>
    <td></td>
  </tr>
  <tr class="d0">
    <td>Moisture coefficient</td>
    <td><b id="cff_mois">0</b></td>
    <td><input id="val_cff_mois" name="val_cff_mois" class="set-inputbox">
     <button id="set_cff_mois" class="set-button">Set</button>
    </td>
  </tr>
  <tr class="d1">
    <td>Estimated moisture</td>
    <td><b id="est_mois">0</b></td>
    <td></td>
  </tr>
 </table>

 <h2>Last picture</h2>

 <img id="webcam_shot" alt="Last picture">

 <p><button id="post_pic" class="do-button">Post on Facebook</button></p>

</body>

如你所见,这里有一个简单的表格,所有数据都在其中报告,用户可以通过 设置 按钮更改湿度系数。然后,植物的图片显示在底部,并带有 在 Facebook 上发布 按钮,允许用户将当前图片发布到 Facebook。

设置 按钮由以下 JavaScript 代码管理,该代码包含在 plant.html 文件中:

<script>
 $(function() {
  $('button[class="set-button"]').click(function() {
   var id = $(this).attr("id");
   var box = document.getElementById(id.replace('set_', 'val_'));

   $.ajax({
    url: "/handler.php",
    type: "POST",
    data: "set=" + id.replace('set_', '') + "&val=" + box.value,      success: function() {
      console.log('set POST success');
     },
     error: function() {
      console.log('set POST error');
     }
   });
  });
 });
</script>

因此,每次按下按钮时,我们都会生成一个 POST 请求,并携带用户输入的值。

类似地,在 Facebook 上发布 按钮由以下代码管理:

    <script>
      $(function() {
        $('button[class="do-button"]').click(function() {
          var id = $(this).attr("id");

          $.ajax({
            url: "/handler.php",
            type: "POST",
            data: "do=" + id,
            success: function() {
              console.log('do POST success');
            },
            error: function() {
              console.log('do POST error');
            }
          });
        });
      });
    </script>

在这种情况下,我们生成另一个POST请求,但使用不同的参数。

另一方面,以下 JavaScript 代码用于两个主要任务:

    <script>
      var polldata = function() {
        $.getJSON('/handler.php', function(data) {
          $.each(data, function(key, val) {
            var e = document.getElementById(key);

            if (e != null) {
              if (e.type == "text")
                e.value = val;
              else
                e.textContent = val;
            }
          });
        });

        var url = '/webcam-shot.jpg';
        var d = new Date();
        $('#webcam_shot').attr('src', url + '?d=' + d.getTime());
      };

      setInterval(polldata, 1000);
    </script>

第一个任务是请求并更新网页上的植物数据,第二个任务是更新植物的图像。

提示

请注意,我们使用了一种技巧来强制浏览器刷新植物的图片:

        var url = '/webcam-shot.jpg';
        var d = new Date();
        $('#webcam_shot').attr('src', url + '?d=' + d.getTime());

在这里,我为图片的属性附加了当前日期的参数,以强制浏览器更新图片。

从前面的代码来看,读者可以注意到,当 PHP 文件执行时,handler.php 脚本也会依次执行。handler.php 文件管理服务器端的数据,下面的代码片段展示了其相关代码:

#
# Ok, do the job
#

# Check the POST requests
if (isset($_POST["val"]))
   $new_cff_mois = floatval($_POST["val"]);
else if (isset($_POST["do"]))
   do_post();

# Wait for lock on /tmp/plant.lock
$lock = file_lock(LOCK_FILE);
if (!$lock)
   die();

# Read the status file and decode it
$ret = file_get_contents(STATUS_FILE);
if ($ret === false)
   die();
$data = json_decode($ret, true);

# Use the stored value reset to a specific defualt
if (isset($new_cff_mois))
   $data['cff_mois'] = $new_cff_mois;

# Write back the new status (if needed)
$status = json_encode($data);
if (isset($new_cff_mois)) {
   $ret = file_put_contents(STATUS_FILE, $status);
   if ($ret === false)
      die();
}

# Release the lock
file_unlock($lock);

# Encode data for JSON
echo $status;

注意

完整代码可以在书中示例代码库中的 chapter_07/handler.php 文件中找到。

在第一步中,我们检查是否有任何 POST 请求,如果有,则处理它们。在第一种情况下,我们更新湿度系数;而在第二种情况下,我们调用下文解释的 do_post() 函数,将植物的图片发布到 Facebook。

然后,我们必须使用 flock() 系统调用来读取(并最终更新)系统的状态文件。在 PHP 中,文件锁由 flock() 函数管理,如下所示,用于获取和释放文件的锁:

function file_lock($name)
{
   $f = fopen($name, 'w');
   if ($f === false)
      return false;

   $ret = flock($f, LOCK_EX);
   if ($ret === false)
      return false;

   return $f;
}

function file_unlock($f)
{
   flock($f, LOCK_UN);
   fclose($f);
}

最后的操作是将植物的数据以适合调用的 JavaScript 的 JSON 格式返回给浏览器。

好的,现在我们要展示最后一项内容,即 do_post() 函数:

function do_post()
{
   # Define the Facebook session
   $fb = new Facebook\Facebook([
      'app_id'                => APP_ID,
      'app_secret'            => APP_SECRET,
      'default_graph_version' => 'v2.4',
      'default_access_token'  => DEF_TOKEN,
      'fileUpload'            => true,
      'cookie'                => true,
   ]);

   # Publish to user's timeline
   $ret = $fb->post('/me/photos', array(
      'message'    => 'My lovely plant!',
      'source'     => $fb->videoToUpload(realpath('webcam-shot.jpg')),
   ));
}

该功能简单地执行与前面展示的 post_pic.php 脚本相同的步骤,以便将植物的图片发布到用户的 Facebook 时间线。

最终测试

为了测试原型,我首先执行了书中示例代码库中的 chapter_07/SYSINIT.sh 文件来设置所有外设:

root@beaglebone:~# ./SYSINIT.sh
done!

现在,在检查了 Web 服务器是否运行之后,我启动了 plant_mon.sh 植物监控脚本,并启用了所有调试信息:

root@beaglebone:~# root@beaglebone:~/chapter_07# ./plant_mon.sh -d -l -f
plant_mon.sh: min=10
plant_mon.sh: signals traps installed
plant_mon.sh: lig_levl=442 int_temp=29.50 ext_temp=29.91 msr_mois=0
plant_mon.sh: curr_date=0010 next_date=
plant_mon.sh: start main loop
plant_mon.sh: lig_levl=428 int_temp=29.50 ext_temp=29.25 msr_mois=0
plant_mon.sh: curr_date=0010 next_date=0011
plant_mon.sh: cff_mois=50
plant_mon.sh: est_mois=221
plant_mon.sh: lig_levl=423 int_temp=29.50 ext_temp=27.99 msr_mois=0
plant_mon.sh: curr_date=0010 next_date=0011
plant_mon.sh: cff_mois=50
plant_mon.sh: est_mois=220
...

然后,我设置了 Web 服务器的根目录以实现 Web 界面。在我的 BeagleBone Black 上,Web 服务器的根目录是 /var/www/,但根据系统设置可能会有所不同。

注意

读者可以阅读由本书作者编写的《BeagleBone Essentials》,Packt Publishing,以获取更多关于如何在 BeagleBone Black 上设置 Web 服务器的信息。

如果您的配置与我的相同,并且 plant_mon.sh 脚本正在运行,那么您的 /var/www/ 目录应该如下所示:

root@beaglebone:~# ls /var/www/
plant.lock  plant.status  webcam-shot.jpg

这些文件是由监控脚本创建的,分别是锁文件、系统状态文件和最后拍摄的图片。除了这些文件之外,您还需要 Facebook API(因此我们需要在此解压其源代码,如前所述)和配置文件 config.phpsetup.php

然后,我们需要添加 plant.htmlplant.csshandler.php 文件,以及 jquery-1.9.1.js 文件,该文件可以通过在主机 PC 上的浏览器或直接在 BeagleBone Black 上使用以下命令行从 code.jquery.com/jquery/ 下载:

root@beaglebone:# wget --no-check-certificate https://code.jquery.com/jquery-1.9.1.js

然后,我们必须确保所有文件都归系统用户 www-user 所有,以便 Web 服务器可以不出问题地读写它们。为此,我们可以使用以下命令:

root@beaglebone:# cd /var/www && chown -R www-data:www-data *

如果一切顺利,您的 Web 服务器根目录应该如下所示:

root@beaglebone:/var/www# ls -l
total 308
-rw-r--r-- 1 www-data www-data    344 Aug 19  2015 config.php
drwxr-xr-x 5 www-data www-data   4096 Aug 18  2015 facebook-php-sdk-v4-5.0-dev
-rw-r--r-- 1 www-data www-data   1846 Aug 26  2015 handler.php
-rw-r--r-- 1 www-data www-data 268381 Oct 24  2014 jquery-1.9.1.js
-rw-r--r-- 1 www-data www-data   2968 Aug 26  2015 plant.html
-rw-rw-rw- 1 www-data www-data      0 Apr 26 01:17 plant.lock
-rw-rw-rw- 1 www-data www-data     95 Apr 26 01:17 plant.status
-rw-r--r-- 1 www-data www-data    183 Aug 24  2015 setup.php
-rw-r--r-- 1 www-data www-data  17583 Apr 26 01:17 webcam-shot.jpg

现在,一切应该就绪,所以在我的主机 PC 上,我通过 USB 电缆将浏览器指向 BeagleBone Black 在模拟以太网线上的 IP 地址,以显示 Web 界面。以下是我的测试截图:

最终测试

请注意,通过智能手机或平板电脑也可以获得类似的结果。事实上,如果我将 BeagleBone Black 连接到我的局域网,然后将智能手机的浏览器指向 BeagleBone Black 的 IP 地址,我会看到以下截图所示的内容:

最终测试

提示

请记住,USB 模拟以太网的 IP 地址通常是 192.168.7.2,而当 BeagleBone Black 连接到您的局域网时,它的 IP 地址可能会根据您的局域网设置而有所不同。可以通过在 BeagleBone Black 的终端上使用 ifconfig eth0 命令来获取。

总结

这次,我们使用了多个传感器来获取关于我们可爱植物的重要数据。然后,我们还发现了一种通过使用普通文件在进程之间交换数据的简单方法。我们学会了如何使用 Facebook PHP API,通过一个简单的脚本在用户的时间线上发布内容。

在下一章中,我们将尝试实现一个入侵报警系统,配备运动检测传感器,当触发报警时,系统将开始拍摄入侵者的照片,并将其发送到用户的电子邮件地址。

第八章. 入侵检测系统

如今,入侵检测系统非常常见,但确实价格昂贵。在本章中,我将展示如何使用我们的 BeagleBone Black 和两台(或更多)摄像头实现一个廉价且质量合理的入侵检测系统。

系统将能够通过发送带有入侵者照片的电子邮件来提醒用户。

基本功能

如前所述,我们将通过 USB 总线将两台摄像头连接到 BeagleBone Black。然后,我们将安装并运行一个特殊的运动检测软件,该软件能够检测动态场景中的运动。当程序检测到运动时,它会拍摄一张或多张运动物体的照片,并通过电子邮件将照片发送到用户账户。

设置硬件

这次连接非常简单,因为它们只是通过几根 USB 电缆连接完成的。

在前几章中,我们已经看到如何设置摄像头(例如,请参见第三章中的 水族馆监控);但这次,由于我们同时使用两台摄像头,因此配置有所不同。

如读者所知,BeagleBone Black 板只有一个 USB 主机端口,因此要连接两个摄像头,我们需要一个 USB 集线器。这些设备(用于将多个设备连接到 USB 主机端口)非常常见,读者可以在互联网上随处找到它们。

小贴士

理论上,集线器端口越多,我们可以在系统中使用的摄像头就越多!但当然,由于每个摄像头都会给系统增加 CPU 负载,因此可用的摄像头数量是有最大限制的。

使用带有三个端口的 USB 集线器的系统简单示意图如下所示:

设置硬件

设置摄像头

对于我的原型,我使用了两台通用摄像头,它们由 Video4Linux 驱动类支持,正如在第三章中所解释的,水族馆监控,并通过 USB 集线器 连接,如下图所示。然而,您可以使用您首选的设备,因为这是一种非常常见的设备。

设置摄像头

注意

好奇的读者可以在 en.wikipedia.org/wiki/USB_hub 获取有关 USB 集线器驱动程序的更多信息。

要验证所有设备是否正确连接并且得到支持,您需要按前一节图示所示连接摄像头。然后,您应该能得到类似于我系统输出的结果,如下所示:

root@beaglebone:~# ls -l /dev/video*
crw-rw---T 1 root video 81, 0 Jan  1  2000 /dev/video0
crw-rw---T 1 root video 81, 1 Jan  1  2000 /dev/video1

小贴士

请注意,您必须为集线器使用外部电源,否则您的 BeagleBone Black 将无法提供足够的电流来同时管理这两台摄像头。

好的,现在我们可以验证网络摄像头是否正确管理,方法与前一章使用fswebcam程序的方式相同。不过,这次我们必须指定使用哪一台网络摄像头来拍摄简单的图片给fswebcam程序。这个技巧可以通过使用-d选项参数来完成,如以下命令行所示:

root@beaglebone:~# fswebcam -d /dev/video0 video0-shot.jpg
--- Opening /dev/video0...
Trying source module v4l2...
/dev/video0 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 352x288.
--- Capturing frame...
Captured frame in 0.00 seconds.
--- Processing captured image...
Writing JPEG image to 'video0-shot.jpg'.

提示

正如在第三章中提到的,水族馆监控,如果你得到一个完全空白的图像并伴随如下信息,你可以通过在命令行中添加-S选项参数来解决该问题:

root@beaglebone:~# fswebcam -d /dev/video0 -S 10 webcam-shot.jpg

然后,为了从另一台网络摄像头拍照,我们可以使用以下命令行:

root@beaglebone:~# fswebcam -d /dev/video1 video1-shot.jpg
--- Opening /dev/video1...
Trying source module v4l2...
/dev/video1 opened.
No input was specified, using the first.
Adjusting resolution from 384x288 to 320x240.
--- Capturing frame...
Captured frame in 0.00 seconds.
--- Processing captured image...
Writing JPEG image to 'video1-shot.jpg'.

以下两张截图展示了来自两台对面放置的网络摄像头的两张照片,即第一台网络摄像头拍到第二台的图像,反之亦然。

设置网络摄像头设置网络摄像头

最终图片

以下截图展示了我用来实现这个项目和测试软件的所有设备:

最终图片

这里没有什么特别需要强调的;所有连接只是非常简单的 USB 连接。不过,我想强调的是,我为 BeagleBone Black 板和 USB 集线器都使用了外部电源,以避免由于网络摄像头的高功耗导致的电力损失。

设置软件

这次,我们需要设置两个程序:邮件发送程序和运动检测系统。前者用于发送报警邮件,设置非常简单;而后者用于实现入侵检测系统,由于支持大量不同的设备和功能,设置会稍微复杂一些。

设置邮件发送程序

根据这个项目的要求,我们应该通过发送邮件来提醒用户可能的入侵。有几种方法可以在类 Unix 系统上发送邮件,而最常用的是mail命令,调用方式如下:

echo "Test message" | mail -s "test mail" email_address@somedomain.com

注意

关于mail程序的更多信息,读者可以查看其手册页(使用man mail命令)或访问en.wikipedia.org/wiki/Mail_%28Unix%29进行阅读。

真正的问题是,这个命令依赖于系统的邮件发送程序,它是实际负责通过互联网发送邮件的程序!默认情况下,我们的 BeagleBone Black 没有有效的邮件发送程序,因此,如果我们尝试使用mail程序发送邮件,就会出现以下错误:

root@beaglebone:~# echo "Test message" | mail -s "test mail"giometti@hce-engineering.com
root@beaglebone:~# /usr/lib/sendmail: No such file or directory
"/root/dead.letter" 8/200
. . . message not sent.

提示

前面输出中使用的-s选项参数是用来为邮件指定主题的。

为了解决我们的问题,我们需要安装一个有效的/usr/lib/sendmail程序,正如前面所提到的,有几种方法可以做到这一点。我决定使用ssmtp工具和我的 Gmail 账户。

注意

请注意,ssmtp 工具是一个通用工具,用于与邮件服务器一起使用,因此它不是 Gmail 相关的产品。有关该工具的更多信息,您可以查看 wiki.debian.org/sSMTP

要安装它,我们可以使用常规的 aptitude 命令以及两个用于电子邮件处理的其他实用工具:

root@beaglebone:~# aptitude install ssmtp mailutils mpack

安装完成后,我们需要根据以下补丁修改 /etc/ssmtp/ssmtp.conf 配置文件:

--- /etc/ssmtp/ssmtp.conf.orig   2015-09-11 21:52:39.531475392 +0000
+++ /etc/ssmtp/ssmtp.conf   2015-09-11 21:56:01.859600416 +0000
@@ -18,4 +18,10 @@
 # Are users allowed to set their own From: address?
 # YES - Allow the user to specify their own From: address
 # NO - Use the system generated From: address
-#FromLineOverride=YES
+FromLineOverride=YES
+
+# Add GMail settings
+mailhub=smtp.gmail.com:587
+AuthUser=rodolfo.giometti@gmail.com
+AuthPass=XXXXXXXXXX
+useSTARTTLS=YES

由于我们希望指定自己的 From: 地址,因此启用了 FromLineOverride 设置;然后,其他字段是必需的,以便我们可以通过 Gmail 账户发送电子邮件。

提示

出于显而易见的原因,我将我的密码替换成了 XXXXXXXXXX 字符串。当然,你需要设置 AuthUserAuthPass 设置,以便与你的 Gmail 账户匹配。

如果一切顺利,我们现在应该能够使用以下命令发送电子邮件:

root@beaglebone:~# echo "Test message" | mail -s "Test subject" -r "BBB Guardian <myaccount@gmail.com>" giometti@hce-engineering.com

提示

请注意,如果您的 Gmail 凭据未正确设置,您可能会收到以下错误消息:

send-mail: Authorization failed (535 5.7.8  https://support.google.com/mail/answer/14257 fr10sm1091535wib.14 – gsmtp)

另外,还要注意,-r 选项的参数用于指定发件人姓名;因此,在前面的示例中,From: 字段中显示了 BBB Guardian <myaccount@gmail.com> 字符串,否则将显示 Gmail 地址。

以下截图显示了我在智能手机上收到的消息:

设置邮件发送程序

使用 motion

Motion 是一个监控来自摄像头的视频信号的程序。它能够检测到图像中是否有显著部分发生了变化;换句话说,它可以检测到运动。
--[Motion WebHome]

注意

访问项目主页请访问 www.lavrsen.dk/foswiki/bin/view/Motion/WebHome

该软件是一个自由的闭路电视(CCTV)软件应用程序,专为基于 GNU/Linux 的系统开发,正如程序主页所述,它可以监控一个或多个摄像头的视频信号,并能够检测图像中是否有显著部分发生变化,检测到运动时保存视频。

该程序是一个命令行驱动的工具,用 C 语言编写,适用于 Video4Linux 接口。它可以作为守护进程运行,占用较小的系统资源和低 CPU 使用率。它可以在某些事件发生时调用用户可配置的 触发器,然后生成图片(.jpeg.netpbm)或视频(.mpeg.avi)。

motion 主要通过配置文件进行操作,尽管最终的视频流可以通过网页浏览器查看。

下载代码

在 BeagleBone Black 上下载和安装 motion 非常简单,因为我们只需使用常规命令安装新软件包,如下所示:

root@beaglebone:~# aptitude install motion
...
Setting up motion (3.2.12-3.4) ...
Adding group `motion' (GID 117) ...
Done.
Adding system user `motion' (UID 111) ...
Adding new user `motion' (UID 111) with group `motion' ...
Not creating home directory `/home/motion'.
Adding user `motion' to group `video' ...
Adding user motion to group video
Done.
[ ok ] Starting motion (via systemctl): motion.service.

当所有代码安装完成后,就可以配置程序了!事实上,如果我们查看系统的日志消息,看到以下输出:

root@beaglebone:~# tail -f /var/log/syslog
...
Sep  4 15:18:41 beaglebone motion[4511]: Not starting motion daemon, disabled via /etc/default/motion ... (warning).

由于守护进程必须在启用之前正确配置,默认情况下它是禁用的。所以,接下来我们来看一下如何配置它。

配置守护进程

为了配置守护进程以使用两个网络摄像头,我们必须修改三个文件:主配置文件 /etc/motion/motion.conf,每个摄像头的配置文件 /etc/motion/thread1.conf/etc/motion/thread2.conf。守护进程为每个使用的网络摄像头创建一个线程,所有与网络摄像头相关的特殊设置必须在相应的文件中进行设置。

我们从修改 /etc/motion/motion.conf 文件开始。首先,我们必须为每个网络摄像头启用一个线程,因此需要应用以下补丁:

--- motion.conf.orig   2014-04-23 21:12:18.511719124 +0000
+++ motion.conf   2014-04-23 21:12:47.710937877 +0000
@@ -630,8 +630,8 @@
 # This motion.conf file AND thread1.conf and thread2.conf.
 # Only put the options that are unique to each camera in the
 # thread config files.
-; thread /usr/local/etc/thread1.conf
-; thread /usr/local/etc/thread2.conf
+thread /etc/motion/thread1.conf
+thread /etc/motion/thread2.conf
 ; thread /usr/local/etc/thread3.conf
 ; thread /usr/local/etc/thread4.conf

然后,我们可以通过以下命令以调试模式运行 motion 守护进程来验证设置:

root@beaglebone:~# motion -s -n
[0] Processing thread 0 - config file /etc/motion/motion.conf
[0] Processing config file /etc/motion/thread1.conf
[0] Processing config file /etc/motion/thread2.conf
[0] Motion 3.2.12 Started
[0] ffmpeg LIBAVCODEC_BUILD 3482368 LIBAVFORMAT_BUILD 3478785
[0] Motion running in setup mode.
[0] Thread 1 is from /etc/motion/thread1.conf
[0] Thread 1 is device: /dev/video0 input 8
[0] Webcam port 8081
[0] Thread 2 is from /etc/motion/thread2.conf
[0] Thread 2 is device: /dev/video1 input 1
[0] Webcam port 8082
[0] Waiting for threads to finish, pid: 3096
[1] Thread 1 started
[0] motion-httpd/3.2.12 running, accepting connections
[0] motion-httpd: waiting for data on port TCP 8080
[2] Thread 2 started
[1] cap.driver: "uvcvideo"
[1] cap.card: "Microsoft LifeCam VX-800"
[1] cap.bus_info: "usb-musb-hdrc.1.auto-1.1"
[1] cap.capabilities=0x84000001
[1] - VIDEO_CAPTURE
[1] - STREAMING
[1] Config palette index 8 (YU12) doesn't work.
[1] Supported palettes:
[1] 0: YUYV (YUV 4:2:2 (YUYV))
[1] Selected palette YUYV
[1] Test palette YUYV (320x240)
[1] Using palette YUYV (320x240) bytesperlines 640 sizeimage 153600 colorspace 00000000
[1] found control 0x00980900, "Brightness", range -10,10
[1]    "Brightness", default 2, current 2
[1] found control 0x00980901, "Contrast", range 0,20
[1]    "Contrast", default 10, current 10
[1] found control 0x00980902, "Saturation", range 0,10
[1]    "Saturation", default 4, current 4
[1] found control 0x00980903, "Hue", range -5,5
[1] 	"Hue", default 0, current 0
[1] found control 0x00980910, "Gamma", range 100,200
[1]    "Gamma", default 130, current 130
[1] found control 0x00980913, "Gain", range 32,48
[1]    "Gain", default 34, current 34
[1] mmap information:
[1] frames=4
[1] 0 length=153600
[1] 1 length=153600
[1] 2 length=153600
[1] 3 length=153600
[1] Using V4L2
[2] cap.driver: "gspca_zc3xx"
[2] cap.card: "USB Camera (046d:08a2)"
[2] cap.bus_info: "usb-musb-hdrc.1.auto-1.2"
[2] cap.capabilities=0x85000001
[2] - VIDEO_CAPTURE
[2] - READWRITE
[2] - STREAMING
[2] Unable to query input 1 VIDIOC_ENUMINPUT: Invalid argument
[2] ioctl (VIDIOCGCAP): Inappropriate ioctl for device
[2] Could not fetch initial image from camera
[2] Motion continues using width and height from config file(s)
[1] Resizing pre_capture buffer to 1 items
[2] Resizing pre_capture buffer to 1 items
[2] Started stream webcam server in port 8082

从前面的输出可以看出,我们可以获取很多关于守护进程状态的有用信息。首先,我们注意到每行开头都有一个方括号中的数字,表示每个线程的输出。数字 0 表示 motion 主线程,数字 1 表示连接到第一个网络摄像头(设备 /dev/video0)的第一个线程,数字 2 表示连接到第二个网络摄像头(设备 /dev/video1)的第二个线程。

然后,我们看到对于第一个网络摄像头,守护进程给出了以下输出:

[1] cap.driver: "uvcvideo"
[1] cap.card: "Microsoft LifeCam VX-800"
[1] cap.bus_info: "usb-musb-hdrc.1.auto-1.1"
[1] cap.capabilities=0x84000001
[1] - VIDEO_CAPTURE
[1] - STREAMING
[1] Config palette index 8 (YU12) doesn't work.
[1] Supported palettes:
[1] 0: YUYV (YUV 4:2:2 (YUYV))
[1] Selected palette YUYV

当前的调色板设置(YU12)对于该网络摄像头无效,系统表示将使用 YUYV。

第二个线程显示了一个错误消息:

[2] cap.driver: "gspca_zc3xx"
[2] cap.card: "USB Camera (046d:08a2)"
[2] cap.bus_info: "usb-musb-hdrc.1.auto-1.2"
[2] cap.capabilities=0x85000001
[2] - VIDEO_CAPTURE
[2] - READWRITE
[2] - STREAMING
[2] Unable to query input 1 VIDIOC_ENUMINPUT: Invalid argument
[2] ioctl (VIDIOCGCAP): Inappropriate ioctl for device
[2] Could not fetch initial image from camera

这次,看起来是一个严重错误,但让我们一步步来,先修复第一个摄像头。在 /etc/motion/thread1.conf 文件中,我们看到以下设置(以下是整个文件的一个片段):

# Videodevice to be used for capturing  (default /dev/video0)
# for FreeBSD default is /dev/bktr0
videodevice /dev/video0

# The video input to be used (default: 8)
# Should normally be set to 1 for video/TV cards, and 8 for USB cameras
input 8

videodeviceinput 设置正确,但缺少视频调色板设置,因此使用默认值。正如前面的输出所示,这是错误的。为了修复它,我们必须添加以下几行:

--- /etc/motion/thread1.conf.orig   2014-04-23 21:12:25.712890999 +0000
+++ /etc/motion/thread1.conf   2014-04-23 20:25:15.089843787 +0000
@@ -12,6 +12,25 @@
 # for FreeBSD default is /dev/bktr0
 videodevice /dev/video0

+# v4l2_palette allows to choose preferable palette to be use by motion
+# to capture from those supported by your videodevice. (default: 8)
+# E.g. if your videodevice supports both V4L2_PIX_FMT_SBGGR8 and
+# V4L2_PIX_FMT_MJPEG then motion will by default use V4L2_PIX_FMT_MJPEG.
+# Setting v4l2_palette to 1 forces motion to use V4L2_PIX_FMT_SBGGR8
+# instead.
+#
+# Values :
+# V4L2_PIX_FMT_SN9C10X : 0  'S910'
+# V4L2_PIX_FMT_SBGGR8  : 1  'BA81'
+# V4L2_PIX_FMT_MJPEG   : 2  'MJPEG'
+# V4L2_PIX_FMT_JPEG    : 3  'JPEG'
+# V4L2_PIX_FMT_RGB24   : 4  'RGB3'
+# V4L2_PIX_FMT_UYVY    : 5  'UYVY'
+# V4L2_PIX_FMT_YUYV    : 6  'YUYV'
+# V4L2_PIX_FMT_YUV422P : 7  '422P'
+# V4L2_PIX_FMT_YUV420  : 8  'YU12'
+v4l2_palette 8
+
 # The video input to be used (default: 8)
 # Should normally be set to 1 for video/TV cards, and 8 for USB cameras
 input 8

请注意,我将条目 v4l2_palette 设置为 6,以选择 YUYV 调色板。现在,如果我们重新启动守护进程,得到以下输出:

[2] Thread 2 started
[1] cap.driver: "uvcvideo"
[1] cap.card: "Microsoft LifeCam VX-800"
[1] cap.bus_info: "usb-musb-hdrc.1.auto-1.1"
[1] cap.capabilities=0x84000001
[1] - VIDEO_CAPTURE
[1] - STREAMING
[1] Test palette YUYV (320x240)
[1] Using palette YUYV (320x240) bytesperlines 640 sizeimage 153600 colorspace 00000000

太好了!现在,让我们修复第二个网络摄像头的配置文件。在 /etc/motion/thread2.conf 文件中,我们看到以下输出:

# Videodevice to be used for capturing  (default /dev/video0)
# for FreeBSD default is /dev/bktr0
videodevice /dev/video1

# The video input to be used (default: 8)
# Should normally be set to 1 for video/TV cards, and 8 for USB cameras
input 1

再次,videodevice 设置正确,但 input 设置不正确!因此,让我们按照以下补丁修复它,然后重新运行守护进程:

--- /etc/motion/thread2.conf.orig   2014-04-23 21:12:30.703125375 +0000
+++ /etc/motion/thread2.conf   2014-04-23 20:31:54.214843835 +0000
@@ -14,7 +14,7 @@

 # The video input to be used (default: 8)
 # Should normally be set to 1 for video/TV cards, and 8 for USB cameras
-input 1
+input 6

 # Draw a user defined text on the images using same options as C #function strftime(3)
 # Default: Not defined = no text

现在,第二个线程的守护进程输出如下所示:

[2] cap.driver: "gspca_zc3xx"
[2] cap.card: "USB Camera (046d:08a2)"
[2] cap.bus_info: "usb-musb-hdrc.1.auto-1.2"
[2] cap.capabilities=0x85000001
[2] - VIDEO_CAPTURE
[2] - READWRITE
[2] - STREAMING
[2] Config palette index 8 (YU12) doesn't work.
[2] Supported palettes:
[1] Resizing pre_capture buffer to 1 items
[2] 0: JPEG (JPEG)
[2] Selected palette JPEG

因此,我们需要再次修改 /etc/motion/thread2.conf 文件,如下所示的补丁:

--- /etc/motion/thread2.conf.orig   2014-04-23 20:34:51.173828231 +0000
+++ /etc/motion/thread2.conf   2014-04-23 20:34:32.744140729 +0000
@@ -12,6 +12,25 @@
 # for FreeBSD default is /dev/bktr0
 videodevice /dev/video1

+# v4l2_palette allows to choose preferable palette to be use by motion
+# to capture from those supported by your videodevice. (default: 8)
+# E.g. if your videodevice supports both V4L2_PIX_FMT_SBGGR8 and
+# V4L2_PIX_FMT_MJPEG then motion will by default use V4L2_PIX_FMT_MJPEG.
+# Setting v4l2_palette to 1 forces motion to use V4L2_PIX_FMT_SBGGR8
+# instead.
+#
+# Values :
+# V4L2_PIX_FMT_SN9C10X : 0  'S910'
+# V4L2_PIX_FMT_SBGGR8  : 1  'BA81'
+# V4L2_PIX_FMT_MJPEG   : 2  'MJPEG'
+# V4L2_PIX_FMT_JPEG    : 3  'JPEG'
+# V4L2_PIX_FMT_RGB24   : 4  'RGB3'
+# V4L2_PIX_FMT_UYVY    : 5  'UYVY'
+# V4L2_PIX_FMT_YUYV    : 6  'YUYV'
+# V4L2_PIX_FMT_YUV422P : 7  '422P'
+# V4L2_PIX_FMT_YUV420  : 8  'YU12'
+v4l2_palette 3
+
 # The video input to be used (default: 8)
 # Should normally be set to 1 for video/TV cards, and 8 for USB cameras
 input 8

现在,如果我们重新启动第二个线程的守护进程,得到以下输出:

[2] cap.driver: "gspca_zc3xx"
[2] cap.card: "USB Camera (046d:08a2)"
[2] cap.bus_info: "usb-musb-hdrc.1.auto-1.2"
[2] cap.capabilities=0x85000001
[2] - VIDEO_CAPTURE
[2] - READWRITE
[2] - STREAMING
[2] Test palette JPEG (320x240)
[2] Using palette JPEG (320x240) bytesperlines 320 sizeimage 29390 colorspace 00000007

完美!现在,网络摄像头已正确配置。

Web 界面

现在是验证视频输出的时间了,通过直接查看视频流来进行。为此,motion 设置了几个 Web 服务器,用于监控主线程(编号为 0)和每个摄像头线程(编号从 1N)。

如果我们查看motion.confthread1.confthread2.conf文件中的webcam_port设置,我们会看到每个线程打开了不同的监控端口,如下所示:

root@beaglebone:~# grep webcam_port /etc/motion/{motion,thread1,thread2}.conf
/etc/motion/motion.conf:webcam_port 8081
/etc/motion/thread1.conf:webcam_port 8081
/etc/motion/thread2.conf:webcam_port 8082

唯一需要修改的设置是control_localhostwebcam_localhost,必须设置为off,以允许第一个线程的远程控制连接和每个摄像头线程的远程摄像头连接。修补程序如下:

--- /etc/motion/motion.conf.orig   2014-04-23 21:12:18.511719124 +0000
+++ /etc/motion/motion.conf   2014-04-23 20:48:18.068359577 +0000
@@ -410,7 +410,7 @@
 webcam_maxrate 1

 # Restrict webcam connections to localhost only (default: on)
-webcam_localhost on
+webcam_localhost off

 # Limits the number of images per connection (default: 0 = unlimited)
 # Number can be defined by multiplying actual webcam rate by desired number of seconds
@@ -426,7 +426,7 @@
 control_port 8080

 # Restrict control connections to localhost only (default: on)
-control_localhost on
+control_localhost off

 # Output for http server, select off to choose raw text plain (default: on)
 control_html_output on

请注意,如果8080端口被另一个运行中的进程(例如 Apache)占用,守护程序将无法启动。请验证这种情况是否存在。

现在,如果我们重新运行守护程序,我们可以使用以下命令行在不同的终端上验证三个motion Web 服务器是否在808080818082端口上运行:

root@beaglebone:~# netstat -pnl | grep motion
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN      2388/motion 
tcp        0      0 0.0.0.0:8081            0.0.0.0:*               LISTEN      2388/motion 
tcp        0      0 0.0.0.0:8082            0.0.0.0:*               LISTEN      2388/motion 

太棒了!现在,我们可以使用普通浏览器连接到主线程(线程编号 0),但是为了检查网络摄像头的输出,我们可以在以下网址获取每个网络摄像头的视频流:http://192.168.7.2:8081http://192.168.7.2:8082,如下面的屏幕截图所示:

网络界面网络界面

小贴士

请注意,在这最后的测试中,我执行了不带-s选项参数的守护程序,以禁用设置模式,即使用以下命令行:

root@beaglebone:~# motion -n

这是因为我注意到在设置模式中,摄像头的视频输出非常糟糕(我不知道这是一个 bug 还是一个特性)。

另一方面,控制线程可以通过网页浏览器在http://192.168.7.2:8080上进行控制。下面的截图显示了主页面:

网络界面

如果我们导航到All | Config | list菜单项,我们会到达http://192.168.7.2:8080/0/config/list,在那里我们可以获取包含主线程所有配置设置的页面,如下面的屏幕截图所示:

网络界面

小贴士

请注意,我们只需点击相关链接并输入新值即可更改每个设置。但是,在本书中我们不会使用这些界面来设置系统。

关于主线程,我们可以通过点击相关链接然后导航到菜单来获取每个运行线程的配置。例如,对于线程 1,我们可以在http://192.168.7.2:8080/1/config/list上读取其当前配置,如下面的屏幕截图所示:

网络界面

管理事件

现在是时候看看当事件发生时我们如何执行某些操作。motion守护程序定义了几个事件,在主配置文件中都有详细说明。实际上,在/etc/motion/motion.conf文件中,我们可以看到以下设置(再次是文件的一部分):

############################################################
# External Commands, Warnings and Logging:
# You can use conversion specifiers for the on_xxxx commands
# %Y = year, %m = month, %d = date,
# %H = hour, %M = minute, %S = second,
# %v = event, %q = frame number, %t = thread (camera) number,
# %D = changed pixels, %N = noise level,
# %i and %J = width and height of motion area,
# %K and %L = X and Y coordinates of motion center
# %C = value defined by text_event
# %f = filename with full path
# %n = number indicating filetype
# Both %f and %n are only defined for on_picture_save,
# on_movie_start and on_movie_end
# Quotation marks round string are allowed.
############################################################

# Do not sound beeps when detecting motion (default: on)
# Note: motion never beeps when running in daemon mode.
quiet on

# Command to be executed when an event starts. (default: none)
# An event starts at first motion detected after a period of no motion defined by gap
; on_event_start value

# Command to be executed when an event ends after a period of no motion
# (default: none). The period of no motion is defined by option gap.
; on_event_end value

# Command to be executed when a picture (.ppm|.jpg) is saved (default: none)
# To give the filename as an argument to a command append it with %f
; on_picture_save value

# Command to be executed when a motion frame is detected (default: none)
; on_motion_detected value

# Command to be executed when motion in a predefined area is detected
# Check option 'area_detect'.   (default: none)
; on_area_detected value

# Command to be executed when a movie file (.mpg|.avi) is created. (default: none)
# To give the filename as an argument to a command append it with %f
; on_movie_start value

# Command to be executed when a movie file (.mpg|.avi) is closed. (default: none)
# To give the filename as an argument to a command append it with %f
; on_movie_end value

# Command to be executed when a camera can't be opened or if it is lost
# NOTE: There is situations when motion doesn't detect a lost camera!
# It depends on the driver, some drivers don't detect a lost camera at all
# Some hang the motion thread. Some even hang the PC! (default: none)
; on_camera_lost value

这些都是守护进程报告的所有可能事件,在这里我们可以定义当某个事件发生时执行的命令。我们只需要输入一个带有特定参数的命令文件,守护进程会在适当的时候调用它。允许的参数在前面列表的注释中有显示。

作为示例,并为了更好地理解机制是如何工作的,我们可以考虑以下名为args.sh的简单Bash脚本:

#!/bin/bash

NAME=$(basename $0)
ID=$RANDOM

function log ( ) { 
    echo "$(date "+%s.%N"): $NAME-$ID: $1"
} 

log "executing with $# args"

n=1
for arg ; do 
    log "$n) $arg" 
    n=$((n + 1))
done

log "done"

exit 0

注意

代码存储在书中示例代码库的chapter_08/bin/args.sh脚本中。

如果我们从命令行执行它,它会简单地打印出其参数(并带有时间戳前缀),如下所示:

root@beaglebone:~/chapter_08# ./args.sh arg1 "arg 2" "..." 'arg-N'
1398299270.472083231: args.sh-12334: executing with 4 args
1398299270.498241897: args.sh-12334: 1) arg1
1398299270.523545772: args.sh-12334: 2) arg 2
1398299270.548859939: args.sh-12334: 3) ...
1398299270.574398731: args.sh-12334: 4) arg-N
1398299270.599793272: args.sh-12334: done

提示

请注意,程序在其名称后面打印了一个随机数字。这是因为我们需要一个唯一的名称来区分线程 1 和线程 2(参见以下部分)。

现在,如果我们将这个脚本复制到/usr/local/bin/目录中,我们可以像下面这样调用它:

root@beaglebone:~/chapter_08# /usr/local/bin/args.sh test command line
1398299445.322540043: args.sh-13425: executing with 3 args
1398299445.348607251: args.sh-13425: 1) test
1398299445.374537126: args.sh-13425: 2) command
1398299445.400261585: args.sh-13425: 3) line
1398299445.429571918: args.sh-13425: done

我们可以使用这个程序与motion一起,来发现当事件发生时,传递给外部程序的参数。例如,我们可以考虑on_picture_save事件。我们可以通过以下补丁启用它:

--- /etc/motion/motion.conf.orig   2014-04-23 21:12:18.511719124 +0000
+++ /etc/motion/motion.conf   2015-09-11 20:58:19.334749400 +0000
@@ -518,7 +518,7 @@

 # Command to be executed when a picture (.ppm|.jpg) is saved (default: #none)
 # To give the filename as an argument to a command append it with %f
-; on_picture_save value
+on_picture_save /usr/local/bin/args.sh %C %t %f

 # Command to be executed when a motion frame is detected (default: none)
 ; on_motion_detected value

通过这种方式,我们要求motion在保存新图片时执行args.sh脚本,并传递给它事件的时间戳、生成事件的线程编号以及图片文件的完整路径名。

在再次运行守护进程之前,我们必须确保在每个线程的配置文件中,通过注释相应的行来禁用相同的事件,就像下面的补丁中所示,例如对于第一个线程:

--- /etc/motion/thread1.conf.orig   2014-04-23 21:12:25.712890999 +0000
+++ /etc/motion/thread1.conf   2015-09-11 20:57:49.828890021 +0000
@@ -50,7 +69,7 @@

 # Command to be executed when a picture (.ppm|.jpg) is saved (default: none)
 # The filename of the picture is appended as an argument for the command.
-on_picture_save /usr/local/motion-extras/camparse1.pl
+; on_picture_save /usr/local/motion-extras/camparse1.pl

 # Command to be executed when a movie file (.mpg|.avi) is closed. #(default: none)

提示

如果你忘记在网络摄像头的线程中禁用事件,就会出现多个错误,例如以下错误:

[2] File of type 1 saved to: /usr/local/apache2/htdocs/cam2/01-20150911205458-00.jpg
 &: 1:  &: /usr/local/motion-extras/camparse2.pl: not found
[1] File of type 1 saved to: /usr/local/apache2/htdocs/cam1/01-20150911205555-01.jpg
 &: 1:  &: /usr/local/motion-extras/camparse1.pl: not found

所以,请记得为所有正在运行的线程禁用此设置!

现在,如果我们再次执行守护进程,并在摄像头前做一些动作,我们将看到如下消息:

[1] File of type 1 saved to: /usr/local/apache2/htdocs/cam1/01-20150911210615-00.jpg
1442005575.375926790: args.sh-7523: executing with 3 args
1442005575.386901248: args.sh-7523: 1) 20150911210615
1442005575.398043498: args.sh-7523: 2) 1
1442005575.408981165: args.sh-7523: 3) /usr/local/apache2/htdocs/cam1/01-20150911210615-00.jpg
1442005575.419728082: args.sh-7523: done

很棒!一切正常工作。现在,完成这个工作非常简单。事实上,我们只需要将args.sh脚本替换为一个可以发送带有图片附件的电子邮件的脚本!下面是这种程序可能实现的一个代码片段:

#
# Local functions
#

function log ( ) { 
    echo "[$cam] $1"
}

function send_alert { 
    # Build the attachments list 
    [ ! -e $ALERT_LIST ] && return 
    for f in $(head -n $ALERT_LIMIT $ALERT_LIST) ; do 
        list="-a $f $list" 
    done 

    # Send the letter 
    echo -e ${ALERT_MESG/\%time/$time} | \ 
        mail -s "$ALERT_SUBJ" -r "$ALERT_FROM" $list "$ALERT_TO"
}

usage() { 
    echo "usage [to add image]: $NAME <timestamp><cam #><filepath>" >&2 
    echo "usage [to send alert]: $NAME <timestamp><cam #>" >&2 
    exit 1
}

#
# Main
#
# Check command line
[ $# -lt 2 ] && usage

( # Wait for lock on LOCK_FILE (fd 99) for 10 seconds
flock -w 10 -x 99 || exit 1

if [ $# -eq 3 ] ; then 
    # Must add the picture to the list 
    time=$1 
    cam=$2 
    path=$3 

    log "got new picture $path at $time" 
    echo "$path" >> $ALERT_LIST

elif [ $# -eq 2 ] ; then 
    # Send the mail alert 
    time=$1 
    cam=$2 

    log "sending alert at $time" 
    send_alert 
    rm $ALERT_LIST

else 

    cam="?" 
    log "invalid command!"

fi

# Release the lock
) 99>$LOCK_FILE

exit 0

注意

完整的代码存储在书中示例代码库的chapter_08/bin/send_alert.sh脚本中。

要使用它,我们必须像之前一样将其复制到/usr/local/bin/目录中,接着我们需要将/etc/motion/motion.conf配置文件中所有出现的args.sh替换为send_alert.sh。完成后,只需重新运行motion守护进程,当检测到运动时,我们应该会看到如下的日志消息:

[1] File of type 1 saved to: /usr/local/apache2/htdocs/cam1/01-20150915210814-01.jpg
[1] got new picture /usr/local/apache2/htdocs/cam1/01-20150915210814-01.jpg at 20150915210814
[1] File of type 1 saved to: /usr/local/apache2/htdocs/cam1/01-20150915210815-00.jpg
[1] got new picture /usr/local/apache2/htdocs/cam1/01-20150915210815-00.jpg at 20150915210814
[1] File of type 1 saved to: /usr/local/apache2/htdocs/cam1/01-20150915210815-01.jpg
[1] got new picture /usr/local/apache2/htdocs/cam1/01-20150915210815-01.jpg at 20150915210814

请注意,拍摄大量图片是很常见的,因此如果没有一些特定的防洪技术,我们可能会冒着发送大量电子邮件的风险!这里的技巧非常简单——通过使用gapon_event_end选项,我们可以在运动检测到当前事件结束时生成一个电子邮件发送事件。事实上,通过查看配置文件,我们可以看到以下内容:

# Gap is the seconds of no motion detection that triggers the end of an event
# An event is defined as a series of motion images taken within a short timeframe.
# Recommended value is 60 seconds (Default). The value 0 is allowed and disables
# events causing all Motion to be written to one single mpeg file and no pre_capture.
gap 60

我们可以设想将图片的文件名存储在一个列表中,然后在on_event_end事件发生时,我们可以读取这些文件名,并发送一封带有附件的电子邮件。

为了启用on_event_end事件,我使用了以下设置:

--- /etc/motion/motion.conf.orig   2014-04-23 21:12:18.511719124 +0000
+++ /etc/motion/motion.conf   2015-09-15 20:55:31.654352880 +0000
@@ -514,11 +514,11 @@

 # Command to be executed when an event ends after a period of no motion
 # (default: none). The period of no motion is defined by option gap.
-; on_event_end value
+on_event_end /usr/local/bin/send_alert.sh %C %t

 # Command to be executed when a picture (.ppm|.jpg) is saved (default: none)
 # To give the filename as an argument to a command append it with %f
-; on_picture_save value
+on_picture_save /usr/local/bin/send_alert.sh %C %t %f

这里,send_alert.sh脚本实现了这个解决方案。如果我们不带参数运行它,它会显示一个简短的使用信息,如下所示:

root@beaglebone:~# ./send_alert.sh
usage [to add image]: send_alert.sh <timestamp> <cam #> <filepath>
usage [to send alert]: send_alert.sh <timestamp> <cam #>

如果使用三个参数执行,它会将filepath存储在由ALERT_LIST变量指定的文件中,而当使用两个参数执行时,它会重新读取文件,并将若干张图片(由ALERT_LIMIT变量限制)作为附件发送电子邮件。

为了测试程序是否正常工作,我们可以尝试只用两个参数执行它,然后验证是否有电子邮件消息到达我们的账户。

最终测试

现在是测试我们的原型的时候了。为此,我决定将网络摄像头对准我的书架,那里存放着我珍贵的邮票收藏。然后,我运行motion工具,开始等待。

请注意,这次没有什么特别的配置工作需要进行。

过了一段时间,我收到以下电子邮件:

最终测试

然后,在查看其中一张图片时,我发现了一个非常危险的入侵者。

最终测试

总结

在本章中,硬件工作非常少,但另一方面,我们发现了如何使用一个非常强大的工具,名为motion。这个工具让我们能够实现(几乎)专业级别的、但最小化的防入侵系统。此外,你还学会了如何通过电子邮件发送带有附件的图片,来通知用户系统中的重要事件。

在下一章,我们将探索如何使用不同的识别设备(如 RFID 读卡器和智能卡读卡器)来实现访问控制系统。

第九章:使用智能卡和 RFID 的 Twitter 访问控制系统

用计算机识别人员或物体可能看起来像是一项简单的任务,但实际上,完成这一任务的设备背后有着大量的技术。

在本章中,我们将使用智能卡读卡器和两种类型的 RFID 读卡器(适用于短距离的低频LF读卡器,以及适用于长距离的超高频UHF读卡器),以展示实现最小化识别系统用于访问控制的不同可能性。

一旦完成检测,系统将向我们的 Twitter 账户发送一条信息,通知我们的关注者发生了什么(在正常情况下,我们可能会锁定或解锁某些东西,但我决定做点不一样的事情)。

工作原理基础

例如,智能卡和智能卡读卡器是复杂的设备,如今无处不在,从我们的信用卡到智能手机。智能卡一词意味着一套技术,包括集成电路、微处理器、存储器、天线等,这些都集成在同一个电路中,形成微芯片,成为智能卡的核心。另一方面,智能卡读卡器是复杂的设备,能够与卡片进行通信并在其上保存数据或将数据返回计算机。

注意

读者可以通过查看en.wikipedia.org/wiki/Smart_card获取更多有关智能卡世界的信息。

智能卡的发展是射频识别RFID)设备的演变,这些设备可以用于以非接触的方式识别人员或物体,识别距离从几厘米到几米不等。RFID 读卡器和相应的标签(或应答器)是高技术的无线设备,能够相互交换数据,从而完成识别任务。

注意

读者可以通过查看en.wikipedia.org/wiki/Radio-frequency_identification来获取更多有关RFID世界的信息。

这次,所有项目的复杂性都集中在硬件设备(智能卡读卡器和 RFID 读卡器)及其相应的软件管理器中,因此我们只需要编写代码以访问这些设备并获取智能卡或 RFID 标签中的数据。

为了展示管理识别设备的不同技术,我们将编写三个程序(每个设备一个)使用三种不同的编程语言。然而,所有程序的结果都是相同的:当一个明确定义的人员(或物体)被识别时,系统将向我们的 Twitter 账户发送一条信息。

为了完成最后的任务,我们将使用一个专用(且有趣的)工具,它允许用户通过命令行管理 Twitter 账户。

每个识别系统都有其独特的特点;然而:

  • 使用智能卡读卡器的解决方案可以用于需要通过插入信用卡(或类似物品)进行身份识别的场合。这不适用于无线应用。我要在我的原型中使用的智能卡读卡器是一个 USB 设备,带有一个插槽,必须插入智能卡才能识别。

  • 第二种解决方案,也就是使用 RFID LF 读卡器的方案,适用于需要无线识别任务,但对象与读卡器之间的距离不超过几厘米的场合。这类设备通常非常简单,比如我将在我的原型中使用的设备。RFID 读卡器通过串口与主机连接,每当检测到标签时,它会简单地返回一个字符串。

  • 最后的解决方案是通过 RFID UHF 读卡器实现的,也就是说,使用一种能够以无线模式检测标签的设备,类似于之前的 RFID LF 读卡器,但距离可达到几米远。这些 UHF 设备比 LF 设备更为复杂,例如我将在我的原型中使用的设备。RFID 读卡器仍然使用串口连接与主机通信,但它实现了一种更复杂的协议来交换数据。

硬件设置

如前面章节所述,这次我们需要连接一个 USB 设备和两个串口设备。关于 USB 设备,主要问题是它有一个非标准的 USB 连接器,因此我们必须找到一个解决方法(见下一部分);而对于串口设备,我们需要在 BeagleBone Black 的扩展连接器上找到两个空闲的串口。

关于最后这个问题,我们应该记住 BeagleBone Black 默认禁用六个板载串口,只有/dev/ttyO0设备是与串口控制台配对的。如果我们登录到系统中,可以通过以下命令轻松验证:

root@BeagleBone:~# ls -l /dev/ttyO*
crw-rw---- 1 root tty 248, 0 Apr 23 20:20 /dev/ttyO0

要启用其他串口,我们需要修改内核设置,以便启用我们希望使用的串口。选择启用哪个端口取决于我们希望使用的引脚来连接设备,以下表格可能有助于我们做出选择:

设备 TxD RxD RTS CTS 名称
/dev/ttyO1 P9.24 P9.26 UART1
/dev/ttyO2 P9.21 P9.22 P8.38 P8.37 UART2
/dev/ttyO4 P9.13 P9.11 P8.33 P8.35 UART4
/dev/ttyO5 P8.37 P8.38 UART5

所有设备都适用于我们的范围,因此我选择使用两个/dev/ttyO1/dev/ttyO2设备,并且为了激活它们,我们可以使用以下命令:

root@BeagleBone:~# echo BB-UART1 > /sys/devices/bone_capemgr.9/slots
root@BeagleBone:~# echo BB-UART2 > /sys/devices/bone_capemgr.9/slots

现在,两个新的串口已准备好使用,如下所示的命令所示:

root@beaglebone:~# ls -l /dev/ttyO*
crw-rw---- 1 root tty     248, 0 Apr 23 20:20 /dev/ttyO0
crw-rw---T 1 root dialout 248, 1 Apr 23 21:48 /dev/ttyO1
crw-rw---T 1 root dialout 248, 2 Apr 23 21:48 /dev/ttyO2

注意

读卡器还可以查看这本书《BeagleBone Essentials》,由本书的作者编写,出版商为Packt Publishing,以便获得更多有关如何管理 BeagleBone Black 的串口,进而与传感器进行通信的信息。

设置智能卡读卡器

我在这个原型中使用的智能卡读卡器如下图所示:

设置智能卡读卡器

注意

该设备可以通过以下链接购买(或通过上网搜索):www.cosino.io/product/http://www.cosino.io/product/smartcard-reader-isoiec-7816

该设备基于Maxim 73S1215F芯片,其数据表可在datasheets.maximintegrated.com/en/ds/73S1215F.pdf上找到。

如前所述,这个设备具有非标准的 USB 连接器,因此我们必须找到一种方法将其连接到我们的 BeagleBone Black 上。

快速且简便的解决方案是使用来自旧 USB 设备的 USB 插头类型 A 适配器,然后必须将其焊接到板上,如下图所示:

设置智能卡读卡器

连接必须按照以下表格进行:

智能卡读卡器引脚 USB 插头类型 A 电缆
VBus 红色
D- 白色
D+ 黄色
GND 绿色

注意

连接器引脚输出可以在en.wikipedia.org/wiki/USB引脚输出框中查看。

如果连接正确,一旦你将设备连接到 BeagleBone Black,应该会得到如下输出:

usb usb1: usb wakeup-resume
usb usb1: usb auto-resume
hub 1-0:1.0: hub_resume
hub 1-0:1.0: port 1: status 0101 change 0001
hub 1-0:1.0: state 7 ports 1 chg 0002 evt 0000
hub 1-0:1.0: port 1, status 0101, change 0000, 12 Mb/s
usb 1-1: new full-speed USB device number 2 using musb-hdrc
usb 1-1: ep0 maxpacket = 16
usb 1-1: skipped 1 descriptor after interface
usb 1-1: skipped 1 descriptor after interface
usb 1-1: default language 0x0409
usb 1-1: udev 2, busnum 1, minor = 1
usb 1-1: New USB device found, idVendor=1862, idProduct=0001
usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
usb 1-1: Product: TSC12xxF CCID-DFU Version 2.10
usb 1-1: Manufacturer: Teridian Semiconductors
usb 1-1: SerialNumber: 123456789
usb 1-1: usb_probe_device
usb 1-1: configuration #1 chosen from 1 choice
usb 1-1: adding 1-1:1.0 (config #1, interface 0)
usb 1-1: adding 1-1:1.1 (config #1, interface 1)
hub 1-0:1.0: state 7 ports 1 chg 0000 evt 0002
hub 1-0:1.0: port 1 enable change, status 00000103

好的,一切正常工作,但现在我们需要一些包来管理我们的智能卡读卡器。那么,让我们通过以下命令安装它们:

root@beaglebone:~# aptitude install pcsc-tools pcscd libccid

完成后,pcsc工具就可以开始工作了。

注意

好奇的读者可以查看以下网址,了解更多关于此工具的信息:ludovic.rousseau.free.fr/softwares/pcsc-tools/

安装完成后,我们可以通过以下命令执行它:

root@beaglebone:~# pcsc_scan
PC/SC device scanner
V 1.4.20 (c) 2001-2011, Ludovic Rousseau <ludovic.rousseau@free.fr>
Compiled with PC/SC lite version: 1.8.3
Using reader plug'n play mechanism
Scanning present readers...
Waiting for the first reader...

提示

如果你收到以下错误而不是前述输出,可以尝试使用/etc/init.d/pcscd restart命令重启守护进程,然后再次执行pcsc_scan工具:

SCardEstablishContext: Service not available.

好的,守护进程已正确启动,但它仍然没有识别我们的设备。在这种情况下,我们需要修补/etc/libccid_Info.plist配置文件,如下所示的补丁所示:

--- /etc/libccid_Info.plist.orig	2014-04-23 20:39:48.664062641 +0000
+++ /etc/libccid_Info.plist	2014-04-23 20:40:28.705078271 +0000
@@ -325,6 +325,7 @@
       <string>0x08C3</string>
       <string>0x08C3</string>
       <string>0x15E1</string>
+      <string>0x1862</string>
    </array>

    <key>ifdProductID</key>
@@ -550,6 +551,7 @@
       <string>0x0401</string>
       <string>0x0402</string>
       <string>0x2007</string>
+      <string>0x0001</string>
    </array>

    <key>ifdFriendlyName</key>
@@ -775,6 +777,7 @@
       <string>Precise Biometrics Precise 250 MC</string>
       <string>Precise Biometrics Precise 200 MC</string>
       <string>RSA RSA SecurID (R) Authenticator</string>
+                <string>TSC12xxF</string>
    </array>

    <key>Copyright</key>

在所有修改完成后,我们必须重启守护进程。现在,输出应该会发生如下变化:

root@beaglebone:~# /etc/init.d/pcscd restart
[ ok ] Restarting pcscd (via systemctl): pcscd.service.
root@beaglebone:~# pcsc_scan
PC/SC device scanner
V 1.4.20 (c) 2001-2011, Ludovic Rousseau <ludovic.rousseau@free.fr>
Compiled with PC/SC lite version: 1.8.3
Using reader plug'n play mechanism
Scanning present readers...
0: TSC12xxF (123456789) 00 00
1: TSC12xxF (123456789) 00 01
2: TSC12xxF (123456789) 00 02
3: TSC12xxF (123456789) 00 03
4: TSC12xxF (123456789) 00 04

Wed Apr 23 20:40:56 2014
Reader 0: TSC12xxF (123456789) 00 00
 Card state: Card removed,
Reader 1: TSC12xxF (123456789) 00 01
 Card state: Card removed,
Reader 2: TSC12xxF (123456789) 00 02
 Card state: Card removed,
Reader 3: TSC12xxF (123456789) 00 03
 Card state: Card removed,
Reader 4: TSC12xxF (123456789) 00 04
 Card state: Card removed,

好的!现在我们可以通过将卡片插入插槽并验证工具是否应该打印出以下内容,来验证读卡器是否正常工作:

Wed Apr 23 20:52:22 2014
Reader 0: TSC12xxF (123456789) 00 00
 Card state: Card inserted,
 ATR: 3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00

ATR: 3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00
+ TS = 3B --> Direct Convention
+ T0 = BE, Y(1): 1011, K: 14 (historical bytes)
 TA(1) = 11 --> Fi=372, Di=1, 372 cycles/ETU
 10752 bits/s at 4 MHz, fMax for Fi = 5 MHz => 13440 bits/s
 TB(1) = 00 --> VPP is not electrically connected
 TD(1) = 00 --> Y(i+1) = 0000, Protocol T = 0
-----
+ Historical bytes: 41 01 38 00 00 00 00 00 00 00 00 01 90 00
 Category indicator byte: 41 (proprietary format)

Possibly identified card (using /usr/share/pcsc/smartcard_list.txt):
3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00
 ACS (Advanced Card System) ACOS-1

该设备正在正常工作;然而,我们需要一个专门的程序来管理卡片。因此,我们先通过常用的aptitude命令安装python-pyscard包,然后考虑以下代码片段:

#
# Smart Card Observer
#

class printobserver(CardObserver):
   def update(self, observable, (addedcards, removedcards)):
      for card in addedcards:
         logging.info("->] " + toHexString(card.atr))
      for card in removedcards:
         logging.info("<-] " + toHexString(card.atr))

#
# The daemon body
#

def daemon_body():
   # The main loop
   logging.info("INFO waiting for card... (hit CTRL+C to stop)")

   try:
      cardmonitor = CardMonitor()
      cardobserver = printobserver()
      cardmonitor.addObserver(cardobserver)

      while True:
         sleep(1000000) # sleep forever

   except:
      cardmonitor.deleteObserver(cardobserver)

注意

完整代码存储在书籍示例代码仓库中的chapter_09/smart_card/smart_card.py脚本中。

程序定义了一个cardmonitor对象,并通过addObserver()方法添加其观察者,以便在插入或移除卡片时调用。

如果执行成功,程序将输出如下内容:

root@beaglebone:~/smart_card# ./smart_card.py
INFO:root:INFO waiting for card... (hit CTRL+C to stop)
INFO:root:->] 3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00
INFO:root:<-] 3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00

提示

如果在执行命令时出现以下错误,则需要安装python-daemon包:

ImportError: No module named daemon

你可以通过使用以下命令解决此问题:

root@beaglebone:~/smart_card# pip install python-daemon

设置 RFID LF 读取器

作为一个 RFID LF 读取器,我们可以使用下图所示的设备,它通过 TTL 3.3V 电平的串口发送数据:

设置 RFID LF 读取器

注意

该设备可以通过以下链接购买(或通过搜索互联网):www.cosino.io/product/lf-rfid-low-voltage-reader

该设备的 datasheet 可以在cdn.sparkfun.com/datasheets/Sensors/ID/ID-2LA,%20ID-12LA,%20ID-20LA2013-4-10.pdf找到。

它可以直接连接到我们的 BeagleBone Black,并连接到扩展连接器P9的以下引脚,对应已启用的串口设备/dev/ttyO1

引脚 RFID LF 读取器引脚 - 标签
P9.4 - Vcc 8 - Vcc
P9.26 - RxD 6 - TX
P9.2 - GND 7 - GND

在所有引脚连接完毕后,标签的数据将在/dev/ttyO1设备上可用。为了快速验证,我们可以使用以下命令:

root@BeagleBone:~# stty -F /dev/ttyO1 9600 raw
root@BeagleBone:~# cat /dev/ttyO1

然后,当将标签靠近读取器时,我们应该能听到哔哔声,并且相应的标签 ID 应显示在命令行中,如下所示。(下面的cat命令为提高可读性而重复,您不需要重新输入它):

root@BeagleBone:~# cat /dev/ttyO1
.6F007F4E1E40

然而,使用cat命令并不是最适合我们需求的操作,因为它的输出并不是完全干净的 ASCII 文本(有关此问题的更多信息,请参阅设备的 datasheet);事实上,在标签 ID 之前接收了一些字节。例如,标签 ID 前的点符号“.”就是这些字节之一。因此,我们可以设想编写一个专用工具来清理从设备接收到的消息,以便获得一个干净的 ASCII ID 字符串。这样的工具的代码片段如下:

# Read the tags' IDs
cat $dev | while read id ; do
   # Remove the non printable characters and print the data
   echo -n $id | tr '\r' '\n' | tr -cd '[:alnum:]\n'
done

注意

完整代码存储在书籍示例代码仓库中的chapter_09/rfid_lf/rfid_lf.sh脚本中。

cat命令从由dev变量指定的设备读取数据,如前面的示例所示;然后,输出通过tr命令传递,以去除不可打印字符。结果如下:

root@beaglebone:~/rfid_lf# ./rfid_lf.sh /dev/ttyO1
6F007F48C199

提示

好奇的读者可以查看tr的 man 页面,以获取有关其用法的更多信息。

设置 RFID UHF 阅读器

作为 RFID UHF 阅读器,我们可以使用以下设备,它通过 TTL 3.3V 级别的串口发送数据:

设置 RFID UHF 阅读器

注意

该设备可以通过以下链接购买(或通过网络搜索):www.cosino.io/product/uhf-rfid-long-range-reader

产品的制造商信息可以在www.caenrfid.it/en/CaenProd.jsp?mypage=3&parent=59&idmod=818查看。

它可以直接连接到我们的 BeagleBone Black,通过扩展连接器P9的以下引脚,这些引脚与已启用的串口设备/dev/ttyO2连接:

引脚 RFID UHF 阅读器引脚 - 标签
P9.6 - Vcc 1 - +5V
P9.21 - TxD 9 - RXD
P9.22 - RxD 10 - TXD
P9.1 - GND 12 - GND

在所有引脚连接完成后,标签的数据将可通过/dev/ttyO2设备获取,但为了获取这些数据,我们需要额外的软件。事实上,这个设备需要一种特殊的协议与主机进行通信,因此我们需要安装一个专用的C库来完成这一任务,具体方法将在下面部分进行说明。

我们需要下载、编译并安装三个库:libmsgbufflibavplibcaenrfid

首先,我们需要一些先决软件包。所以,让我们安装它们:

root@beaglebone:~# aptitude install git debhelper dctrl-tools

现在,我们可以使用以下命令开始下载第一个库:

root@beaglebone:~# git clone http://github.com/cosino/libmsgbuff.git

然后,我们需要进入新目录libmsgbuff并执行autogen.sh命令,如下所示:

root@beaglebone:~# cd libmsgbuff
root@beaglebone:~/libmsgbuff# ./autogen.sh

提示

可能会出现以下错误:

aclocal:configure.ac:11: warning: macro `AM_SILENT_RULES' not found in library
aclocal:configure.ac:18: warning: macro `AM_PROG_AR' not found in library
configure.ac:11: error: possibly undefined macro: AM_SILENT_RULES
 If this token and others are legitimate, please use m4_pattern_allow.
 See the Autoconf documentation.

在这种情况下,带有宏AM_SILENT_RULESAM_PROG_AR的行应该被删除,如下所示的补丁所示:

 index dcfd1ce..333e417 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,14 +8,12 @@ AC_CONFIG_SRCDIR([msgbuff.c])
 AC_CONFIG_HEADERS([configure.h])

 AM_INIT_AUTOMAKE([1.9 foreign -Wall -Werror])
-AM_SILENT_RULES([yes])

 # Global settings
 AC_SUBST(EXTRA_CFLAGS, ['-Wall -D_GNU_SOURCE -include configure.h'])

 # Checks for programs
 AC_PROG_CXX
-AM_PROG_AR
 AC_PROG_AWK
 AC_PROG_CC
 AC_PROG_CPP

然后,我们可以安全地重新启动autogen.sh命令。

然后,要重新编译库,我们可以使用以下命令行:

root@beaglebone:~/libmsgbuff# ./debian/rules binary
dpkg-deb: building package `libmsgbuff0' in `../libmsgbuff0_0.60.0_armhf.deb'
.
dpkg-deb: building package `libmsgbuff-dev' in `../libmsgbuff-dev_0.60.0_armhf.deb'.

好的,既然软件包已经准备好,我们可以使用dpkg命令安装它们,如下所示:

root@beaglebone:~/libmsgbuff# dpkg -i ../libmsgbuff0_0.60.0_armhf.deb ../libmsgbuff-dev_0.60.0_armhf.deb
Setting up libmsgbuff0 (0.60.0) ...
Setting up libmsgbuff-dev (0.60.0) ...

现在轮到第二个库了。步骤与前面的示例相同。完成后,进入上级目录,然后使用以下git命令下载新源代码:

root@beaglebone:~# git clone http://github.com/cosino/libavp.git

然后,在库的目录中执行autogen.sh脚本:

root@beaglebone:~# cd libavp
root@beaglebone:~/libavp# ./autogen.sh

提示

同前面的例子一样,如果发生undefined macro错误,只需按照前面示例中的补丁修复当前autogen.sh脚本。

然后,开始以下编译:

root@beaglebone:~/libavp# ./debian/rules binary
dpkg-deb: building package `libavp0' in `../libavp0_0.80.0_armhf.deb'.
dpkg-deb: building package `libavp-dev' in `../libavp-dev_0.80.0_armhf.deb'.

最后,执行dpkg命令来安装软件包:

root@beaglebone:~/libavp# dpkg -i ../libavp0_0.80.0_armhf.deb ../libavp-dev_0.80.0_armhf.deb

好的,最后一个库的过程类似,但有一个小提示。首先,进入上级目录。然后,下载代码并执行autogen.sh脚本(如有需要,可以像之前一样修补):

root@beaglebone:~# git clone http://github.com/cosino/libcaenrfid.git
root@beaglebone:~# cd libcaenrfid/
root@beaglebone:~/libcaenrfid# ./autogen.sh

然后,我们需要为 BeagleBone Black 的架构(在 Debian 中名为armhf)创建两个新文件。命令如下:

root@beaglebone:~/libcaenrfid# cp src/linux-gnueabi.c src/linux-gnueabihf.c
root@beaglebone:~/libcaenrfid# cp src/linux-gnueabi.h src/linux-gnueabihf.h

现在,我们可以执行常规的包生成命令,紧接着是安装命令,如下所示:

root@beaglebone:~/libcaenrfid# ./debian/rules binary
...
dpkg-deb: building package `libcaenrfid0' in `../libcaenrfid0_0.91.0_armhf.deb'.
dpkg-deb: building package `libcaenrfid-dev' in `../libcaenrfid-dev_0.91.0_armhf.deb'.
root@beaglebone:~/libcaenrfid# dpkg -i ../libcaenrfid0_0.91.0_armhf.deb ../libcaenrfid-dev_0.91.0_armhf.deb

到目前为止,所需的库已经就绪,我们可以编译我们的程序来访问 RFID UHF 读卡器。以下是可能实现的一个代码片段:

int main(int argc, char *argv[])
{
   int i;
   struct caenrfid_handle handle;
   char string[] = "Source_0";
   struct caenrfid_tag *tag;
   size_t size;
   char *str;
   int ret;

   if (argc < 2)
      usage();

      /* Start a new connection with the CAENRFIDD server */
      ret = caenrfid_open(CAENRFID_PORT_RS232, argv[1], &handle);
      if (ret < 0)
         usage();

      /* Set session "S2" for logical source 0 */
      ret = caenrfid_set_srcconf(&handle, "Source_0",
         CAENRFID_SRC_CFG_G2_SESSION, 2);
      if (ret < 0) {
         err("cannot set session 2 (err=%d)", ret);
         exit(EXIT_FAILURE);
      }

      while (1) {
         /* Do the inventory */
         ret = caenrfid_inventory(&handle, string, &tag, &size);
         if (ret < 0) {
            err("cannot get data (err=%d)", ret);
            exit(EXIT_FAILURE);
         }

         /* Report results */
         for (i = 0; i < size; i++) {
            str = bin2hex(tag[i].id, tag[i].len);
            EXIT_ON(!str);

            info("%.*s %.*s %.*s %d",
               tag[i].len * 2, str,
               CAENRFID_SOURCE_NAME_LEN, tag[i].source,
               CAENRFID_READPOINT_NAME_LEN, tag[i].readpoint,
               tag[i].type);

            free(str);
         }

         /* Free inventory data */
         free(tag);
      }

      caenrfid_close(&handle);

      return 0;
}

注意

完整的代码存储在书籍示例代码库中的chapter_09/rfid_uhf/rfid_uhf.c脚本中。

该程序简单地使用caenrfid_open()方法与读卡器建立连接,并使用caenrfid_inventory()方法来检测标签。caenrfid_set_srcconf()方法用于设置一个内部特殊功能,以避免多次读取相同的标签。

该程序可以通过在rfid_uhf目录中执行make命令进行编译,工具可以如下使用:

root@beaglebone:~/rfid_uhf# ./rfid_uhf /dev/ttyO2

如果读卡器天线附近没有标签,程序将不会有输出,但如果我们靠近一些标签,则会得到如下所示的内容:

root@beaglebone:~/chapter_09/rfid_uhf# ./rfid_uhf /dev/ttyO2
rfid_uhf.c[ 110]: main: e280113020002021dda500ab Source_0 Ant0 3

请注意,在这种情况下,与 RFID LF 的情况相反,读卡器可以在几米外检测到标签(具体距离取决于你使用的天线!)

最终图像

以下图像展示了我实现该项目并测试软件时制作的原型:

最终图像

请注意,要使用 RFID UHF 读卡器,必须使用外部电源,而其他两种读卡器则不需要。

设置软件

在硬件设置完成后,大部分工作已经完成;为了完成我们的任务,我们首先需要安装一个工具来访问我们的 Twitter 帐户,然后我们必须添加一个机制,每次成功完成识别过程时调用它。所以,在接下来的章节中,我将展示如何安装并正确设置一个命令行工具与 Twitter 进行通信,然后如何在三种不同的编程语言中调用它,以支持三种不同的识别系统。

为了简化项目,我们可以在每个程序中使用一个已知 ID 的静态列表,但你可以理解,这个列表可以通过外部数据库轻松管理。所以,我将这个实现留给你作为练习。

设置 Twitter 工具

我将用于访问 Twitter 帐户的工具命名为单字符t。根据其主页的描述,t程序源自 Twitter 的短信命令:

该 CLI 从 Twitter 的短信命令中获取语法提示,但它提供了比 SMS 命令更多的命令和功能。

实际上,一旦安装完成,它使用简单的命令来更新我们的 Twitter 状态、关注/取消关注用户、获取 Twitter 用户的详细信息、为你关注的所有人创建列表等等。

注意

对于 t 工具的完整参考,github.com/sferik/t网址是一个很好的起点。

要将此工具安装到我们的 BeagleBone Black 上,我们首先需要安装 ruby-dev 包,并使用 aptitude 程序:

root@beaglebone:~# aptitude install ruby-dev

然后,使用以下命令安装 t

root@beaglebone:~# gem install t -V

提示

执行此命令可能非常缓慢!所以,请耐心等待。

安装结束后,我们可以执行程序,如果一切正常,应该会显示一个长长的可用命令列表,如下所示:

root@beaglebone:~# t -h
Commands:
 t accounts                          # List accounts
 t authorize                         # Allows an application to request user...
 t block USER [USER...]              # Block users.
 t delete SUBCOMMAND ...ARGS         # Delete Tweets, Direct Messages, etc.
 t direct_messages                   # Returns the 20 most recent Direct Mes...
 t direct_messages_sent              # Returns the 20 most recent Direct Mes...
 t dm USER MESSAGE                   # Sends that person a Direct Message.
 t does_contain [USER/]LIST USER     # Find out whether a list contains a user.
 t does_follow USER [USER]           # Find out whether one user follows ano...
 ...

此时,与其他社交网络一样,我们需要为我们的 Twitter 账户创建一个特殊的应用程序,以获取对我们的数据的访问权限。为此,让我们将浏览器指向apps.twitter.com/app/new网址。我们将看到一个表单,在其中填写有关新应用程序的信息。只需填写三个字段:名称描述网站。请注意,应用程序的名称必须在所有 Twitter 用户中唯一,且不能包含 twitter 一词,而网站可以是任意的(例如,http://www.mydomain.com),如下图所示:

设置 Twitter 工具

关于回调 URL字段,您可以将其留空。然后,点击页面底部的开发者条款同意框,再点击创建您的 Twitter 应用程序按钮。

一旦您的应用程序成功创建,您将看到一个页面,可以在此管理您的应用程序设置,如下图所示:

设置 Twitter 工具

现在,转到权限选项卡,并将访问类型更改为读取、写入和访问直接消息,然后保存。

下一步是授权您的应用程序访问您的 Twitter 账户。为此,运行以下命令:

root@beaglebone:~# t authorize
Welcome! Before you can use t, you'll first need to register an
application with Twitter. Just follow the steps below:
 1\. Sign in to the Twitter Application Management site and click
 "Create New App".
 2\. Complete the required fields and submit the form.
 Note: Your application must have a unique name.
 3\. Go to the Permissions tab of your application, and change the
 Access setting to "Read, Write and Access direct messages".
 4\. Go to the API Keys tab to view the consumer key and secret,
 which you'll need to copy and paste below when prompted.

Press [Enter] to open the Twitter Developer site.

然后,按下回车键后,会显示以下输出:

xprop:  unable to open display ''
xprop:  unable to open display ''
Enter your API key: /usr/bin/xdg-open: 1: eval: www-browser: not found
/usr/bin/xdg-open: 1: eval: links2: not found
/usr/bin/xdg-open: 1: eval: elinks: not found
/usr/bin/xdg-open: 1: eval: links: not found
/usr/bin/xdg-open: 1: eval: lynx: not found
/usr/bin/xdg-open: 1: eval: w3m: not found
xdg-open: no method available for opening 'https://apps.twitter.com'

除了由于 t 无法执行任何浏览器而导致的错误信息外,我们还需要转到密钥和访问令牌选项卡,并在应用程序设置下的消费者密钥(API 密钥)字段中输入密钥。然后,工具会要求输入 API 密钥,因此您需要在之前的页面中输入消费者密钥(API 密钥)

设置 Twitter 工具

完成后,如果两个密钥有效,工具将显示以下输出:

In a moment, you will be directed to the Twitter app authorization page.
Perform the following steps to complete the authorization process:
 1\. Sign in to Twitter.
 2\. Press "Authorize app".
 3\. Copy and paste the supplied PIN below when prompted.

Press [Enter] to open the Twitter app authorization page.

与之前一样,工具会再次尝试打开浏览器,以显示 Twitter 应用程序授权页面,但当然它无法打开,因此会显示以下错误信息:

xprop:  unable to open display ''
xprop:  unable to open display ''
Enter the supplied PIN: /usr/bin/xdg-open: 1: eval: www-browser: not found
/usr/bin/xdg-open: 1: eval: links2: not found
/usr/bin/xdg-open: 1: eval: elinks: not found
/usr/bin/xdg-open: 1: eval: links: not found
/usr/bin/xdg-open: 1: eval: lynx: not found
/usr/bin/xdg-open: 1: eval: w3m: not found
xdg-open: no method available for opening 'https://api.twitter.com/oauth/authorize?oauth_callback=oob&oauth_consumer_key=sHSeFMEGPRqRyf9V0UB4LtQOg&oauth_nonce=9T9rSHXiaSiWXkh0ksVE5ioTcop0srz7xMG92VhVI&oauth_signature=oNWj1Lj%225BUmrFkD%252B065axJv6WSeM%253D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1443370645&oauth_token=J2fp-gAAAAAAhyrAAABAUA-YNw8&oauth_version=1.0'

好的,我们只需要复制并粘贴上面的 URL 到主机 PC 的浏览器中以完成此操作。明确来说,URL 如下:

https://api.twitter.com/oauth/authorize?oauth_callback=oob&oauth_consumer_key=sHSeFMEGPRqRyf9V0UB4LtQOg&oauth_nonce=9T9rSHXiaSiWXkh0ksVE5ioTcop0srz7xMG92VhVI&oauth_signature=oNWj1Lj%225BUmrFkD%252B065axJv6WSeM%253D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1443370645&oauth_token=J2fp-gAAAAAAhyrAAABAUA-YNw8&oauth_version=1.0

然后,应出现一个新页面,要求输入您的 Twitter 凭据,如下图所示:

设置 Twitter 工具

提示

对不起,虽然是意大利语,但这是我 Twitter 账户默认语言设置的语言。

输入你的 Twitter 凭证,如果它们正确,系统应该会提供一个 PIN,用于完成授权过程(见下图):

设置 Twitter 工具

只需复制并粘贴 PIN 到工具运行的终端中,然后按 Enter(再次提醒,启动浏览器时不必担心错误)。不过,如果所有步骤正确,t工具的最后一条消息应该是:

Authorization successful.

太好了!现在,我们准备从 BeagleBone Black 的命令行发布第一条推文了!命令如下:

root@beaglebone:~# t update 'Hello there! This is my first tweet from the command line!'
Tweet posted by @RodolfoGiometti.

Run `t delete status 648174339569897474` to delete.

下图展示了我的 Twitter 账户的一部分,最近发送的消息已发布:

设置 Twitter 工具

智能卡实现

现在让我们从获取智能卡的访问权限开始,实施我们的识别系统。代码是用 Python 编写的,展示了通过使用智能卡读卡器实现访问控制系统的一种可能实现方式。

提示

请注意,这种实现非常简约,因为我们只关注 ATR 参数,而 ATR 参数在所有情况下不能唯一地识别智能卡。

该程序与存储在chapter_09/smart_card/smart_card.py文件中的程序非常相似,所以我这里只展示相关的差异:

# The known IDs
ID2NAME = {
        '11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11 11': "user1",
        '22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22 22': "user2",
        '3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00': 'Rodolfo Giometti'
}
...
#
# Smart Card Observer
#

class printobserver(CardObserver):
   def update(self, observable, (addedcards, removedcards)):
      for card in addedcards:
         try:
            id = toHexString(card.atr)
         except:
            pass
         if len(id) == 0:
            continue
         logging.info("got tag ID " + id)

         # Verify that the tag ID is known and then
         # tweet the event
         try:
            name = ID2NAME[id]
        except:
           logging.info("unknow tag ID! Ignored")
           continue

        logging.info("Twitting that " + name + " was arrived!")
        call([t_cmd, t_args, name + " was arrived!"])

注意

完整代码存储在本书示例代码库中的chapter_09/smart_card/smart_card2twitter.py脚本中。

ID2NAME数组保存了已知 ID 的列表,也就是我们与知名人物关联的有效 ID 的 数据库。在这里,容易想象如果使用真实数据库会是更好的实现,但这种解决方案对于我们的教学目的已经足够。

update()方法提取智能卡的 ATR 字段,然后,它并非简单地打印出来,而是将当前 ID 与我们的内部数据库进行比较,在匹配成功的情况下,调用t工具更新 Twitter 帐户。

RFID LF 实现

如同前面的示例,我们需要稍微修改chapter_09/rfid_lf/rfid_lf.sh Bash 脚本,以便在当前标签 ID 出现在ID2NAME数组中的已知 ID 列表时,调用t工具。修改后的代码片段如下:

# The known IDs
declare -gA 'ID2NAME=(
   [111111111111]="user1",
   [222222222222]="user2"
   [6F007F4E1E40]="Rodolfo Giometti"
)'
…
# Read the tags' IDs
cat $dev | while read id ; do
   # Remove the non printable characters
   id=$(echo $id | tr -cd '[:alnum:]')
   info "got tag ID $id"

   # Verify that the tag ID is known and then tweet the event
   name=${ID2NAME[$id]}
   if [ -z "$name" ] ; then
      info "unknow tag ID! Ignored"
   else
      info "Twitting that $name was arrived!"
      $t_cmd $t_args "$name was arrived!"
   fi
done

注意

完整代码存储在本书示例代码库中的chapter_09/rfid_lf/rfid_lf2twitter.sh脚本中。

RFID UHF 实现

最后的实现是用 C 编写的,并且使用 RFID UHF 读卡器获取识别字符串。这个方法现在已经非常流行;我们只需修改chapter_09/rfid_uhf/rfid_uhf.c程序,以便检查当前标签 ID 是否与ID2NAME数组中已知的 ID 匹配。代码片段如下:

/* The known IDs */
struct associative_array_s {
   char *id;
   char *name;
} ID2NAME[] = {
   { "111111111111111111111111", "user1" },
   { "222222222222222222222222", "user2" },
   { "e280113020002021dda500ab", "Rodolfo Giometti" },
};
...
   /* The main loop */
   while (1) {
      /* Do the inventory */
      ret = caenrfid_inventory(&handle, string, &tag, &size);
      if (ret < 0) {
         err("cannot get data (err=%d)", ret);
         exit(EXIT_FAILURE);
      }

      /* Report results */
      for (i = 0; i < size; i++) {
         str = bin2hex(tag[i].id, tag[i].len);
         EXIT_ON(!str);
         info("got tag ID %.*s", tag[i].len * 2, str);

         for (j = 0; j < ARRAY_SIZE(ID2NAME); j++)
            if (strncmp(str, ID2NAME[j].id,
               tag[i].len * 2) == 0)
            break;
         if (j < ARRAY_SIZE(ID2NAME)) {
            info("Twitting that %s was arrived!",
               ID2NAME[j].name);
            ret = asprintf(&cmd, "%s %s %s was arrived!", t_cmd, t_arg, ID2NAME[j].name);
            EXIT_ON(ret < 1);
            ret = system(cmd);
            EXIT_ON(ret < 0);
            free(cmd);
         } else
         info("unknow tag ID! Ignored");

         free(str);
   }

   /* Free inventory data */
   free(tag);
}

注意

完整的代码存储在书中示例代码库中的chapter_09/rfid_uhf/rfid_uhf2twitter.c文件中。

执行之前,别忘了先编译它!

最终测试

为了测试我们的原型,我们必须运行前一节中介绍的三种不同的程序。和前几章一样,我首先执行了书中示例代码库中的chapter_09/SYSINIT.sh文件,以设置所有外设:

root@beaglebone:~# ./SYSINIT.sh
done!

然后,让我们启动智能卡程序并插入智能卡,如下所示:

root@beaglebone:~/smart_card# ./smart_card2twitter.py
INFO:root:got tag ID 3B BE 11 00 00 41 01 38 00 00 00 00 00 00 00 00 01 90 00
INFO:root:Twitting that Rodolfo Giometti was arrived!
Tweet posted by @RodolfoGiometti.

Run `t delete status 649586168313552896` to delete.

现在,按下CTRL + C键停止程序,然后按照如下方式尝试 RFID LF 程序,通过接近相应标签来进行测试:

root@beaglebone:~/rfid_lf# ./rfid_lf2twitter.sh /dev/ttyO1
rfid_lf2twitter.sh: got tag ID 6F007F4E1E40
rfid_lf2twitter.sh: Twitting that Rodolfo Giometti was arrived!
Tweet posted by @RodolfoGiometti.

Run `t delete status 649586168313552896` to delete.

再次使用CTRL + C键停止程序,然后按照如下方式重新接近相应标签来测试最后一个程序:

root@beaglebone:~/rfid_uhf# ./rfid_uhf2twitter /dev/ttyO2
rfid_uhf2twitter.c[ 122]: main: Twitting that Rodolfo Giometti was arrived!
Tweet posted by @RodolfoGiometti.

Run `t delete status 649586168313552896` to delete.

总结

在这一章,我们探索了如何向 Twitter 账户发布消息,以及使用不同的识别技术和编程语言识别人员或物体的三种不同方法。

在下一章,我们将探索如何使用普通遥控器(或任何红外设备)管理一些灯光。我们将看到如何通过我们的电视遥控器让 BeagleBone Black 板接收一些命令。

第十章:使用电视遥控器的灯光管理器

在这个项目中,我们将通过使用普通的电视遥控器来管理我们家的灯光。

实际上,我们可以使用任何我们拥有的遥控器,但这个想法是通过红外机制将远程控制功能添加到我们家中的任何设备上。实际上,在本章中,我将展示如何管理一个简单的开关设备;但这个概念可以很容易地扩展到我们可以连接到 BeagleBone Black 的任何其他设备!

我们将看到如何通过使用合适的电路来捕捉遥控器发送到我们的 BeagleBone Black 的红外消息,然后,我们将使用专用的内核驱动程序来管理这些消息,并将它们转换为我们用户空间程序的明确命令。

工作原理基础

我们将要实现的原型的功能非常简单。我们需要一个电子电路,能够检测遥控器发出的红外光,然后生成一些脉冲,这些脉冲会被一个特殊的软件捕捉并存储在配置文件中,以便稍后使用。然后,通过使用一个特殊的守护进程,我们可以将遥控器上的按键按下转换为适合我们 BeagleBone Black 的命令。

在这种情况下,我们需要实现的硬件非常简单。我们只需要一个带红外功能的光电二极管(红外接收器)的简单电路。另一方面,软件部分稍微复杂一些,因为我们首先需要一个内核驱动程序来可靠地检测来自遥控器的消息,然后是一个用户级程序来记录这些消息,一个程序来识别按下的是哪个按钮,最后一个程序将这些按键转换为开关命令(或者我们希望控制的其他命令)。

由于空间限制,我将使用第三章中使用的继电器阵列,水族馆监控,并留给你连接你需要的设备。

注意

警告——请记住,即使使用的继电器阵列适合控制高电压,出于安全原因,如果你不知道自己在做什么,请不要连接任何电压高于 12V 的设备!

设置硬件

如前所述,硬件设置非常简单。继电器阵列已经在前一章中设置好了,而红外接收器电路则非常复杂。所以,让我们继续吧!

设置红外探测器

我在这个原型中使用的红外探测器(或接收器)如下图所示(实际上,接收器是带有红点的设备;另一个是我们在这里不使用的发射器)。

设置红外探测器

注意

这些设备可以通过以下链接购买(或通过上网搜索):www.cosino.io/product/infrared-emitter-detector

数据表可以在 www.sparkfun.com/datasheets/Components/LTR-301.pdf 获取。

提示

请注意,图像仅显示了红外设备的最上部分。实际上,它们看起来类似于普通二极管。

管理该电路的电路图如下所示:

设置红外检测器

红外接收器IR)是前面图示中带红点的二极管,R 是一个 6.8KΩ 的电阻。下表显示了与 BeagleBone Black 的连接:

引脚 IR 接收器标签
P9.3 - Vcc 3.3V
P9.12 - GPIO60 GPIO @R
P9.1 - GND GND

要测试功能,可以通过以下命令将 GPIO 60 引脚设置为输入引脚:

root@arm:~# ./bin/gpio_set.sh 60 in

提示

请记住,系统中不能加载任何使用该引脚的驱动程序,否则会出现错误信息!

然后,我们可以使用以下脚本持续读取 GPIO 状态,并在其变为 0 时在终端打印出来:

root@arm:~# while true ; do cat /sys/class/gpio/gpio60/value | grep 0 ; done

当你将遥控器对准红外接收器并按下一个按钮时,你应该会得到如下输出:

root@arm:~# while true ; do cat /sys/class/gpio/gpio60/value | grep 0 ; done
0
0
0
0
0
...

要停止脚本,只需按下 CTRL + C 键组合。

设置继电器阵列

继电器阵列在下图中显示。它是已在第三章中使用的设备,水族馆监控,因此你可以参考那里了解更多信息,而在这里我只会展示此原型所需的连接。

设置继电器阵列

注意

该设备可以通过以下链接(或通过上网搜索)购买:www.cosino.io/product/5v-relays-array

BeagleBone Black 的 GPIO 引脚和我用来与这些设备配合使用的继电器阵列板的引脚显示在下表中:

引脚 继电器阵列引脚
P8.10 - GPIO68 IN1
P8.9 - GPIO69 IN2
P8.12 - GPIO44 IN3
P8.11 GPIO45 IN4
P9.1 - GND GND
P9.6 - 5V Vcc

要测试每个 GPIO 引脚的功能,我们可以通过以下命令启用其中一个引脚,作为示例:

root@arm:~# ./bin/gpio_set.sh 68 out 1

提示

请注意,继电器的关断状态是 1,而开启状态是 0

然后,我们可以通过在 /sys/class/gpio/gpio68/value 文件中写入 01 来打开和关闭继电器,如下所示:

root@arm:~# echo 0 > /sys/class/gpio/gpio68/value
root@arm:~# echo 1 > /sys/class/gpio/gpio68/value

最终图片

下图显示了我为实现这个项目并测试软件所制作的原型。你应该注意到红外接收器位于右下角。

最终图片

请注意,要使用需要 5V 电源电压的继电器阵列,必须使用外部电源为 BeagleBone Black 供电。

设置软件

现在,是时候设置软件来管理我们的红外探测器了。为此,我们将使用LIRCLinux 红外遥控)子系统,这是为此目的开发的一个特殊代码。

注意

若要进一步了解 LIRC 子系统,您可以查看www.lirc.org/

我们将需要一个内核驱动程序,将红外探测器生成的脉冲转换为定义良好的消息,然后通过 LIRC 设备将它们发送到用户空间程序。在用户空间级别,我们将使用 LIRC 项目的一个特殊工具,将红外消息转换为输入事件,即普通键盘发送到内核的消息。

注意

若要进一步了解 Linux 输入子系统,您可以查看www.kernel.org/doc/Documentation/input/input.txt

设置内核驱动程序

为了设置内核驱动程序以管理我们的红外接收器,我们可以使用类似于第四章中所使用的程序,Google Docs 气象站。一旦从 GitHub 仓库下载了源代码,我们需要按照第四章,Google Docs 气象站中的程序,直到需要应用我们特殊补丁的步骤。事实上,在这种情况下,我们必须应用位于书籍示例代码仓库中的chapter_10/0001-Add-support-for-Homebrew-GPIO-Port-Receiver-Transmit.patch文件中的补丁,以便添加对红外接收器的支持。

命令如下:

$ git am --whitespace=nowarn 0001-Add-support-for-Homebrew-GPIO-Port-Receiver-Transmit.patch

提示

请注意,--whitespace=nowarn命令行选项是必需的,以防您的git系统被配置为自动修复空格错误,但在此情况下这是错误的。

如果一切顺利,git log 命令应该显示以下内容:

$ git log -1
commit be816108417ce82c7114ebd578ac32a45aef934a
Author:     Rodolfo Giometti <giometti@linux.it>
AuthorDate: Sun Oct 11 08:43:49 2015 +0200
Commit:     Rodolfo Giometti <giometti@linux.it>
CommitDate: Thu Oct 22 14:53:44 2015 +0200

 Add support for Homebrew GPIO Port Receiver/Transmitter

 Signed-off-by: Rodolfo Giometti <giometti@linux.it>

在开始内核编译之前,先让我简要说明一下这个补丁。它仅仅是将一个新驱动程序添加到 Linux 源代码中的KERNEL/drivers/staging/media/lirc目录下。所以,应用补丁后,如果我们查看新文件lirc_gpio.c,就能发现它是如何工作的。

提示

以下是对驱动代码的简要说明。如果您不关心这些内容,并且只希望直接使用驱动程序原样,您可以安全地跳过这一部分,直到内核编译命令部分。

一开始,我们有如下内核模块参数:

/*
 * Module parameters
 */

/* Set the default GPIO input pin */
staticintgpio_in = -1;
MODULE_PARM_DESC(gpio_in, "GPIO input/receiver pin number "
                   "(warning: it MUST be an interrupt capable pin!)");
module_param(gpio_in, int, S_IRUGO);

/* Set the default GPIO output pin */
staticintgpio_out = -1;
MODULE_PARM_DESC(gpio_out, "GPIO output/transmitter pin number");
module_param(gpio_out, int, S_IRUGO);

/* Set the sense mode: -1 = auto, 0 = active high, 1 = active low */
staticint sense = -1;
MODULE_PARM_DESC(sense, "Override autodetection of IR receiver circuit: ""0 = active high, 1 = active low (default -1 = auto)");
module_param(sense, int, S_IRUGO);

/* Use softcarrier by default */
static unsigned int softcarrier = 1;
MODULE_PARM_DESC(softcarrier, "Software carrier: 0 = off, 1 = on (default on)");
module_param(softcarrier, uint, S_IRUGO);

我们将使用gpio_in参数来指定红外接收器连接的输入引脚。接下来,跟随一些本地函数(此处不予列出),然后我们会看到文件操作定义:

static const struct file_operationslirc_fops = {
        .owner          = THIS_MODULE,
        .write          = lirc_write,
        .unlocked_ioctl = lirc_ioctl,
        .read           = lirc_dev_fop_read,
        .poll           = lirc_dev_fop_poll,
        .open           = lirc_dev_fop_open,
        .release        = lirc_dev_fop_close,
        .llseek         = no_llseek,
};

每个函数都与一个明确定义的系统调用相关,我们可以在新的 LIRC 设备上使用它。

在文件的最底部,有一个 lirc_gpio_init_module() 函数,它负责设置新设备。作为第一步,这个函数尝试请求所有所需的 GPIO 引脚:

 /*  
   * Check for valid gpio pin numbers 
   */ 
   ret = gpio_request(gpio_in, LIRC_GPIO_NAME " ir/in"); 
   if (ret) { 
      pr_err("failed to request GPIO %u\n", gpio_in); 
      return -EINVAL; 
   } 
   ret = gpio_direction_input(gpio_in); 
   if (ret) { 
      pr_err("failed to set pin direction for gpio_in\n"); 
      ret = -EINVAL; 
      goto exit_free_gpio_in; 
   } 
   pr_info("got GPIO %d for receiving\n", gpio_in); 
   /* Is GPIO in pin IRQ capable? */ 
   irq = gpio_to_irq(gpio_in); 
   if (irq < 0) { 
      pr_err("failed to map GPIO %d to IRQ\n", gpio_in); 
      ret = -EINVAL;
      goto exit_free_gpio_in; 
   } 
   ret = request_irq(irq, (irq_handler_t) irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_ RISING, LIRC_GPIO_NAME, (void *) 0); 
   if (ret < 0) { 
      pr_err("unable to request IRQ %d\n", irq); 
      goto exit_free_gpio_in; 
   } 
   pr_info("got IRQ %d for GPIO %d\n", irq, gpio_in); 
   if (gpio_out >= 0) { 
      ret = gpio_request(gpio_out, LIRC_GPIO_NAME " ir/ out"); 
      if (ret) { 
         pr_err("failed to request GPIO %u\n", gpio_ out); 
         goto exit_free_irq; 
      } 
      ret = gpio_direction_output(gpio_out, 0); 
      if (ret) { 
         pr_err("failed to set pin direction for gpio_ out\n"); 
         ret = -EINVAL; 
         goto exit_free_gpio_out; 
      } 
      pr_info("got GPIO %d for transmitting\n", gpio_out); 
   } 

在请求 gpio_in 引脚后,函数将其设置为输入引脚,然后检查该 GPIO 引脚是否支持中断;否则,驱动程序将无法正常工作。如果支持,函数请求 IRQ 引脚,然后继续处理 gpio_out 引脚(注意这不是强制性的)。

然后,驱动程序通过一个小的自动检测程序设置传感模式(如果用户在加载时没有直接指定),如下所示代码片段:

     /* Set the sense mode */
     if (sense != -1) {
             pr_info("manually using active %s receiver on GPIO %d\n",
                     sense ? "low" : "high", gpio_in);
     } else {
             /* wait 1/2 sec for the power supply */
             msleep(500);

             /*
              * probe 9 times every 0.04s, collect "votes" for
              * active high/low
              */
             nlow = 0;
             nhigh = 0;
             for (i = 0; i < 9; i++) {
                     if (gpio_get_value(gpio_in))
                             nlow++;
                     else
                             nhigh++;
                     msleep(40);
             }
             sense = (nlow >= nhigh ? 1 : 0);
             pr_info("auto-detected active %s receiver on GPIO pin %d\n",
                     sense ? "low" : "high", gpio_in);
     }

然后,我们可以通过首先调用 lirc_buffer_init() 函数来正确分配一个内存缓冲区用于消息管理,接着调用 lirc_register_driver(),将驱动程序注册到系统中,如下所示代码片段:

     /*
      * Setup the LIRC driver
      */

     ret = lirc_buffer_init(&rbuf, sizeof(int), RBUF_LEN);
     if (ret < 0) {
          pr_err("unable to init lirc buffer!\n");
             ret = -ENOMEM;
             goto exit_free_gpio_out;
     }

     ret = platform_driver_register(&lirc_gpio_driver);
     if (ret) {
             pr_err("error in lirc register\n");
             goto exit_free_buffer;
        }

        lirc_gpio_dev = platform_device_alloc(LIRC_GPIO_NAME, 0);
        if (!lirc_gpio_dev) {
                pr_err("error on platform device alloc!\n");
                ret = -ENOMEM;
goto exit_driver_unreg;
        }

        ret = platform_device_add(lirc_gpio_dev);
        if (ret) {
                pr_err("error on platform device add!\n");
goto exit_device_put;
        }

        driver.features = LIRC_CAN_REC_MODE2;
        if (gpio_out >= 0) {
                driver.features |= LIRC_CAN_SET_SEND_DUTY_CYCLE |
                          LIRC_CAN_SET_SEND_CARRIER |
                          LIRC_CAN_SEND_PULSE;
        }

        driver.dev = &lirc_gpio_dev->dev;
        driver.minor = lirc_register_driver(&driver);

        if (driver.minor < 0) {
                pr_err("device registration failed!");
                ret = -EIO;
goto exit_device_put;
        }

        pr_info("driver registered!\n");

        return 0;

好的,现在我们可以使用以下命令开始编译内核:

$ ./build_kernel.sh

提示

这一步,以及接下来的步骤,都比较耗时且需要耐心,所以你可以喝杯你喜欢的茶或咖啡,耐心等待。

一段时间后,程序将显示标准的内核配置面板,现在我们应该验证所需的驱动程序是否已启用。你应该在菜单中导航至 设备驱动程序 | 暂存驱动程序 | 媒体暂存驱动程序 | Linux 红外遥控接收器/发射器驱动程序,其中 Homebrew GPIO 端口接收器/发射器 条目应选择为模块()。

然后,退出配置菜单,内核编译应该开始。接着,当编译完成时,新的内核镜像将准备就绪,并且应该显示如下消息:

-----------------------------
Script Complete
eewiki.net: [user@localhost:~$ export kernel_version=3.13.11-bone12]
-----------------------------

现在,我们可以使用以下安装工具将其安装到 microSD 上:

$ ./tools/install_kernel.sh

如果一切正常,经过常规登录后,我们可以使用以下命令验证新内核是否真的在运行:

root@arm:~# uname -a
Linux arm 3.13.11-bone12 #1 SMP Sun Oct 11 09:15:46 CEST 2015 armv7l GNU/Linux

提示

请注意,你系统上的内核版本可能比我的更新。

好的,新内核已经准备好了!现在,我们可以通过以下命令加载 LIRC 驱动程序:

root@arm:~# modprobe lirc_gpio gpio_in=60

提示

请注意,GPIO 60 必须没有被使用,否则你可能会遇到如下错误:

ERROR: could not insert 'lirc_gpio': Invalid argument

内核消息应类似如下:

lirc_dev: IR Remote Control driver registered, major 241
lirc_gpio: module is from the staging directory, the quality is unknown, you have been warned.
lirc_gpio: got GPIO 60 for receiving
lirc_gpio: got IRQ 204 for GPIO 60
lirc_gpio: auto-detected active low receiver on GPIO pin 60
lirc_gpio lirc_gpio.0: lirc_dev: driver lirc_gpio registered at minor = 0
lirc_gpio: driver registered!

此外,现在应该在 /dev 目录下准备好一个新条目:

root@arm:~/chapter_10# ls -l /dev/lirc*
crw-rw---T 1 root video 241, 0 Aug 13 16:35 /dev/lirc0

LIRC 工具

现在内核模块已设置并运行,我们需要一些用户空间工具来管理它。所以,让我们通过常规的 aptitude 命令安装 lirc 包:

root@arm:~# aptitude install lirc
...
Setting up lirc (0.9.0~pre1-1) ...
[ ok ] No valid /etc/lirc/lircd.conf has been found..
[ ok ] Remote control support has been disabled..
[ ok ] Reconfigure LIRC or manually replace /etc/lirc/lircd.conf to enable..

如前述所示,为了启用 lircd 守护进程(即我们需要的工具),我们必须替换配置文件 /etc/lirc/lircd.conf;然而,我们并不打算以这种方式使用守护进程。实际上,我们可以通过执行以下命令来测试驱动程序是否按预期工作:

root@arm:~# mode2 --driver default --device /dev/lirc0

直到你将遥控器对准红外接收器并按下一个按钮,什么都不会发生。在这种情况下,你应该看到一些输出,如下所示:

space 3333126
pulse 8985
space 4503
pulse 564
space 535
pulse 564
space 561
pulse 542
space 551
...

好的!/dev/lirc0设备正在正常工作,驱动程序正确地检测到来自遥控器的消息!现在,我们必须创建一个自定义配置文件,将每个遥控器按钮与输入事件关联起来。

提示

由于空间不足,我将在以下示例中仅配置几个按钮;但你可以根据需要添加任何按钮。

使用的命令是irrecord,如下所示:

root@arm:~# irrecord --driver default --device /dev/lirc0 myremote.conf

myremote.conf是我们希望保存配置的文件。程序随后将显示如下输出:

irrecord -  application for recording IR-codes for usage with lirc

Copyright (C) 1998,1999 Christoph Bartelmus(lirc@bartelmus.de)

This program will record the signals from your remote control
and create a config file for lircd.

A proper config file for lircd is maybe the most vital part of this
package, so you should invest some time to create a working config
file. Although I put a good deal of effort in this program it is often
notpossible to automatically recognize all features of a remote
control. Often short-comings of the receiver hardware make it nearly
impossible. If you have problems to create a config file READ THE
DOCUMENTATION of this package, especially section "Adding new remote
controls" for how to get help.

If there already is a remote control of the same brand available at
http://www.lirc.org/remotes/ you might also want to try using such a
remote as a template. The config files already contain all
parameters of the protocol used by remotes of a certain brand and
knowing these parameters makes the job of this program much
easier. There are also template files for the most common protocols
available in the remotes/generic/ directory of the source
distribution of this package. You can use a template files by
providing the path of the file as command line parameter.

Please send the finished config files to <lirc@bartelmus.de> so that I
can make them available to others. Don't forget to put all information
that you can get about the remote control in the header of the file.

Press RETURN to continue.

好的,按下return/Enter键,程序将继续显示以下消息:

Now start pressing buttons on your remote control.

It is very important that you press many different buttons and hold them
down for approximately one second. Each button should generate at least one dot but in no case more than ten dots of output.
Don't stop pressing buttons until two lines of dots (2x80) have been
generated.

Press RETURN now to start recording.

好的,现在非常重要的是仔细遵循前面的指示。因此,开始按下不同的按钮并保持大约一秒钟,以便为每次按下生成至少一个点,但每次按下的输出点数不能超过十个!

所以,程序将开始打印点,直到它到达终端的末尾,如下所示:

....................................................................
Found const length: 107736

当第一行完成时,程序会显示以下消息,并且会出现新的点,但这次每按一个按钮只会出现一个点!:

Please keep on pressing buttons like described above.
............irrecord: signal too long
Creating config file in raw mode.
Now enter the names for the buttons.

现在,第一阶段的检测已经完成,我们可以开始真正的检测,每次按一个按钮。系统会提示输入按钮名称或按Enter键完成:

Please enter the name for the next button (press <ENTER> to finish recording)

现在,我通过插入KEY_0字符串来输入按钮0的名称,如下所示。然后,系统会提示你按住按钮0,直到它识别到:

KEY_0

Now hold down button "KEY_0".
Got it.
Signal length is 67

提示

有效的按钮名称可以通过使用irrecord命令列出,如下所示:

root@arm:~# irrecord --list-namespace
KEY_0
KEY_102ND
KEY_1
KEY_2
KEY_3
KEY_4
KEY_5
KEY_6
KEY_7
KEY_8
KEY_9
KEY_A
KEY_AB
...

然后,程序将为下一个按钮重新开始如下过程:

Please enter the name for the next button (press <ENTER> to finish recording)
KEY_1

Now hold down button "KEY_1".
Got it.
Signal length is 67

Please enter the name for the next button (press <ENTER> to finish recording)
KEY_2

Now hold down button "KEY_2".
Got it.
Signal length is 67

Please enter the name for the next button (press <ENTER> to finish recording)
KEY_3

Now hold down button "KEY_3".
Got it.
Signal length is 67

此时,我什么也不输入,只需按Enter键退出,然后会再次显示提示符:

Please enter the name for the next button (press <ENTER> to finish recording)

root@arm:~#

现在,一个名为myremote.conf的新文件应该已经准备好了。以下是我的文件片段:

# Please make this file available to others
# by sending it to <lirc@bartelmus.de>
#
# this config file was automatically generated
# using lirc-0.9.0-pre1(default) on Wed Aug 13 15:54:26 2014
#
# contributed by
#
# brand:                       myremote.conf
# model no. of remote control:
# devices being controlled by this remote:
#

begin remote

  name  myremote.conf
  flags RAW_CODES
  eps            30
  aeps          100

  gap          96036

begin raw_codes

          name KEY_0
             8998    4478     566     541     570     541
              570     541     570     542     570     541
              570     541     570     541     578     533
              570     541     570     541     570     542
              570     540     570    1679     571     541
              570     541     569     543     569     542
              570     541     570    1679     570    1678
              571     541     570     541     570     542
              570     540     570    1679     570    1679
              570     541     571     540     571    1685
              563    1679     570    1678     571    1678
              571   47910    9003    2231     570

          name KEY_1
             8969    4507     537     571     539     571
              540     572     539     572     539     572
              540     571     540     572     546     565
              539     572     540     571     540     571
              540     571     540    1709     540     572
              539     572     546     566     538    1709
              540    1709     540     572     539     572
              539     572     539     573     545     566
              538     572     539     573     538     572
              539    1710     540    1709     539    1712
              539    1709     539    1709     539    1710
              539   47930    8983    2261     539
...

现在,我们准备好测试我们的工作了。我们需要验证是否所有按钮都已正确识别。为此,我们必须从命令行执行lircd守护进程,命令如下:

root@arm:~# lircd --nodaemon --device /dev/lirc0 --driver default --uinput myremote.conf
lircd-0.9.0-pre1[2235]: lircd(default) ready, using /var/run/lirc/lircd

最后一个参数--uinput用于指示lircd守护进程将按钮按压转换为输入事件,就像它们来自普通键盘一样,因此我们可以使用evtest命令来测试它们。由于前一个命令必须与evtest同时运行,因此该命令必须在另一个终端中执行!命令如下:

root@arm:~# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:      lircd
Select the device event number [0-0]:

现在,我们必须选择(唯一的)可用输入设备,使用0号,程序将继续显示以下输出:

Input driver version is 1.0.1
Input device ID: bus 0x0 vendor 0x0 product 0x0 version 0x0
Input device name: "lircd"
Supported events:
 Event type 0 (EV_SYN)
 Event type 1 (EV_KEY)
 Event code 1 (KEY_ESC)
 Event code 2 (KEY_1)
 Event code 3 (KEY_2)
 Event code 4 (KEY_3)
 ...
 Event code 237 (KEY_BLUETOOTH)
 Event code 238 (KEY_WLAN)
 Event code 239 (KEY_UWB)
 Event code 240 (KEY_UNKNOWN)
 Event type 20 (EV_REP)
Properties:
Testing ... (interrupt to exit)

然后,当我按下遥控器上的按钮时,我会看到以下输出:

Event: time 1445765562.506427, type 1 (EV_KEY), code 11 (KEY_0), value 1
Event: time 1445765562.506427, -------------- SYN_REPORT ------------
...
Event: time 1445765566.745716, type 1 (EV_KEY), code 2 (KEY_1), value 1
Event: time 1445765566.745716, -------------- SYN_REPORT ------------
...
Event: time 1445765568.216621, type 1 (EV_KEY), code 3 (KEY_2), value 1
Event: time 1445765568.216621, -------------- SYN_REPORT ------------
...
Event: time 1445765569.357041, type 1 (EV_KEY), code 4 (KEY_3), value 1
Event: time 1445765569.357041, -------------- SYN_REPORT ------------
...

提示

evtest程序可以使用以下命令安装:

root@arm:~# aptitude install evtest

请注意,遥控器上的0按钮对应于KEY_0输入事件,该事件的代码为11,而123按钮分别对应KEY_1KEY_2KEY_3输入事件,它们的代码分别是234。因此,我们可以通过使用如下查找表将这些事件映射到相应的 GPIO 线路(Python 语法):

GPIO = [-1, -1, 69, 44, 45, -1, -1, -1, -1, -1, -1, 68]

-1值表示无 GPIO。因此,当我们按下0按钮时,我们会收到KEY_0输入事件,该事件的代码为11,在数组的第 11 个位置(从0开始计数)我们有68值,所以GPIO68与遥控器上的0按钮连接。以类似的方式,123按钮分别对应KEY_1(代码为 2)、KEY_2(代码为 3)和KEY_3(代码为 4)输入事件,它们分别连接到GPIO 69(数组索引 2)、GPIO 44(数组索引 3)和GPIO 45(数组索引 4)。

输入事件管理器

现在,我们只需要添加最后一个元素,即:处理输入事件并打开或关闭相应继电器的软件。为了以一种快速且简单的方式做到这一点,我们可以使用 Python 语言和evdev库,该库可以通过以下命令轻松安装到我们的 BeagleBone Black 上:

root@arm:~# pip install evdev

注意

好奇的读者可以在python-evdev.readthedocs.org/en/latest/获取更多关于此库的信息。

在安装了该库后,我们可以考虑我们输入事件管理器的一个可能实现,如下所示的代码片段:

#
# Local functions
#

def gpio_get(gpio):
   fd = open("/sys/class/gpio/gpio" + str(gpio) + "/value", "r")
   val = fd.read()
   fd.close()
return int(val)

def gpio_set(gpio, val):
   fd = open("/sys/class/gpio/gpio" + str(gpio) + "/value", "w")
   v = fd.write(str(val))
   fd.close()

def usage():
   print("usage: ", NAME, " [-h] <inputdev>", file=sys.stderr)
   sys.exit(2);

#
# Main
#

try:
   opts, args = getopt.getopt(sys.argv[1:], "h",
      ["help"])
except getopt.GetoptError, err:
   # Print help information and exit:
   print(str(err), file=sys.stderr)
   usage()

for o, a in opts:
   if o in ("-h", "--help"):
      usage()
   else:
      assert False, "unhandled option"

# Check command line
if len(args) < 1:
   usage()

# Try to open the input device
try:
   dev = InputDevice(args[0])
except:
   print("invalid input device", args[0], file=sys.stderr)
   sys.exit(1);

logging.info (dev)
logging.info("hit CTRL+C to stop")

# Start the main loop
for event in dev.read_loop():
    if event.type == ecodes.EV_KEY and event.value == 1:
           # Get the key code and convert it to the corresponding GPIO
           code = event.code
           if code < 0 or code > len(GPIO):
                   gpio = -1
           else:
                   gpio = GPIO[code]
           logging.info("got code %d -> GPIO%d" % (code, gpio))

           if gpio > 0:
                   # Get current GPIO status and invert it
                   status = gpio_get(gpio)
                   status = 1 - status
                   gpio_set(gpio, status)
                   logging.info("turning GPIO%d %d -> %d" %
                           (gpio, 1 - status, status))
           else:
                  logging.info("invalid button")

注意

完整的代码存储在本书示例代码库中的chapter_10/read_events.py文件中。

代码本身很容易理解,但让我解释一些要点。首先,请注意GPIO数组是在前一节中定义的,然后gpio_get()gpio_set()方法用于获取和设置 GPIO 状态。程序在对命令行进行简单检查后,开始通过InputDevice()方法打开用户提供的输入设备,然后进入大循环,在此循环中等待按键按下,之后它会切换相应 GPIO 的状态(如果有的话)。

以下是一个示例用法:

root@arm:~# ./read_events.py /dev/input/event0
INFO:root:device /dev/input/event0, name "lircd", phys ""
INFO:root:hit CTRL+C to stop
INFO:root:got code 2 -> GPIO68
INFO:root:turning GPIO68 1 -> 0
INFO:root:got code 3 -> GPIO69
INFO:root:turning GPIO69 1 -> 0
INFO:root:got code 3 -> GPIO69
INFO:root:turning GPIO69 0 -> 1
INFO:root:got code 2 -> GPIO68
INFO:root:turning GPIO68 0 -> 1

现在,在继续之前,我想向您推荐使用 Linux 输入层的一个有趣功能。

尽管使用输入层而不是直接访问lircd守护进程可能看起来有点复杂,但这种方法有一个巨大优势,那就是我们可以使用任何输入设备来测试我们的继电器管理器!事实上,如果你尝试将普通键盘连接到 BeagleBone Black 的 USB 端口,你将获得如下所示的新输入设备:

root@arm:~# evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0:   lircd
/dev/input/event1:   HID 04d9:1203
Select the device event number [0-1]:

现在,选择新的输入设备/dev/input/event1,我们只需按下0123键,就可以生成与之前相同的输入事件:

Event: time 1445766356.367407, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70027
Event: time 1445766356.367407, type 1 (EV_KEY), code 11 (KEY_0), value 1
Event: time 1445766356.367407, -------------- SYN_REPORT ------------
...
Event: time 1445766365.537391, type 4 (EV_MSC), code 4 (MSC_SCAN), value 7001e
Event: time 1445766365.537391, type 1 (EV_KEY), code 2 (KEY_1), value 1
Event: time 1445766365.537391, -------------- SYN_REPORT ------------
...
Event: time 1445766367.437377, type 4 (EV_MSC), code 4 (MSC_SCAN), value 7001f
Event: time 1445766367.437377, type 1 (EV_KEY), code 3 (KEY_2), value 1
Event: time 1445766367.437377, -------------- SYN_REPORT ------------
...
Event: time 1445766369.537383, type 4 (EV_MSC), code 4 (MSC_SCAN), value 70020
Event: time 1445766369.537383, type 1 (EV_KEY), code 4 (KEY_3), value 1
Event: time 1445766369.537383, -------------- SYN_REPORT ------------
...

提示

请注意,尽管由于空间限制这里未显示,但键盘会生成比通常的EV_KEY事件更多的输入事件。不过,我们可以通过选择正确的输入事件类型轻松跳过它们。

在这种情况下,如果我们按以下命令行执行程序,我们就可以像使用遥控器一样管理继电器:

root@arm:~# ./read_events.py /dev/input/event1
INFO:root:device /dev/input/event1, name "HID 04d9:1203", phys "usb-musb-hdrc.1.auto-1/input0"
INFO:root:hit CTRL+C to stop
INFO:root:got code 11 -> GPIO68
INFO:root:turning GPIO68 1 -> 0
INFO:root:got code 2 -> GPIO69
INFO:root:turning GPIO69 1 -> 0
INFO:root:got code 3 -> GPIO44
INFO:root:turning GPIO44 1 -> 0
INFO:root:got code 4 -> GPIO45
INFO:root:turning GPIO45 1 -> 0

最终测试

和前几章一样,我们首先需要执行本书示例代码库中的chapter_10/SYSINIT.sh文件,以便设置所有 GPIO 线路并加载内核模块:

root@beaglebone:~# ./SYSINIT.sh
done!

然后,我们必须执行lircd守护进程,通过命令行并且不带--nodaemon选项参数:

root@arm:~# lircd --device /dev/lirc0 --driver default --uinputmyremote.conf

然后,我们可以执行前面的read_events.py程序来管理继电器:

root@arm:~# ./read_events.py /dev/input/event0

现在,技巧已经完成。我们只需要将遥控器对准红外探测器,按下0123按钮。当我们按下按钮时,开关会打开;而当我们再次按下按钮时,开关会关闭,从而改变连接的继电器的状态,最后,改变连接到继电器的设备状态。

总结

本章中,我们查看了一个内核驱动程序来管理红外设备。接着,我们学习了如何使用 LIRC 项目的用户空间工具接收遥控器发送的消息,并将其转化为特定的 Linux 输入事件。这使我们能够管理连接到 BeagleBone Black 的设备。

在下一章中,我们将探讨如何管理无线设备来控制墙插,并通过一种常见的家居自动化通信系统——Z-Wave协议,来监控连接到该插座的设备的电力消耗。

第十一章:Z-Wave 无线家居控制器

在这个项目中,我们将展示如何通过使用 Z-Wave 控制器(连接到我们的 BeagleBone Black)和两个 Z-Wave 设备:一个壁式插座和一个多功能传感器设备,来实现一个小型的无线家居控制器。借助前者,我们可以开启或关闭连接到它的每个家电,同时测量其电力消耗;借助后者,我们可以测量多个环境变量,如温度、湿度和光照强度(并且还具备运动探测功能)。

Z-Wave 通信协议使我们能够无线管理多个家居自动化传感器和执行器,因此我们无需修改现有的布置。此外,我们还可以轻松添加一个电力消耗测量系统或多个环境传感器,对现有家居布局的影响很小。

最后一步,为了保持代码的简洁性,并让用户能够轻松管理系统,我们将编写一个简单的 Python Web 界面,便于管理原型。

功能基础

这次项目比之前更为复杂,但所有的复杂性并不在硬件方面(连接非常简单,只需插入 USB 加密狗,操作就完成了!),而是在软件方面!实际上,设置和控制这些设备的管理软件需要一些技能。此外,由于 Z-Wave 世界非常庞大,而且本书的篇幅有限(我想我可以要求我的编辑专门写一本书来讲解如何使用 Z-Wave 进行家居自动化项目!),所以我这里只会介绍 Z-Wave 协议的基本知识,展示一个最小化的应用,您可以在此基础上扩展。

如前所述,我们将使用一个 Z-Wave 控制器连接到 BeagleBone Black 的 USB 主机端口来管理两个 Z-Wave 设备:一个用于测量环境数据,另一个用于开关连接的设备。因此,我们需要编写一些软件,能够通过控制器发送和接收这些设备之间的消息,以便在 BeagleBone Black 和这两个从设备之间交换数据和命令。我们将编写的代码应该包括一个用于管理 Z-Wave 消息的部分和一个与用户互动的部分。至于后者,我决定使用 Python 编写一个 Web 界面,并辅以少量的 HTML 和 JavaScript 代码。

设置硬件

Z-Wave 技术面向住宅控制和自动化市场,设计上适合电池供电的设备。实际上,其主要目标之一就是尽量减少功耗。尽管如此,它依然能够提供可靠且低延迟的小数据包传输,数据传输速率最高可达 100 kbps,并且提供一种简单而可靠的方法来无线管理传感器,控制家庭中的灯光和电器。

注意事项

要了解更多关于 Z-Wave 的信息,一个好的起点是en.wikipedia.org/wiki/Z-Wave

在我们的项目中,我们将使用一个 USB 加密狗上的 Z-Wave 控制器,一个由它连接的插座供电的从设备,以及一个可以由电池或通过外部 USB 连接供电的多传感器设备。

设置 Z-Wave 控制器

我在这个原型中使用的 Z-Wave 控制器如下图所示:

设置 Z-Wave 控制器

注意

该设备可以通过以下链接(或在互联网上搜索)购买:www.cosino.io/product/usb-z-wave-controller

参考设计可以在这里找到:

z-wave.sigmadesigns.com/docs/brochures/UZB_br.pdf

一旦通过lsusb命令连接到 BeagleBone Black 的 USB 主机端口,我们应该得到如下输出:

root@beaglebone:~# lsusb
Bus 001 Device 002: ID 0658:0200 Sigma Designs, Inc.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

我们还应该看到以下内核活动:

hub 1-0:1.0: hub_resume
hub 1-0:1.0: port 1: status 0101 change 0001
hub 1-0:1.0: state 7 ports 1 chg 0002 evt 0000
hub 1-0:1.0: port 1, status 0101, change 0000, 12 Mb/s
usb 1-1: new full-speed USB device number 2 using musb-hdrc
usb 1-1: ep0 maxpacket = 8
usb 1-1: skipped 4 descriptors after interface
usb 1-1: udev 2, busnum 1, minor = 1
usb 1-1: New USB device found, idVendor=0658, idProduct=0200
usb 1-1: New USB device strings: Mfr=0, Product=0, SerialNumber=0
usb 1-1: usb_probe_device
usb 1-1: configuration #1 chosen from 1 choice
usb 1-1: adding 1-1:1.0 (config #1, interface 0)
cdc_acm 1-1:1.0: usb_probe_interface
cdc_acm 1-1:1.0: usb_probe_interface - got id
cdc_acm 1-1:1.0: This device cannot do calls on its own. It is not a modem.
cdc_acm 1-1:1.0: ttyACM0: USB ACM device
usb 1-1: adding 1-1:1.1 (config #1, interface 1)
hub 1-0:1.0: state 7 ports 1 chg 0000 evt 0002
hub 1-0:1.0: port 1 enable change, status 00000103

查看倒数第四行,我们可以发现 Z-Wave 控制器已经连接到/dev/ttyACM0设备文件。因此,设备已正确连接。但要真正测试它,我们需要安装合适的管理软件。为此,我们可以使用名为Open Z-Wave的 Z-Wave 协议的开源实现,在那里我们可以找到很多适合测试 Z-Wave 网络的软件。

注意

Open Z-Wave 项目的主页在www.openzwave.com

使用以下命令,我们可以将所需的代码下载到我们的原型中:

root@beaglebone:~# git clone https://github.com/OpenZWave/open-zwave openzwave

然后,我们需要一些额外的软件包来编译所需的工具。因此,接下来我们用以下命令安装它们:

root@beaglebone:~# aptitude install build-essential make git libudev-dev libjson0 libjson0-dev libcurl4-gnutls-dev

现在,只需进入openzwave目录,然后使用make命令如下所示:

root@beaglebone:~# cd openzwave
root@beaglebone:~/openzwave# make

提示

编译过程比较慢,所以请耐心等待。

完成后,我们需要使用以下命令将另一个仓库下载到当前目录:

root@beaglebone:~/openzwave# git clone https://github.com/OpenZWave/open-zwave-control-panel openzwave-control-panel

然后,在下载之后,我们需要安装一个额外的软件包来继续进行编译。因此,接下来我们再次使用aptitude命令,如下所示:

root@beaglebone:~/openzwave# aptitude install libmicrohttpd-dev

现在,作为最后一步,进入openzwave-control-panel目录,并使用以下命令行重新运行make命令:

root@beaglebone:~/openzwave# cd openzwave-control-panel/
root@beaglebone:~/openzwave/openzwave-control-panel# make

当编译完成后,ozwcp程序应该就可以使用了。接下来,让我们通过以下命令行执行它:

root@beaglebone:~/openzwave/openzwave-control-panel# ln -s ../config
root@beaglebone:~/openzwave/openzwave-control-panel# ./ozwcp -d -p 8080
2014-04-23 21:12:52.943 Always, OpenZwave Version 1.3.526 Starting Up
webserver starting port 8080

提示

请注意,ln命令只需要使用一次,用来与 Open Z-Wave 配置目录config创建一个正确的链接,该目录位于上级目录。

如果在执行程序时遇到以下错误,意味着很可能是你的 Web 服务器占用了8080端口,所以你需要禁用它:

Failed to bind to port 8080: Address already in use

现在,我们应该将主机 PC 上的网页浏览器指向地址http://192.168.7.2:8080/,以查看下图所示的内容:

设置 Z-Wave 控制器

好的,现在我们需要在设备名称字段中输入/dev/ttyACM0路径名,然后按下初始化按钮以启动通信。如果一切正常,您应该看到在设备标签中列出了一个新设备,如下图所示:

设置 Z-Wave 控制器

现在,控制器已经启动并运行,因此我们可以继续安装 Z-Wave 从设备。

设置 Z-Wave 墙插

第一个 Z-Wave 从设备是下图所示的墙插:

设置 Z-Wave 墙插

注意

该设备可以通过以下链接(或通过浏览互联网)购买:www.cosino.io/product/z-wave-wall-plug

可以在此处找到参考手册:

www.fibaro.com/manuals/en/FGWPx-101/FGWPx-101-EN-A-v1.00.pdf

该设备是无线的,一旦与通电的插座连接,它会自供电;因此,我们不需要特别的连接来设置它。然而,我们需要将一些家用电器连接到它,如下图所示,以便进行功率消耗测量:

设置 Z-Wave 墙插

现在,为了测试该设备及其与控制器的通信,我们可以再次使用ozwcp程序。只需点击控制器标签中的选择操作菜单项,选择添加设备条目,然后按下开始按钮。在左侧,您应该看到添加设备:等待用户操作的消息。因此,我们可以通过将设备插入墙插并按下设备上的按钮来启动配对程序(就像蓝牙设备一样)。

提示

请注意,较新版本的该设备不需要按下按钮来启动配对程序——它会在插入第一个插座后自动启动。

如果一切正常,新的设备应出现在设备标签中,如下所示:

设置 Z-Wave 墙插

现在,我们可以通过选择新设备,然后点击设备列表标签下的配置选项来更改设备的某些设置。应该会出现类似下图的面板设置:

设置 Z-Wave 墙插

现在,我们可以通过在相关字段中输入新值,然后按下提交按钮来更改标准功率负载报告条目。通过这种方式,我们可以定义一个较低的值,表示必须更改多少功率负载(以百分比计),才能报告给主控制器(我使用的值是5)。

设置 Z-Wave 多传感器

第二个 Z-Wave 从设备是下图所示的多传感器:

设置 Z-Wave 多传感器

注意

该设备可以通过以下链接购买(或通过上网搜索):www.cosino.io/product/z-wave-multi-sensor

参考手册可以在这里找到:

aeotec.com/z-wave-sensor/47-multisensor-manual.html

要为设备供电,我们可以使用 4 节电池或如下面图示那样连接 USB 电缆。然后,为了测试设备及其与控制器的通信,我们可以再次使用ozwcp程序。所以,只需点击控制器选项卡中的选择操作菜单项,然后选择添加设备条目。接着,按下开始按钮,以便再次执行配对过程(配对按钮是位于电池盖下方灵敏度调节器旁的黑色按钮,在下图中位于右上角)。

设置 Z-Wave 多传感器

如果一切正常,一个新设备应该出现在设备选项卡中,如下所示:

设置 Z-Wave 多传感器

现在,和之前一样,我们可以更改默认设置。特别是,我们可以通过将组 1 报告条目设置为224,将组 2 报告条目设置为1,然后将组 1 间隔设置为10,将组 2 间隔设置为60,来设置环境报告的频率和内容。

这些特殊设置将指示多传感器启用第 7 位(亮度)、第 6 位(湿度)和第 5 位(温度)到组 1,以及第 0 位(电池电量)到组 2,并每 10 秒重复组 1,每 60 秒重复组 2(见下图):

设置 Z-Wave 多传感器

好的,现在所有设备都准备好操作了!我们可以通过按下CTRL + C 键序列来停止ozwcp程序,并继续下一部分。

最终图片

以下是显示我为实现此项目并测试软件而实现的原型的图片。

这里没什么特别要说的;只需要 BeagleBone Black 配合 Z-Wave 控制器 USB 加密狗以及之前描述的两台 Z-Wave 设备。

最终图片

设置软件

如前所述,原型的复杂部分在于软件。我们需要将多个软件包安装到 BeagleBone Black 中,而且我们需要自己编写的软件也需要一些技能。不过,别担心,我将逐步解释所有需要的步骤!

设置 Python 绑定

安装Python绑定相当复杂,因为名为python-openzwave的软件包似乎仍在开发中,并且它依赖于大量的Python包!不过,我通过以下命令行获取了项目的特定版本,成功完成了安装:

root@beaglebone:~# wget http://bibi21000.no-ip.biz/python-openzwave/python-openzwave-0.3.0b5.tgz

注意

python-openzwave包的其他版本可以在bibi21000.no-ip.biz/python-openzwave/获取。

现在,要探索归档文件,我们可以使用以下命令:

root@beaglebone:~# tar xvfz python-openzwave-0.3.0b5.tgz

创建了一个新目录python-openzwave-0.3.0b5;但是,要成功编译代码,我们需要多次使用以下命令行安装一些 Python 包:

root@beaglebone:~# pip install 
<package>

在这里,使用时,我使用了以下名称:LouieurwidFlask-SocketIOversiontoolsgevent-socketioWebObFlask-ThemesFlask-Babel

提示

实际上,pip install命令可以一次接受多个由空格分隔的包,因此你可以使用单个命令一次性安装所有需要的包。

还要注意,要安装urwid包,我需要使用不同的命令来更新已经安装的包。该命令如下:

root@beaglebone:~# pip install --upgrade urwid

同时,为了安装版本为 0.9.5 的Flask-WTF包,我使用的命令如下:

root@beaglebone:~# pip install Flask-WTF==0.9.5

然而,在我的系统上,我无法成功执行它,因此我通过对python-openzwave-0.3.0b5目录中的setup-web.py文件应用以下补丁,使用了一种脏技巧

--- ./setup-web.py.orig   2014-04-24 03:50:41.398440723 +0000
+++ ./setup-web.py	2014-04-24 03:39:52.212893771 +0000
@@ -49,7 +49,7 @@
   install_requires = [
                      'openzwave == %s' % pyozw_version,
                      'Flask == 0.10.1',
-                     'Flask-WTF == 0.9.5',
+                     'Flask-WTF >= 0.9.5',
                      'Babel >= 1.0',
                      'Flask-Babel == 0.9',
                      #'Flask-Fanstatic == 0.2.0',

幸运的是,即使使用比 0.9.5 更新的版本,代码也能正常工作!

安装所有依赖项后,只需转到python-openzwave-0.3.0b5目录并使用make命令完成任务,如下所示:

root@beaglebone:~/python-openzwave-0.3.0b5# make deps build

提示

编译过程非常缓慢,请耐心等待!

完成后,我们必须使用以下命令行安装新代码:

root@beaglebone:~/python-openzwave-0.3.0b5# make install

我们做到了!为了测试我们新编写的代码,我们现在可以通过进入examples目录并执行以下命令行来使用提供的示例:

root@beaglebone:~/python-openzwave-0.3.0b5# cd examples/
root@beaglebone:~/python-openzwave-0.3.0b5/examples# ./test_lib.py --device=/dev/ttyACM0

提示

请注意,为了更好地呈现test_lib.py命令的输出行,所有时间参考已被删除。

此外,对于所有后续的 Python 代码,你可以安全地忽略以下形式的所有警告消息:

./test_lib.py:28: UserWarning: Module libopenzwave was already imported from None, but /usr/local/lib/python2.7/dist-packages/libopenzwave-0.3.0b5-py2.7-linux-armv7l.egg is being added to sys.path

上面命令的输出非常长,由于空间有限,我无法完全报告,因此我将只报告相关部分。

在前几行,我们得到了如下的基本信息消息:

Always, OpenZwave Version 1.3.482 Starting Up
Add watcher
Add device
Info, Setting Up Provided Network Key for Secure Communications
Warning, Failed - Network Key Not Set
Info, mgr,     Added driver for controller /dev/ttyACM0
Sniff network during 60.0 seconds
Info,   Opening controller /dev/ttyACM0
Info, Trying to open serial port /dev/ttyACM0 (attempt 1)
Info, Serial port /dev/ttyACM0 opened (attempt 1)

这里有一些关于软件版本和我们正在访问的设备(即/dev/ttyACM0)的信息。然后,列出了待执行的命令队列,如下所示:

Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_VERSION: 0x01, 0x03, 0x00, 0x15, 0xe9
Detail, contrlr, Queuing (Command) FUNC_ID_ZW_MEMORY_GET_ID: 0x01, 0x03, 0x00, 0x20, 0xdc
Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_CONTROLLER_CAPABILITIES: 0x01, 0x03, 0x00, 0x05, 0xf9
Detail, contrlr, Queuing (Command) FUNC_ID_SERIAL_API_GET_CAPABILITIES: 0x01, 0x03, 0x00, 0x07, 0xfb
Detail, contrlr, Queuing (Command) FUNC_ID_ZW_GET_SUC_NODE_ID: 0x01, 0x03, 0x00, 0x56, 0xaa
Detail, contrlr, Sending (Command) FUNC_ID_ZW_GET_VERSION: 0x01, 0x03, 0x00, 0x15, 0xe9
Detail, contrlr, Received: 0x01, 0x10, 0x01, 0x15, 0x5a, 0x2d, 0x57, 0x61, 0x76, 0x65, 0x20, 0x33, 0x2e, 0x37, 0x39, 0x00, 0x01, 0x9b

这里是一些初步的回答:

Info, contrlr,   Received reply to FUNC_ID_ZW_GET_VERSION:
Info, contrlr,   Static Controller library, version Z-Wave 3.79

接下来,关于探测所有可用Z-Wave节点的许多消息紧随其后:

Info, contrlr,     Node 001 - New
Detail, Node001, AdvanceQueries queryPending=0 queryRetries=0 queryStage=None live=1
Detail, Node001, QueryStage_ProtocolInfo
Detail, Node001, Queuing (Query) Get Node Protocol Info (Node=1): 0x01, 0x04, 0x00, 0x41, 0x01, 0xbb
Detail, Node001, Queuing (Query) Query Stage Complete (ProtocolInfo)
Info, Node001, Initilizing Node. New Node: false (false)
Info, contrlr,     Node 009 - New
Detail, Node009, AdvanceQueries queryPending=0 queryRetries=0 queryStage=None live=1
Detail, Node009, QueryStage_ProtocolInfo
Detail, Node009, Queuing (Query) Get Node Protocol Info (Node=9): 0x01, 0x04, 0x00, 0x41, 0x09, 0xb3
Detail, Node009, Queuing (Query) Query Stage Complete (ProtocolInfo)
Info, Node009, Initilizing Node. New Node: false (false)
Info, contrlr,     Node 010 - New
Detail, Node010, AdvanceQueries queryPending=0 queryRetries=0 queryStage=None live=1
Detail, Node010, QueryStage_ProtocolInfo
Detail, Node010, Queuing (Query) Get Node Protocol Info (Node=10): 0x01, 0x04, 0x00, 0x41, 0x0a, 0xb0
Detail, Node010, Queuing (Query) Query Stage Complete (ProtocolInfo)

然后,系统开始添加新发现的节点。第一个是读卡器,如下所示:

--------------------
[DriverReady]:

homeId: 0xe4056d54
nodeId: 1
--------------------

2014-04-24 13:56:31.961 Detail, Node001, Notification: NodeNew

--------------------
[NodeNew]:

homeId: 0xe4056d54
nodeId: 1
--------------------

2014-04-24 13:56:31.963 Detail, Node001, Notification: NodeAdded

--------------------
[NodeAdded]:

homeId: 0xe4056d54
nodeId: 1
---------------
-----

然后是墙壁插座,如下所示:

2014-04-24 13:56:31.966 Detail, Node009, Notification: NodeNew

--------------------
[NodeNew]:

homeId: 0xe4056d54
nodeId: 9
--------------------

2014-04-24 13:56:31.967 Detail, Node009, Notification: NodeAdded

--------------------
[NodeAdded]:

homeId: 0xe4056d54
nodeId: 9
--------------------

最终,出现了多传感器,如下所示:

2014-04-24 13:56:31.969 Detail, Node010, Notification: NodeNew

--------------------
[NodeNew]:

homeId: 0xe4056d54
nodeId: 10
--------------------

2014-04-24 13:56:31.972 Detail, Node010, Notification: NodeAdded

--------------------
[NodeAdded]:

homeId: 0xe4056d54
nodeId: 10
--------------------

在探测阶段之后,系统会请求设备信息,并返回大量信息!你可以看到当前的值、标签、计量单位、只读状态等:

 --------------------
[NodeProtocolInfo]:

homeId: 0xe4056d54
nodeId: 1
--------------------

2014-04-24 13:56:32.015 Detail, Node001, Notification: ValueAdded

--------------------
[ValueAdded]:

homeId: 0xe4056d54
nodeId: 1
valueID: 72057594055229441
Value: None
Label: None
Units: None
ReadOnly: False
--------------------

--------------------
[NodeProtocolInfo]:

homeId: 0xe4056d54
nodeId: 9
--------------------

2014-04-24 13:56:32.094 Detail, Node009, Notification: ValueAdded

--------------------
[ValueAdded]:

homeId: 0xe4056d54
nodeId: 9
valueID: 72057594193723392
Value: False
Label: Switch
Units:
ReadOnly: False
--------------------
...
--------------------
[NodeProtocolInfo]:

homeId: 0xe4056d54
nodeId: 10
--------------------

2014-04-24 13:56:32.150 Detail, Node010, Notification: ValueAdded

--------------------
[ValueAdded]:

homeId: 0xe4056d54
nodeId: 10
valueID: 72057594210680832
Value: False
Label: Sensor
Units:
ReadOnly: True
--------------------

正如你在这些示例中看到的,协议确实非常强大,而且也相当复杂!因此,为了更好地建立我们新的 Z-Wave 网络模型,我们可以使用另一个工具,如下所示:

root@beaglebone:~/python-openzwave-0.3.0b5/examples# ./api_demo.py --log=Info --device=/dev/ttyACM0

再次出现大量消息,不过这次,在接近结束时,我们可以看到以下输出:

Try to autodetect nodes on the network
------------------------------------------------------------
Nodes in network : 3
------------------------------------------------------------
Retrieve switches on the network
------------------------------------------------------------
node/name/index/instance : 9//0/1
 label/help : Switch/
 id on the network : e4056d54.9.25.1.0
 state: False
------------------------------------------------------------
Retrieve dimmers on the network
------------------------------------------------------------
------------------------------------------------------------
Retrieve sensors on the network
------------------------------------------------------------
node/name/index/instance : 10//0/1
 label/help : Sensor/
 id on the network : e4056d54.10.30.1.0
 value: True
node/name/index/instance : 10//1/1
 label/help : Temperature/
 id on the network : e4056d54.10.31.1.1
 value: 0.0 F
node/name/index/instance : 10//3/1
 label/help : Luminance/
 id on the network : e4056d54.10.31.1.3
 value: 675.0 lux
node/name/index/instance : 10//5/1
 label/help : Relative Humidity/
 id on the network : e4056d54.10.31.1.5
 value: 48.0 %
node/name/index/instance : 9//32/1
 label/help : Exporting/
 id on the network : e4056d54.9.32.1.32
 value: False
node/name/index/instance : 9//4/1
 label/help : Power/
 id on the network : e4056d54.9.31.1.4
 value: 0.0 W
node/name/index/instance : 9//0/1
 label/help : Energy/
 id on the network : e4056d54.9.32.1.0
 value: 0.0 kWh
node/name/index/instance : 9//8/1
 label/help : Power/
 id on the network : e4056d54.9.32.1.8
 value: 0.0 W
------------------------------------------------------------
Retrieve switches all compatibles devices on the network 
------------------------------------------------------------
node/name/index/instance : 9//0/1
 label/help : Switch All/
 id on the network : e4056d54.9.27.1.0
 value / items: Disabled / set([u'Disabled', u'On and Off Enabled', u'On Enabled', u'Off Enabled'])
 state: False
------------------------------------------------------------
Retrieve protection compatibles devices on the network 
------------------------------------------------------------
Retrieve battery compatibles devices on the network 
------------------------------------------------------------
node/name/index/instance : 10//0/1
 label/help : Battery Level/
 id on the network : e4056d54.10.80.1.0
 value : 100
------------------------------------------------------------
Retrieve power level compatibles devices on the network 

在这个输出中,更容易找到关于我们从设备的所有相关信息;也就是,第九部分的墙插(可以作为普通开关工作,并且能返回一些能耗信息),以及第十部分的多传感器(能够返回温度、湿度、环境亮度和运动活动信息)。

好的,Python 支持现在已经完全正常了!接下来,让我们进入下一节,看看如何为我们的 Z-Wave 原型编写代码!

Z-Wave 管理器

安装了管理 Z-Wave 设备的 Python 绑定后,我们需要编写自己的代码来实现原型的软件。

如前所述,我们需要实现一个控制器,它能够记录来自传感器的传入消息,可以向执行器发送命令,同时还能够与用户进行交互。前者是 Z-Wave 相关的,后者则可以通过使用 Python 创建的 web 界面,并结合一些额外的 HTML/JavaScript 和 CSS 文件来实现。

现在,让我们来看看 Python 代码。整段代码相当长,所以我只会展示相关部分,但你可以在本书示例代码仓库中的 chapter_11/zwmanager.py 文件中找到完整代码。

一开始,我们需要声明导入的代码:

from __future__ import print_function
import os
import sys
import getopt
import string
import syslog
import resource
import time

from openzwave.node import ZWaveNode
from openzwave.value import ZWaveValue
from openzwave.scene import ZWaveScene
from openzwave.controller import ZWaveController
from openzwave.network import ZWaveNetwork
from openzwave.option import ZWaveOption
from louie import dispatcher, All

from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
import json
import cgi

正如你所见,我们需要从 openzwave 包中包含多个模块,而 BaseHTTPServerjsoncgi 则用于管理 Web 界面。

接下来是一些默认设置:

NAME = os.path.basename(sys.argv[0])
debug = False
logstderr = False
log = "Info"
timeout_s = 20
port = 8080

# Default system status
values = {
   "switch" :  "off",
   "power"  :    0.0,
   "temp"   :      0,
   "hum"    :      0,
   "lum"    :      0,
   "bat_lvl":      0,
   "sensor" :   "no",
}

在这里,最重要的是 values 变量,我们将在其中存储所有设备的状态。

然后,定义了与 Z-Wave 相关的函数,如下所示:

def louie_value(network, node, value):
   # Record all new status changing
   if (value.label == "Switch"):
      values["switch"] = "on" if value.data else "off"
   elif (value.label == "Power"):
      values["power"] = value.data
   elif (value.label == "Temperature"):
      values["temp"] = value.data
   elif (value.label == "Relative Humidity"):
      values["hum"] = value.data
   elif (value.label == "Luminance"):
      values["lum"] = value.data
   elif (value.label == "Battery Level"):
      values["bat_lvl"] = value.data
   elif (value.label == "Sensor"):
      values["sensor"] = "yes" if value.data else "no"
   dbg("dev=%s(%d) name=%s data=%d" % \
      (node.product_name, node.node_id, value.label, value.data))

def louie_network_started(network):
   dbg("network is started: homeid %0.8x" % network.home_id)

def louie_network_resetted(network):
   dbg("network is resetted")

def louie_network_ready(network):
   dbg("network is now ready")
   dispatcher.connect(louie_value, ZWaveNetwork.SIGNAL_VALUE)

相关函数包括 louie_network_ready() 函数,它在 Z-Wave 网络准备好时安装一个新的分发器,以及 louie_value() 函数,它读取所有设备的通知并将其存储在 values 变量中。

提示

请注意,这段代码远未完美,因为我们假设一次只会有一个多传感器设备和一个墙插存在!如果你想管理更多设备,你需要完全重写这些函数。

接下来是与 HTTP 相关的函数:

class myHandler(BaseHTTPRequestHandler):
   # Disable standard logging messages
   def log_message(self, format, *args):
      return

   # Handler for the GET requests
   def do_GET(self):
      if self.path == "/":
         self.path = "/house.html"
      elif self.path == "/get":
         #dbg("serving %s..." % self.path)

         # Return the current status in JSON format
         self.send_response(200)
         self.send_header('Content-type', 'application/json')
         self.end_headers()
         self.request.sendall(json.dumps(values))

         return

      # Otherwise try serving a file
      try:
         # Open the file and send it
         f = open(os.curdir + os.sep + self.path)
         self.send_response(200)
         self.send_header('Content-type', 'text/html')
         self.end_headers()
         self.wfile.write(f.read())
         f.close()
         dbg("file %s served" % self.path)

      except IOError:
         self.send_error(404, 'File Not Found: %s' % self.path)
         dbg("file %s not found!" % self.path)

      return

   # Handler for the POST requests
   def do_POST(self):
      if self.path == "/set":
         # Parse the data posted
         dbg("managing %s..." % self.path)
         data = cgi.FieldStorage(fp = self.rfile, headers = self.headers, environ = {'REQUEST_METHOD':'POST', 'CONTENT_TYPE':self.headers['Content-Type'],})

         self.send_response(200)
         self.end_headers()
         dbg("got label=%s" % data["do"].value)

         # Set the device according to user input
         if data["do"].value == "switch":
         network.nodes[sw_node].set_switch(sw_val, False if values["switch"] == "on" else True)

         return

      # Otherwise return error
      self.send_error(404, 'File Not Found: %s' % self.path)
      dbg("file %s not found!" % self.path)

      return

上述代码实现了一个 Web 服务器,我们只需要管理 GETPOST HTTP 请求来完成任务。GET 请求由 do_GET() 方法处理,该方法简单地尝试将正常的文件提供给客户端,除了当 URL 中使用特殊路径 /get 时,在这种特殊情况下,服务器会以 JSON 格式返回 values 变量的内容。

作为相反的功能,当接收到POST请求时,它将传递给do_PUT()方法,该方法反过来将在未使用特殊路径/set时返回错误代码;如果使用,系统将解析客户端发布的数据,然后根据用户请求切换壁式插座状态。

提示

请注意,我们再次假设只有一个壁式插座存在!因此,如果您希望管理多个壁式插座设备,您必须重新编写代码。

现在,我们必须展示系统如何设置。在对命令行进行一些健全性检查后,我们开始设置 Z-Wave 网络:

# Define some manager options and create a network object
options = ZWaveOption(device, config_path = "./openzwave/config", user_path = ".", cmd_line = "")
options.set_log_file(NAME + ".log")
options.set_append_log_file(False)
#options.set_console_output(True)
options.set_console_output(False)
options.set_save_log_level(log)
options.set_logging(True)
options.lock()
network = ZWaveNetwork(options, log = None)

# Add the basic callbacks
dispatcher.connect(louie_network_started, ZWaveNetwork.SIGNAL_NETWORK_STARTED)
dispatcher.connect(louie_network_resetted, ZWaveNetwork.SIGNAL_NETWORK_RESETTED)
dispatcher.connect(louie_network_ready, ZWaveNetwork.SIGNAL_NETWORK_READY)
dbg("callbacks installed")

info("Starting...")

# Waiting for driver to start
for i in range(0, timeout_s):
   if network.state >= network.STATE_STARTED:
      break
   else:
      sys.stdout.flush()
      time.sleep(1.0)
if network.state < network.STATE_STARTED:
   err("Can't initialize driver! Look at the logs file")
   sys.exit(1)

info("use openzwave library   = %s" % network.controller.ozw_library_version)
info("use python library      = %s" % network.controller.python_library_version)
info("use ZWave library       = %s" % network.controller.library_description)
info("network home id         = %s" % network.home_id_str)
info("controller node id      = %s" % network.controller.node.node_id)
info("controller node version = %s" % (network.controller.node.version))

ZwaveOption()函数用于设置网络的选项,然后ZwaveNetwork()函数根据所选的选项实际执行工作。然后,我们设置回调函数,每当收到 Z-Wave 信号时调用它们,并且必须使用dispatcher.connect()方法来管理它。

好的,现在一切就绪,我们只需等待 Z-Wave 驱动程序启动。完成后,我们打印一些网络信息。现在,下一步是等待网络正常运行,以便我们可以转到检测壁式插座设备并使用以下代码存储其相关节点信息:

# Waiting for network is ready
time_started = 0
for i in range(0, timeout_s):
   if network.state >= network.STATE_READY:
      break
   else:
      time_started += 1
      sys.stdout.flush()
      time.sleep(1.0)

dbg("detecting the switch node...")
for node in network.nodes:
   for val in network.nodes[node].get_switches():
      data = network.nodes[node].values[val].data
      values["switch"] = "on" if data else "off"
      sw_node = node
      sw_val = val
      dbg(" - device %s(%s) is %s" % \
         (network.nodes[node].values[val].label, node, values["switch"]))

      # We can manage just one switch!
      break

info("Press CTRL+C to stop")

get_switches()方法用于获取所有可以作为开关的节点,因此我们使用它来检测我们的壁式插座,然后将其信息存储到sw_nodesw_val变量中,以便稍后在do_POST()方法中根据用户请求打开/关闭开关。

提示

在这里,很明显代码是为网络中一次只有一个壁式插座编写的!

现在,我们只需定义 Web 服务器即可完成工作,我们可以使用以下代码来实现:

# Create a web server and define the handler to manage the incoming requests
try:
   server = HTTPServer(('', port), myHandler)
   info("Started HTTP server on port %d" % port)

   # Wait forever for incoming HTTP requests
   server.serve_forever()

except KeyboardInterrupt:
   info("CTRL+C received, shutting down...")
   server.socket.close()
   network.stop()

info("Done.")

主要函数是启动监听8080端口的内部 Web 服务器的HTTPServer()函数。

现在,为了完成软件演示,我需要展示house.html文件的工作原理。这是每次新客户端连接到服务器时提供的文件。

注意

同样,与以前一样,我只打算显示相关部分,但您可以在书中示例代码库中的chapter_11/house.html文件中获取完整的代码。

在头部部分,我定义了 CSS 文件名和要使用的 JavaScript 代码:

   <head>
      <link href="house.css" rel="stylesheet" type="text/css">

      <script src="img/jquery-1.9.1.js"></script>

      <script>
         var polldata = function() {
            $.getJSON('/get', function(data) {
               $.each(data, function(key, val) {
                  var e = document.getElementById(key);

                  if (e != null) {
                     if (e.type == "text")
                        e.value = val;
                  else
                     e.textContent = val;
                 }
               });
            });
         };

         setInterval(polldata, 1000);
      </script>

      <script>
         $(function() {
            $('button[class="do-button"]').click(function() {
               var id = $(this).attr("id");

               $.ajax({
                  url: "/set",
                  type: "POST",
                  data: "do=" + id,
                  success: function() {
                     console.log('do POST success');
                  },
                  error: function() {
                     console.log('do POST error');
                  }
               });
            });
         });
      </script>
   </head>

在这段代码中,我们采用了与第七章中Facebook 植物监控相同的技术,其中我安装了一个轮询函数,每秒执行一次GET请求到服务器,以更新以JSON格式返回的显示数据。此外,每当按下按钮时,我们都会向服务器发送一个POST请求,将按钮 ID 传递给服务器进行管理。

house.html文件的主体部分,我们定义了表格以便美观地显示我们的数据:

 <body>
  <h1>Home monitor status</h1>

  <h2>Internal variables</h2>

  <table class="status">
   <tr class="d0">
     <td>Switch</td>
     <td><b id="switch">off</b></td>
     <td><button id="switch" class="do-button">switch</button</td>
   </tr>
   <tr class="d0">
     <td>Power[KW]</td>
     <td><b id="power">0</b></td>
     <td></td>
   </tr>
   <tr class="d1">
     <td>Temperature[C]</td>
     <td><b id="temp">0</b></td>
     <td></td>
   </tr>
   <tr class="d1">
     <td>Relative Humidity[%]</td>
     <td><b id="hum">0</b></td>
     <td></td>
   </tr>
   <tr class="d1">
     <td>Luminance[lux]</td>
     <td><b id="lum">0</b></td>
     <td></td>
   </tr>
   <tr class="d1">
     <td>Battery Level[%]</td>
     <td><b id="bat_lvl">0</b></td>
     <td></td>
   </tr>
   <tr class="d1">
     <td>Motion</td>
     <td><b id="sensor">no</b></td>
     <td></td>
   </tr>
  </table>
 </body>

关于 CSS 文件,没有什么重要的要说的(它只是一个 CSS 文件!),而jquery-1.9.1.js文件是已经在最终测试部分中使用的文件,如第七章中的Facebook 植物监控所示;因此,只需参考该部分即可了解如何获取和安装它。

最终测试

现在,为了测试原型,我将墙插连接到我的打印机(负载电源),并用 PC 的 USB 端口为多传感器供电(仅为避免使用电池)。然后,我按照以下方式启动了zwmanager.py程序:

root@beaglebone:~# ./zwmanager.py -d -l /dev/ttyACM0
zwmanager.py[2732]: callbacks installed
zwmanager.py[2732]: Starting...
zwmanager.py[2732]: network is started: homeid e4056d54
zwmanager.py[2732]: use openzwave library   = 1.3.482
zwmanager.py[2732]: use python library      = 0.3.0b5
zwmanager.py[2732]: use ZWave library       = Static Controller version Z-Wave 3.79
zwmanager.py[2732]: network home id         = 0xe4056d54
zwmanager.py[2732]: controller node id      = 1
zwmanager.py[2732]: controller node version = 4
zwmanager.py[2732]: network is now ready
zwmanager.py[2732]: detecting the switch node...
zwmanager.py[2732]:  - device Switch(9) is off
zwmanager.py[2732]: Press CTRL+C to stop
zwmanager.py[2732]: Started HTTP server on port 8080

接下来,我将浏览器连接到192.168.7.2:8080网址,但在此之前,我稍等了一会儿,查看了一些来自传感器的消息:

zwmanager.py[4915]: dev=Multi Sensor(10) name=Battery Level data=100
zwmanager.py[4915]: dev=Multi Sensor(10) name=Battery Level data=100
zwmanager.py[4915]: dev=Multi Sensor(10) name=Luminance data=51
zwmanager.py[4915]: dev=Multi Sensor(10) name=Luminance data=51
zwmanager.py[4915]: dev=Multi Sensor(10) name=Relative Humidity data=48
zwmanager.py[4915]: dev=Multi Sensor(10) name=Relative Humidity data=48
zwmanager.py[4915]: dev=Multi Sensor(10) name=Temperature data=20
zwmanager.py[4915]: dev=Multi Sensor(10) name=Temperature data=20

然后,当我启动浏览器时,收到了以下消息:

zwmanager.py[4937]: file /house.html served
zwmanager.py[4937]: file /house.css served
zwmanager.py[4937]: file /jquery-1.9.1.js served

正如预期的那样,主要的 HTML 文件、CSS 文件和 JavaScript 文件被提供给客户端,客户端展示了如下内容,如下图所示:

最终测试

现在,我可以通过按下开关按钮来尝试开启连接到墙插的打印机。我从zwmanager.py程序中收到了以下消息:

zwmanager.py[5002]: managing /set...
zwmanager.py[5002]: got label=switch
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Switch data=1
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Power data=0
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Power data=21
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Power data=30
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Power data=36
zwmanager.py[5002]: dev=FGWPE Wall Plug(9) name=Power data=13

与此同时,网页面板发生了如下变化:

最终测试

总结

在本章中,我们发现了如何实现一个基本的家居管理系统,该系统具有一个 Web 界面,可以控制两个 Z-Wave 设备,用来监控一些环境数据并控制墙插。

尽管所展示的代码有点复杂,但它可以很容易地扩展以支持更多的 Z-Wave 设备,从而管理一个真正复杂的网络。

posted @ 2025-07-05 15:45  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报