Xbee-树莓派和-Arduino-传感器网络编程-全-

Xbee、树莓派和 Arduino 传感器网络编程(全)

原文:Beginning Sensor Networks with XBee, Raspberry Pi, and Arduino

协议:CC BY-NC-SA 4.0

一、传感器网络简介

传感器网络不再是昂贵的工业建筑。您可以从容易获得的低成本硬件构建简单的传感器网络。你所需要的只是一些简单的传感器和一个微控制器或具有输入/输出能力的计算机。是的,您的 Arduino 和 Raspberry Pi 是构建传感器网络的理想平台。如果您使用过这两个平台中的任何一个,并且曾经想要监控您的花园池塘、跟踪您家里或办公室的活动、监控您家里的温度、监控环境,或者甚至构建一个低成本的安全系统,那么您已经成功了一半!

虽然听起来很诱人也很简单,但不要马上开始加热烙铁。关于传感器网络,你需要了解很多东西。这并不像把东西插在一起然后打开它们那么简单。如果你想建立一个可靠和信息丰富的传感器网络,你需要知道这样的网络是如何构建的。

另外,你可能听说过一个叫物联网(IoT)的东西。这个短语指的是可以通过网络(本地或互联网)进行通信的设备的使用。因此,物联网设备是网络感知设备,可以向其他资源发送数据,从而虚拟化设备对用户及其体验的影响。传感器网络在物联网中扮演着重要角色。您将在本书中学到的知识将为使用传感器网络构建物联网解决方案奠定坚实的基础。

如果您想从总体上了解更多关于物联网的知识,已经有几本书以此为主题,包括以下内容。如果您有兴趣了解更多有关物联网和传感器网络使用方式的信息,请查看以下书籍:

  • charalappos Doukas(CreateSpace 独立出版平台,2012 年)用 Arduino 构建物联网

  • 由 Dieter Uckelmann、Mark Harrison 和 Florian Michahelles (Springer,2011 年)共同构建物联网

  • 物联网入门:将传感器和微控制器连接到云作者:库诺·菲斯特(2011 年,奥赖利)

在本章中,我们将通过简要描述什么是传感器网络以及它们是如何构建的来探索传感器网络。我们还将研究构成传感器网络的组件,包括传感器概述、可用的传感器类型以及它们可以感知的事物。

传感器网络的剖析

传感器网络无处不在。它们通常被认为是制造和医疗应用的复杂监控系统。然而,它们并不总是复杂的,它们就在你的周围。

在本节中,我们将研究传感器网络的构建模块以及它们是如何连接的(逻辑上)。首先,让我们来看一些传感器网络的例子,以便直观地了解这些组件。

传感器网络的例子

虽然您可能不太熟悉其中的一些示例,但是在阅读这些示例时,尝试想象一下应用程序的组件是个不错的主意。可视化传感器本身——它们被放置在哪里,它们可能正在读取哪些数据,并将这些数据发送到网络的另一部分进行处理和记录。

汽车的

几乎每辆现代汽车都有一个复杂的传感器网络,用来监控发动机及其子系统的性能。一些汽车有额外的传感器,用于监控外部气温、轮胎压力,甚至物体和其他车辆的接近度。新型汽车有一系列安全机制,包括车道偏离、避障、自动刹车等等。 1

如果你把一辆新型汽车送去维修,并有机会在车库里看看,你可能会注意到几台类似电脑终端、平板电脑或在某些情况下类似 iPad 的机器。这些系统是诊断机器,旨在连接到您的汽车,并读取传感器和计算机存储的所有数据。一些制造商使用称为车载诊断(OBD)的行业标准接口。 2 这个接口及其协议有多个版本;大多数经销商都有支持所有最新协议的设备。

然而,一些制造商使用他们自己的专有诊断系统,但许多制造商使用与 OBD-II 相同的连接器。你可能想在买车前问一下这个问题。如果您的新车需要专有的电子工具进行维护,您可能需要将它送到合格的机械师或其他经销商处进行维修。对于那些生活在农村地区的人来说,找到一家经销商甚至是一名训练有素的机械师来修理你的车可能需要一些旅行,因此需要提前计划。

例如,保时捷使用了它所谓的保时捷综合车间信息系统(PIWIS)。虽然 PIWIS 使用与 OBD-II 相同的连接器,但 Porsche 实施了专有系统来读取和更改数据。只有经过培训(并购买了专用工具)的机械师才能维修车辆。

有趣的是,虽然使用专有诊断系统的制造商要求您在授权经销商处维修您的汽车,但一些有进取心的技术专家已经创建了兼容系统。以保时捷为例,Durametric ( www.durametric.com/default.aspx )制造了一系列产品,这些产品能够实现基本的维护功能,如故障和维修提醒重置,甚至为许多保时捷车型提供高级故障排除功能。图 1-1 显示了从保时捷 Cayman 上读取传感器数据的 Durametric 软件的一个屏幕。

img/313992_2_En_1_Fig1_HTML.jpg

图 1-1

来自 Durametric 的保时捷诊断数据

请注意显示的详细程度。该图显示了跟踪中的三个指标,但是如果您看屏幕的顶部,您会看到更多可以监控的指标。图表中显示的数据是通过保时捷采用的复杂传感器网络实时收集和显示的。

传感器在汽车中的应用已经开始扩展到相关的机械,如摩托车、船,甚至古老的农用拖拉机。许多现代农业机械(如联合收割机)都配有先进的传感器,可实现自动割台高度、自动驾驶等惊人功能。

例如,现代联合收割机可以与一套基于 GPS 的工具一起购买,这些工具允许操作员绘制收割区域的边界,并计算出最短时间和最大收获量的最佳路径。在收割田地非常大的情况下,操作员几乎可以在联合收割机工作时小睡片刻。 3 这与需要手动调节割台的老式联合收割机相去甚远。

环境

环境是许多人关心的问题,许多科学家正在积极地监测它。监控环境的动机包括检查特定区域或房间的气体和跟踪该区域的温度和湿度,以及监控和报告敏感设备的异常情况,例如对洁净室进行化学分析。环境传感器网络的例子包括用于监测空气污染、探测和跟踪森林火灾、探测滑坡、提供地震预警以及提供工业和结构监测的网络。

传感器网络非常适合各种形式的环境监控。由于传感器尺寸小、能耗低、成本低,因此可以轻松安装在特定位置或特定机器上,以获得精确的报告。例如,洁净室环境通常需要非常精确的温度和湿度控制以及极低水平的污染物(漂浮在空气中的松散颗粒)。传感器可用于在关键位置(窗户、门、通风口等)测量这些观察结果;数据被发送到计算机,计算机记录数据并产生阈值警报。大多数复杂的洁净室将过滤、加热和冷却系统连接到同一个计算机系统中(使用它们自己的传感器),以根据从传感器网络收集的数据来控制环境。

环境传感器不限于温度、湿度、露点和空气质量。用于监控电磁干扰和无线电频率的传感器可用于医院,以保护依赖敏感电子医疗设备(如心脏起搏器和类似的救生电子设备)的患者。 4 用于监测水纯度、氧气水平和污染物的传感器可用于养鱼场,以使作物产量最大化。

科学家和工业工程师不是唯一建立环境传感器网络的人。您可以使用成本相对较低的传感器构建自己的传感器。艾米丽·格茨和帕特里克·迪·高山重友在他们的书《Arduino 环境监测:构建简单的设备来收集我们周围世界的数据》中展示了如何构建简单的传感器网络来监测噪音、水的纯度,当然还有天气。

如果这听起来好得不像是真的,现在考虑一下你的普通家庭供暖、通风和制冷系统(HVAC)。它有一个非常简单的传感器网络,通常采用一个或多个环境温度传感器(墙上的恒温器)的形式,将数据输入到控制板,控制板打开机械装置,将气体泵送到系统中,并通过风扇移动空气。一些现代 HVACs 使用额外的传感器来监测空气质量,并使用额外的主动电子过滤器 5 或将热量和冷却转移到最需要的地方。如果你已经购买了一个现代的 Wi-Fi 恒温器,你可能会惊讶地发现它是一个物联网设备,因为大多数允许你从任何房间甚至当你不在家时控制你的 HVAC 系统。

Is a Thermostat a Sensor Node?

如果你曾经在一个有恒温器的家里使用滑动或旋转臂来设置所需的温度,很可能你遇到了一个简单的传感器节点。老式恒温器结合使用温度感应线圈和安装在线圈上的倾斜开关。这个线圈依次安装在一个板上,该板可以向一个方向或另一个方向倾斜,以调节所需的温度。随着室温的变化,线圈会膨胀或收缩,重新调整倾斜开关的方向。一旦线圈膨胀或收缩,倾斜开关断开,流向暖风、通风与空调装置的电压停止,从而关闭该装置。

一些制造商正在制造越来越复杂的恒温器。有些甚至能够记录数据和预测趋势。例如,Nest Learning 恒温器( www.nest.com/living-with-nest/ )可以检测何时有人在家,并可以通过互联网远程访问。

大气的

与环境监测密切相关的是大气监测:一种旨在监测空气质量的传感器网络。大气监测是环境监测的一种形式,但更侧重于研究大气。显而易见的原因是,没有空气,哺乳动物根本无法生存(至少,不会长久)。

与环境传感器网络一样,有专门的传感器来测量所有形式的空气质量,包括游离气体、颗粒污染、烟雾、湿度等。建立大气传感器网络的其他动机包括测量工厂和汽车的污染(大多数汽车在发动机和机舱系统中集成了几个大气传感器),确保来自水处理厂的清洁饮用水,以及测量气溶胶的影响。

幸运的是,对于业余爱好者和有抱负的大气科学家来说,气体传感器有很多,而且很多都不贵。更好的是,互联网上的许多示例项目演示了如何构建大气传感器网络。

Environment vs. Atmosphere: What’s The Difference?

如果你想知道环境和气氛之间的区别,你并不孤单。简单来说,环境是主体周围影响主体的事物(人、物、事)的集合体。因此,它可以是你周围的所有事物,包括环境温度、湿度等等。

大气(字面意思是空气)是指充满物体周围空间的气体集合。气氛是环境的要素之一。科学家已经定义了地球周围的许多层大气。大多数大气传感器设计用于测量特定水平的独特气体。我们居住的低层大气叫做对流层。

像前面讨论的环境监测传感器网络一样,您可以构建自己的大气传感器网络。在他们的书《使用 Arduino 进行大气监测:构建简单的设备来收集环境数据》中,艾米丽·格茨和帕特里克·迪·高山重友还展示了如何构建简单的传感器网络来测量气体,如丁烷和甲烷、光波长、臭氧等。

安全

一些最流行和最多产的传感器网络是用于安全和监视的。您可能不认为安全系统是传感器网络,但让我们考虑一下典型的家庭或办公室安全系统包含哪些内容。

一个基本的安全系统被设计成在门或窗被打开时进行记录和报警。这种网络中的传感器是开关(所有传感器中最简单的一种),可以检测门或窗何时被打开或关闭。中央处理器或微控制器可用于监控传感器并采取行动:例如,用蜂鸣器或铃产生信号。

监控系统不仅仅包括一组开关。通常情况下,这种系统包括视频传感器(相机-具有或不具有红外功能,以增强夜间照片)、边界传感器(运动、视线中断等)。),甚至还有音频传感器(麦克风)。该系统还可以包括某种形式的监视器,该监视器记录数据并使用户能够查看该数据(查看门何时被打开、听音频和看视频)。

大多数家庭监控系统包括数字录像机(DVR)或类似的专用系统以及一个或多个摄像机。一种流行的家庭系统包括四个带音频的摄像机。该系统允许您以编程方式记录来自传感器的数据,并实时查看视频。图 1-2 展示了一个来自 Harbor Freight ( www.harborfreight.com )的典型且经济实惠的家庭监控系统。

img/313992_2_En_1_Fig2_HTML.jpg

图 1-2

安全传感器网络:港口货运的家庭监控系统

企业中使用的监控系统类似于家庭监控系统,但通常包括额外的传感器和数据跟踪,如员工徽章、设备监控和集成,以及非现场支持服务,如守夜人和数据归档。

另一个例子包括在门铃、安全灯和类似的外部设备上增加摄像机。例如,一些最新的摄像头门铃有运动和类似的传感器来检测运动,甚至在晚上增强视频。有些,像门铃,允许两个或更多的门铃链接在一起,形成一个“邻里守望”系统( https://shop.ring.com/pages/neighbors )。最重要的是,它们提供实时警报,可以帮助您更快地发现犯罪并向当局报警。是的,这也是一个物联网设备!

尽管它们不像温度、湿度、光线或气体传感器那样便宜,但麦克风和摄像头正变得越来越便宜。你可以在电子商店找到这些传感器,比如 Adafruit 工业。例如,Adafruit 有一个摄像头( http://adafruit.com/products/397 ),你可以将其连接到你的 Arduino 或 Raspberry Pi 来记录图像和低帧率视频(见图 1-3 )。

img/313992_2_En_1_Fig3_HTML.jpg

图 1-3

Adafruit Industries 的摄像头传感器(由 Adafruit 提供)

许多安全传感器网络可供消费者使用。它们从简单的音频/视频监控到集成到您家中的远程监控系统,可以跟踪从移动到门户漏洞,甚至温度和照明等一切。

传感器网络的拓扑结构

现在您已经看到了一些例子,让我们来讨论一下传感器网络的组件:在本例中,是一个花园池塘监控系统。具体来说,该系统监测鱼塘的健康状况。因此,该系统是一个环境传感器网络。

这样做的动机是为了确保鱼有一个安全的环境。这意味着水温应在鱼类的耐受范围内,水深应保持在适当的深度,以避免水位过高或过低,并且应监测水中的含氧量,以确保有足够的氧气供鱼类生存。类似地,可以使用传感器来确保诸如其他水生动物或藻类的共生生命的健康水平。

大多数池塘主人已经学会了用生命周期来建造池塘,以确保池塘能够维持其环境。然而,事情可能会出错。另一种物种的引入(如两栖动物 6 或可怕的藻类泛滥)会导致失衡,威胁到你珍贵的锦鲤。如果能够检测到不平衡何时开始,那么解决方案就更容易实施。

图 1-4 显示了描述传感器及其位置的简图。在这个系统中,有三个传感器,一个监控或记录系统,以及一个通信介质——传感器向监控器发送数据的一种方式。让我们从讨论传感器开始。

img/313992_2_En_1_Fig4_HTML.jpg

图 1-4

典型的鱼塘监控系统

如果我要建造这个系统,我会使用低压传感器,这样我就可以用电池或太阳能为它们供电。大多数传感器都是分立元件,接收电压并产生数字或模拟数据。它们需要另一个组件来读取数据并将其发送到池塘监控控制系统。如果你认为这将是一个 Arduino 的好用途,你是对的!Arduino 是从一个或多个传感器读取数据并将其发送到另一个系统进行处理的优秀平台。一些有事业心的 Arduino 爱好者仅使用一个 Arduino 和多个传感器构建了监控系统。

让我们假设池塘监控系统是一台连接了 Arduino 的计算机,这样您就可以远程记录、查看或访问数据。现在,您已经将传感器连接到一个 Arduino(称为传感器节点),并将池塘监控系统连接到另一个 Arduino(称为聚合器节点)。缺少的是如何将数据从传感器节点传送到汇聚节点。

有许多方法可以让两个 Arduinos 进行通信或共享数据,但本书将讨论限制在允许长距离通信的媒体上——有线或无线。在这种情况下,有线通信可以通过以太网屏蔽(一种设计用于 Arduino 顶部的特殊子板)或安装在每个 Arduino 上的无线保真(Wi-Fi)屏蔽进行。

正如您所看到的,构建传感器网络涉及许多级别的硬件和协议。现在,您已经对主要组件有了大致的了解,让我们来研究一下通信介质,然后讨论一下传感器节点的类型。

传播介质

现在,您已经了解了传感器网络的拓扑结构,让我们考虑传感器如何将数据传递给网络中的其他节点。他们通过两种基本形式的网络通信来实现:有线和无线。

有线网络

有线网络可以采取多种形式,包括某种形式的硬件,用于通过电线或电缆将电信号从一个设备发送到另一个设备。因此,采用有线通信的传感器网络也必须向网络中的节点添加网络硬件。

正如我前面提到的,您可以使用带有以太网屏蔽的 Arduino 将传感器节点连接到聚合或数据收集节点。如果您的传感器由 Raspberry Pi 计算机托管,您就已经拥有了连接两台 Raspberry Pi 计算机的必要硬件——它们都有 RJ-45 局域网端口。

当然,使用有线以太网并不像将一根电缆插入两台设备那么简单。除非使用交叉电缆,否则需要某种形式的以太网交换机来连接设备。以太网网络和硬件的详细讨论超出了本书的范围,但它是传感器网络的一种可行的通信介质。

虽然由于许多 Wi-Fi 解决方案的可用性,有线网络的使用在今天没有那么流行,但使用有线网络有助于提高传输速度和可靠性,在某些情况下,还可以提高安全性。

无线网络

一种更受欢迎和更通用的媒体是无线通信。在这种情况下,您可以为每个 Arduino 使用无线设备,例如 Wi-Fi shield,或者为 Raspberry Pi 电脑使用 Wi-Fi 适配器。与有线以太网一样,无线以太网(Wi-Fi)需要添加无线路由器。但是,Wi-Fi 的最大距离要短得多,因此可能不适合某些网络。

但是你有另一种无线方式可以使用。您可以使用 XBee 无线模块来代替以太网(Wi-Fi)。XBee 提供了一种专门的轻量级协议,非常适合用于传感器节点、小型微控制器和嵌入式系统。甚至还有支持蓝牙 Mesh 的模块,但我们将把重点放在 Wi-Fi 模块上。本书的其余部分使用 XBee 模块作为示例传感器网络项目的通信机制。

XBee 模块的一个特性是它们是低功率的,并且可以被置于周期性睡眠模式以节省功率。然而,最好的特性是 XBee 模块可以直接连接到传感器,允许您构建更轻(更便宜)的传感器节点。XBee 模块将在第二章中详细讨论。

混合网络

一些复杂的传感器网络需要混合两种通信介质。例如,工业传感器网络可以使用安装在许多不同建筑物或房间中的传感器节点来收集数据。您可能希望将传感器网络隔离成子系统,因为每个区域可能需要不同形式的传感器网络。在这种情况下,对于难以使用有线网络的某些部分(例如移动的工业机器人上的传感器),最好使用无线网络,并使用有线以太网将子系统连接到中央数据记录或数据监控系统。

传感器节点的类型

传感器节点由一个或多个传感器(虽然本书每个节点只使用一个传感器)和一个通信设备组成,用于传输数据。如上所述,通信设备可以是 Arduino 之类的微控制器、嵌入式系统,甚至是 Raspberry Pi 之类的小型计算机。典型地,传感器节点被设计用于无人值守的操作;它们有时安装在移动物体上或有线通信不切实际的地方。在这些情况下,传感器节点可以被设计成在不受电源或通信源限制的情况下工作。

从逻辑上讲,传感器节点可以根据其使用方式分为不同的类型。以下部分详细介绍了本书中使用的传感器节点类型。按角色考虑传感器节点有助于您使用逻辑构建块来设计和规划传感器网络。

基本传感器节点

在传感器网络的最低(或叶)层是基本传感器节点。这是迄今为止所描述的节点类型——它具有单个传感器和通信机制。这些节点不以任何方式存储或处理捕获的数据,它们只是将数据传递给网络中的另一个节点。

数据节点

下一种类型的节点是数据节点。数据节点是存储数据的传感器节点。这些节点可以将数据发送到另一个节点,但通常是将数据发送到数据卡等存储机制、通过计算机发送到数据库或直接发送到 LCD 屏幕、面板仪表或 LED 指示灯等视觉输出设备的设备。

数据节点需要的设备不仅仅是将数据传递给另一个节点。他们需要能够记录或展示数据。这是微控制器的一个很好的用途,你会在后面的章节中看到。XBee 的制造商 Digi 拥有专用的传感器节点,可以测量温度、湿度和光线信息,并在网络上传输数据。那有什么好玩的?在本书中,您将构建自己的传感器节点。

数据节点可用于形成自主或无人值守的传感器网络,记录数据供以后存档。回到鱼池的例子,许多商业池塘监控系统采用具有多个传感器的独立传感器设备,这些传感器将数据发送到数据节点;用户可以访问数据节点并在计算机上读取用于分析的数据。

聚合器节点

另一种类型的节点是聚合节点。这些节点通常使用通信设备和记录设备(或网关)而没有传感器。它们用于从一个或多个数据或传感器节点收集数据。在迄今为止讨论的示例中,监控系统将具有一个或多个聚集器节点来从传感器读取数据。图 1-5 显示了每种类型的节点将如何在一个虚构的传感器网络中使用。

img/313992_2_En_1_Fig5_HTML.jpg

图 1-5

传感器网络中的节点类型

对于更一般的情况,图表可能应该显示多个数据节点(以便聚合器节点聚合东西)。

在这个例子中,顶部的几个传感器节点向中间的一个数据节点无线发送数据。数据节点收集数据并将其保存到安全数字卡中,然后安全数字卡将数据发送到聚合器节点,聚合器节点通过有线计算机网络与数据库服务器通信以存储数据。将数据节点与聚合器节点混合使用,可以确保在聚合器节点出现故障或者记录和监控系统出现故障或离线时不会丢失任何数据。

现在您已经了解了传感器网络中的节点类型,让我们来看看传感器:它们如何测量数据,以及可用于构建低成本传感器网络的传感器示例。

传感器

谈到传感器、什么是传感器网络以及它们如何传递数据,您可能想知道传感器到底是什么,它们有什么意义。本节及其小节回答了这些问题以及更多问题。让我们从传感器的定义开始。

传感器是一种测量物理世界现象的装置。这些现象可以是你看到的东西,像光、气体、水蒸气等等。它们也可以是你感觉到的东西,像温度、电、水、风等等。人类的感觉就像传感器一样,让我们能够体验周围的世界。然而,有些东西你的传感器看不到或感觉不到,比如辐射、无线电波、电压和安培数。在测量这些现象时,传感器的工作是以电压表示或数字的形式传递测量结果。

传感器有多种形式。它们通常是为单一用途设计的低成本设备,处理能力有限。大多数简单的传感器都是分立元件;甚至那些具有更复杂部件的设备也可以被视为单独的组件。传感器可以是模拟的,也可以是数字的,通常只用于测量一种东西。但是越来越多的传感器模块被设计用来测量一系列相关现象,例如来自 SparkFunElectronics(www.sparkfun.com/products/10586)的 USB 天气板(见图 1-6 )。

img/313992_2_En_1_Fig6_HTML.jpg

图 1-6

USB 天气板(由 SparkFun 和胡安·佩尼亚提供)

注意上面写着 XBee 的蓝色模块。这是一个无线模块,允许传感器板将其数据发送到另一个或多个节点。XBee 将在第二章中详细讨论。

以下部分探讨传感器如何测量数据、如何存储数据,以及一些常见传感器的示例。

传感器如何测量

传感器是基于其化学和机械结构的独特属性产生电压的电子设备。他们不能操纵他们被设计用来测量的现象。相反,传感器对一些物理变量进行采样,并将其转换为成比例的电信号(电压、电流、数字信号等)。

例如,湿度传感器测量空气中水(湿气)的浓度。湿度传感器对这些现象做出反应,并产生一个电压,微控制器或类似设备可以读取该电压,并使用该电压来计算秤的数值。一种基本的低成本湿度传感器是 DHT-22,可从大多数电子商店买到(见图 1-7 )。

img/313992_2_En_1_Fig7_HTML.jpg

图 1-7

DHT-22 湿度传感器(由 Adafruit 提供)

DHT-22 设计用于测量温度和湿度。它在输出端(数据引脚)产生一个数字信号。虽然使用简单,但它有点慢,应该用于以合理的慢速度跟踪数据(不超过每 3 或 4 秒一次)。

当该传感器产生数据时,数据以一系列高(解释为 1)和低(解释为 0)电压的形式传输,微控制器可以读取并使用这些电压来形成一个值。在这种情况下,微控制器从传感器读取长度为 40 位(40 个高或低电压脉冲)的值,即 5 个字节,并将其放入程序变量中。前两个字节是湿度值,后两个字节是温度值,第五个字节是校验和值,以确保读数准确。幸运的是,所有这些艰苦的工作都以专门为 DHT-22 和类似传感器设计的库的形式为您完成了。让我们看看这在实践中是如何工作的。

清单 1-1 展示了 Adafruit 为 Arduino 平台提供的 DHT 库的摘录。你可以在 https://github.com/adafruit/DHT-sensor-library 找到这个库。该列表显示了用于从 Arduino 上的 DHT-22 传感器库中读取湿度的方法。

/*
  Beginning Sensor Networks, 2nd Edition

  This sketch demonstrates a basic sensor node using a DHT22 sensor to read temperature and humidity printing the results
  in the serial monitor.

  Dr. Charles Bell
*/
#include <DHT.h>
#include <DHT_U.h>

#define DHTPIN 2        // Digital pin connected to the DHT sensor
#define DHTTYPE DHT22   // DHT 22  (AM2302), AM2321

DHT dht(DHTPIN, DHTTYPE);

void setup() {
}

void loop() {
  float humidity = dht.readHumidity();
  float temperature = dht.readTemperature();

  // Make sure they are numbers or fail.
  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("ERROR: DHT values are not numbers!");
  } else {
    Serial.print("Temperature (C): ");
    Serial.print(temperature);
    Serial.print("Humidity: ");
    Serial.print(humidity);
  }
}

Listing 1-1Reading Temperature and Humidity with a DHT-22

请注意,DHT 库提供了使读取温度(摄氏度)和湿度并显示这些值变得非常容易的方法。 7 没错,就是这么简单!如果你想尝试 DHT-22,在 Adafruit 的网站上有一个很好的教程( http://learn.adafruit.com/dht )。

回想一下,DHT-22 产生一个数字值。不是所有的传感器都这样做;一些产生电压范围。这些被称为模拟传感器。让我们花一点时间来理解它们的区别。这将成为您规划和构建传感器节点的重要信息。

模拟传感器

模拟传感器是产生电压范围的装置,通常在 0 到 5 伏之间。 8 需要一个模数转换电路将电压转换成数字。大多数微控制器都内置了这一功能,Arduino 就是一个很好的例子。Arduino 有一组有限的引脚,用于处理模拟数据,并集成了模数(A/D)转换电路。

但事情并没有那么简单(是吗?).模拟传感器的工作原理类似于电阻,当连接到微控制器时,通常需要另一个电阻来“上拉”或“下拉”电压,以避免称为浮动的虚假电压变化。这是因为流过电阻的电压在时间和幅度上都是连续的。因此,即使传感器不产生值或测量值,仍有电压流过传感器,可能导致虚假读数。您的项目需要明确区分关(零电压)和开(正电压)。上拉和下拉电阻确保您拥有这两种状态之一。模数转换器负责从传感器读取电压,并将其转换为可被解读为数据的值。

What is a Resistor?

电阻器是电子学的标准构件之一。它的作用是阻止电流并降低电压(转化为热量)。它的影响被称为电阻,用欧姆来衡量。电阻可用于降低其它元件的电压,限制频率响应,或保护敏感元件免受过压影响。

当电阻用于上拉电压(通过将一端连接到正电压)或下拉电压(通过将一端连接到地)(电阻是双向的)时,它消除了电压在不确定状态下浮动的可能性。因此,上拉电阻确保稳定状态为正电压,下拉电阻确保稳定状态为零电压(地)。

一本优秀的入门电子书籍是查尔斯·普拉特(O'Reilly,2012)的《电子元件百科全书》。

采样时(从传感器读取值时),电压读数必须解释为给定传感器指定范围内的值。请记住,比方说,一个模拟传感器输出的 2 伏电压可能与另一个模拟传感器输出的 2 伏电压不是一回事。每个传感器的数据表显示了如何解释这些值。

当您使用 Arduino 之类的微控制器时,模数转换器可以方便地将电压转换为 10 位值,从而得到 0 到 1023 之间的整数值。例如,一个传感器可以测量由 200 个点组成的范围内的现象。最低值通常表示 0,最高值表示 1023。在这种情况下,可以对 Arduino 进行编程,将从 A/D 转换器读取的值转换为传感器刻度上的值。

如您所见,使用模拟传感器比使用上一节中的 DHT-22 数字传感器要复杂得多。稍加练习,您会发现,一旦了解如何将模拟传感器连接到微控制器,以及如何在传感器校准工作的范围内解释其电压,大多数模拟传感器都不难使用。

数字传感器

像 DHT-22 这样的数字传感器被设计成使用串行传输产生一串比特(一次一个比特)。然而,一些数字传感器通过并行传输产生数据(一次一个或多个字节 9 )。如前所述,这些位表示为电压,其中高电压(比如 5 伏)或开是 1,低电压(0 甚至-5 伏)或关是 0。这些开和关值序列称为离散值,因为传感器以脉冲形式产生一个或另一个值,即开或关。

与模拟信号相比,数字传感器的采样频率更高,因为它们生成数据的速度更快,而且不需要额外的电路来读取数值(例如 A/D 转换器以及将数值转换为刻度的逻辑或软件)。因此,数字传感器通常比模拟传感器更精确和可靠。但是,数字传感器的精度与其用于采样数据的位数成正比。

数字传感器最常见的形式是按钮或开关。什么,按钮是传感器?为什么,是的,它是一个传感器。考虑一下安装在家庭安全系统窗户上的传感器。这是一个简单的开关,当窗户关闭时关闭,当窗户打开时打开。当开关连接到电路中时,当车窗关闭且开关闭合时,电流是恒定且不间断的(使用上拉电阻测量正电压),但当车窗和开关打开时,电流中断(测量零电压)。这是最基本的开关传感器。

大多数数字传感器都是由几个元件组成的小电路,用于产生数字数据。与模拟传感器不同,读取它们的数据很容易,因为这些值无需转换就可以直接使用(除了转换到其他刻度或测量单位)。有些人可能认为这比使用模拟传感器更困难,但这取决于你的观点。电子爱好者会认为使用模拟传感器更容易,而程序员会认为数字传感器更容易使用。

那么,一旦数据被测量出来,你会怎么处理呢?以下部分简要描述了传感器数据的一些方面以及存储这些数据的注意事项。

存储传感器数据

存储传感器数据取决于如何解释数据以及最终如何使用数据。如果你计划使用计算机,或者更好的是数据库来存储数据,你应该以一种有意义的方式来存储它。

例如,存储模拟信号的电压序列可能被认为是以最纯粹的形式保存数据,但如果没有上下文或模数转换器,数据可能毫无意义。存储电压的数字转换可能也不明智,因为您必须记住刻度和范围,以获得想要表示的值。因此,存储结果转换成比例更有意义。幸运的是,当您使用数字传感器时,您唯一需要记住的是所使用的测量单位(摄氏度、华氏度、英尺、米等等)。因此,最好保存测量的最终形式。

但是你把这些信息存储在哪里呢?商业传感器网络将数据存储在嵌入式数据库或文件存储设备中,将其传输到另一个系统进行存储,或者将其存储在可移动数字介质上。较老的传感器网络(如测谎仪或 EKG 机器)使用图表将数据存储为硬拷贝(使它们非常过时)。

有几种简单的存储设备和技术可以用来构建自己的传感器网络,从 Arduino 的本地设备到 Raspberry Pi 上的现代硬盘。这里列出了这些存储机制,并在本书研究构建传感器网络时使用的硬件类型和技术应用时进行了更详细的讨论:

  • 硬拷贝打印机

  • 安全数字卡

  • USB 硬盘

  • 网络服务器

  • 数据库服务器(MySQL)

现在,让我们看看一些可用的传感器及其测量的现象类型。

传感器的例子

所有传感器网络都始于一个传感器和一种读取和解释数据的方法。本章介绍了许多关于传感器的信息。你可能在想各种各样有用的东西,你可以在家里或办公室,甚至在你的院子里或周围测量。你可能想测量你的新阳光房的温度变化,检测邮递员什么时候把最新的通知扔进你的邮箱,或者记录你的狗使用狗门的次数。我希望到现在为止,当你想象你能测量什么的时候,你能看到这些只是冰山一角。你应该在思考你想要建立一个什么样的传感器网络;你可以用这本书来学习如何建造它。

有哪些类型的传感器可用?下表描述了一些比较流行的传感器及其测量内容。这只是可用资源的一个样本。细读 Mouser Electronics ( www.mouser.com )、spark fun Electronics(www.sparkfun.com)和 Adafruit Industries ( http://adafruit.com/ )等在线电子产品供应商的目录,会发现更多的例子:

  • 加速度计:这些传感器测量传感器或它所连接的任何东西的运动或移动。它们被设计用来感知几个轴上的运动(速度、倾斜度、振动等)。).一些包括回转仪特征。大多数是数字传感器。Wii 双截棍(或 WiiChuck)包含一个复杂的加速度计,用于跟踪运动。啊哈:现在你知道 Wii 附带的那些有趣的小东西的秘密了。

  • 音频传感器:也许这是显而易见的,但是麦克风是用来测量声音的。大多数是模拟的,但一些更好的安全和监控传感器具有数字版本,用于传输数据的更高压缩。

  • 条形码读取器:这些传感器用于读取条形码。最常见的是,条形码阅读器生成代表条形码的数字等价物的数字数据。这种传感器通常用在库存跟踪系统中,以在工厂或运输过程中跟踪设备。它们数量众多,而且许多价格经济实惠,使您能够将它们整合到自己的项目中。

  • RFID 传感器:射频识别使用无源设备(有时称为 RFID 标签)通过电磁感应使用射频进行数据通信。例如,RFID 标签可以是信用卡大小的塑料卡、标签或类似的包含特殊天线的东西,通常以线圈、细线或箔层的形式调谐到特定频率。当标签被放置在阅读器附近时,阅读器发射无线电信号;标签可以使用电磁能量以无线电信号的形式传输嵌入在天线中的非易失性消息,该无线电信号然后被转换成字母数字串。 10

  • 生物传感器:读取指纹、虹膜或掌纹的传感器包含一个用于识别模式的特殊传感器。鉴于指纹和掌纹等图案的独特性,它们是安全访问系统的优秀组件。大多数生物传感器产生一组代表指纹或掌纹的数字数据。

  • 电容传感器:电容传感器的一种特殊应用,脉搏传感器被设计用来测量你的脉搏率,通常使用指尖作为传感部位。称为脉搏血氧仪(一些医疗专业人员称为 pulseox)的特殊设备通过电容传感器测量脉搏率,并通过光传感器确定血液中的含氧量。如果你拥有现代电子设备,你可能会遇到触敏按钮,它们使用特殊的电容传感器来检测触摸和压力。一些更新的版本可以用来测量液位。

  • 硬币传感器:这是一种最不寻常的传感器。这些设备就像典型的自动售货机上的投币口。像它们的商业等价物一样,它们可以被校准以感应何时插入一定大小的硬币。虽然不像商业单位那样复杂,可以区分真假硬币,硬币传感器可以用来为您的项目添加一个新的维度。想象一个投币 Wi-Fi 站。现在,这应该可以防止孩子们在互联网上花太多时间!

  • 电流传感器:用于测量电压和电流强度。有些是为测量变化而设计的,而有些是为了测量负载。

  • 挠曲/力传感器:电阻传感器测量一块材料的挠曲或压力对传感器的影响。弯曲传感器可能有助于测量扭转效应或测量手指运动(就像任天堂的电动手套一样)。当传感器弯曲时,传感器电阻增加。

  • 气体传感器:气体传感器种类繁多。有些测量潜在有害气体,如液化石油气和甲烷以及其他气体,如氢气、氧气等。其他气体传感器与光传感器相结合来感测空气中的烟雾或污染物。下次当你从烟雾探测器中听到那种泄露秘密且经常令人讨厌的低电量警告声 12 时,想想那个设备包含了什么。为什么,这是一个传感器节点!

  • 光传感器:测量光线强弱的传感器是特殊类型的电阻:光敏电阻(LDRs),有时也称为光敏电阻或光电池。因此,它们本质上是相似的。如果你有一台 Mac 笔记本电脑,当你的发光键盘在弱光下自动打开时,你很可能已经看到了光敏电阻的作用。或者,您的手机可以使用光传感器来改变亮度。特殊形式的光传感器可以检测其他光谱,如红外线(如旧的电视遥控器)。

  • 液体流量传感器:这些传感器类似于阀门,内嵌在管道系统中。他们测量液体通过时的流量。基本的流量传感器使用旋转轮和磁铁来产生霍尔效应(快速开/关序列,其频率等于通过的水量)。

  • 液位传感器:一种特殊的电阻固态装置可以用来测量水体的相对高度。一个例子是当水位高时产生低电阻,当水位低时产生高电阻。

  • 位置传感器:现代智能手机有 GPS 传感器来感应位置,当然 GPS 设备也使用 GPS 技术来帮助你导航。幸运的是,GPS 传感器以低成本的形式提供,使您能够将位置检测添加到您的传感器网络中。GPS 传感器以经度和纬度的形式生成数字数据,但有些传感器也可以感知海拔高度。

  • 磁条阅读器:这些传感器从磁条(像信用卡上的磁条)读取数据,并返回数字形式的字母数字数据(实际的字符串)。

  • 磁力计:这些传感器通过磁场强度测量方位。指南针是一种用来寻找磁北的传感器。一些磁力计提供多个轴,以便更好地检测磁场。

  • 近程传感器:近程传感器通常被认为是距离传感器,它使用红外线或声波来检测物体的距离或范围。受低成本机器人套件的欢迎,视差超声波传感器通过感应发送脉冲和接收脉冲之间的时间量(回声),使用声波来测量距离。对于近似的距离测量, 13 把时间转换成距离是一个简单的数学问题。这有多酷?

  • 辐射传感器:比较严重的传感器是那些检测辐射的传感器。这也可能是电磁辐射(也有传感器),但盖革计数器使用辐射传感器来检测有害的电离。事实上,使用一个传感器和一个 Arduino(以及一些电子元件)来建造你自己的盖革计数器是可能的。

  • 速度传感器:像流量传感器一样,许多自行车上的简单速度传感器使用磁铁和簧片开关来产生霍尔效应。频率结合车轮的周长可以用来计算速度,以及随着时间的推移,行驶的距离。是的,自行车计算机是简单传感器网络的另一个例子:车轮和前叉上的速度传感器为车把上的监视器提供数据。

  • 开关和按钮:这些是最基本的数字传感器,用于检测某个东西是否被设置(开)或重置(关)。

  • 倾斜开关:这些传感器可以检测设备何时向某个方向倾斜。虽然非常简单,但它们对于低成本运动检测传感器非常有用。它们是数字的,本质上是开关。

  • 触摸传感器:形成小键盘、键盘、定点设备等的触敏薄膜是一种有趣的传感器形式。你可以将像这样的触摸感应设备用于需要从人类身上收集数据的传感器网络。

  • 视频传感器:如前所述,有可能获得非常小的视频传感器,这些传感器使用相机和电路来捕捉图像并将其作为数字数据传输。

  • 气象传感器:温度、气压、降雨量、湿度、风速等传感器都属于气象传感器。大多数生成数字数据,可以组合起来创建全面的环境传感器网络。是的,通过十几个廉价的传感器、一个 Arduino(或一个 Raspberry Pi)和一些解释和组合数据的程序,你可以建立自己的气象站。

摘要

传感器无处不在。它们在你的办公室、车里、家里和我们的个人电子设备中(对大多数人来说,这意味着我们身边总是有一个传感器)。你遇到的大多数传感器都是分立的,比如烟雾探测器或恒温器。有时它们是一个更大的传感器集合的一部分,旨在实现某些功能,例如当你设置巡航控制时,你车上的传感器可以保持你的速度恒定,下雨时启动挡风玻璃雨刷,或者当你转向太靠近车道分界线时振动你的座位。

现在,您已经了解了更多关于传感器类型及其通信数据的信息,您可能已经开始考虑构建一些很酷的项目了。这本书将帮助你了解那些项目。本章研究了什么是传感器网络,它们是如何构建的,它们是如何通信的,以及传感器是如何工作的。您甚至看到了一些代码!

下一章通过深入到新 XBee 3 无线模块的一个简短教程来关注本书中使用的通信媒介。您将了解如何设置和配置这些设备,以便将传感器数据传输到数据和聚合节点。

二、微型通话模块:XBee 无线模块简介

传感器网络的应用通常排除了有线传感器的使用。虽然可以使用安装在支持电缆设备的受控环境中的有线传感器,但你很少有这种奢侈。有时,您可以将传感器网络的某些部分连接到有线网络,但传感器位于无法使用有线网络的区域。因此,大多数传感器网络需要使用无线技术将数据从传感器传输到网络中的其他节点。

无线通信有多种形式。这本书使用了最简单的一个:Digi 的 XBee 无线模块。在本章中,您将探索使用 XBee 模块的基础知识,从选择模块到将其配置为与微控制器一起使用,最后创建一个简单的网络。

什么是 XBee?

XBee 是一个独立的、模块化的、经济高效的组件,它使用射频(RF)在 XBee 模块之间交换数据。XBee 模块在 2.4 GHz 或远程 900 MHz 上传输,并有自己的网络协议。

XBee 模块本身非常小,大约只有一个大邮票的大小,这使得它很容易集成到传感器节点这样的小项目中。这些模块也是低功耗的,并且可以使用特殊的睡眠模式来进一步降低功耗。

虽然 XBee 不是一个微控制器,但它的处理能力有限,可以用来控制模块。其中一个功能,睡眠模式,可以帮助延长电池供电(或太阳能供电)传感器节点的电池寿命。您还可以指示 XBee 模块监控其数据引脚,并将读取的数据传输到另一个 XBee 模块。啊哈!因此,您可以使用 XBee 模块将传感器节点链接到数据聚合器节点。

虽然 XBee 可以用来读取传感器数据,但其有限的处理能力可能意味着它并不适合所有的传感器节点。例如,需要算法来解释或推断有意义的数据的传感器可能不适合单独使用 XBee。您可能需要使用微控制器或计算机来执行额外的计算。

更好的是,最新型号的 XBee(版本 3)可以使用 MicroPython 在芯片上编程。您甚至可以与该模块交互(对话)并交互式地执行您的 MicroPython 代码(就像您在 PC 上使用 Python 一样)。我们将在本章的后面看到更多关于这个令人兴奋的新特性的内容。

Note

要配置 XBee 模块,您必须使用 Digi 配置工具 XCTU,它(现在)可以在 Windows、macOS 和 Linux 上使用。旧版本仅限于 Windows。

下面几节探讨如何开始使用 XBee 模块,从如何选择 XBee 模块开始。我鼓励你在着手这个项目之前通读这一章。我在章节总结之前列出了完成本章项目所需的材料。

XBee 第一个

本节描述了可用的 XBee 模块的类型,如何为您的项目选择模块,以及如何配置它们。我让这一节保持简短,同时提供了足够的信息来解释您将使用什么 XBee 模块以及为什么使用。

但是有一件事让 XBee 世界的大多数新手感到困惑:如何配置模块。所以,让我们先澄清一下。XBee 模块有许多控制和设置,允许您为您的项目配置它。有两种方法可以更改这些设置:(1)使用 Digi 提供的 XCTU 桌面应用程序,以及(2)使用带有 XBee USB explorer 板的终端应用程序来手动更改设置。这是在一个特定的模式下(称为 AT 模式)使用一组命令(称为 AT 命令)来完成的。

您将了解不同的连接方法以及两种操作模式。您还将看到配置 XBee 模块的两种方法的演示。现在,让我们从选择 XBee 模块开始,深入我们的 XBee 教程。

选择 XBee 模块

如果你访问 Digi XBee 网站( www.digi.com/products/embedded-systems/digi-xbee/rf-modules ,你会看到一个最新模块的列表可供选择。有支持专有(Digi)协议、WiFi (UART 或 SPI 到 802.11 b/g/n)、ZigBee 1 和 802.15.4 协议的模块。那你怎么知道选哪个呢?

一些最流行的 XBee 模块支持 ZigBee 协议。你将在本书的项目中使用这些模块。如果您单击 ZigBee 模块的链接( www.digi.com/products/embedded-systems/digi-xbee/rf-modules/2-4-ghz-modules/xbee3-zigbee-3 ,您会发现有三种外形规格可供选择,以满足您的硬件要求,并且这些模块支持多种协议,包括 ZigBee 功能集、Digi Mesh、蓝牙低能耗(BLE)和 802.15.4 2 协议。本书使用支持 ZigBee Pro 特性集的模块。

有许多 XBee 模块提供三种外形规格中的一种,包括通孔和两种表面贴装选项。如果您点击Part Numbers & Accessories链接,您将看到一长串按外形分组的模块。图 2-1 显示了左侧通孔格式的两种可用格式。两者在大多数零售商处都有售。

img/313992_2_En_2_Fig1_HTML.jpg

图 2-1

XBee 3 外形规格

Ok, What’s a Zigbee?

ZigBee 是基于 IEEE 802.15.4 标准的网络通信开放标准。该协议支持网状网络的形成,该网状网络可以自动配置(通过协调器和路由器角色),修复断开的链路,并允许使用中间节点在更长的范围内传输数据(数据通过网格从一个节点传递到另一个节点)。尽管名字如此,ZigBee 并不归 Digi 所有,也不局限于类似命名的 XBee 模块。

对于这本书,我们将使用通孔格式。Digi 提供这些选项以允许更大的设计自由度,因此 XBee 模块可以用于几乎任何需要低成本无线选项的应用。

此外,您会注意到每种外形都有两个版本:标准型号和专业型号。两种型号具有相同的引脚排列,您可以在同一网络中混合搭配使用。pro 型号可能稍大(更长),耗电更多,成本稍高,但提供的范围比标准(也称为“常规模块”)模块更大。但是,如果不将 XBee 3 pro 和常规模块连接到电脑,您可能无法区分它们。例如,PCB 选项 XBee 3 pro 和常规模块看起来完全相同,但在电路板顶部用黑色墨水印刷有细微的差异。常规模块(我正在使用的批次)标有“109 202”,而专业模块标有“941 201”如果您使用 XBee 3 模块,您可能想要在 pro 模块上做一个小标记,以便于识别。

虽然你不会在 Digi 网站上找到它们,但 XBee-ZB 模块有几个迭代(称为系列或版本)。系列 1 模块使用支持点对点通信的旧芯片组。32 系列和 2.5 系列拥有更新的芯片组,支持多种通信形式,包括网状网络。Series 3 拥有最新的芯片组和片上 MicroPython,可实现更快、更轻松的开发。在本书中,您将使用系列 2 和系列 3 模块。

Which Series Should I Use?

有这么多系列的 XBee 模块可用,您可能想知道应该选择使用哪个。答案取决于你想如何使用它们以及你自己的经验水平。对于一些人来说,使用系列 2 和 2.5 将满足他们的所有需求。对于其他人来说,series 3 的易用性可能更适合。

另一个方面可能是模块的成本。随着系列 3 模块的发布,旧系列模块可能会更便宜。

那么,你应该使用哪一个呢?如果你已经有了一些模块,那也没关系,因为 series 3 是向后兼容 series 2 模块的。因此,您可以使用现有的模块,并在需要时添加新的系列 3 模块。如果您没有任何模块,使用系列 3 可能是让您的传感器网络运行的最简单的途径。

但是你还没有完成。你还必须选择你想要使用的天线类型。普通或专业模块有三种天线可供选择。 4 图 2-2 描绘了 XBee-ZB 模块可用的每种类型。下面的列表更详细地描述了每一项:

  • U.FL :该选项有一个非常小的连接器,需要一根适配器电缆(称为尾纤)来连接外部天线。这些天线的优点是 XBee 模块可以封装在外壳(甚至是金属)中,并且天线安装在外壳的外部。这些模块往往要多花几美元,并且需要购买尾纤和天线。

  • RPSMA :和 U.FL 选项一样,这款提供了外置天线;但是它使用更大的 RPSMA 连接器。您可以将旋转天线直接安装到连接器上,但天线承受压力的风险太大。因此,您应该使用延长线并将天线安装在外部。像 U.FL 选项一样,这些模块的成本稍高,并且需要购买天线。

  • PCB: The antenna is printed or embedded as a wire trace onto the module itself. This type of module is similar to the chip antenna and may be a bit less expensive to manufacture. Currently, only the PRO modules are available with this antenna option.

    img/313992_2_En_2_Fig2_HTML.jpg

    图 2-2

    XBee 模块天线选项(SparkFun 提供)

Where’s The Whip?

如果你在过去遇到过 XBee 模块,你可能会熟悉一个天线选项,其中一根小电线安装在模块上,称为“鞭”或线天线。这些在系列 2.5 和更早版本中可用。它们稍微便宜一点,并提供全方位信号,使它们在某些应用中更容易使用。如果你能找到这些模块,你也许能在你的项目上节省几美元。

然而,线状天线不耐用,如果弯曲得太频繁,很容易损坏。幸运的是,你可以用一些相同规格和长度的绞合线焊接一个替换天线。通过剥离一点绝缘材料将旧天线焊接回原位是另一种选择,但这确实会稍微改变它的辐射特性。

现在你知道有许多类型的 XBee 模块,并且这本书的项目仅限于 XBee-ZB 系列 2 和 3 模块,让我们讨论如何与模块通信。

与 XBee-ZB 模块交互

当您检查 XBee 模块时,首先注意到的是引脚布局比设计用于试验板的典型分立元件小得多。此外,您不能将电脑直接连接到 XBee。您需要一个 USB 适配器来安装 XBee,以便与模块进行通信。幸运的是,有几种变体可供选择。您使用 USB 适配器来配置模块。

您可以使用 USB 加密狗,如 spark fun Electronics(www.sparkfun.com/products/11697)的 XBee Explorer 加密狗。此选项允许您将 XBee 模块安装在 PCB 上的接头(两排十针连接器)中,并将整个装置插入 USB 端口。由于它只比 XBee 模块本身大一点点,并且不需要电缆,所以它可能是在远程位置使用 XBee 的最佳选择。

图 2-3 显示了不带 XBee 模块的 XBee Explorer 加密狗。它接受系列 1、2、2.5 和 3 标准或专业型号。

img/313992_2_En_2_Fig3_HTML.jpg

图 2-3

XBee Explorer 加密狗(SparkFun 提供)

注意 PCB 右侧 XBee 模块的白色轮廓。这表示模块在电路板上的正确方向。在将其插入 USB 端口之前,请务必检查针脚是否对齐。

一个类似的选项是 XBee Explorer USB,也来自 SparkFun ( www.sparkfun.com/products/11812 )。它不是一个加密狗,而是一个带迷你 USB 连接器的独立 PCB 基座单元。它还支持系列 1、2、2.5 和 3 标准或专业模块。它需要一根 USB 转迷你 USB 线。图 2-4 显示 XBee Explorer USB 设备。

img/313992_2_En_2_Fig4_HTML.jpg

图 2-4

XBee Explorer USB(由 SparkFun 提供)

SparkFun 的两个选项都包括可以与试验板一起使用的接头安装孔,让您可以接触到 XBee 模块的所有引脚。虽然它们不带有焊接到位的引脚,但如果您愿意,也可以轻松添加引脚。在后面的章节中你会看到这将会有所帮助。

SparkFun 还搭载了其他几个 XBee Explorer 板,包括一个可调节的分线板( www.sparkfun.com/products/11373 )和一个 Arduino 盾( www.sparkfun.com/products/12847 )。如果你打算用 Arduino 来托管 XBee,SparkFun 盾是必须的。图 2-5 为火花防护罩。

img/313992_2_En_2_Fig5_HTML.jpg

图 2-5

XBee Arduino shield(spark fun 提供)

在他们的“SparkFun Thing”系列主板中也有几款支持 XBee 模块的主板,如 SparkFun Thing Plus ( www.sparkfun.com/products/15454 )。

What is a Shield?

屏蔽是一种 PCB,通过连接到 Arduino 上的接头,安装在 Arduino 的顶部。屏蔽用于扩展 Arduino 的硬件功能。有用于控制 LCD、以太网、XBees 等等的屏蔽。

Digi 还生产 XBee 3 开发套件,其中包含三个带天线的 XBee 标准表面贴装模块和三个 USB 接口板。它还包括所有你需要开始使用的线缆,包括一个方便的储物盒。虽然价格相当吓人(建议零售价 110.95 美元),但它确实为那些寻求最大实用性和无组装(除了将模块插入接口板之外)的人提供了一站式购买选择。图 2-6 展示了 SparkFun ( www.sparkfun.com/products/15216 )出售的 Digi ZigBee 开发套件。

img/313992_2_En_2_Fig6_HTML.jpg

图 2-6

Digi ZigBee 开发套件

该套件的一个很好的特性是 explorer 板支持即插即用的 Grove 接口(也称为 Grove 系统),这使得将模块连接到支持 Grove 的传感器就像将它们插在一起一样简单。有关 Grove 接口的更多信息,请参见 http://wiki.seeedstudio.com/Grove_System/

虽然许多供应商提供了其他选项,包括旧的串行接口模块,但这些是我发现的使用 XBee 模块的最佳和最容易使用的选项。当您看到我们将 XBee 模块与 Arduino 一起使用时,您会看到一个 XBee 屏蔽示例,它支持 XBee 与 Arduino 引脚的直接连接。

引脚布局

如果您查看 XBee 模块,您会看到总共有 20 个引脚。如果从顶部(带天线的一侧)查看模块,从左上角开始,引脚标记为 1–10,从右下角开始,标记为 11–20。因此,插针 1 在左上角,插针 20 在右上角。但是所有这些东西都有什么作用呢?

你将在后面的章节中更详细地探索这些引脚,但是现在(如果你好奇的话),表 2-1 描述了典型 XBee 模块的引脚布局。在本例中,我展示的是 XBee-ZB 系列 2 模块的引脚布局。系列 3 模块在引脚上有一个区别;3 系列模块不支持针脚 14,因此不应与这些模块一起使用。

表 2-1

XBee 引脚布局

|

别针

|

名字

|

描述

|

方向

|

默认

|
| --- | --- | --- | --- | --- |
| one | VCC | 电源 | 不适用的 | 不适用的 |
| Two | 熄 | UART 数据输出 | 在外 | 在外 |
| three | DIN/CONFIG | UART 数据输入 | 在…里 | 在…里 |
| four | DIO12 | 数字输入/输出 12 | 两者 | 有缺陷的 |
| five | 重置 | 模块复位 | 两者 | 带上拉电阻的集电极开路 |
| six | RSSI PWM/二极管 10 | RX 信号强度,数字 I/O 10 | 两者 | 在外 |
| seven | DIO11 | 数字输入/输出 11 | 两者 | 在…里 |
| eight | 内向的; 寡言少语的; 矜持的 | 不连接 | 钠 | 有缺陷的 |
| nine | DTR/sleep _ rq/神 08 | 睡眠控制,数字 I/O 8 | 两者 | 在…里 |
| Ten | 地线 | 地面 | 不适用的 | 不适用的 |
| Eleven | DIO4 | 数字输入/输出 4 | 两者 | 有缺陷的 |
| Twelve | CTS/DIO7 | 允许发送,数字 I/O 7 | 两者 | 在外 |
| Thirteen | 开启/睡眠 | 状态,数字输入/输出 9 | 在外 | 在外 |
| Fourteen | 参考电压(VoltageReference) | 不连接 | 在…里 | 不适用的 |
| Fifteen | 助理/DIO5 | 相关指示器,数字输入/输出 5 | 两者 | 在外 |
| Sixteen | RTS/DIO6 | 请求发送,数字输入/输出 6 | 两者 | 在…里 |
| Seventeen | a3/上帝 3 | 模拟输入/输出 3,数字输入/输出 3 | 两者 | 有缺陷的 |
| Eighteen | a2/神 2 | 模拟输入/输出 2,数字输入/输出 2 | 两者 | 有缺陷的 |
| Nineteen | a1/神 1 | 模拟 I/O 1、数字 I/O 1 | 两者 | 有缺陷的 |
| Twenty | ado/神 0 | 模拟输入输出 0,数字输入输出 0 | 两者 | 有缺陷的 |

要了解 XBee 3 模块硬件的更多信息,请参见 Digi at www.digi.com/resources/documentation/digidocs/pdfs/90001543.pdf 的硬件参考文档。

在下一节中,您将看到如何开始配置在您的项目中使用的模块。

配置模块

配置 XBee 模块并不十分困难。因为您使用的是 ZigBee 模块,所以需要为每个模块设置地址,选择要在网络中执行的角色,并将您的模块配置为与您用来处理传感器数据的任何传感器或微控制器接口。让我们从讨论 ZigBee 寻址开始。

Can Sensors Be Connected Directly To The XBee?

XBee 模块可以通过其 I/O 端口读取传感器数据。然而,并非所有传感器都可以直接连接到 XBee 模块。如果传感器需要使用特殊通信协议的直接 I/O,您需要一个微控制器来读取传感器数据,然后将其发送到 XBee 进行传输。在下一章中,当您探索如何使用 DHT-22 温度传感器时,您将会看到这一点。

地址

XBee 模块标有特定的序列号或地址,位于模块底部。这有点不方便,因为当模块安装好后,你通常看不到它的背面。不过,您可以使用 Digi 配置应用程序或简单的串行终端应用程序来查找地址。

图 2-7 显示了 XBee 3 模块的底面(放大以便更好地观察)。请注意型号下面打印的数字。您一起使用它们来形成一个对每个 XBee 模块唯一的 64 位地址。许多出版物称之为无线电地址,分为两部分:高位和低位地址(或值)。例如,图中所示的地址,0013A200,是“高”地址,4192DA30是“低”地址。在接下来的例子中,我们将需要这些。

img/313992_2_En_2_Fig7_HTML.jpg

图 2-7

XBee 地址印在模块背面

Tip

经常看到 XBee 被称为模块或收音机。这些术语经常互换。当提到无线电本身的发射和接收能力时,我一般用模块和无线电来指代 XBee。

电台的地址用于确定要发送的消息的目标。在许多方面,它类似于 IP 地址,但在这种情况下,它是一个特定的无线电地址。

除了特定的 64 位无线电地址之外,ZigBee 网络在每个网络内使用分配给每个无线电的 16 位地址。此外,您可以指定一个简短的文本字符串来标识每个收音机。除此之外,还有一个个人区域网络(PAN)地址,可用于对网络中的无线电进行逻辑分组。最后,所有无线电必须在相同的信道(频率)上发射和接收。概括地说,当 XBee 无线电想要向另一个无线电发送消息时,它必须使用相同的频道,并设置目的地 PAN 和特定的 16 位无线电地址。在接下来的章节中,您将看到这些选项的作用。

ZigBee 网络

像以太网一样,ZigBee 网络基于预定义的网络堆栈,其中堆栈中的每一层负责数据消息的特定转换。与其他网络一样,ZigBee 网络支持消息路由、自组织网络创建和自愈网状拓扑。因此,需要无线电地址和 PAN 地址来支持这些特征。

通过增加每个节点(无线电)可以在网络中执行的不同角色,对网状拓扑的支持成为可能。以下列表从最复杂的角色开始,更详细地描述了每个角色:

  • 协调器:每个网络需要一个协调器。该节点负责管理地址,形成和管理网络。所有其他节点在启动时搜索协调器并交换握手信息。

  • 路由器:被配置为路由器的节点被设计为将信息传递(路由)到其他无线电。路由器通过加入网络和交换来自其他节点的消息来实现网状网络的修复。路由器通常由可靠的电源供电,因为它们必须是可靠的。因此,数据聚合节点将是路由器无线电模式的一个好选择。

  • 终端设备:终端设备是向路由器节点和协调器发送或接收信息的节点。它的一个优点是正在进行的处理更少,因此功耗更低。终端设备支持睡眠模式,以进一步降低功耗要求。大多数传感器节点将被配置为终端设备。

只要网络中至少有一个协调器,就可以用任何方式配置 XBee 模块。要形成网状网络,只需使用几个路由器,其中一个或多个终端设备交换消息,路由器与协调器交换消息。图 2-8 显示了一个典型的网状网络。

img/313992_2_En_2_Fig8_HTML.jpg

图 2-8

ZigBee 网状网络

配置 XBee 模块有时会出错。发生这种情况时,诊断和纠正该问题会非常困难。我在这一章的结尾包括了一个故障排除部分,它将帮助你解决许多常见的问题。如果您遇到问题,请查看故障排除部分。

更新固件

开始配置 XBee 模块时,您应该做的第一件事是加载最新版本的固件并设置角色。在这种情况下,固件是指 XBee 的嵌入式微控制器的程序。如果您的系列 2 或 2.5 模块使用的是旧版本,或者您想尝试不同的配置,您只需更改固件。同样,如果您想将系列 3 模块与旧模块一起使用,您必须在系列 3 模块上加载不同的固件。例如,如果您想通过 802.15.14 协议同时使用系列 2 和系列 3,您应该在每个模块上安装相同的固件。

Digi 通过提供一个名为 XCTU 的优秀配置应用程序使这变得简单。只能使用 XCTU 应用程序加载固件。图 2-9 显示 XCTU 在没有连接任何模块的情况下运行。

img/313992_2_En_2_Fig9_HTML.jpg

图 2-9

XCTU 主窗口

正如你所看到的,XCTU 软件会给你友好的提醒和如何做事的提示。在下面的段落中,我们将看到如何联系和配置模块。您可以从以下 URL 下载最新版本的 XCTU 软件:

www.digi.com/products/embedded-systems/digi-xbee/digi-xbee-tools/xctu#productsupport-utilities

您将找到大多数平台的安装程序,包括 macOS、Windows 和 Linux,以及发行说明和许可文档的链接。只需下载适合您平台的安装程序,并使用该平台通用的方法进行安装。例如,在 Windows 上,您将运行并执行安装程序,而在 macOS 上,您必须首先从压缩文件中提取安装程序,然后执行安装程序。

一旦安装了软件并且启动了 XCTU 软件,就需要连接到 XBee 模块。要连接到 XBee,只需将 XBee 模块插入适配器,并将其连接到您的电脑。例如,如果您使用 SparkFun Explorer USB 加密狗,您只需首先将 XBee 模块插入加密狗。将加密狗插入 USB 插槽后,您应该会看到电源指示灯亮起。

一旦浏览器连接到您的计算机,您可以使用窗口左上角的任一按钮添加模块,如图 2-10 所示。左边的一个允许您通过指定模块的串行连接参数来添加模块,右边的一个扫描模块的所有串行连接。让我们使用添加模块按钮。

img/313992_2_En_2_Fig10_HTML.jpg

图 2-10

添加和扫描模块按钮

准备就绪后,点击添加无线电按钮。您应该看到一个对话框打开,允许您选择串行连接并设置连接参数。默认串行连接参数为 9600 波特、无流量控制、8 位、无奇偶校验、1 个停止位(也可写成 96008N1)。如果您发现您的 XBee 无法通信,那么它可能是以不同的波特率运行的。如果您更改波特率,您应该更改所有模块的波特率。图 2-11 显示添加模块对话框。如果您选择使用发现选项,则可以选择多个串行配置进行搜索。

img/313992_2_En_2_Fig11_HTML.jpg

图 2-11

添加模块对话框

只需选择串行连接,验证连接参数,然后单击 Finish。XCTU 需要几秒钟来建立连接,一旦建立连接,您将会看到模块出现在主窗口的左栏,如图 2-12 所示。较新的系列 3 模块将以黑色背景出现,较旧的系列模块将以蓝色背景出现。这允许您查看列表并快速识别您的旧模块。

img/313992_2_En_2_Fig12_HTML.jpg

图 2-12

添加了无线电模块

无线电列表还显示了分配给模块的角色。例如,路由器将出现一个“R ”,协调器将出现一个“C”图标在模块图像上。您还会看到协议图标根据加载的固件而变化。每个模块右侧的图标允许您关闭无线电连接,运行发现操作来查找同一网络上的其他模块,以及隐藏模块详细信息。图 2-13 显示了系列 3 模块的示例。

img/313992_2_En_2_Fig13_HTML.jpg

图 2-13

收音机入口—概述

请注意,在中间有附加数据,包括模块名称(您可以更改名称)、功能(加载的固件)、串行连接和媒体访问控制(MAC)地址。 5 回想一下,每个模块都有一个唯一的 MAC 地址,印在模块底部。请注意,还有一个带“R”的菱形。这表示模块当前被编程执行的角色。在这种情况下,它是一个路由器。协调器显示为“C ”,终端设备显示为“E”。这只是 XCTU 应用程序使 XBee 模块工作变得容易的一个非常好的方面。

接下来,我们可以单击左侧列中的模块,查看该模块的详细信息。图 2-14 显示了之前显示的系列 3 模块的无线电模块细节。

img/313992_2_En_2_Fig14_HTML.jpg

图 2-14

XCTU 无线电模块设置

收音机模块设置包括一长串您可以更改的内容。虽然大多数是您通常不会更改的,但有些是您在配置模块时需要更改的。例如,您可能需要设置在 ZigBee 网络中使用的角色和其他参数。幸运的是,这些设置被分组到类别中,您可以折叠或展开这些类别,以便更容易地找到您想要的设置。只需单击每个类别的三角形即可折叠或打开它。还有一个搜索框,可让您搜索设置。这是一个很好的接触。

让我们来看看如何做一个小小的改变。请注意,图中所示的系列 3 模块没有名称。每当对任何设置进行更改时,首先要找到设置,进行更改,然后将更改写入模块。在这种情况下,我们希望名称的参数是节点标识符或 NI。要在设置中找到它,只需在搜索框中键入 NI。然后您可以更改名称,如图 2-15 所示。

img/313992_2_En_2_Fig15_HTML.jpg

图 2-15

更改模块的名称标识符

请注意,我更改了节点标识符。要将更改写入模块,我单击设置旁边的铅笔图标。您也可以对其他参数进行更改,依次应用每一个参数,或者等待并更新所有更改,方法是点击写入按钮将所有更改写入模块。图 2-16 显示了应用了更改的无线电。

img/313992_2_En_2_Fig16_HTML.jpg

图 2-16

无线电模块名称已更改

命名模块也有助于在无线电模块列表中识别它们。事实上,XCTU 允许您以多种方式对模块进行排序,包括按名称排序。设置其他参数也同样简单。例如,您可能想要设置 PAN 地址、目的地址和节点标识符。

关于 XBee 模块,您还应该了解另一个方面——管理固件。如果您有较旧的模块,系统可能会提示您是否要下载旧固件。可能会出现如图 2-17 所示的对话框。您有三个下载选项:

  • 寻找并安装新固件:仅下载 XCTU 和“已知”模块的更新。

  • 安装旧固件:下载所有旧固件—如果您想使用任何旧系列模块,请选择此项。

  • 从文件安装固件:从文件加载固件——如果您已经下载了自定义固件(很少使用),请使用此选项。

第一次使用 XCTU 或第一次使用旧系列模块时,您应该选择第二个选项并安装旧固件包。否则,第一个选项是默认的,事实上,XCTU 会自动检查更新(但是您可以在首选项中关闭它)。

img/313992_2_En_2_Fig17_HTML.jpg

图 2-17

下载固件

根据您的互联网连接速度,可能需要一段时间来下载旧固件,但您应该这样做,以保持您的模块是最新的。拥有较旧的固件还允许您在较新的系列 3 模块上加载正确的固件,以便它们在协议和功能上匹配。

XBee 模块有两种通信模式:AT 或 API。AT 表示模块通过其本地串行连接接受 AT 命令,并使用 Hayes 调制解调器命令集的衍生物以人类可读的格式显示信息。API 意味着模块被配置为通过其协议栈发送和接收数据。因此,当您想要使用控制台连接与模块通信以对其进行配置时,可以使用 AT 方法。API 方法将在本书的其余部分使用。所有模块必须使用相同的通信固件(AT 或 API)和版本用于其角色。

在本章中,我们将使用 AT 模式。为了确保您加载了正确的固件,您应该将您的模块插入浏览器,将浏览器插入您的计算机,然后在 XCTU 中添加无线电模块。然后点击更新按钮,如图 2-18 所示。

img/313992_2_En_2_Fig18_HTML.jpg

图 2-18

选择更新固件

您将看到一个对话框,允许您选择要下载(更新)到模块上的固件。图 2-19 显示了在 AT 模式下选择 ZigBee 协议固件的示例,以及 2.5 系列和旧模块的最新版本。XBee 系列 3 模块的固件以不同方式列出。

img/313992_2_En_2_Fig19_HTML.jpg

图 2-19

选择固件(2.5 系列及更旧版本)

图 2-20 显示了 XBee 系列 3 模块的固件选择。

img/313992_2_En_2_Fig20_HTML.jpg

图 2-20

选择固件(系列 3)

选择所需固件后,点击更新按钮。你可能需要回答一个或多个“你确定吗?”查询。这是因为更新固件会抹掉您所做的任何设置,包括任何编程。显然,只有在你确定不需要先保存任何东西的情况下,才说“OK”。一旦固件更新,你会得到一个“确定”对话框。关闭后,XCTU 会将收音机重新加载到您的收音机列表中。

现在您已经看到了如何管理 XBee 模块的固件和设置,让我们来看看一种更简单的方法来设置用户定义的值。

使用终端应用程序更改设置

XBee 模块的大多数设置都可以使用终端应用程序来更改(AT 模式)。在过去,我们必须使用控制台应用程序,但 XCTU 现在提供了一个控制台模式,效果非常好。该控制台具有使用 XBee 模块所需的所有功能。您可以连接、断开连接,甚至记录您的会话。您也可以发送命令或形成数据包,并将它们发送到模块。在本节中,我们将发送命令。该界面还允许我们看到十六进制的命令,这有助于诊断连接或解密数据。

XBee 模块有两种模式:命令和透明。命令模式由特殊命令+++启动,其中模块通过串行连接发回响应。透明模式是默认模式:模块向指定的无线电目的地发送数据。换句话说,当您想要与模块对话时,请使用命令模式;当您想要通过模块与另一个模块对话时,请使用透明模式。例如,通过 XBee 向另一个 XBee 发送数据使用透明模式。

因此,要在加载正确的固件后配置 XBee 模块,您需要打开一个终端应用程序并发出适当的命令。表 2-2 显示了一些用于配置 XBee 模块的常见 AT 命令。

表 2-2

常见 XBee AT 命令

|

命令

|

描述

|

使用

|

反应

|
| --- | --- | --- | --- |
| +++ | 进入命令模式 | 将模块置于命令模式 | 好 |
| ATCN | 退出命令模式 | 返回透明模式 | 好 |
| AT | 注意力 | 查看该模块是否可用 | 好 |
| ATWR | 救援 | 将设置写入固件 | 好 |
| ATID | 平移 ID | 显示平移 ID | 平移 ID |
| ATID nnnn | 平移 ID | 更改平移 ID | 好 |
| ATSH | 64 位串行高电平 | 显示 64 位序列号的高部分 | 地址 |
| ATSL | 64 位串行低电平 | 显示 64 位序列号的低位部分 | 地址 |
| ATDH | 64 位目标高电平 | 显示 64 位目的地址的高位部分 | 地址 |
| ATDH nnnn | 64 位目标高电平 | 设置 64 位目标地址的高部分 | 好 |
| ATDL | 64 位目标低电平 | 显示 64 位目的地址的低位部分 | 地址 |
| ATDL nnnn | 64 位目标低电平 | 设置 64 位目的地址的低位部分 | 好 |
| ATMY | 16 位地址 | 显示协调员分配的 16 位地址 | 地址 |
| ATNI | 节点 ID | 显示文本字符串节点标识符 | 编号 |
| ATNI text | 节点 ID | 设置文本字符串节点标识符 | 好 |
| ATRE | 重置 | 将 XBee 重置为出厂默认值 | 好 |

Tip

有关 AT 命令的更多信息,请参见 XBee 3 手册 at www.digi.com/resources/documentation/digidocs/pdfs/90001539.pdf

一些命令需要一个值来设置变量。省略变量会导致显示当前值。除了+++以外的所有命令都需要你按下Enter才能执行。如果你按下+++命令而没有反应,再试一次,在每次尝试之间等待一两秒钟。您也可以尝试键入得快一点(或慢一点),直到命令模式切换生效。

Tip

所有数值都以十六进制值的形式输入。

为了演示这些命令是如何工作的,让我们使用 XCTU 的控制台对话框连接到一个已经加载了 ZigBee 路由器固件的模块。首先在左侧列表中选择你的收音机,然后点击控制台选项卡,如图 2-21 所示。

img/313992_2_En_2_Fig21_HTML.jpg

图 2-21

选择控制台模式

控制台窗格允许您使用左侧的三个图标执行三项操作。您可以打开一个连接(连接到一个模块)、记录会话(仅在连接后可选择)和分离,这允许您将控制台分离到它自己的窗口中。如果你正在跟随,现在继续断开控制台,然后点击打开按钮。图 2-22 显示了一个典型的配置会话,从连接到模块并显示其值开始,然后退出命令模式。

Note

第一次不带参数运行 ATDH、ATDL 或 ATMY 命令时,可能会看到结果为 0。这表明该值尚未设置。

img/313992_2_En_2_Fig22_HTML.jpg

图 2-22

在控制台中获取有关模块(AT 模式)的信息

Tip

在命令模式下,在模块返回透明模式之前,您只有 10 秒钟的时间输入命令。如果发生这种情况,当输入命令时,您将看不到任何响应。只需再次发出++命令,然后重新发出该命令。

接下来,图 2-23 显示了使用 XCTU 控制台设置目的地址(您想要连接的 XBee)及其 PAN ID 的会话。

img/313992_2_En_2_Fig23_HTML.jpg

图 2-23

在控制台中配置模块(AT 模式)

虽然 XCTU 应用程序有一个非常好的串行终端,运行得非常好(当在 at 模式下使用 XBee 模块时,我更喜欢使用它),但是您可以使用任何您想要的终端应用程序,比如 CoolTerm。你只需要设置串行连接来匹配你的 XBee(波特率等。)并连接到 XBee 所连接的串行端口。图 2-24 显示了在 macOS 上使用 CoolTerm 的示例。

img/313992_2_En_2_Fig24_HTML.jpg

图 2-24

使用 CoolTerm 配置模块(AT 模式)

现在,您已经知道了使用 ZigBee 协议组建无线网络所需的模块类型以及如何配置它们,您可以开始构建无线网络了。下一节将解释如何创建最基本的 XBee 项目:“Hello,World!”XBee 等价物。

更多信息

如果你想了解更多关于 XBee 模块以及它们如何通信的信息,Digi 网站( www.digi.com )是一个很好的资源。你也可以在谷歌上搜索“XBee”和“ZigBee ”,找到一些博客、操作页面等,这些都有助于展示使用 XBee-ZB 模块解决的不同项目和解决方案。

如果您正在使用较旧的系列模块,也有一些优秀的书籍可供您参考,以获得更多信息、项目想法等。我在这里列出了两个较好的标题:

  • 构建无线传感器网络:使用 ZigBee、XBee、Arduino 和处理,作者 Robert Faludi (O'Reilly,2010 年),ISBN 978-0596807733

  • 《XBEE 实验室实践手册:教你 XBEE 无线通信的实验》,作者乔纳森·泰特斯(纽尼斯,2012 年),ISBN 978-0123914040

但是,Digi 提供了一些优秀的资源,您应该考虑研究一下,包括 XCTU 文档、XBee 模块硬件手册和 ZigBee 手册。您可以在以下站点找到所有这些文档:

www.digi.com/support/productdetail?pid=5637&type=documentation

在我们开始使用 XBee 模块的第一个示例应用程序之前,让我们讨论一下 series 3 模块最强大的选项之一:MicroPython。

介绍 MicroPython

使用 Python 语言控制硬件已经有一段时间了。Raspberry Pi、pcDuino 和其他低成本计算机以及类似主板的用户已经拥有了使用 Python 控制硬件的优势。在这种情况下,他们在基于 Linux 的本地操作系统上使用了完整版本的 Python 编程语言。

然而,这需要构建特殊的库来与硬件通信。这些库旨在与通用输入输出(GPIO)引脚接口。GPIO 引脚通常出现在板上的一排或多排公引脚中。一些电路板使用母头引脚。

虽然这些电路板使那些想要开发电子项目的人成为可能,但它要求用户购买电路板以及键盘、鼠标和显示器等外围设备。不仅如此,用户还必须学习操作系统。对于那些不习惯 Linux 的人来说,这本身就是一个挑战。

MicroPython 的愿景是将学习 Python 的简单性与微控制器板的低成本和易用性结合起来,这将允许更多的人在艺术和科学项目中使用电子产品。初学者不必学习新的操作系统或学习更复杂的编程语言。答案是 MicroPython。

MicroPython 6 由 Damien P. George、Paul Sokolovsky 和其他贡献者创建和维护。它被设计成 Python 3 语言的精简、高效版本,并安装在一个小型微控制器上。由于 Python 是一种解释语言,因此(一般来说)比编译语言慢,所以 MicroPython 被设计得尽可能高效,以便它可以在通常比典型的个人计算机慢得多且内存少得多的微控制器上运行。

另一方面,Arduino 等微控制器板需要一个编译步骤,您必须在计算机上执行该步骤,并首先将二进制可执行文件加载到板上。相比之下,由于 MicroPython 的解释器直接在硬件上运行,我们不需要中间步骤来准备代码;我们可以直接在硬件上运行解释语言!

这使得硬件制造商可以制造小型、廉价的主板,在与微处理器相同的芯片上包含 MicroPython(通常情况下)。这使您能够连接到电路板,编写代码,并执行它,而无需任何额外的工作。

您可能会想,将 Python 3 缩减到适合内存有限的小芯片的大小,这种语言被剥离了,缺少了一些特性。这不可能比事实更进一步。事实上,MicroPython 是 Python 3 核心特性的完整实现,包括紧凑的运行时和交互式解释器。它支持读写文件、加载模块、与 GPIO 引脚等硬件交互、错误处理等等。最重要的是,Python 3 代码的优化允许它被编译成需要大约 256K 内存来存储二进制文件的二进制文件,并且运行时只需要 16K 的 RAM。

MicroPython 因此允许 Digi 在 XBee series 3 模块本身上放置函数式编程语言和解释器!是的,这意味着我们可以连接到我们的 XBee 系列 3 板,编写代码,并执行它。这为使用 XBee 模块和传感器网络打开了一个全新的世界。

Note

MicroPython 仅适用于 series 3 模块。

XCTU 有一个 MicroPython 控制台,可以用来连接到 series 3 模块。要使用 MicroPython,我们必须对模块进行一些修改。回想一下,在更改设置后,我们使用右侧的更新图标(铅笔)来设置更改。其中包括更改波特率(更快),启用 API + MicroPython 模式。图 2-25 显示了 XCTU 中的设置。

img/313992_2_En_2_Fig25_HTML.jpg

图 2-25

MicroPython 的安装模块

一旦这些设置被写入模块,只需将 XBee series 3 模块连接到 USB XBee Explorer,将其插入您的计算机,并将无线电添加到 XCTU。在那里,您可以单击工具图标并选择 MicroPython 终端。图 2-26 显示了选择。

img/313992_2_En_2_Fig26_HTML.jpg

图 2-26

打开 MicroPython 控制台

这将打开 MicroPython 控制台,您可以在其中发出 Python 命令。图 2-27 以无处不在的“你好,世界!”的形式展示了一个简单的例子程序。

img/313992_2_En_2_Fig27_HTML.jpg

图 2-27

示例 MicroPythonsession

这个例子是使用 ZigBee 系列 3 模块显示的。根据您使用的模块,MicroPython 的特性可能略有不同。表 2-3 显示了某些 3 系列模块的主要特性及其可用性。

表 2-3

XBee Series 3 按型号划分的 MicroPython 功能

|

特征

|

XBee 3 蜂窝电话

|

XBee 3 ZigBee、DigiMesh 和 802.15.4

|
| --- | --- | --- |
| 数字输入输出 | 是 | 是 |
| I2C | 是 | 是 |
| 动力管理 | 是 | 是 |
| Digi 远程管理器 | 是 1 | 不 |
| 辅助 UART | 是 | 不 |
| 实时时钟 | 是 | 不 |
| 文件系统 | 是 | 是 |
| 文件系统—并发文件写入 | 是 | 不 |
| 文件系统—重命名 | 是 | 不 |
| 文件系统—创建后编辑文件 | 是 | 不 |
| 文件系统—删除 | 是 | 否 2 |
| 文件系统—安全文件 | 是 | 不 |
| 跨更新保留文件系统 | 是 | 不 |

我们将在下一章看到更多关于 MicroPython 的内容。但是首先,让我们通过一个简短的演示项目来看看 XBee 模块的运行情况,在这个项目中,我们使用 AT 模式通过终端连接进行设置和配置。

XBee 无线聊天室

对于这个示例,您需要两个 XBee 模块、两个 USB 适配器和所需的电缆,以及一台或两台计算机。您可以将一台电脑的每个模块连接到不同的 USB 串行端口。

此示例使用一个 series 2 和一个 series 3 XBee 模块来演示如何使用每个系列。如果只有一个系列,可以跳过演示其他系列的部分。最大的区别在于你如何加载固件。在 AT 模式下对模块编程是相同的。

这个项目有点像“你好,世界!”在硬件级别测试 XBee。您将使用两个 XBee 模块,配置为一个简单的点对点网络,带有一个协调器和一个路由器,而不是编写一个简单的程序来打印消息。在示例中,我包括了一个系列 2 和一个系列 3 模块,但是如果您愿意,也可以使用两个相同的系列。只需遵循以下章节中特定系列的说明。

您将把这两个模块都设置为使用 AT 固件,这样您就可以演示透明模式,并看到您以明文形式传递的消息。这是聊天成功的关键。在一个模块上键入的内容或输入的信息会出现在另一个模块上。酷吧。

加载模块的固件

你需要做的第一件事是为每个模块加载固件。回想一下,您使用 XCTU 应用程序来加载固件。我们将回顾为 2.5 系列和更早版本以及 3 系列模块加载固件的细节。

2.5 系列及更早版本

较旧的 2.5 系列和更早的模块具有针对三种角色之一预配置的固件。这由版本号表示。对于我在编写本章时使用的 XBee 模块,协调器的版本号是 20A7。前两位是角色,后两位是版本。如果你的模块有不同于 A7 的版本,这并不重要,只要它们有相同的版本。以下是一些主要角色及其价值:

  • 20xx,协调器,AT/透明操作

  • 21xx,API 操作协调员

  • 22xx,路由器,AT/透明操作

  • 23xx,路由器,API 操作

  • 28xx,终端设备,AT/透明操作

  • 29xx,终端设备,API 操作

系列 3

系列 3 模块的配置略有不同。这些模块对于 ZigBee 固件只有如下三种选择。要将 3 系列模块与旧的 ZigBee 模块一起使用,请确保加载 ZigBee 固件:

  • Digi XBee3 802.15.4 TH :当与使用较旧的 802.15.4 协议的其他模块一起工作时,使用此固件。

  • Digi XBee3 DigiMesh 2.4 TH :与使用 DigiMesh 协议的其他模块一起工作时,使用此固件。

  • Digi XBee3 ZigBee 3.0 TH :与使用 ZigBee 协议的其他模块一起工作时,使用此固件。

由于我们在本书中使用 ZigBee 协议,如果您使用任何 3 系列模块,您将需要加载 ZigBee 固件。目前最新版本是 1008。同样,拥有最新的并不重要,但保持最新也无妨。

您可能想知道如何为三个角色之一配置 series 3 模块。这是通过选择两种设置的组合来完成的。首先,我们选择设备角色 (CE)并将其设置为形成网络 (1)或加入网络 (0)。其次,我们选择睡眠模式 (SM)并将其设置为 0(对于路由器)或> 0(用作终端设备)。表 2-4 显示了帮助您选择正确设置的矩阵。

表 2-4

设置系列 3 角色

|   |

掌握

|
| --- | --- |
|

ZigBee 角色

|

设备角色(ce)

|

睡眠模式(SM

|
| --- | --- | --- |
| 协调者 | one | Zero |
| 路由器 | Zero | Zero |
| 终端设备 | Zero | > 0 |

例如,要使用 AT 模式将 series 3 模块设置为协调器,我们发出ATCEATSM命令。或者,更好的方法是,我们可以使用 XCTU 应用程序并在那里设置参数。

但是还有一个特定于系列 3 模块的设置。请注意,没有 AT 或 API 固件选项。这通过使用 API 使能 (AP)设置来控制。对于 AT 模式,将其设置为 0;对于 API 模式,将其设置为 1。您可以使用 ATAP 命令或使用应用程序来实现这一点。

Tip

更改设置时记得点击设置旁边的写入按钮,或者如果您已经更改了两个或更多设置,点击工具栏上的写入按钮。

为协调器加载固件

使用 XCTU 应用程序将第一个 XBee 模块配置为功能集的协调器。在这种情况下,我们将使用系列 3 模块。

连接模块并点击添加发现按钮(在 XCTU 窗口的左上角)。按照对话框操作,一旦添加了模块并读取了其配置,就可以更新固件了。

点击更新图标,在对话框中选择 ZigBee 固件。图 2-28 显示了加载 ZigBee 3.0 固件的正确选择。一旦选定,点击对话框中的更新按钮。

img/313992_2_En_2_Fig28_HTML.jpg

图 2-28

为协调器加载固件(系列 3)

写过程完成后,必须设置设备角色睡眠模式,如表 2-4 所示。您还需要确保将 API 模式设置为 0。下面的代码显示了如何使用终端连接来实现这一点。回想一下,我们可以点击 XCTU 界面右上角的终端图标,然后点击打开按钮开始一个会话。记住键入+++命令(在控制台日志的左侧),并在输入命令前等待几秒钟的响应。

+++
ATCE 1
ATSM 0
ATAP 0
ATWR
ATCN

如果您想使用 XCTU 应用程序设置参数,记得点击配置图标切换到配置模式(或使用菜单)。

为路由器加载固件

现在,让我们将第二个 XBee 模块配置为路由器 AT 功能集。在这种情况下,我们将使用系列 2 模块。

连接模块并点击添加发现按钮(在 XCTU 窗口的左上角)。按照对话框操作,一旦添加了模块并读取了其配置,就可以更新固件了。

点击更新图标,在对话框中选择 ZigBee 路由器 AT 固件。图 2-29 显示了在固件中加载 ZigBee 路由器的正确选择。一旦选定,点击对话框中的更新按钮。

img/313992_2_En_2_Fig29_HTML.jpg

图 2-29

为路由器加载固件(2.5 系列及更早版本)

接下来,我们需要为每个模块设置目的地址,使它们相互指向对方。因此,点对点的命名。

捕获序列号

回想一下,XBee 无线电需要目标无线电的 64 位地址(序列号,也称为 MAC 地址)来发送数据。您需要在开始项目之前记录这些内容。花点时间记录下每个 XBee 模块的 64 位序列号。

如果您已经将 XBee 模块插入到它们的适配器中,那么使用 XCTU 应用程序就可以很容易地看到地址。图 2-30 显示了连接的系列 3 和系列 2 模块。注意显示的地址。我们将把每个模块的目的地设置为另一个模块的地址。

img/313992_2_En_2_Fig30_HTML.jpg

图 2-30

使用 XCTU 识别 64 位地址

如果您喜欢或需要该操作模式,您也可以使用终端应用程序,使用ATSHATSL命令向模块查询地址。

确定地址后,将信息写在表 2-5 中。 7 这里有您将使用的附加信息的空间,因此在您继续项目时请参考此表。

表 2-5

XBee 配置数据

|

作用

|

串行高电平

|

串行低电平

|

平移 ID

|

节点 ID

|
| --- | --- | --- | --- | --- |
| 协调员在 |   |   |   |   |
| 路由器在 |   |   |   |   |

现在,让我们从协调器开始配置 AT 模式参数。

配置协调器

要配置协调器,您需要将此无线电的目的地址设置为另一个无线电(路由器)的序列号。因此,您将协调器上的目的地址设置为路由器的地址。我们还需要为网络设置 PAN ID。

我们将使用 XCTU 应用程序来更改设置,但是如果您愿意,也可以使用终端并发出ATDHATDLATID命令。

您还必须选择要在网络上使用的 PAN ID。还是用标志性的 8088 吧。 8 在这种情况下,只要网络上的所有模块具有相同的 PAN ID,并且该值在 0000-FFFF(十六进制)范围内,您使用什么都没关系。还要将节点 ID 设置为 COORDINATOR,以便于识别。图 2-31 显示了协调器的配置会话。

img/313992_2_En_2_Fig31_HTML.jpg

图 2-31

配置协调器

配置路由器

要配置路由器,您需要将此无线电(路由器)的目的地址设置为另一个无线电(协调器)的序列号。与协调器一样,您将 PAN ID 设置为 8088。还要将节点 ID 设置为 ROUTER,以便于识别。图 2-32 显示了路由器的配置会话。

img/313992_2_En_2_Fig32_HTML.jpg

图 2-32

配置路由器

让聊天开始

就这样:您已经准备好开始聊天会话了。如果您一直在使用终端进行更改,那么您现在需要做的就是通过使用ATCN命令或者简单地等待 10 秒钟,将模块返回到透明模式。

一旦你为模块设置了目的地地址,它们就会“找到”彼此并开始聊天。要查看聊天的运行情况,只需单击其中一个模块并打开一个终端连接。您应该点击分离按钮,这样您就可以点击另一个模块并为该模块打开一个终端。

接下来,在每个终端点击打开按钮。此时您可能看不到任何事情发生,因为模块正在等待来自另一个模块的数据。我们可以自己点击控制台输出的左侧。继续在那里键入一些东西。

如果您的配置有效,您应该会看到文本从一个终端出现在另一个终端,反之亦然。如果你这样做了,那么恭喜你——你已经建立了你的第一个 XBee 网络(尽管是一个非常简单的点对点网络)。图 2-33 显示了我使用 XCTU 从电脑上运行的测试结果。这个终端功能很好,因为它对消息进行了颜色编码。如果您自己运行它,您应该看到红色文本是文本接收,蓝色是文本发送。

img/313992_2_En_2_Fig33_HTML.jpg

图 2-33

成功聊天

为了加分,拔掉你的 USB 适配器,把它们从一台电脑切换到另一台电脑;然后重启你的终端程序。注意到什么特别的吗?这是一个棘手的问题,因为您应该看到聊天示例像以前一样工作。在这种情况下,谁是协调者并不重要;因为您将值写入 XBee 非易失性存储器,所以即使拔掉电源,模块也会“记住”它们的设置。很好,是吧?

Is Point-to-Point Good Enough?

您可能想知道是否可以在传感器网络中使用点对点网络。简短的回答是,在某些情况下你可以。例如,如果您有少量不太可能脱机的节点,您也许能够形成一个具有点对点网络的网络。在这种情况下,您将形成一个星型拓扑网络。

但是,也有一些限制,例如,如果中间的一个节点发生故障,它会将该节点一侧的所有节点从另一侧孤立出来。您也不能形成多点连接,广播可能需要额外的编程才能完成。由于这些原因以及更多原因,复杂的传感器网络可以从使用网状拓扑中受益。

为了更多乐趣

如果您想使用 chat 示例更多地尝试点对点网络和 AT 固件,请尝试向网络添加第三个 XBee。将其连接到您的路由器节点,并键入一些数据。它出现在哪里:协调器还是路由器?再次尝试,将新模块连接到协调器。文本是否出现在您期望的位置?提示:使新模块也成为路由器,并将其目的地址设置为第一个路由器。

Got Hub?

如果你想测试你的 XBee 网络,但你的电脑没有足够的 USB 端口,端口过于接近, 9 或者你不想使用第二台(或第三台)电脑,USB XBee 探索者将使用一个有电源的集线器,甚至一些更好的无电源集线器,如下所示。

img/313992_2_En_2_Figa_HTML.jpg

然而,我发现有些集线器不起作用。我有一个非常好(也非常贵)的 USB 3 集线器,除了我的 XBee Explorer 板之外,它对任何东西都很好。因此,您的里程可能会有所不同。奇怪的是,我的老式 USB 2.0 集线器工作良好,并有足够的空间来放置 XBee Explorer 板。

构建 XBee-ZB 网状网络

现在,您已经知道了什么是 XBee 模块,如何选择项目中使用的模型,以及如何配置它们以在点对点网络中发送和接收数据,让我们来看看更复杂、更适合传感器网络的一些内容。

在这个项目中,您将配置三个 XBee 模块:一个作为协调器,另一个作为路由器,最后一个作为终端设备。我将为协调者使用一个系列 3 模块,但是像以前一样,它不是必需的,并且该项目将与早期的模块一起工作。

但是,您可以使用形成网状网络所需的 API 固件,而不是 AT 固件。目标不是深入探究 API 固件;相反,它是为了看看如何使用 XBee 模块通过网络传输数据。

回想一下,API 固件被设计为实现完整的 ZigBee 协议,这意味着数据消息被封装在具有报头层的分组中。换句话说,信息是以二进制数据的形式传输的,而不是像您在 AT 固件中看到的文本。

API 固件和 ZigBee 协议有很多内容。幸运的是,您不必为了使用它而深入了解细节。但是,知道数据包是如何形成的确实有助于您诊断和调试数据消息。在您阅读本书的过程中,我将介绍一些经常遇到的包。

如果您想了解有关 ZigBee 协议及其多种数据包格式(称为帧类型)的更多信息,请参阅以下资源之一:

*### 加载模块的固件

您需要做的第一件事是使用 XCTU 应用程序加载每个模块的固件。在本例中,我们将为协调器使用一个系列 3 模块,为路由器和终端设备使用两个系列 2 模块。

由于我们使用的是 series 3 模块,回想一下,我们只需将 API 模式(AP)设置为 1 即可打开 API 模式。对于早期的模块,我们必须加载正确的固件。更具体地说,我们在一个模块上加载终端设备 API 函数集,在另一个模块上加载路由器 API 函数集,如果我们没有 series 3 模块,我们将需要在第三个模块上加载协调器 API 函数集。

配置 XBee 模块

建议在开始新项目时,将 XBee 模块重置为出厂默认值。您可以在配置页面上使用 XCTU 应用程序来完成此操作。点击默认按钮设置出厂默认,然后点击确认。这就是你需要做的!您也可以在 AT 命令模式下从终端使用ATRE命令。

但是那些地址和身份证什么的呢?简单来说,你不需要他们。模块将自动连接到协调器(或路由器),协调器将为每个模块分配 16 位地址。显然,这比点对点模式更容易配置。

虽然这是真的,但使用 API 固件进行实验也更加困难。回想一下,API 固件以二进制形式发送和接收数据消息。为了观察这个网络的运行,您需要形成称为传输请求包的特殊包。

形成测试消息

测试消息是一个简单的数值,嵌入在一个称为传输请求包的包中。(在某些文档中,它被称为传输请求帧。)数据包需要非常特殊的格式。

如果您曾经使用过以太网中遇到的低级数据包(TCP 数据包)或 MySQL 客户端协议等其他通信协议,那么您应该已经熟悉了这些基本概念。然而,如果你没有,数据的布局可能看起来很奇怪。表 2-6 显示了示例传输请求包的布局。后面会有更详细的描述。

表 2-6

传输请求包

|

|

抵消

|

例子

|

描述

|
| --- | --- | --- | --- |
| 定界符 | Zero | 7E | 数据包开始定界符 |
| 长度 | one | 00 10 | 长度和校验和之间的字节数 |
| 框架类型 | three | 10 | 请求传输 |
| 框架 ID | four | 01 | UART 数据帧 |
| 64 位目标地址 | five | 00 00 00 00 00 00 00 00 FF FF | 协调器的 64 位地址 |
| 目的地址 16 位 | Thirteen | 00 00 | 16 位目的地址 |
| 广播半径 | Fifteen | 00 | 最大跳数 |
| 选择 | Sixteen | 00 | 选择 |
| 射频数据 | Seventeen | 99 99 | 数据有效载荷 |
| 校验和 | Nineteen | BC | 0xff 减去包中字节的总和 |

该数据包的重要部分是长度、地址和数据有效载荷。这些是你最想改变的部分。在这种情况下,64 位地址是协调器的默认地址。协调器使用默认设置获取该地址。16 位地址也是如此。该示例生成十六进制的有效负载 99 99(十进制的 39321)。因为您对此仅使用了 2 个字节,所以长度(长度部分之后的数据包所有部分的字节长度,直到但不包括校验和部分)为 16 (1+1+8+2+1+1+2)。校验和的计算方法是 0xff 减去长度和校验和之间的字节数的 8 位和。

听起来很复杂?可以的。幸运的是,您不需要经常手动这样做。事实上,当您使用 XBee 发送数据时,您在与 XBee 本身通信时使用的编程库会为您构建这些包。您用来从 Arduino 和 Raspberry Pi 与 XBee 对话的库也将使您的生活变得更加轻松。

那么,你为这个项目做什么?你自己创建包吗?如果你担心计算字节和计算校验和,不用担心。Digi 已经用一个叫做帧生成器工具的漂亮的包创建对话框构建了 XCTU 应用程序。你也可以用它来创建任何你想要的 ZigBee 包。

当您打开终端并连接到要发送数据包的模块时,您可以使用此功能。只需点击界面发送帧部分的添加新帧按钮。图 2-34 显示了一个终端会话的例子。

img/313992_2_En_2_Fig34_HTML.jpg

图 2-34

添加新的帧数据包

点击添加帧按钮后,您将看到一个对话框,允许您使用帧生成器工具编辑现有数据包或添加新数据包。如图 2-35 所示,要添加新的数据包,点击使用‘帧生成器’工具创建帧按钮。

img/313992_2_En_2_Fig35_HTML.jpg

图 2-35

打开框架生成器工具

接下来,您将看到一个允许您填充数据包的对话框,但是首先您必须选择数据包类型。对于这个项目,我们希望选择发送请求包(0x10)。该对话框将自动刷新,显示该数据包类型的正确字段。图 2-36 显示了发送请求包的对话框。回想一下,该数据包用于向另一个模块发送数据。

img/313992_2_En_2_Fig36_HTML.jpg

图 2-36

创建发送请求包

注意这里我用00 00 00 00 00 00 FF FF设置了目的地址。这是广播地址,它将数据包发送到所有模块。如果您想将数据包发送到一个特定的模块,您需要填写该模块的 64 位 MAC 地址。因为我们使用的是广播,所以我们会看到网络中的所有模块都收到了这个数据包。

请注意,我还在 RF 数据框中添加了一条消息。我只是简单地输入了“你好”这个词。因此,这是一个你好,世界!打字练习。

花点时间打开一个终端,自己创建这个框架。新 XCTU 接口的一个非常好的地方是它会保存你创建的数据包帧,这样你就可以一次创建并测试几个。现在,让我们看看如何设置网络。

测试网络

现在是时候启动 XBee 模块了。从协调器开始,然后是第一台路由器,最后是终端设备。然后,您可以将终端应用程序连接到所有模块。

Wait, What About the Baud Rate?

如果您一直在阅读,并且想知道为什么我没有演示如何设置您的模块的串行选项(比如波特率),以及您是否需要设置它们,答案就在 XCTU 应用程序如何工作。它允许你以任何你想要的参数使用模块。另一方面,如果您想使用不同的终端应用程序,您可能需要更改参数。幸运的是,这在 XCTU 应用程序中很容易做到。只需进入每个模块的配置,查找如下所示的串行参数。

img/313992_2_En_2_Figb_HTML.jpg

更改所需的设置,然后将更改写入模块。但是同样,您不需要这样做,也不需要让所有的模块使用 XCTU 应用程序的相同连接参数。酷吧。

对于这个例子,我使用了一台带有 XCTU 的计算机,使用终端模式连接到所有三个模块。将所有模块连接到终端应用程序后,应该给模块五到十分钟的时间进行自我配置,然后再继续。

那么,你怎么知道一切都正常呢?嗯,其实挺简单的。XCTU 有一个你可以使用的网络模式。通过阅读所有正在创建网络图的模块,您将看到网络的布局。网络按钮位于 XCTU 窗口的右上角。接下来,点击扫描按钮。一旦你点击那个按钮,可能需要一点时间来发现网络,但是它应该向你显示一个类似于图 2-37 的布局。

img/313992_2_En_2_Fig37_HTML.jpg

图 2-37

XCTU 的网络模式

请注意,该图显示了每个模块的 64 位地址(MAC 地址),以及显示为实线的通信路径。很好。

好了,你已经等了,一切都应该正常了。我们来看看。在任一模块连接上,点击添加帧按钮,创建一个传输请求包,其广播地址如图 2-36 所示,包括“Hello”的有效载荷。务必点击添加帧按钮保存数据包。

要发送数据包,在左侧选择它并点击发送所选帧按钮。如果您正在观察其他模块中的终端,您可能会看到一些数据包经过。这是因为默认情况下会显示所有数据包,但是您可以使用过滤器来减少混乱。我把它作为一个练习——只需点击过滤器按钮,并选择您想要查看(或不查看)的数据包类型。所有模块收到数据包后,您可以单击数据包并查看数据。图 2-38 显示了输入命令的结果。

img/313992_2_En_2_Fig38_HTML.jpg

图 2-38

所有模块接收到的广播数据包

请注意每个模块上数据包周围的方框。在这里,我们看到数据包是从协调器发出的,并在其他模块上被接收。但是,数据有效载荷在哪里呢?它就在那里,就嵌在信息里。在这种情况下,它是十六进制形式,十六进制的 Hello 是48 65 6C 6C 6F。看看你能否在图像中找到它。

广播消息可以方便地向所有传感器节点发送断电或睡眠模式命令,或者向所有数据聚合节点发送命令,以将其数据保存到正在使用的介质中。

接下来,让我们创建一个新的传输请求消息,并为其他模块之一指定 64 位地址。为数据添加您想要的任何内容,然后发送帧。例如,我将目的地址设置为我的示例网络中的路由器(0013A200408CCD0F)。我还在射频数据字段中输入了一个随机数(90125 11 )。图 2-39 显示了我创建并添加的框架。

img/313992_2_En_2_Fig39_HTML.jpg

图 2-39

传输带有目的地的请求帧

当我发送数据包时,它显示在路由器上,但没有显示在其它模块上。这是因为这是发送到特定模块的消息;其他人忽略了这条信息。图 2-40 显示了“发送”模块在上方而“接收”模块在下方的结果。

img/313992_2_En_2_Fig40_HTML.jpg

图 2-40

发送和接收的帧

请注意帧日志左侧的小箭头。指向右边的箭头是发送事件,指向左边的箭头是接收事件。不错吧。

Tip

您只看到了 XCTU 应用程序的一小部分功能。您应该花些时间阅读手册,了解更多关于它的功能。例如,精明的用户可能想了解 XBee 模块的无线功能。

至此,您已经看到了如何将消息从一个模块直接发送到协调器,以及如何将广播消息发送到所有模块。如果您能够在自己的 XBee 模块上复制或执行类似的操作,那么恭喜您!你现在有一个非常简单的无线网状网络。

虽然该项目不包含任何传感器节点,但如果您将终端设备视为传感器节点,将键盘视为发送数据的传感器,您就可以看到典型传感器网络的运行情况。在这种情况下,默认情况下,终端设备将其数据包发送到协调器,如果需要,可以广播数据。将所有数据发送到协调器也是关于如何用 XBee 模块配置数据聚集节点的线索。在接下来的章节中,你将建立在这个前提之上。

为了更多乐趣

如果您想更多地练习创建测试包,请尝试从路由器发送网络节点检测命令,看看您会得到什么。提示:您需要一个ATND命令。

部件购物清单

你需要一些组件来完成本章中的项目。表 2-7 列出了它们。

表 2-7

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| XBee-ZB (ZB)系列 2、2.5 或 3 | www.sparkfun.com | $25.00–48.00 | three |
|   | www.adafruit.com |   |   |
|   | www.makershed.com |   |   |
| XBee 浏览器加密狗 | www.sparkfun.com/products/11697 | $24.95 | 1 |
| xbee 浏览器 | www.sparkfun.com/products/11812 | $29.95 | 1 |
| USB 转迷你 USB 电缆,用于 XBee Explorer USB | www.sparkfun.com/products/11301 | $3.95 | 1□□□□□□□□□□□□□□□□□□ |
| usb 适配器 | www.adafruit.com/product/247 | $29.95 | 1 |

你只需要三个 USB 适配器。

每个 XBee USB 适配器需要一个-加密狗不需要。

确保获得正确的电缆。还记得你需要三个匹配的 XBee-ZB 模块。适配器板不必相同,但您应该有三个。

Cable Trouble Solution

如果你像我一样,有许多 USB 项目,你可能会遇到的第一个挫折是 USB 连接器选择的随机性。好像每次我买一个新的组件都需要一根不同的 USB 线!我没有随身携带一套电缆,而是从 SparkFun 找到了一个解决方案,让我的生活变得更加轻松。USB Cerberus 电缆( www.sparkfun.com/products/12016 )的一端包括一个标准的 USB A 型插头连接器,另一端包括一组三个通用连接器(B、mini-B 和 micro-B)。我建议为您的每个电子设备套件购买一个。

另一个令人沮丧的问题是用 USB 集线器之类的东西给设备供电。再说一次,我过去常常带着一堆电源线来给我所有的组件供电。在这种情况下,SparkFun 凭借其 Hydra 电源线( sparkfun.com/products/11579 )再次出手相救。该电缆的一端有一个标准的 USB A 型连接器,另一端有一组三个连接器(用于 Arduino、JST 和鳄鱼夹的筒形插头)。非常酷。

故障排除提示和常见问题

如果你在运行这两个项目时遇到问题,不要难过,也不要放弃!尽管它们体积小巧,功能强大,但如果配置不正确,这些小害虫会给你带来很多麻烦。本节探讨了解决一些更常见问题的一些最佳实践。

要检查的内容

以下是帮助您确定问题所在以及如何解决问题的提示列表:

  • 布线:这听起来可能有点傻,但是请检查以确保您的所有模块都正确供电——无论是通过主机微控制器还是 USB 适配器(或 USB 集线器!).您会惊讶地发现,告诉您的操作系统弹出 USB 加密狗是多么容易。如果发生这种情况,很可能您的适配器的所有电源指示灯都亮起,但终端无法连接。尝试拔下电缆,然后重新插入。还要检查串行端口,因为如果从一个端口移动到另一个端口,一些操作系统可能会重新分配串行端口。

  • 插上电源了吗?您还应该检查模块是否以正确的方向插入其插座,并且没有针脚被跳过(错位)。

  • 串行设置:如果您使用的终端与 XCTU 中的不同,请检查您的波特率。如果您在 XBee 上更改了它,您的终端应用程序可能没有存储该设置。如果您想要更改设置,请确保为所有 XBee 模块以及终端应用程序设置相同的设置。

  • AT 地址:如果您正在使用 AT 固件构建点对点系统,请务必检查您的地址!请记住,目的地址需要指向您希望发送数据的模块的地址(ATDH / ATDL)。确保使用ATWR命令保存数值。

  • 版本:最好确保你使用的固件版本在所有模块上都是一样的。有些版本与其他版本不兼容。最好总是使用相同的版本。

  • XBee 死了吗?如果你的 XBee 模块不能被你的终端应用程序读取,或者它停止响应 XCTU,你可能遇到了一些人所说的阻塞,这使得该模块除了作为门挡或砖块之外毫无价值。如果发生这种情况,请尝试重置模块。如果您的适配器没有重置按钮(只有少数有),您可以连接适配器,然后轻轻地(非常轻轻地)移除模块并重新插入。当模块开始响应时,重新加载固件。极端情况见 www.instructables.com/id/Restoring-your-broken-XBee/

  • 旧值持续恢复:如果您在 AT 固件中更改了您的设置,但即使在您使用ATWR后旧值仍持续恢复,请使用重置命令(ATRE)将所有值恢复至出厂默认值。

常见问题

以下是您可能会遇到的一些更常见的情况以及如何应对:

  • AT 命令不起作用:如果+++命令不能唤醒模块,确保模块加载了 AT 固件。我忙乱了将近 15 分钟,我以为这是一个死模块,才发现它加载了 API 固件。不要被那个自我诱导的恶作剧所欺骗!

  • AT 模式的奇怪错误:确保你的模块配置了相同版本的 AT 固件。您可以使用ATVR命令来检查每个模块。

  • 设置丢失或恢复:最常见的错误之一是做了所有的设置却没有用ATWR写值。您需要使用此命令来保存这些值。在您完成此操作并返回透明模式之前,XBee 模块可能无法工作。

  • 无法使用退格键:尝试使用 AT 固件输入带有值的命令可能会非常令人沮丧,因为退格键在大多数终端应用程序中不起作用。出现错误时,请按 Enter 键,然后再次尝试该命令。始终使用命令的显示选项(没有值的命令)检查设置。

  • API 固件不工作:如果您确定您的所有模块都配置了相同版本的 API 固件,请尝试拔下所有模块,首先插入协调器,然后插入路由器,最后插入终端设备。协调器可能需要长达 10 分钟的时间将所有节点加入网络。

你也可以访问罗伯特·法卢迪的网页了解 XBee 的常见错误( www.faludi.com/projects/common-xbee-mistakes/ )。他列举了很多你对 XBees 不熟悉时会出错的地方,以及如何配置。正如他所说,它们并不像看起来那样不可靠或古怪。大多数怪癖是用户错误的结果。可悲的是,事实并非总是如此。

最后,使用 Digi 网站及其知识库( www.digi.com/support/kbase/ )。外面有大量的信息。可能有人遇到过类似的问题,简单搜索知识库和论坛就能找到解决方案。

摘要

哇,这一章我们讲了很多内容。向您介绍了 XBee-ZB 模块和 ZigBee 协议,并尝试了 AT 和 API 固件。您还了解了大量关于 XBee 的知识,它的许多特性包括 series 3 模块的一些 MicroPython 特性。虽然看起来我已经讨论了很多,但事实是您才刚刚开始了解 XBee 以及如何在您的传感器网络中使用它。

在本书的剩余部分,我将回到 XBee 主题。第四章探讨如何用 XBee 模块托管传感器,第五章探讨如何用 Raspberry Pi 托管传感器,第六章探讨如何用 Arduino 微控制器托管传感器。如果你喜欢本章的项目,下一章的项目可能会更有趣,因为你可以看到一个真正的传感器在工作。

但是首先,让我们了解更多关于编程 MicroPython 的知识。下一章将介绍 MicroPython 以及一个简短的教程。让我们开始编码吧!

*

三、MicroPython 编程

现在我们已经对 XBee 模块有了基本的了解,我们可以学习更多关于 MicroPython 编程的知识,这是一种非常健壮和强大的语言,可以用来编写非常强大的应用程序。掌握 MicroPython 非常容易,有些人可能认为使用它不需要任何正式的培训。这在很大程度上是正确的,因此您应该只需要一点点语言知识就能够编写 MicroPython 脚本。

鉴于 MicroPython 是 Python,我们可以先通过我们 PC 上的例子来学习 Python 语言的基础。因此,本章介绍了 Python 编程基础的速成课程,包括对一些最常用语言特性的解释。因此,本章将为您提供理解互联网上可用的 Python 物联网项目示例所需的技能。本章还通过可以在 PC 上运行的例子演示了如何用 Python 编程。所以,让我们开始吧!

Note

在本章中,我使用术语 Python 来描述同时适用于 MicroPython 和 Python 的编程概念。MicroPython 特有的概念使用术语 MicroPython。

现在让我们学习一些 Python 编程的基本概念。我们将从语言的构件开始,如变量、模块和基本语句,然后进入更复杂的流控制和数据结构的概念。虽然这些材料看起来很仓促,但是本 Python 教程只涵盖了该语言的最基础知识,以及如何在 PC 和 XBee 模块上使用它。它旨在让您开始为 XBee 编写 MicroPython 代码。

如果你知道 Python 编程的基础,请随意浏览这一章。但是,我建议您完成本章末尾的示例项目,尤其是如果您没有编写过很多 Python 应用程序的话。

下面几节介绍了 Python 编程的许多基本特性,您需要了解这些特性才能理解本书中的示例项目。

Note

XBee(以及类似的)模块和主板上的 MicroPython 不支持 PC 版 Python 上可用的完整函数库。但是,您需要在 XBee 模块上使用的所有工具都可以在 MicroPython 中找到。无论如何,在 PC 上学习 Python 是获得编程 XBee 模块所需技能的一个很好的方法。

在开始 Python 编程之前,让我们讨论一下 MicroPython 有哪些可用的特性和库,以及常见的限制。

MicroPython 的特性和局限性

虽然 MicroPython 看起来和感觉上完全像 Python,但是有一些东西 MicroPython 在 Python 3 语言中没有实现。下面几节将向您介绍使用 MicroPython 能做什么,不能做什么。

MicroPython 功能

MicroPython 最大的特点当然是运行 Python。这允许你创建简单的、有效的、易于理解的程序。我认为,这是它相对于 Arduino 等其他主板的最大优势。下面的列表是 MicroPython 支持的一些特性。我们将在本书中更详细地了解这些特性:

  • 互动解释器(Interactive interpreter): MicroPython 主板内置了一个特殊的互动控制台,你可以通过 USB 电缆(或者在某些情况下,通过 Wi-Fi)连接到主板来访问它。这个控制台被称为读取-评估-打印循环,它允许您键入代码并一次执行一行。这是一个很好的方法来原型化你的代码或者只是在你开发的时候运行一个项目。

  • Python 标准库 : MicroPython 也支持许多标准 Python 库。总的来说,你会发现 MicroPython 支持 80%以上最常用的库。其中包括解析 JavaScript 对象符号(JSON)、 1 套接字编程、字符串操作、文件输入/输出,甚至正则表达式支持。

  • 硬件级库 : MicroPython 内置了一些库,允许您直接访问硬件来打开或关闭模拟引脚、读取模拟数据、读取数字数据,甚至使用脉宽调制(PWM)来控制硬件,这是一种通过快速调制设备功率来限制设备功率的方法,例如,使风扇转速比全功率时慢。

  • 可扩展 : MicroPython 也是可扩展的。对于需要在底层(用 C 或 C++)实现一些复杂的库并在 MicroPython 中包含新库的高级用户来说,这是一个很好的特性。是的,这意味着您可以构建自己独特的代码,并使其成为 MicroPython 特性集的一部分。

回答你的问题“但是,我能用 MicroPython 做什么?”,答案还挺多的!您可以控制连接到 XBee 模块的硬件,向其他节点发送数据,等等。您可以连接的硬件包括打开和关闭 led,控制伺服系统和读取传感器。您可以创建任何形式的传感器节点,就像使用 Raspberry Pi 或 Arduino(或其他 MicroPython 板)一样。

但是如果你还没有任何 MicroPython 板,你可以在 https://micropython.org/unicorn/ 查看在线 MicroPython 解释器。

然而,在芯片上运行 MicroPython 有一些限制。

MicroPython 限制

MicroPython 最大的限制是易用性。Python 的易用性意味着代码是动态解释的。尽管 MicroPython 得到了高度优化,但对解释器来说还是有损失的。这意味着需要高精度的项目,如高速数据采样或通过连接(USB、硬件接口等)进行通信。)可能无法以 PC 速度运行。对于这些领域,我们可以通过用处理低级通信的优化库来扩展 MicroPython 语言来克服这个问题。

MicroPython 使用的内存也比 Arduino 等其他微控制器平台多一点。通常,这不是一个问题,但是如果你的程序开始变大,你应该考虑一下。使用大量库的大型程序消耗的内存可能会超出您的预期。这又一次与 Python 的易用性有关——这是要付出的另一个代价。

最后,如前所述,MicroPython 没有实现所有 Python 3 库的所有特性。但是,您应该会发现它拥有构建物联网项目所需的一切(甚至更多)。

现在,让我们学习用 Python 编程吧!

基本概念

Python 是一种高级的、解释性的、面向对象的脚本语言。Python 的最大目标之一是拥有一个清晰、易于理解的语法,读起来尽可能接近英语。也就是说,即使你没有学过 Python 语言,你也应该能够阅读和理解 Python 脚本。Python 也比其他语言有更少的标点符号(特殊符号)和更少的语法机制。下面列出了 Python 的一些关键特性:

  • 解释器在运行时处理 Python。不使用外部(单独的)编译器。你只需通过 Python 解释器“运行”你的代码。

  • Python 通过类的方式支持面向对象的编程结构。

  • 对于初级程序员来说,Python 是一种很好的语言,并且支持各种应用程序的开发。

  • Python 是一种脚本语言,但可用于广泛的应用。

  • Python 非常受欢迎,在全世界范围内使用,这给了它一个巨大的支持基础。

  • Python 关键字少,结构简单,语法定义清晰。这使得学生能够很快学会这门语言。

  • Python 代码定义更清晰,肉眼可见。

Python 可以在你可能遇到或使用的几乎所有平台上下载(python.org/downloads)——甚至是 Windows!Python 是一种非常容易学习的语言,它的结构非常少,甚至有点难学。与其抛出一个示例应用程序,不如让我们以类似 Python 的方式来学习 Python 的基础知识:一步一步来。

Tip

如果你还没有在你的电脑上安装 Python,你应该现在就安装,这样你就可以运行本章中的例子。

代码块

你应该学习的第一件事是 Python 不像其他语言那样使用用符号划分的代码块。更具体地说,对于诸如函数、条件或循环之类的构造来说,局部的代码是使用缩进来指定的。因此,清单 3-1 中的行是缩进的(通过空格或制表符),这样开始的字符就可以与结构的代码体对齐。清单 3-1 展示了这个概念的实际应用。

Caution

如果缩进不一致,Python 解释器会抱怨并产生奇怪的结果。

if (expr1):
    print("inside expr1")
    print("still inside expr1")
else:
    print("inside else")
    print("still inside else")
print("in outer level")

Listing 3-1Using Code Blocks

这里我们看到一个条件或 if 语句。注意函数调用print()是缩进的。这向解释器发出信号,表明这些行属于它上面的结构。例如,提到expr1的两个 print 语句构成了 if 条件的代码块(当表达式的值为 true 时执行)。类似地,接下来的两个 print 语句构成了 else 条件的代码块。最后,非缩进的行不是条件行的一部分,因此在 if 或 else 之后执行,这取决于表达式的计算。

如您所见,缩进是编写 Python 时需要学习的一个关键概念。尽管这很简单,但是在缩进中出错可能会导致代码意外执行,或者解释器出现更糟糕的错误。

Tip

在讨论 Python 时,我将“程序”和“应用程序”与“脚本”互换使用。虽然从技术上讲 Python 代码是一个脚本,但我们经常在“程序”或“应用程序”更合适的上下文中使用它。

有一个你会经常遇到的特殊符号。注意前面代码中冒号(:)的使用。这个符号用来终止一个构造,并向解释器发出信号,说明声明已经完成,代码块的主体跟在后面。我们把它用于条件、循环、类和函数。

评论

任何编程语言中最基本的概念之一是用不可执行的文本注释源代码的能力,这不仅允许您在代码行之间做笔记,还形成了一种记录源代码的方法。

要向源代码添加注释,请使用井号(#)。在行首至少放置一个,为该行创建一个注释,为后面的每一行重复#符号。这将创建所谓的块注释,如图所示。注意,在清单 3-2 中,我使用了不带任何文本的注释来创建空白。这有助于提高可读性,并且是块注释的常见做法。

#
# Beginning Sensor Networks, 2nd Edition
#
# Example Python application.
#
# Created by Dr. Charles Bell
#

Listing 3-2Adding Comments to Source Code

您也可以将注释放在源代码所在的同一行。编译器将忽略从井号到行尾的所有内容。例如,以下代码显示了记录变量的常见样式:

zip = 35012# Zip or postal code
address1= "123 Main St."  # Store the street address

算术

您可以在 Python 中执行许多数学运算,包括常见的原语以及逻辑运算和用于比较值的运算。与其详细讨论这些,我在表 3-1 中提供了一个快速参考,显示了操作和如何使用操作的例子。

表 3-1

Python 中的算术、逻辑和比较运算符

|

类型

|

操作员

|

描述

|

例子

|
| --- | --- | --- | --- |
| 算术 | + | 添加 | int_var + 1 |
|   | | 减法 | int_var – 1 |
|   | * | 增加 | int_var * 2 |
|   | / | 分开 | int_var / 3 |
|   | % | 系数 | int_var % 4 |
|   | | 一元减法 | –int_var |
|   | + | 一元加法 | +int_var |
| 逻辑学的 | & | 按位 and | var1&var2 |
|   | &#124; | 按位或 | var1&#124;var2 |
|   | ^ | 按位异或 | var1^var2 |
|   | ~ | 逐位补码 | ~var1 |
|   | and | 逻辑与 | var1 and var2 |
|   | or | 逻辑或 | var1 or var2 |
| 比较 | == | 平等的 | expr1==expr2 |
|   | != | 不相等 | expr1!=expr2 |
|   | < | 不到 | expr1<expr2 |
|   | > | 大于 | expr1>expr2 |
|   | <= | 小于或等于 | expr1<=expr2 |
|   | >= | 大于或等于 | expr1>=expr2 |

按位运算产生对每个位执行的值的结果。逻辑运算符(and、or)产生一个值,该值可为真或为假,通常与表达式或条件一起使用。

Tip

真与假在 Python 中分别表示为TrueFalse(初始大写)。

输出到屏幕

我们已经看到了一些如何将消息打印到屏幕上的例子,但是没有对所显示的语句进行任何解释。虽然不太可能为您部署的项目打印 XBee 模块的输出,但是当您在屏幕上显示消息时,学习 Python 会容易得多。

你可能想要打印的一些东西——正如我们在清单 3-1 中看到的——是为了传达你的程序内部正在发生的事情。这可以包括简单的消息(字符串),但也可以包括变量、表达式等的值。

正如我们所见,内置的print()函数是显示包含在单引号或双引号中的输出文本的最常见方式。我们还看到了一些使用另一个名为format()的函数的有趣例子。format()函数为每个传递的参数生成一个字符串。这些参数可以是其他字符串、表达式、变量等等。该函数与一个特殊字符串一起使用,该字符串包含由花括号{ }分隔的替换键(称为字符串插值 2 )。每个替换键包含一个索引(从 0 开始)或一个命名关键字。这个特殊字符串称为格式字符串。让我们看几个例子来说明这个概念。你可以在你的电脑上自己运行这些。我包括了输出,这样您可以看到每个语句做了什么。

注意清单 3-3 中的>>>符号。这表明我正在使用 Python 解释器执行代码。您可以从任何命令窗口(终端)启动 Python 解释器,方法是键入命令pythonpython3来运行 3。Python 的 x 版本。

>>> a = 42
>>> b = 1.5
>>> c = "seventy"
>>> print("{0} {1} {2} {3}".format(a,b,c,(2+3)))
42 1.5 seventy 5
>>> print("{a_var} {b_var} {c_var} {0}".format((3*3),c_var=c,b_var=b,a_var=a))
42 1.5 seventy 9

Listing 3-3Python Interpreter Example

Note

对于那些已经学会用另一种语言如 C 或 C++编程的人来说,Python 允许你用分号(;)终止一个语句;然而,包含它是不必要的,并且被认为是不好的形式。

注意,我创建了三个变量(我们将在下一节讨论变量),用等号(=)给它们分配不同的值。然后,我使用带有四个替换键的格式字符串打印了一条消息,这四个替换键使用索引进行标记。请注意打印语句的输出。请注意,我在结尾处包含了一个表达式,以展示format()函数如何计算表达式。

最后一行更有趣。这里,我使用了三个命名参数(a_varb_varc_var),并在format()函数中使用了一个特殊的参数选项,在这里我给参数赋值。请注意,我以不同的顺序列出了它们。这是使用命名参数的最大优点;它们可以以任何顺序出现,但被放在格式字符串中指定的位置。

如您所见,这只是用来自format()函数的键替换{ }键的一个例子,该函数将参数转换为字符串。我们在任何需要包含从多个区域收集的数据的字符串的地方使用这种技术。我们可以在前面的例子中看到这一点。

Tip

有关格式字符串和可用选项的更多信息,请参见 https://docs.python.org/3/library/string.html#formatstrings

现在让我们看看如何在我们的程序(脚本)中使用变量。

变量和数据类型

现在我们已经看到了简单 Python 代码的基本构造,让我们先来探索一下您需要掌握的基本概念:变量和数据类型。在本节中,我们将了解如何创建变量来存储数据,包括它们的类型(它们可以存储什么类型的数据)以及使用变量的简单语句。我们将在下一节学习更多关于复杂数据类型的知识。

变量

Python 是一种动态类型语言,这意味着变量的类型(它可以存储的数据类型)是由遇到或使用的上下文决定的。这与 C 和 C++等其他语言形成对比,在这些语言中,必须在使用变量之前声明类型。

Python 中的变量只是命名的内存位置,可以用来在执行过程中存储值。我们通过使用等号赋值来存储值。Python 变量名可以是您想要的任何名称,但是大多数 Python 开发人员都遵循一些规则和约定。Python 编码标准中列出了这些规则。 3

然而,一般的、首要的规则要求变量名是描述性的、在上下文中有意义的并且容易阅读。也就是说,您应该避免使用带有随机字符、强制缩写、首字母缩略词以及类似的晦涩难懂的名称。按照惯例,变量名应该长于一个字符(除了一些可接受的循环计数变量),并且足够短以避免过长的代码行。

What is a Long Code Line?

大多数人会说一个代码行不应该超过 80 个字符,但这源于编程的黑暗时代,那时我们使用穿孔卡,每张卡最多允许 80 个字符(或更少),后来的显示设备也有同样的限制。对于现代的宽屏显示器来说,这没什么大不了的,但我仍然建议保持短行以确保更好的可读性。没有人喜欢向下滚动阅读!或者,更糟的是,需要打开自动换行或使用 34 英寸宽屏显示器来阅读代码。

因此,给变量命名有很大的灵活性。在 PEP8 标准中有额外的规则和指南,如果您希望使您的项目源代码与标准保持一致,您应该查看 PEP8 函数、类等的命名标准。有关规则和标准的完整列表,请参见 www.python.org/dev/peps/pep-0008 的 pep 8 Python 编码指南。

清单 3-4 展示了一些简单变量及其动态确定类型的例子。

# floating point number
length = 10.0
# integer
width = 4
# string
box_label = "Tools"
# list
car_makers = ['Ford', 'Chevrolet', 'Dodge']
# tuple
porsche_cars = ('911', 'Cayman', 'Boxster')
# dictionary
address = {"name": "Joe Smith", "Street": "123 Main", "City": "Anytown", "State": "New Happyville"}

Listing 3-4Simple Variable Examples

那么,我们怎么知道可变宽度是一个整数呢?仅仅因为数字 4 是一个整数。同样,Python 将把“Tools”解释为一个字符串。我们将在下一节看到更多关于最后三种类型和 Python 支持的其他类型的内容。

Tip

有关 Python 编码标准(PEP8)管理的命名约定的更多信息,请参见 www.python.org/dev/peps/pep-0008/#naming-conventions

类型

如前所述,Python 不像其他语言那样有正式的类型规范机制。但是,您仍然可以定义变量来存储您想要的任何内容。事实上,Python 允许您基于上下文创建和使用变量,并且您可以使用初始化来“设置”变量的数据类型。清单 3-5 展示了几个例子。

# Numbers
float_value = 9.75
integer_value = 5

# Strings
my_string = "He says, he's already got one."

print("Floating number: {0}".format(float_value))
print("Integer number: {0}".format(integer_value))
print(my_string)

Listing 3-5Setting the Variable Data Type

对于需要转换类型或希望确保值以某种方式键入的情况,有许多用于转换数据的函数。表 3-2 显示了一些更常用的类型转换函数。我将在后面的章节中讨论一些数据结构。

表 3-2

Python 中的类型转换

|

功能

|

描述

|
| --- | --- |
| int(x [,base]) | 将 x 转换为整数。基数是可选的(例如,十六进制为 16) |
| long(x [,base]) | 将 x 转换为长整数 |
| float(x) | 将 x 转换为浮点 |
| str(x) | 将对象 x 转换为字符串 |
| tuple(t) | 将 t 转换为元组 |
| list(l) | 将 l 转换为列表 |
| set(s) | 将转换为集合 |
| dict(d) | 创建词典 |
| chr(x) | 将整数转换为字符 |
| hex(x) | 将整数转换为十六进制字符串 |
| oct(x) | 将整数转换为八进制字符串 |

但是,您应该小心使用这些转换函数,以避免数据丢失或舍入。例如,将浮点数转换为整数可能会导致截断。同样,打印浮点数会导致舍入。

现在让我们看看一些常用的数据结构,包括这个叫做字典的奇怪的东西。

基本数据结构

到目前为止,你所学到的关于 Python 的知识足以编写最基本的程序,而且对于处理本章后面的示例项目来说也绰绰有余。然而,当您开始需要对来自用户或来自传感器和类似来源的数据进行操作时,您将需要一种方法来组织和存储数据,以及对内存中的数据执行操作。下面几节按照复杂程度的顺序介绍了三种数据结构:列表、元组或字典。

虽然您不太可能在 XBee MicroPython 脚本中使用这些结构,但是任何 Python 教程如果不包含这些都是不负责任的。这并不意味着您不能在 MicroPython 中使用这些构造——您可以——但是大多数小型项目的 MicroPython 脚本可能不会在同一个脚本中使用所有的数据结构。同样,如果你需要使用它们,那么就这样做吧!

列表

列表是 Python 中组织数据的一种方式。这是一种构建集合的自由形式的方法。也就是说,项目(或元素)不必是相同的数据类型。列表还允许你做一些有趣的操作,比如在末尾、开头或特殊索引处添加内容。清单 3-6 展示了如何创建一个列表。

# List
my_list = ["abacab", 575, "rex, the wonder dog", 24, 5, 6]
my_list.append("end")
my_list.insert(0,"begin")
for item in my_list:
  print("{0}".format(item))

Listing 3-6Creating a List

这里,我们看到我使用方括号([])创建了列表。列表定义中的项目用逗号分隔。注意,您可以简单地通过设置一个等于[]的变量来创建一个空列表。因为列表和其他数据结构一样,都是对象,所以有几种操作可用于列表,例如:

  • append(x):在列表末尾添加 x。

  • extend(l):将所有项目添加到列表末尾。

  • insert(pos,item):在位置pos.插入项目

  • remove(value):删除第一个匹配(==)值的项目。

  • pop([i]):移除并返回位置i或列表末端的项目。

  • index(value):返回第一个匹配项的索引。

  • count(value):统计值的出现次数。

  • sort():对列表进行排序(升序)。

  • reverse():反向排序列表。

列表就像其他语言中的数组一样,对于构建动态数据集合非常有用。

元组

另一方面,元组是一种限制性更强的集合类型。也就是说,它们是由一组特定的数据构建的,不允许像列表一样进行操作。事实上,您不能更改元组中的元素。因此,我们可以对不应该改变的数据使用元组。清单 3-7 展示了一个元组的例子以及如何使用它。

# Tuple
my_tuple = (0,1,2,3,4,5,6,7,8,"nine")
for item in my_tuple:
  print("{0}".format(item))
if 7 in my_tuple:
  print("7 is in the list")

Listing 3-7Using Tuples

这里,我们看到我使用括号()创建了元组。元组定义中的各项用逗号分隔。注意,您可以简单地通过设置一个等于()的变量来创建一个空元组。因为元组像其他数据结构一样是对象,所以有几种操作可用,例如下面包括对诸如包含、定位等序列的操作:

  • x in t:判断t是否包含x

  • x not in t:判断t是否不包含x

  • s + t:连接元组。

  • s[i]:获取元素i

  • len(t):长度t(元素个数)。

  • min(t) : Minimal(最小值)。

  • max(t):最大值(最大值)。

如果你想在内存中存储更多的数据,你可以使用一个叫做字典的特殊结构(对象)。

字典

字典是一种数据结构,允许您存储键、值对,通过键来评估数据。字典是一种非常结构化的数据处理方式,也是我们在收集复杂数据时想要使用的最符合逻辑的形式。清单 3-8 显示了一个字典的例子。

# Dictionary
my_dictionary = {
  'first_name': "Chuck",
  'last_name': "Bell",
  'age': 36,
  'my_ip': (192,168,1,225),
  42: “What is the meaning of life?”,
}
# Access the keys:
print(my_dictionary.keys())
# Access the items (key, value) pairs
print(my_dictionary.items())
# Access the values
print(my_dictionary.values())
# Create a list of dictionaries
my_addresses = [my_dictionary]

Listing 3-8Using Dictionaries

这里发生了很多事情!我们看到一个使用花括号创建字典的基本字典声明。在里面,我们可以创建尽可能多的键、值对,用逗号分隔。使用字符串(我习惯使用单引号,但双引号也可以)或整数定义键,值可以是我们想要的任何数据类型。对于my_ip属性,我们也存储了一个元组

按照字典,我们看到在字典上执行的几个操作,包括打印键、打印所有值和只打印值。清单 3-9 显示了从 Python 解释器执行这个代码片段的输出。

[42, 'first_name', 'last_name', 'age', 'my_ip']
[(42, 'what is the meaning of life?'), ('first_name', 'Chuck'), ('last_name', 'Bell'), ('age', 36), ('my_ip', (192, 168, 1, 225))]
['what is the meaning of life?', 'Chuck', 'Bell', 36, (192, 168, 1, 225)]
'42': what is the meaning of life?
'first_name': Chuck
'last_name': Bell
'age': 36
'my_ip': (192, 168, 1, 225)

Listing 3-9Performing Operations on Dictionaries

正如我们在清单 3-9 的例子中看到的,有几个操作(函数或方法)可用于字典,包括如下。这些操作使得字典成为一个非常强大的编程工具:

  • len(d):在d.中的项目数

  • d[k]:带k.键的d

  • d[k ] = x:给键k赋值x.

  • del d[k]:用k.键删除项目

  • k in d:判断d是否有带k.键的项目

  • d.items():返回d.中(键,值)对的列表(视图)

  • d.keys():返回d.中按键的列表(视图)

  • d.values():返回 d 中值的列表(视图)

最重要的是,对象可以放在其他对象中。例如,您可以像我前面做的那样创建一个字典列表,一个包含列表和元组的字典,以及您需要的任何组合。因此,列表、元组和字典是管理程序数据的强大方法。

在下一节中,我们将学习如何控制程序的流程。

流量控制语句

现在我们对 Python 的基础有了更多的了解,我们可以发现一些完成项目所需的更复杂的代码概念,比如条件语句和循环。

条件语句

我们还看到了一些简单的条件语句:根据一个或多个表达式的计算来改变执行流程的语句。条件语句允许我们根据一个或多个表达式的计算,将程序的执行指向代码段(块)。Python 中的条件语句是if语句。

我们已经在示例代码中看到了if语句。注意,在示例中,我们可以有一个或多个(可选的)else短语,一旦 if 条件的表达式计算为 false,我们就执行这些短语。我们可以链接if / els e 语句来包含多个条件,其中执行的代码取决于几个条件的评估。清单 3-10 显示了 if 语句的一般结构。注意在注释中我是如何解释执行是如何到达每个条件的主体的。

if (expr1):
    # execute only if expr1 is true
elif ((expr2) or (expr3)):
    # execute only if expr1 is false *and* either expr2 or expr3 is true
else:
    # execute if both sets of if conditions evaluate to false

Listing 3-10Conditional Statements

虽然您可以尽可能多地链接语句,但在这里要小心,因为您拥有的elif部分越多,就越难理解、维护和避免表达式中的逻辑错误。

还有另一种形式的条件语句,称为三元运算符。在 Python 中,三元运算符通常被称为条件表达式。这些操作符基于条件的真或假来评估某些东西。在 2.4 版本中,它们成为 Python 的一部分。条件表达式是赋值语句中(通常)使用的 if-then-else 结构的简写符号,如下所示:

variable = value_if_true if condition else value_if_false

这里,我们看到如果条件被评估为真,则使用 if 前面的值,但是如果条件被评估为假,则使用 else 后面的值。清单 3-11 给出了一个简短的例子。

>>> numbers = [1,2,3,4]
>>> for n in numbers:
...   x = 'odd' if n % 2 else 'even'
...   print("{0} is {1}.".format(n, x))
...
1 is odd.
2 is even.
3 is odd.
4 is even.
>>>

Listing 3-11Evaluation of Conditional Statements

条件表达式允许您快速测试条件,而不是使用多行条件语句,这有助于使您的代码更容易阅读(也更短)。

循环用于控制代码块的重复执行。有三种形式的循环,它们的行为略有不同。所有循环都使用条件语句来决定是否重复执行。也就是说,如果条件为真,它们会重复。这两种类型的循环是whilefor。我用一个例子来解释每一个。

while循环的条件位于代码块的“顶部”或开始处。因此,while 循环仅当且仅当条件在第一次通过时评估为 true 时才执行主体。下面的代码阐释了 while 循环的语法。只有当某些表达式的计算结果为 true 时,才需要执行代码,这种形式的循环最适合使用,例如,迭代一个元素数量未知的集合(循环,直到集合中的所有元素都用完):

while (expression):
   # do something here

由于其独特的形式,循环有时被称为计数循环。For 循环允许您定义一个计数变量和一个要迭代的范围或列表。下面的代码说明了for循环的结构。这种形式的循环最适合用于在集合中执行操作。在这种情况下,Python 会在每次循环中自动将集合中的每一项放入变量中,直到没有更多项可用为止。

for variable_name in list:
  # do something here

你也可以做范围循环或计数循环。这使用了一个名为range()的特殊函数,它最多接受三个参数range([start], stop[, step]),其中 start 是起始数字(一个整数),stop 是序列中的最后一个数字,step 是增量。所以,你可以从 1、2、3 等一系列数字开始计数。以下代码显示了一个简单的示例:

for i in range(2,9):
   # do something here

你可能会遇到range()的其他用法。更多信息,请参见 https://docs.python.org/3/library/functions.html 中关于此功能和其他内置功能的文档。

Python 还提供了一种使用一些特殊关键字来控制循环流(例如,持续时间或终止)的机制,如下所示:

  • break:立即退出循环体。

  • continue:跳到循环的下一次迭代。

  • else:循环结束时执行代码(如果用 break 语句停止循环,则不执行)。

这些关键字有一些用途,尤其是 break,但它不是终止和控制循环的首选方法。也就是说,专业人士认为条件表达式或错误处理代码应该表现得足够好,不需要这些选项。

模块化:模块、函数和类

最后一组主题是最高级的,包括模块化(代码组织)。正如我们将看到的,我们可以使用函数对代码进行分组,消除重复,并将功能封装到对象中。同样,您可能不会在 XBee 的 MicroPython 代码中使用这些构造,但是您应该了解这些技术,因为您可能会在某个时候遇到它们。

包括模块

Python 应用程序可以从 Python 环境提供的可重用库构建。它们也可以从您自己创建或从第三方下载的自定义模块或库构建。这些通常作为一组 Python 代码文件分发(例如,文件扩展名为.py的文件)。当我们想使用一个库(函数、类等)时。)包含在一个模块中,我们使用import关键字并列出模块的名称。以下代码显示了一些示例:

import os
import sys

前两行演示了如何导入 Python 提供的基本或公共模块。在这种情况下,我们为ossys模块(操作系统和 Python 系统函数)使用或导入模块。

Tip

习惯上(但不是必需的)按照字母顺序列出您的导入,首先是内置模块,然后是第三方模块,最后是您自己的模块。

功能

Python 允许在代码中使用模块化。虽然它通过类的方式支持面向对象编程(例如,对于 Raspberry Pi 上的大多数 Python GPIO 示例来说,这是一个不太可能遇到的更高级的特性),但在更基本的层面上,您可以使用函数将代码分成更小的块。

函数使用特殊的关键字构造(Python 中很少见)来定义函数。我们简单地使用def,后跟一个函数名和一个用逗号分隔的参数列表,放在括号中。冒号用于终止声明。清单 3-12 显示了一个例子。

def print_dictionary(the_dictionary):
    for key, value in the_dictionary.items():
      print("'{0}': {1}".format(key, value))

# define some data
my_dictionary = {
  'name': "Chuck",
  ‘age’: 37,
}

Listing 3-12Defining Functions

您可能想知道这个奇怪的代码是做什么的。注意,该循环从items()函数的结果中分配了两个值。这是字典object. 4 中提供的一个特殊函数。items()函数返回键、值对,因此是变量的名称。

下一行打印出这些值。对于 Python 3 应用程序来说,使用格式化字符串(其中花括号定义了从 0 开始的参数编号)是很常见的。有关格式化字符串( https://docs.python.org/3/library/string.html#format-string-syntax )的更多信息,请参见 Python 文档。

函数体是缩进的。该函数声明下缩进的所有语句都属于该函数,并在调用该函数时执行。我们可以通过名字调用函数,提供如下参数。注意我是如何使用键名引用字典中的值的。

print_dictionary(my_dictionary)
print(my_dictionary['age'])
print(my_dictionary['name'])

这个例子和前面的代码一起,在执行时,生成清单 3-13 中所示的输出。

$ python3
Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def print_dictionary(the_dictionary):
...     for key, value in the_dictionary.items():
...       print("'{0}': {1}".format(key, value))
...
>>> # define some data
... my_dictionary = {
...     'name': "Chuck",
...     'age': 41,
... }
>>> print_dictionary(my_dictionary)
'name': Chuck
'age': 41
>>> print(my_dictionary['age'])
41
>>> print(my_dictionary['name'])
Chuck

Listing 3-13Output of Function Example

现在让我们看看 Python 中最复杂的概念——面向对象编程。同样,您可能不希望在您的 MicroPython 中使用这些概念——但是您可以——您仍然应该理解基础知识,以确保您的 Python 知识是完整的。

类别和对象

你可能听说过 Python 是一种面向对象的编程语言。但这意味着什么呢?简单地说,Python 是一种编程语言,它提供了描述对象(事物)以及可以用对象做什么(操作)的工具。对象是数据抽象的一种高级形式,其中数据对调用者是隐藏的,只能由对象提供的操作(方法)来操作。

我们在 Python 中使用的语法是class语句,您可以使用它来帮助您的项目模块化。所谓模块化,我们的意思是源代码被安排得更容易开发和维护。通常,我们将类放在单独的模块(代码文件)中,这有助于更好地组织代码。虽然这不是必需的,但我建议使用这种将类放在它自己的源文件中的技术。这使得修改类或修复问题(错误)更加容易。

那么,什么是 Python 类呢?让我们从将构造视为一种组织技术开始。我们可以使用类将数据和方法分组在一起。类名紧跟在关键字 class 之后,后面是一个冒号。像任何其他方法一样声明其他类方法,除了第一个参数必须是 self,它在执行时将方法绑定到类实例。

Function or Method: Which is Correct?

我更喜欢使用语言设计者或开发人员社区已经采用的术语。例如,有些人使用“函数”,但其他人可能使用“方法”还有一些可能使用子程序、例行程序、过程等等。你使用哪个术语并不重要,但是你应该努力使用一致的术语。一个可能让一些人感到困惑的例子是,我在讨论面向对象的例子时使用了术语方法。也就是说,一个类有方法,没有函数。然而,你可以用函数代替方法,你仍然是正确的(大多数情况下)。

通过使用类(创建实例)和使用点标记来引用数据成员或函数,使用一种或多种方法来访问数据。让我们看一个例子。清单 3-14 显示了一个完整的类,它描述(模拟)了用于运输的车辆的最基本特征。我创建了一个名为vehicle.py的文件来包含这段代码。

#
# Beginning Sensor Networks 2nd Edition
#
# Class Example: A generic vehicle
#
# Dr. Charles Bell
#
class Vehicle:
    """Base class for defining vehicles"""
    axles = 0
    doors = 0
    occupants = 0

    def __init__(self, num_axles, num_doors):
        self.axles = num_axles
        self.doors = num_doors

    def get_axles(self):
        return self.axles

    def get_doors(self):
        return self.doors

    def add_occupant(self):
        self.occupants += 1

    def num_occupants(self):
        return self.occupants

Listing 3-14Vehicle Class

注意这里的一些事情。首先,有一个名为__init__()的方法。这是构造函数,在创建类实例时调用。您将所有初始化代码像设置变量一样放在这个方法中。我们也有返回轴、门和居住者数量的方法。我们在这个类中有一个方法:添加居住者。

另外,请注意,我们使用self.<name>来处理每个类属性(数据)。这就是我们如何确保我们总是访问与创建的实例相关联的数据。

让我们看看这个类如何被用来定义一个家庭轿车。清单 3-15 显示了使用这个类的代码。我们可以将这段代码放在一个名为sedan.py的文件中。

#
# Beginning Sensor Networks 2nd Edition
#
# Class Example: Using the generic Vehicle class
#
# Dr. Charles Bell
#
from vehicle import Vehicle

sedan = Vehicle(2, 4)
sedan.add_occupant()
sedan.add_occupant()
sedan.add_occupant()
print("The car has {0} occupants.".format(sedan.num_occupants()))

Listing 3-15Using the Vehicle Class

注意,第一行从 vehicle 模块导入了 Vehicle 类。注意,我大写了类名,而不是文件名。这是一种非常常见的命名方案。接下来,在代码中,我们创建类的一个实例。注意我把 2,4 传递给了类名。这将导致在实例化类时调用__init__()方法。变量 sedan 变成了我们可以操作的类实例变量(object ),我添加了三个乘员,然后使用 Vehicle 类中的方法打印出乘员的数量。

我们可以使用下面的命令在 PC 上运行代码。正如我们所看到的,当代码运行时,它告诉我们车上有三个人。不错。

$ python ./sedan.py
The car has 3 occupants.

面向对象编程(OOP)术语

像任何技术或概念一样,有一定数量的术语,您必须学会这些术语,才能理解技术并与他人交流。下面的列表简要描述了一些您需要了解的术语,以便更好地了解面向对象编程:

  • 属性:类中的一个数据元素。

  • Class :用于以属性(数据)和对数据进行操作的方法(函数)的形式定义对象的代码构造。Python 中的方法和属性可以使用点符号来访问。

  • 类实例变量:用于存储对象实例的变量。它们像其他变量一样使用,与点符号结合,允许我们操作对象。

  • 实例:类的可执行形式,通过将类赋给一个将代码初始化为对象的变量来创建。

  • 继承:将一个类的属性和方法包含到另一个类中。

  • 实例化:创建一个类的实例。

  • 方法重载:创建两个或多个同名但参数不同的方法。这允许我们创建具有相同名称的方法,但是根据所传递的参数,操作可能不同。

  • 多态性:从基类继承属性和方法,添加额外的方法或者覆盖(改变)方法。

还有很多 OOP 术语,但这些是你最常遇到的。

现在,让我们看看如何使用 vehicle 类来演示继承。在这种情况下,我们将创建一个名为PickupTruck的新类,该类使用 vehicle 类,但是向结果类添加了专门化。清单 3-16 显示了新的类。我将这段代码放在一个名为pickup_truck.py的文件中。如你所见,皮卡是一种交通工具。

#
# Beginning Sensor Networks 2nd Edition
#
# Class Example: Inheriting the Vehicle class to form a
# model of a pickup truck with maximum occupants and maximum
# payload.
#
# Dr. Charles Bell
#
from vehicle import Vehicle

class PickupTruck(Vehicle):
    """This is a pickup truck that has:
    axles = 2,
    doors = 2,
    __max occupants = 3
    The maximum payload is set on instantiation.
    """
    occupants = 0
    payload = 0
    max_payload = 0

    def __init__(self, max_weight):
        super().__init__(2,2)
        self.max_payload = max_weight
        self.__max_occupants = 3

    def add_occupant(self):
        if (self.occupants < self.__max_occupants):
            super().add_occupant()
        else:
            print("Sorry, only 3 occupants are permitted in the truck.")

    def add_payload(self, num_pounds):
        if ((self.payload + num_pounds) < self.max_payload):
            self.payload += num_pounds
        else

:
            print("Overloaded!")

    def remove_payload(self, num_pounds):
        if ((self.payload - num_pounds) >= 0):
            self.payload -= num_pounds
        else:
            print("Nothing in the truck.")

    def get_payload(self):
        return self.payload

Listing 3-16Pickup Truck Class

注意这里的一些事情。首先,注意类语句:class PickupTruck(Vehicle) .当我们想从另一个类继承时,我们用基类的名字加上括号。这确保 Python 将使用基类,允许派生类使用其所有可访问的数据和内存。如果要从多个类继承,可以(称为多重继承);只需用逗号分隔的列表列出基(父)类。

接下来,注意__max_occupants变量。按照惯例,在一个类中为一个属性或方法使用两个下划线会使该项成为该类的私有项。 5 也就是说,它应该只能从类内部访问。该类的调用方(通过类变量/实例)不能访问私有项,从该类派生的任何类也不能访问私有项。隐藏属性(数据)总是一个好的做法。

您可能想知道 occupant 方法发生了什么变化。他们为什么不在新班级?它们不在那里,因为我们的新类继承了基类的所有行为。不仅如此,还修改了代码,将乘员限制为三人。

我还想指出我添加到该类中的文档。我们使用文档字符串(前后使用一组三个双引号)来记录这个类。您可以将文档放在这里解释该类及其方法。稍后我们会看到它的一个很好的用途。

最后,请注意构造函数中的代码。这演示了如何调用基类方法,我这样做是为了设置轴和门的数量。如果我们想调用基类方法的版本,我们可以在其他方法中做同样的事情。

现在,让我们写一些代码来使用这个类。清单 3-17 显示了我们用来测试这个类的代码。在这里,我们创建了一个名为 pickup.py 的文件,该文件创建了一个皮卡实例,添加了乘员和有效载荷,然后打印出卡车的内容。

#
# Beginning Sensor Networks 2nd Edition
#
# Class Example: Exercising the PickupTruck class.
#
# Dr. Charles Bell
#
from pickup_truck import PickupTruck

pickup = PickupTruck(500)
pickup.add_occupant()
pickup.add_occupant()
pickup.add_occupant()
pickup.add_occupant()
pickup.add_payload(100)
pickup.add_payload(300)

print("Number of occupants in truck = {0}.".format(pickup.num_occupants()))
print("Weight in truck = {0}.".format(pickup.get_payload()))
pickup.add_payload(200)
pickup.remove_payload(400)
pickup.remove_payload(10)

Listing 3-17Using the PickupTruck Class

注意,我添加了几个对add_occupant()方法的调用,新类继承并覆盖了它。我还添加了一些调用,这样我们就可以在检查过度占用和最大有效负载能力的方法中测试代码。当我们运行这段代码时,我们将看到如下所示的结果:

$ python ./pickup.py
Sorry, only 3 occupants are permitted in the truck.
Number of occupants in truck = 3.
Weight in truck = 400.
Overloaded!
Nothing in the truck.

关于类,我们还应该了解一件事:内置属性。回忆一下__init__()方法。Python 自动提供了几个内置属性,每个都以__开头,您可以使用它们来了解更多关于对象的信息。下面列出了一些可用于类的运算符:

  • __dict__:包含类名称空间的字典

  • __doc__:类文档字符串

  • __name__:类名

  • __module__:定义类的模块名

  • __bases__:继承顺序中的基类

下面的代码显示了这些属性为前面的PickupTruck类返回的内容。我将这段代码添加到 pickup.py 文件中。

print("PickupTruck.__doc__:", PickupTruck.__doc__)
print("PickupTruck.__name__:", PickupTruck.__name__)
print("PickupTruck.__module__:", PickupTruck.__module__)
print("PickupTruck.__bases__:", PickupTruck.__bases__)
print("PickupTruck.__dict__:", PickupTruck.__dict__)

运行此代码时,我们会看到以下输出:

PickupTruck.__doc__: This is a pickup truck that has:
    axles = 2,
    doors = 2,
    max occupants = 3
    The maximum payload

is set on instantiation.

PickupTruck.__name__: PickupTruck
PickupTruck.__module__: pickup_truck
PickupTruck.__bases__: (<class 'vehicle.Vehicle'>,)
PickupTruck.__dict__: {'__module__': 'pickup_truck', '__doc__': 'This is a pickup truck that has:\n    axles = 2,\n    doors = 2,\n    max occupants = 3\n    The maximum payload is set on instantiation.\n    ', 'occupants': 0, 'payload': 0, 'max_payload': 0, ' _PickupTruck__max_occupants': 3, '__init__': <function PickupTruck.__init__ at 0x1018a1488>, 'add_occupant': <function PickupTruck.add_occupant at 0x1018a17b8>, 'add_payload': <function PickupTruck.add_payload at 0x1018a1840>, 'remove_payload': <function PickupTruck.remove_payload at 0x1018a18c8>, 'get_payload': <function PickupTruck.get_payload at 0x1018a1950>}

当您需要关于一个类的更多信息时,您可以使用内置属性。注意字典中的_PickupTruck__max_occupants条目。回想一下,我们制作了一个伪私有变量__max_occupants。在这里,我们看到 Python 是如何通过在变量前添加类名来引用变量的。记住,以两个下划线(而不是一个)开头的变量表示它们应该被认为是类的私有变量,并且只能在类内部使用。

Tip

有关 Python 中类的更多信息,请参见 https://docs.python.org/3/tutorial/classes.html

For More Information

如果您需要更深入的 Python 知识,有几本关于这个主题的优秀书籍。我在这里列出了几个我最喜欢的:

  • Pro Python ,第二版(Apress 2014),J. Burton Browning,Marty Alchin

  • 学习 Python ,第五版(奥莱利媒体 2013),马克·卢茨

  • 用 Python 自动化枯燥的东西:面向所有初学者的实用编程(无淀粉出版社,2015 年),Al Sweigart

Python 网站上的文档是一个很好的资源:python.org/doc/.

摘要

哇!那是一次疯狂的旅行,不是吗?我希望这个简短的 Python 速成课程已经对到目前为止展示的示例程序进行了足够的解释,现在您已经知道它们是如何工作的了。这个速成课程也为理解本书中的其他 Python 和 MicroPython 示例奠定了基础。

如果您正在学习如何使用传感器网络项目,但不知道如何使用 Python 编程,学习 Python 会很有趣,因为它的语法简单易懂。虽然互联网上有很多例子可以使用,但是很少有以这种方式记录的,可以为 Python 新手提供足够的信息来理解,更不用说开始使用和部署示例了!但至少代码很容易阅读。

本章提供了 Python 速成课程,涵盖了您在研究大多数较小的示例项目时将会遇到的基本内容。我们发现了 Python 应用程序的基本语法和结构,包括编写 Python 和 MicroPython 脚本时可能会遇到的所有基本语句和数据结构。

在下一章,我们将更深入地研究 MicroPython 编程,并开始为我们的 XBee 模块编写 MicroPython。

四、基于 XBee 的传感器节点

到目前为止,在我们的传感器网络之旅中,我们已经发现了传感器网络是如何形成的,节点的类型以及它们在传感器网络中的作用,并且我们已经花了一些时间了解 XBee 模块以及如何使用 MicroPython 对它们进行编程。

现在是时候看看我们如何使用 XBee 模块读取传感器数据了。正如您将看到的,这可以通过两种方式之一来实现:使用 XBee 丰富的本机功能来读取传感器并将数据广播到几个甚至单个节点,使用编写的 MicroPython 来读取和管理传感器数据,并将数据传递到其他节点。我们将集中讨论第一种方法,但我们将看到两种方法的简短示例。

让我们首先简要概述一下我们可以用 XBee 模块做什么。

如何用 XBee 托管传感器

用 XBee 模块托管传感器有两种基本方法 1 。您可以配置 XBee 模块对传感器进行采样,并按时间表发送其数据(XBee 硬件选项),或者您可以编写一个 MicroPython 脚本(MicroPython 选项)来执行相同的操作。有一个主要的区别。使用 MicroPython 选项意味着您可以在传输传感器数据之前对其进行一些额外的处理。这可以包括基本的错误处理、数据转换等等。如果您需要在传输传感器数据之前对其进行任何处理,或者如果您想要控制连接到 XBee 模块的其他设备,MicroPython 是一个明显的优势。

例如,您可以将一个 LED 连接到 XBee,每当从传感器读取数据时就打开它。这可能有助于射频识别(RFID)阅读器等解决方案,在传感器上刷卡可以打开门。在这种情况下,您可以使用 MicroPython 触发发光二极管(LED ),让用户知道锁何时被解除(或被接合)。

相反,硬件选项允许您将 XBee 配置为以原始形式捕获传感器数据,并且没有修改它的规定(很容易)。因此,发送的数据是传感器生成的原始数据。虽然这是最佳实践之一—以原始形式存储数据—但有时您可能希望在传输之前处理数据。我们将看到如何用 MicroPython 选项做到这一点。

在这两种情况下,传感器都通过输入/输出引脚直接连接到 XBee。更具体地说,您将传感器连接到 XBee,读取数据,并将其发送到网络上的一个或多个 XBee 模块。您可以通过地址将数据发送到特定的模块,也可以通过广播将数据发送到网络上的所有模块。

在下一节中,我们将看到两种方法的示例,使用相同的硬件设置来实现简单的环境传感器。

构建 XBee 环境传感器

本例中的 XBee 环境传感器节点是一个 XBee 模块,带有一个简单的模拟温度传感器(TMP36 ),连接到一个模拟输入引脚,该引脚使用模数转换器(ADC)将电压转换为 0–1024 范围内的数字。对于这个项目,您告诉 XBee 使用短时间周期发送数据;但是对于一个实际的项目,你可能会考虑使用一个较慢的采样速率,或者使用睡眠模式,在这种模式下 XBee 会休眠一段时间,然后发送数据,并重复执行。我们将在本章稍后配置 XBee 模块时设置采样率。现在,让 XBee 更频繁地发送样本,这样您就可以看到发生了什么。

XBee 还有一个非常棒的功能,可以监控电池电量。您可以告诉 XBee 将当前电源作为数据包的一部分发送。在这种情况下,它发送提供给 XBee 的任何电压。这很有帮助,因为它允许您在解决方案中构建一个触发器来提醒您更换传感器节点中的电池。

如果你有一个装有烟雾探测器的家或公寓,你可能已经经历过类似的电路,当电池电压下降时,它会发出音调或警报。对于那些家里有多个烟雾探测器的人来说,这有点像“沃尔多在哪里?”游戏寻找探测器是啁啾!这就是为什么每当第一个探测器开始鸣叫时,我就更换所有探测器的电池。

硬件设置

为了使项目易于构建,您将使用传感器节点的试验板。使用试验板可以更容易地试验元件,一旦完善电路,就可以将它们移到印刷电路板(PCB)试验板上进行半永久安装,或者为传感器节点设计和构建自己的定制 PCB。

XBee 传感器节点的硬件包括一个试验板、一个试验板电源、一个 TMP36 温度传感器和一个 0.10mF 电容。您还需要一个 XBee Explorer 板和一组插头(面包板的间距为 0.1 英寸),就像 Adafruit 或 SparkFun 提供的那样。图 4-1 显示了 SparkFun regulated explorer 板。稳压板稍微贵一点,但是它内置了功率调节,所以如果你不小心接了 5V,它不会把你的 XBee 炸了。作为一个现在完全无用的 XBee(它甚至不够大,不能用作杯垫)的所有者,我可以告诉你,它值得额外的成本。

img/313992_2_En_4_Fig1_HTML.jpg

图 4-1

SparkFun 监管的 XBee Explorer(由 SparkFun 提供)

Note

大多数分线板没有安装试验板接头。你必须自己焊接,找个人来帮你焊接。

组装好元件后,如图 4-2 所示,将它们插入试验板。请注意,图中没有安装 XBee 模块,因此您可以清楚地看到连接。确保将试验板电源设置为 3.3V。

Caution

在给传感器节点通电之前,请务必仔细检查您的布线。

现在还不需要安装 XBee 模块,因为在电路中使用它之前,您需要配置它的设置。您将在下一节中执行该操作。

img/313992_2_En_4_Fig2_HTML.jpg

图 4-2

XBee 温度传感器节点

值得注意的是,该图显示正电源流向 XBee 的引脚 1。请务必检查分线板上的针脚,以确定您连接到了正确的针脚。例如,SparkFun regulated explorer 输入电压不在引脚 1 上。

Note

试验板电源可以是 6V 到 12V 的任何电源。大多数人用来给他们 Arduino 供电的 9V 壁式电源适配器将会很好。

请注意,您还将 TMP36 的数据线连接到引脚 XBee 上的模拟 3 或浏览器板上的 DIO3),并将地连接到分线板(或浏览器)上的地引脚。确保 TMP36 的平面朝向如图所示。也就是说,平的一面朝向你,引脚 1 在左边,连接到输入电源,中间的引脚是数据,引脚 3 连接到地。您可以在任一方向放置电容,但要确保它连接到 TMP36 的引脚 1 和 3。

Caution

确保分线板电源设置为 3.3V

Alternative to a Breadboard Power Supply

如果您计划制作几个 XBee 传感器节点用于半永久安装,您可能不想使用试验板。相反,您可能希望使用 PCB 试验板,并将 XBee 分线板、传感器和支持电子设备焊接到位。在这种情况下,试验板电源可能不太方便。同样,如果你想降低成本,你可以用几个部件构建一个基本的电源,它可以接受高达 12V 的电压,同时仍然可以将 XBee 的电源调节到 3.3V。

你所需要的只是一个 7833-电压调节器、一个 1mF 电容器、一个 10mF 和一个双端接线板(或类似的电源连接器)。总的来说,即使在电子产品零售店,你也应该可以花几美元买到这些组件,而在电子产品网上商店就更便宜了。布置电路很容易。下图显示了连接到试验板上的组件。

img/313992_2_En_4_Figa_HTML.jpg

你只需要一点想象力和一些电线来转移电路到 PCB 试验板。注意电容的方向——让白条在负极一侧!

现在我们已经设置了硬件部分,让我们看看如何使用前面描述的每种方法在 XBee 上启用传感器。

对于每个选项,我们将使用 XCTU 应用程序测试传感器节点,以观察数据。这将允许我们在不建立完整的传感器网络的情况下测试读取传感器。事实上,建议您以类似的方式测试每个传感器节点。一旦所有节点都使用一个连接,您就可以开始在一个更大的网络中将它们连接在一起。这将为你节省无数的时间和挫折。 2

我们将从 XBee 硬件选项开始。

XBee 硬件选项

在本节中,我们将使用 XBee 模块的硬件配置选项从 TMP36 传感器读取温度数据,并将其传递给网络上的另一个 XBee 模块。我们将使用 ZigBee 网络来简化事情。更具体地说,我们将不使用目的地址(DH 和 DL 代码)来设置目标节点。这将允许 XBee 作为传感器节点使用数据样本包来广播数据。

我们需要一个 XBee 模块读取传感器,另一个模块接收数据。为了使这个例子简单和易于设置,我们将在这个例子中使用 XBee series 2 模块。如果不使用协调器,并且清除了目的地址(DH 和 DL ),您可以将第二章中使用的模块之一用于传感器节点。您将使用配置为协调器的 XBee 模块来测试 XBee 传感器节点。以下部分详细介绍了您需要进行的所有设置。

配置 XBee 传感器节点

用作 XBee 传感器节点的 XBee 模块可以是终端设备,也可以是带有 API 固件的路由器。您使用 XCTU 应用程序通过 USB 适配器连接到 XBee。回想一下,我们必须使用 USB 加密狗连接 XBee,然后打开 XCTU,并添加模块(或搜索所有模块)。一旦找到模块,打开配置模式选项卡。从那里,我们将设置几个设置,使 XBee 能够读取传感器。如果您尚未上传路由器或终端设备固件,您应该先上传。

在这种情况下,您希望 XBee 模块每 15 秒(15,000 毫秒)发送一次数据,读取模拟线路 3(数字 I/O 3 或 DIO3)上的数据,并包括参考电压。因此,在 XCTU 应用程序中,您想要更改相应的设置。表 4-1 显示了您需要更改的设置。使用搜索框中显示的代码快速查找每个设置。回想一下,所有值都是以十六进制输入的,您可以通过搜索代码来更改 XCTU 中的值,然后选择一个值或将其键入该设置的文本框中。如图所示更改设置,然后点击写入将设置保存到 XBee 模块。

表 4-1

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| D3 | a3/上帝 3 | 触发模拟或数字数据记录 | 2—ADC |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 红外的 | 输入输出采样率 | 等待发送数据的时间 | 3a 98—15000 毫秒 |
| 镍 | 节点标识符 | 节点的名称 | 传感器节点 |
| V+ | 电源电压阈值 | 电源电压 | FFFF(总是发送) |

设置协调员

接下来,移除 XBee 传感器节点,并将其插入试验板上的 explorer。确保已加载协调器固件,并使用表 4-2 中的设置。

表 4-2

XBee 协调器选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | 协调者 |

现在我们准备测试我们的传感器节点。

测试 XBee 传感器节点

要测试 XBee 传感器节点,您需要使用 XBee 协调器,API 固件安装在连接到 PC 的 USB 适配器上。首先这样做,以便协调器可以在您启动 XBee 传感器节点时启动并运行。将其插入您的电脑,并打开 XCTU 应用程序。使用 XCTU 发现 XBee 模块,然后打开一个终端。参见第二章了解如何操作的说明。

接下来,将电源连接到 XBee 传感器节点。XBee 需要一些时间来连接到协调器并加入网络。一旦完成,您就会看到协调器正在接收数据,如图 4-3 所示。

Tip

网络的形成需要一些时间。如果在协调器上没有看到数据样本,请关闭传感器节点的电源,然后再打开。如果您仍然看不到任何数据,请仔细检查您的设置,以确保两个节点都在同一个网络 PAN ID 上。

img/313992_2_En_4_Fig3_HTML.jpg

图 4-3

串行监视器输出

您应该看到一个或多个 IO 数据样本接收 RX 指示符数据包。请注意,图像中的第一行以 7E(十六进制)开始。这是包的起始定界符。您应该看到如下所示的正确数据。这是一系列十六进制值。

7E 00 14 92 00 13 A2 00 40 A0 D4 5C FC F1 01 01 00 00 88 02 41 0A BC 28

所有 ZigBee 分组都有特定的格式或布局。表 4-3 显示了 IO 数据采样接收指示器包的布局。

表 4-3

IO 数据采样接收指示器包

|

价值

|

字段名

|

笔记

|
| --- | --- | --- |
| 7E | 开始分隔符 |   |
| 00 14 | 包长度 | 20 字节校验和 |
| 92 | 框架类型 | I/O 数据采样接收指示器 |
| 00 13 A2 00 40 A0 D4 5C | 64 位地址 | XBee 传感器节点的地址 |
| FC F1 | 16 位地址 |   |
| 01 | 选择 |   |
| 01 | 样本数目 | 1 个数据样本 |
| 00 00 | 数字掩模 | 有数据的数字引脚 |
| 88 | 模拟掩模 | 有数据的模拟引脚 |
| 02 41 | 样品 | 来自传感器的温度 |
| 0A BC | 电源电压 |   |
| 28 | 校验和 |   |

该数据包代表从 XBee 传感器节点发送的数据。在这种情况下,您设置 XBee 每 15 秒从模拟引脚 3(数字 IO 3)发送一次任何值。您还可以设置发送电源电压值的选项。注意模拟掩码的值:十六进制的值 88 被转换成二进制的值1000 1000。字节的第一部分表示电源电压也包含在数据包中。该字节的第二部分表示 AD3/DIO3(引脚 3)是样本源。如果对多个传感器进行采样,掩码将包含数据引脚设置的位,或引脚 0 的0001、引脚 1 的0010和引脚 2 的0100

从表中可以看到,确实有一个数据样本的值为02 41(十六进制,十进制 577)。该值为 577,因为这是从传感器读取的毫伏电压。要计算温度,必须使用以下公式:

temp =  ((sample * 1200/1024) - 500)/10

因此,你有((577 * 1200/1024)-500)/10 = 17.61摄氏度。电源电压的公式类似:

voltage = (sample * 1200/1024)/1000

这里,您将读取的数据转换为伏特而不是毫伏。因此,数据包包含0A BC(十六进制,2748),电压读数为 3.22 伏。如果使用电池为 XBee 传感器供电,您可以使用该值来确定何时需要更换电池或给电池充电。

花一些时间研究示例中的其他样本,并检查温度读数的数据样本。如果您非常小心,可以将手指放在 TMP36 上,观察温度变化(再进行一两次采样后,温度应该会开始升高)。一旦您确信 XBee 传感器节点正在发送类似的数据,您就可以断定传感器节点工作正常。

接下来,让我们看看 MicroPython 选项。

MicroPython 选项

在本节中,我们将在 XBee 模块上使用 MicroPython 脚本从 TMP36 传感器读取温度数据,并将其传递给网络上的另一个 XBee 模块。我们将使用 ZigBee 网络来简化事情。更具体地说,我们将提供目的地址(DH 和 DL 代码)来将数据发送到特定的节点。

我们需要一个 XBee 模块读取传感器,另一个模块接收数据。在这个例子中,我们必须为传感器节点使用 XBee series 3 模块,但是我们可以使用前面例子中的同一个协调器。如果不使用协调器,并且清除了目的地址(DH 和 DL ),您可以将第二章中使用的模块之一用于传感器节点。您将使用配置为协调器的 XBee 模块来测试 XBee 传感器节点。以下部分详细介绍了您需要进行的所有设置。

配置 XBee 传感器节点

用作 XBee 传感器节点的 XBee 模块可以是终端设备,也可以是路由器,其 API 固件配置为运行 MicroPython。同样,您使用 XCTU 应用程序通过 USB 适配器连接到 XBee。

回想一下,我们将把 XBee 模块置于 MicroPython 模式。虽然我们仍将使用 ZigBee 网络,但我们将设置模块来连接(加入)网络。因此,我们需要一名协调员。幸运的是,我们可以使用与上一节相同的协调器。

表 4-4 显示了您需要更改的设置。回想一下,所有值都是以十六进制输入的,您可以通过搜索代码来更改 XCTU 中的值,然后选择一个值或将其键入该设置的文本框中。如图所示更改设置,然后点击写入将设置保存到 XBee 模块。

表 4-4

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 美国联合通讯社(Associated Press) | API 已启用 | 设置 API 模式 | 4—MicroPython |
| 神学士 | UART 波特率 | 串行连接的速度 | One hundred and fifteen thousand two hundred |
| 这一个 | 设备角色 | ZigBee 网络中的角色 | 0-加入网络 |
| D3 | a3/上帝 3 | 触发模拟或数字数据记录 | 2—ADC |
| 身份 | PAN ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | Python TMP36 |
| 附言(同 postscript);警官(police sergeant) | MicroPython 自动启动 | 自动启动 REPL | 1—启用 |

传感器节点编程

继续对传感器节点进行配置更改,然后将其写入(保存)到模块中。回想一下第二章中的内容,我们可以交互地编写我们的 MicroPython 脚本,然后将其保存到一个文件中,或者将其写入一个文件,然后上传到模块中。在本例中,我们将看到交互模式。

接下来,我们将如图 4-4 所示,通过从菜单中选择它来打开 MicroPython 终端。

img/313992_2_En_4_Fig4_HTML.jpg

图 4-4

开放式 MicroPython 终端

一旦 MicroPython 终端打开,按回车几次得到响应。你应该看到提示 > > > 。如果您正在重用以前项目中的 XBee 模块,该项目加载了一个 MicroPython 脚本,其中通过复制文件或使用 REPL 控制台的交互模式覆盖了main.py脚本,您可能需要按 Ctrl+C 来停止 main.py 脚本。

接下来,我们将输入清单 4-1 中所示的代码。您可以下载这本书的源代码,打开名为listing4-1.py的示例文件,在交互式文件模式下复制并粘贴一次代码。如果你愿意,可以省略注释行。还记得您必须按下 Ctrl+C 来中断您之前加载的 MicroPython 脚本。然后,您可以使用 Ctrl+F 将终端置于文件模式。

#
# Beginning Sensor Networks 2nd Edition
#
# XBee Sensor Node Example: Reading a TMP36 temperature sensor.
#
# Dr. Charles Bell
#
from machine import ADC
from time import sleep
import xbee

# Target address to send data
TARGET_64BIT_ADDR = b'\x00\x13\xA2\x00\x40\x8C\xCD\x0F'
wait_time = 15 # seconds between measurements
cycles = 10 # number of repeats

for x in range(cycles):
    # Read temperature value & print to debug
    temp_pin = ADC("D3")
    temp_raw = temp_pin.read()
    print("Raw pin reading: %d" % temp_raw)

    # Convert temperature to proper units
    temp_c = ((float(temp_raw) * (1200.0/4096.0)) - 500.0) / 10.0
    print("Temperature: %.2f Celsius" % temp_c)
    temp_f = (temp_c * 9.0 / 5.0) + 32.0
    print("Temperature: %.2f Fahrenheit" % temp_f)

    # Send data to coordinator

    message = "raw: %d, C: %.2f, F: %.2f" % (temp_raw, temp_c, temp_f)
    print("Sending: %s" % message)
    try:
        xbee.transmit(TARGET_64BIT_ADDR, message)
        print("Data sent successfully")
    except Exception as e:
        print("Transmit failure: %s" % str(e))

    # Wait between cycles
    sleep(wait_time)

Listing 4-1Reading a TMP36 Sensor

清单 4-2 显示了复制和粘贴前面代码的交互会话(没有注释)。注意在最后,我们用 Ctrl+D 将文件保存到 main.py,按 Y 确认。

Tip

如果在复制和粘贴整个文件时遇到问题,请尝试一次复制和粘贴一行。如果忽略空行,就会发生这种情况,空行会触发 REPL 控制台关闭代码块并执行代码。

flash compile mode; Ctrl-C to cancel, Ctrl-D to finish
   1^^^ from machine import ADC
   2^^^ from time import sleep
   3^^^ import xbee
   4^^^
   5^^^ # Target address to send data
   6^^^ TARGET_64BIT_ADDR = b'\x00\x13\xA2\x00\x40\x8C\xCD\x0F'
   7^^^ wait_time = 15 # seconds between measurements
   8^^^ cycles = 10 # number of repeats
   9^^^
  10^^^ for x in range(cycles):
  11^^^     # Read temperature value & print to debug
  12^^^     temp_pin = ADC("D3")
  13^^^     temp_raw = temp_pin.read()
  14^^^     print("Raw pin reading: %d" % temp_raw)
  15^^^
  16^^^     # Convert temperature to proper units
  17^^^     temp_c = ((float(temp_raw) * (1200.0/4096.0)) - 500.0) / 10.0
  18^^^     print("Temperature: %.2f Celsius" % temp_c)
  19^^^     temp_f = (temp_c * 9.0 / 5.0) + 32.0
  20^^^     print("Temperature: %.2f Fahrenheit" % temp_f)
  21^^^
  22^^^     # Send data to coordinator
  23^^^     message = "raw: %d, C: %.2f, F: %.2f" % (temp_raw, temp_c, temp_f)
  24^^^     print("Sending: %s" % message)
  25^^^     try:
  26^^^         xbee.transmit(TARGET_64BIT_ADDR, message)
  27^^^         print("Data sent successfully")
  28^^^     except Exception as e:
  29^^^         print("Transmit failure: %s" % str(e))
  30^^^
  31^^^     # Wait between cycles

  32^^^     sleep(wait_time)
  33^^^
Erasing /flash/main.mpy...
Compiling 1008 bytes of code...
Saved compiled code to /flash/main.mpy (619 bytes).
Automatically run this code at startup [Y/n]? Y
Stored code will run at startup.

Listing 4-2Interactive File Mode for TMP36 Sensor Example

一旦你保存了文件,我们可以通过按下 Ctrl+R 来运行文件,如清单 4-3 所示。但是,请记住,我们使用的是 REPL 控制台,它将以交互方式执行代码。因为我们还没有连接 TMP36 传感器,所以当代码执行时,您可能会看到虚假值。让它运行几次迭代,然后按下 Ctrl+C 停止执行。

MicroPython v1.11-1290-g9da1b0c on 2019-11-14; XBee3 Zigbee with EFR32MG
Type "help()" for more information.
Press CTRL-R in the REPL to run the code at any time.

Try running it with CTRL+R. Interrupt with CTRL+C.

Loading /flash/main.mpy...
Running bytecode...
Raw pin reading: 4095
Temperature: 69.97 Celsius
Temperature: 157.95 Fahrenheit
Sending: raw: 4095, C: 69.97, F: 157.95
Data sent successfully
Raw pin reading: 4095
Temperature: 69.97 Celsius
Temperature: 157.95 Fahrenheit
Sending: raw: 4095, C: 69.97, F: 157.95
Data sent successfully
Raw pin reading: 4095
Temperature: 69.97 Celsius
Temperature: 157.95 Fahrenheit
Sending: raw: 4095, C: 69.97, F: 157.95
Data sent successfully
Traceback (most recent call last):
  File "<stdin>", line 32, in <module>
KeyboardInterrupt:

>>>

Listing 4-3Interactive Execution of TMP36 Example

现在,让我们回到代码上,注意从原始输入计算摄氏温度值的公式,为了清楚起见,如下所示。您可能会注意到,公式使用了不同的最大读取值(4096 而不是 1024)。这是因为 MicroPython 从 ADC 返回 0–4095 的范围,因此我们必须在公式中考虑这一点。

    temp_raw = temp_pin.read()
    print("Raw pin reading: %d" % temp_raw)

    # Convert temperature to proper units
    temp_c = ((float(temp_raw) * (1200.0/4096.0)) - 500.0) / 10.0

一旦你确信这个公式是正确的,你可以关闭 MicroPython 终端,断开 XBee 与 XCTU 的连接,并移除 USB explorer。接下来,将 XBee 模块移至之前设置的试验板。

您不需要接通电路电源,但是如果您已经配置了协调器或者正在使用上一个示例中的,您可以跳过以下部分接通电路电源。

设置协调员

接下来,移除 XBee 传感器节点,并将其插入试验板上的 explorer。如果您正在重用上一节中的协调器,那么您不需要进行更改。如果您正在使用新的 XBee 模块或来自另一个项目的模块,请确保您已经加载了协调器固件,并使用表 4-5 中的设置。

表 4-5

XBee 协调器选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | 协调者 |

一旦写入设置并且传感器节点通电,请查看网络以确保您的模块正在连接。图 4-5 显示了您应该看到的示例。回想一下,你可以选择协调人,从主窗口打开网络视图,然后点击扫描

img/313992_2_En_4_Fig5_HTML.jpg

图 4-5

检查网络

Tip

检查您的 ZigBee 网络以确保模块连接正确总是一个好主意。如果您没有看到预期的模块,请仔细检查所有设置并重新扫描网络。

现在我们准备测试我们的传感器节点。

测试 XBee 传感器节点

要测试 XBee 传感器节点,您需要使用 XBee 协调器,API 固件安装在连接到 PC 的 USB 适配器上。首先这样做,以便协调器可以在您启动 XBee 传感器节点时启动并运行。将其插入您的电脑,并打开 XCTU 应用程序。使用 XCTU 发现 XBee 模块,然后打开一个终端。参见第二章了解如何操作的说明。

接下来,将电源连接到 XBee 传感器节点。XBee 需要一些时间来连接到协调器并加入网络。一旦完成,您就会看到协调器正在接收数据,如图 4-6 所示。

img/313992_2_En_4_Fig6_HTML.jpg

图 4-6

串行监视器输出

您应该会看到一个或多个显式接收指示器数据包。我们得到这个包而不是广播,因为我们通过地址将包直接传输给协调器。注意图像中第一行以7E(十六进制)开始。这是包的起始定界符。您应该看到如下所示的正确数据。这是一系列十六进制值。

7E 00 2F 91 00 13 A2 00 41 92 DB A4 94 CC E8 E8 00 11 C1 05 01 72 61 77 3A 20 32 32 37 30 2C 20 43 3A 20 31 36 2E 35 30 2C 20 46 3A 20 36 31 2E 37 31 24

你可能想知道我们把信息传送到了哪里。它就在那里,但是很难在十六进制输出中看到。它出现在邮件的末尾。下面显示了示例中的消息。

72 61 77 3A 20 32 32 37 30 2C 20 43 3A 20 31 36 2E 35 30 2C 20 46 3A 20 36 31 2E 37 31

如果您将十六进制值转换为美国信息交换标准码(ASCII), 3 您将看到消息。使用 ASCII 图表进行的有些繁琐的查找将揭示以下 29 个十六进制值以 ASCII 表示,如下所示。很漂亮,是吗?

raw: 2270, C: 16.50, F: 61.71

现在,让我们仔细看看这个包。所有 ZigBee 分组都有特定的格式或布局。表 4-6 显示了显式接收指示器数据包的布局。

表 4-6

显式 Rx 指示符包

|

价值

|

字段名

|

笔记

|
| --- | --- | --- |
| 7E | 开始分隔符 |   |
| 00 2F | 包长度 | 47 字节校验和 |
| 91 | 框架类型 | 显式 Rx 指示器 |
| 00 13 A2 00 41 92 DB A4 | 64 位地址 | XBee 传感器节点的地址 |
| 94 CC | 16 位地址 |   |
| E8 | 源端点 |   |
| E8 | 目的端点 |   |
| 00 11 | 集群 Id |   |
| C1 05 | 个人资料 Id |   |
| 01 | 接收选项 | 0x 01—数据包已确认 |
| 0A BC | 电源电压 |   |
| N bytes | 接收日期 | 示例:29 |
| N+1 byte | 校验和 | 例如:0x24 |

该数据包代表从 XBee 传感器节点发送的数据。在这种情况下,您设置 XBee 在 10 个周期内每隔 15 秒从模拟引脚 3(数字 IO 3)发送任何值。 4

花一些时间研究示例中的其他样本,并检查温度读数的数据样本。如果您非常小心,可以将手指放在 TMP36 上,观察温度变化(再进行一两次采样后,温度应该会开始升高)。一旦您确信 XBee 传感器节点正在发送类似的数据,您就可以断定传感器节点工作正常。

接下来,我们将看看这个项目的一个例子,它使用了一种不同形式的传感器,简单一些。

示例:使用 XBee 模块收集数据

在这个例子中,我们将通过切换到一个更简单(但稍微贵一点)的选项来将传感器连接到 XBee 模块,从而稍微提升一下配置。我们还将看到与不同接口通信的不同形式的传感器。

我们将使用 XBee Grove 开发板来托管我们的 XBee 模块,如图 4-7 所示。XBee Grove 开发板有几个连接器以及六个 Grove 连接器、用户可控按钮和 LED 等等。有关主板的完整信息,请参见 www.digi.com/resources/documentation/Digidocs/90001457-13/ 中的指南。

img/313992_2_En_4_Fig7_HTML.jpg

图 4-7

XBee Grove 开发委员会

Grove 是 Seeed Studio(seeedstudio.com)制定的硬件原型标准,旨在使用简单的四线连接简化设备连接。您可以找到各种传感器和输出组件来快速创建您的项目。参见 Seeed Studio wiki 关于格罗夫系统了解更多( http://wiki.seeedstudio.com/Grove_System/ )。

我们将使用的传感器是 BMP280 温度/湿度传感器。你可以在 Adafruit ( adafruit.com)或者 SparkFun ( sparkfun.com)找到这个传感器。图 4-8 显示了 Adafruit 的 BMP280 模块。你可以在 www.adafruit.com/product/2651 这里得到一个。BMP280 有一个 Grove 模块( http://wiki.seeedstudio.com/Grove-Barometer_Sensor-BMP280/ ),但是因为是比较老的模块,所以比较难找。

img/313992_2_En_4_Fig8_HTML.jpg

图 4-8

BMP280 分线板

该传感器使用集成电路间(I2C)接口 5 ,7 位寻址。这需要四个连接:电源、地、时钟(SCL)和数据(SDA)。因为您可以将多个传感器连接到同一个 I2C 总线,所以每个传感器都有自己的地址,这样您就可以与您想要的传感器“交谈”。不幸的是,每个 I2C 传感器(设备)都有自己的通信协议,因此与模块通信以获取数据需要一个特殊的库(称为驱动程序)来使用传感器。幸运的是,BMP280 有一个 MicroPython I2C 驱动程序。我们将下载它并复制到我们的 XBee 模块。然而,由于它是针对与 XBee MicroPython 稍有不同的 MicroPython 版本编写的,我们需要做一些小的改动。

虽然这个例子很简洁,并且显示了使它工作所需的最低限度,但是我们将在接下来的两章中学习更多关于 I2C 接口的内容。

让我们从配置 XBee 传感器节点的硬件开始。

硬件设置

为这个项目设置硬件比前面的例子更容易。您所需要的只是一根 Grove 到母跳线电缆或(4)母到母跳线、XBee Grove 开发板和 BMP280 模块。如果您的 BMP280 模块没有焊接接头,您可能需要自己焊接或找人为您焊接。

为了将传感器连接到电路板,我们将只使用 BMP280 上的四个连接。Adafruit 的模块支持 I2C 和 x (SPI)接口,所以我们只需要那些用于 I2C 的接口。这些在板上标记如下:3V0 (3V 电源)、GND(地)、SDK(开发板上的 SCL)和 SDI(开发板上的 SDA)。我们可以将 Grove PWM 连接器用于电源和接地,但必须将 Grove D10 连接器用于 I2C 接口。进行连接的几种选择如下所示。现在就开始连接吧。此时不要插入 XBee 模块或将主板连接到您的 PC。

带跳线的 BMP280

如果您想使用单独的跳线,您应该使用两个 Grove 连接器:第一个用于 SCL 和 SDA 连接,另一个用于电源。这是因为跳线略大于 Grove 连接器中的引脚。

img/313992_2_En_4_Fig9_HTML.jpg

图 4-9

使用跳线连接 BMP280 分线板

带 Grove 分支电缆的 BMP280

Seeed Studio 的 Grove 分支电缆是跳线的绝佳替代品。它们的一端有一个 Grove 连接器,另一端有每根电线的母连接器,非常适合连接到分线板,如本例中的 BMP280。详见 www.seeedstudio.com/Grove-4-pin-Female-Jumper-to-Grove-4-pin-Conversion-Cable-5-PCs-per-Pack.html 。像 Grove BMP280 模块一样,这些电缆更难找到。他们还搞了一个小树林给公电缆。www.seeedstudio.com/Grove-4-pin-Male-Jumper-to-Grove-4-pin-Conversion-Cable-5-PCs-per-Pack.html见。

图 4-10 显示了如何使用凹槽连接跳线电缆。

img/313992_2_En_4_Fig10_HTML.jpg

图 4-10

使用 Grove 连接 BMP280 分线板至跳线电缆

Grove BMP280 模块连接

如果您购买了 BMP280 Grove 模块,您只需使用 Grove 电缆将模块连接到电路板,如图 4-11 所示。

img/313992_2_En_4_Fig11_HTML.jpg

图 4-11

连接 BMP280 Grove 模块

接下来,让我们配置 XBee 传感器节点。

配置 XBee 传感器节点

用作 XBee 传感器节点的 XBee 模块可以是终端设备,也可以是路由器,其 API 固件配置为运行 MicroPython。再次使用 XCTU 应用程序连接到 XBee。在这种情况下,我们将使用 XBee Grove 开发板。

只需将 XBee 模块连接到主板,然后使用随附的 micro-USB 电缆将主板连接到 PC。这就像我们在其他项目中使用的 USB explorer 一样。如果您正在重用我们之前用作传感器节点的 XBee 模块,除了数字 IO 引脚设置(D1)之外,许多设置将保持不变。

表 4-7 显示了您需要更改的设置。回想一下,所有值都是以十六进制输入的,您可以通过搜索代码来更改 XCTU 中的值,然后选择一个值或将其键入该设置的文本框中。如图所示更改设置,然后点击写入将设置保存到 XBee 模块。

表 4-7

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 美国联合通讯社(Associated Press) | API 已启用 | 设置 API 模式 | 4—MicroPython |
| 神学士 | UART 波特率 | 串行连接的速度 | One hundred and fifteen thousand two hundred |
| 这一个 | 设备角色 | ZigBee 网络中的角色 | 0-加入网络 |
| D1 | DIO1 | 数字数据读/写 | 6–I2C SCL |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | Python BMP280 |
| 附言(同 postscript);警官(police sergeant) | MicroPython 自动启动 | 自动启动 REPL | 1—启用 |

传感器节点编程

继续对传感器节点进行配置更改,然后将其写入(保存)到模块中。回想一下第二章中的内容,我们可以交互地编写我们的 MicroPython 脚本,然后将其保存到一个文件中,或者将其写入一个文件,然后上传到模块中。在本例中,我们将看到文件拷贝模式。

在这种情况下,我们需要将 BMP280 I2C 库复制到 XBee 模块上的lib文件夹中,并将我们的 MicroPython 脚本复制到 XBee 模块中,并将其重命名为main.py。与其盲目地复制文件来进入项目,不如让我们学习如何使用 BMP280 库来使用定制的 MicroPython 库。

当您想使用 I2C 传感器或设备时,您需要有一个 MicroPython 驱动程序库。回想一下,这是因为每个器件都有自己的协议,要求将某些值写入特定的一个或多个字节,以触发或设置某些选项,然后使用另一个地址读取数据。听起来很复杂?可以的。幸运的是,有人替我们做了所有的工作。

使用浏览器并导航至 https://github.com/dafvid/micropython-bmp280/ 。这个库是由 David Wahlund 编写的,是如何用 MicroPython 编写 I2C 驱动程序的一个很好的例子。如果您想为另一个 I2C 设备编写自己的驱动程序,这段代码是一个很好的模板。

要下载驱动程序,请单击克隆或下载按钮,并将 Zip 文件保存到您的 PC。下载完成后,打开 Zip 库并解压文件。你需要找到bmp280.py文件。在我们修改这个文件之后,我们将把它复制到我们的 XBee 模块中。

Tip

该模块的修改版本可以从 Apress 网站上的该书的源代码下载中获得。

简而言之,我们必须添加一个新的导入,并在构造函数中注释掉几行。这些修改将允许代码在 XBee 模块上工作。我们还需要删除模块中的一些方法,因为代码对于 XBee 来说有点太大了。

Caution

如果您发现想要在 XBee 上使用的其他模块并遇到内存错误,您可能需要减小该模块的大小。您可以通过移除不需要的方法、常量和类似的特性来实现这一点。注意只移除你不需要的东西(也是剩下的方法不需要的)。

打开下载的文件,在文件顶部添加这一行:

from micropython import const

接下来,找到下面的行,并对它们进行注释(在行首放置一个#)。你可以在构造函数中找到这两者。如果忘记删除这些行,您可能会遇到诸如无效 I2C 操作或内存错误之类的错误:

self._bmp_i2c.start()
self.use_case(BMP280_CASE_HANDHELD_DYN)

最后,为了减小模块的大小,删除 pressure()方法之后的所有方法,然后保存文件。清单 4-4 显示了结果代码。您的编辑应该非常相似(考虑到模块作者的微小改进)。

Tip

这本书的源代码中还包括一个差异文件(bmp280.diff),如果你熟悉差异补丁,你可以用它来应用代码。

from micropython import const
from ustruct import unpack as unp

# Author David Stenwall Wahlund (david at dafnet.se)

# Power Modes
BMP280_POWER_FORCED = const(1)
BMP280_POWER_NORMAL = const(3)

BMP280_SPI3W_ON = const(1)
BMP280_SPI3W_OFF = const(0)

BMP280_TEMP_OS_SKIP = const(0)
BMP280_TEMP_OS_1 = const(1)
BMP280_TEMP_OS_2 = const(2)
BMP280_TEMP_OS_4 = const(3)
BMP280_TEMP_OS_8 = const(4)
BMP280_TEMP_OS_16 = const(5)

BMP280_PRES_OS_SKIP = const(0)
BMP280_PRES_OS_1 = const(1)
BMP280_PRES_OS_2 = const(2)
BMP280_PRES_OS_4 = const(3)
BMP280_PRES_OS_8 = const(4)
BMP280_PRES_OS_16 = const(5)

# Standby settings in ms
BMP280_STANDBY_0_5 = const(0)
BMP280_STANDBY_62_5 = const(1)
BMP280_STANDBY_125 = const(2)
BMP280_STANDBY_250 = const(3)
BMP280_STANDBY_500 = const(4)

BMP280_STANDBY_1000 = const(5)
BMP280_STANDBY_2000 = const(6)
BMP280_STANDBY_4000 = const(7)

# IIR Filter setting
BMP280_IIR_FILTER_OFF = const(0)
BMP280_IIR_FILTER_2 = const(1)
BMP280_IIR_FILTER_4 = const(2)
BMP280_IIR_FILTER_8 = const(3)
BMP280_IIR_FILTER_16 = const(4)

# Oversampling setting
BMP280_OS_ULTRALOW = const(0)
BMP280_OS_LOW = const(1)
BMP280_OS_STANDARD = const(2)
BMP280_OS_HIGH = const(3)
BMP280_OS_ULTRAHIGH = const(4)

# Oversampling matrix

# (PRESS_OS, TEMP_OS, sample time in ms)
_BMP280_OS_MATRIX = [
    [BMP280_PRES_OS_1, BMP280_TEMP_OS_1, 7],
    [BMP280_PRES_OS_2, BMP280_TEMP_OS_1, 9],
    [BMP280_PRES_OS_4, BMP280_TEMP_OS_1, 14],
    [BMP280_PRES_OS_8, BMP280_TEMP_OS_1, 23],
    [BMP280_PRES_OS_16, BMP280_TEMP_OS_2, 44]
]

# Use cases
BMP280_CASE_HANDHELD_LOW = const(0)
BMP280_CASE_HANDHELD_DYN = const(1)
BMP280_CASE_WEATHER = const(2)
BMP280_CASE_FLOOR = const(3)
BMP280_CASE_DROP = const(4)
BMP280_CASE_INDOOR = const(5)

_BMP280_CASE_MATRIX = [
    [BMP280_POWER_NORMAL, BMP280_OS_ULTRAHIGH, BMP280_IIR_FILTER_4, BMP280_STANDBY_62_5],
    [BMP280_POWER_NORMAL, BMP280_OS_STANDARD, BMP280_IIR_FILTER_16, BMP280_STANDBY_0_5],
    [BMP280_POWER_FORCED, BMP280_OS_ULTRALOW, BMP280_IIR_FILTER_OFF, BMP280_STANDBY_0_5],
    [BMP280_POWER_NORMAL, BMP280_OS_STANDARD, BMP280_IIR_FILTER_4, BMP280_STANDBY_125],
    [BMP280_POWER_NORMAL, BMP280_OS_LOW, BMP280_IIR_FILTER_OFF, BMP280_STANDBY_0_5],
    [BMP280_POWER_NORMAL, BMP280_OS_ULTRAHIGH, BMP280_IIR_FILTER_16, BMP280_STANDBY_0_5]
]

_BMP280_REGISTER_ID = const(0xD0)
_BMP280_REGISTER_RESET = const(0xE0)
_BMP280_REGISTER_STATUS = const(0xF3)
_BMP280_REGISTER_CONTROL = const(0xF4)
_BMP280_REGISTER_CONFIG = const(0xF5)  # IIR filter config

_BMP280_REGISTER_DATA = const(0xF7)

class BMP280:
    def __init__(self, i2c_bus, addr=0x76):
        self._bmp_i2c = i2c_bus
        self._i2c_addr = addr

        self.chip_id = self._read(_BMP280_REGISTER_ID, 2)

        # read calibration data
        # < little-endian
        # H unsigned short
        # h signed short
        self._T1 = unp('<H', self._read(0x88, 2))[0]
        self._T2 = unp('<h', self._read(0x8A, 2))[0]
        self._T3 = unp('<h', self._read(0x8C, 2))[0]
        self._P1 = unp('<H', self._read(0x8E, 2))[0]
        self._P2 = unp('<h', self._read(0x90, 2))[0]
        self._P3 = unp('<h', self._read(0x92, 2))[0]
        self._P4 = unp('<h', self._read(0x94, 2))[0]
        self._P5 = unp('<h', self._read(0x96, 2))[0]
        self._P6 = unp('<h', self._read(0x98, 2))[0]
        self._P7 = unp('<h', self._read(0x9A, 2))[0]
        self._P8 = unp('<h', self._read(0x9C, 2))[0]
        self._P9 = unp('<h', self._read(0x9E, 2))[0]

        # output raw
        self._t_raw = 0
        self._t_fine = 0
        self._t = 0

        self._p_raw = 0
        self._p = 0

        self.read_wait_ms = 0  # interval between forced measure

and readout
        self._new_read_ms = 200  # interval between
        self._last_read_ts = 0

    def _read(self, addr, size=1):
        return self._bmp_i2c.readfrom_mem(self._i2c_addr, addr, size)

    def _write(self, addr, b_arr):
        if not type(b_arr) is bytearray:
            b_arr = bytearray([b_arr])
        return self._bmp_i2c.writeto_mem(self._i2c_addr, addr, b_arr)

    def _gauge(self):
        # TODO limit new reads
        # read all data at once (as by spec)
        d = self._read(_BMP280_REGISTER_DATA, 6)

        self._p_raw = (d[0] << 12) + (d[1] << 4) + (d[2] >> 4)
        self._t_raw = (d[3] << 12) + (d[4] << 4) + (d[5] >> 4)

        self._t_fine = 0
        self._t = 0
        self._p = 0

    def reset(self):
        self._write(_BMP280_REGISTER_RESET, 0xB6)

    def load_test_calibration(self):
        self._T1 = 27504
        self._T2 = 26435
        self._T3 = -1000
        self._P1 = 36477
        self._P2 = -10685
        self._P3 = 3024
        self._P4 = 2855
        self._P5 = 140
        self._P6 = -7
        self._P7 = 15500
        self._P8 = -14600

        self._P9 = 6000

    def load_test_data(self):
        self._t_raw = 519888
        self._p_raw = 415148

    def print_calibration(self):
        print("T1: {} {}".format(self._T1, type(self._T1)))
        print("T2: {} {}".format(self._T2, type(self._T2)))
        print("T3: {} {}".format(self._T3, type(self._T3)))
        print("P1: {} {}".format(self._P1, type(self._P1)))
        print("P2: {} {}".format(self._P2, type(self._P2)))
        print("P3: {} {}".format(self._P3, type(self._P3)))
        print("P4: {} {}".format(self._P4, type(self._P4)))
        print("P5: {} {}".format(self._P5, type(self._P5)))
        print("P6: {} {}".format(self._P6, type(self._P6)))
        print("P7: {} {}".format(self._P7, type(self._P7)))
        print("P8: {} {}".format(self._P8, type(self._P8)))
        print("P9: {} {}".format(self._P9, type(self._P9)))

    def _calc_t_fine(self):
        # From datasheet page 22
        self._gauge()
        if self._t_fine == 0:
            var1 = (((self._t_raw >> 3) - (self._T1 << 1)) * self._T2) >> 11
            var2 = (((((self._t_raw >> 4) - self._T1)
                      * ((self._t_raw >> 4)
                         - self._T1)) >> 12)
                    * self._T3) >> 14
            self._t_fine = var1 + var2

    @property
    def temperature(self):
        self._calc_t_fine()
        if self._t == 0:
            self._t = ((self._t_fine * 5 + 128) >> 8) / 100.
        return self._t

    @property
    def pressure(self):
        # From datasheet page 22

        self._calc_t_fine()
        if self._p == 0:
            var1 = self._t_fine - 128000
            var2 = var1 * var1 * self._P6
            var2 = var2 + ((var1 * self._P5) << 17)
            var2 = var2 + (self._P4 << 35)
            var1 = ((var1 * var1 * self._P3) >> 8) + ((var1 * self._P2) << 12)
            var1 = (((1 << 47) + var1) * self._P1) >> 33

            if var1 == 0:
                return 0

            p = 1048576 - self._p_raw
            p = int((((p << 31) - var2) * 3125) / var1)
            var1 = (self._P9 * (p >> 13) * (p >> 13)) >> 25
            var2 = (self._P8 * p) >> 19

            p = ((p + var1 + var2) >> 8) + (self._P7 << 4)
            self._p = p / 256.0
        return self._p

Listing 4-4Modified bmp280.py Module

要将修改后的文件复制到我们的 XBee 模块,使用 Grove 开发板将您的 XBee 连接到您的 PC,然后使用工具菜单打开文件系统管理器,如图 4-12 所示。

img/313992_2_En_4_Fig12_HTML.jpg

图 4-12

打开文件系统管理器

一旦文件系统管理器打开,您将需要连接到 XBee。如果您在打开文件系统管理器之前选择了 XCTU 中的 XBee 模块,您可以点击打开按钮,管理器将连接到您的模块。否则,您可以使用设置按钮为 XBee 选择 UART(串行)参数,然后连接到它。

使用管理器在界面左侧找到bmp280.py文件,在界面右侧导航到 XBee 上的lib文件夹。然后,点击bmp280.py并将其拖至右侧并放下。这将把文件复制到您的 XBee 模块。结果应该类似于图 4-13 。

img/313992_2_En_4_Fig13_HTML.jpg

图 4-13

使用文件系统管理器复制 BMP 驱动程序

现在,让我们使用一个简单的脚本来测试这个库。我们将在交互模式下使用 MicroPython 终端来实现这一点。但是首先点击左上角的关闭按钮,然后点击右下角的关闭按钮关闭管理器。

接下来,从工具菜单打开 MicroPython 终端,然后连接到你的 XBee 模块。回想一下,你可能需要按回车或者 Ctrl+C 才能得到>>>提示。在那里,输入以下代码行:

from machine import I2C
from bmp280 import BMP280
bmp280 = BMP280(I2C(1, freq=100000), 0x77)
print(bmp280.temperature)
print(bmp280.pressure)

此代码显示了如何使用 I2C BMP280 驱动程序,告诉驱动程序传感器位于 DIO 1 上,并使用采样频率 100,000 和 I2C 地址 0x77(十六进制)。然后,我们读取温度,打印出来,并重复气压。以下代码显示了您应该看到的结果(值可能会有所不同):

>>> from machine import I2C
>>> from bmp280 import BMP280
>>> bmp280 = BMP280(I2C(1, freq=100000), 0x77)
>>> print(bmp280.temperature)
21.71
>>> print(bmp280.pressure)
102357.4
>>>

如果您遇到诸如地址或NOENV错误之类的错误,请仔细检查您的接线。有时候跳线会有点松。将它们压接在开发板侧,连接应该会改善。

现在我们已经复制并测试了 BMP 驱动程序,我们可以编写读取传感器的脚本了。清单 4-5 显示了完整的代码。它应该看起来非常熟悉,因为它遵循与前面的示例相同的模板。花点时间检查一下代码,看看它是如何工作的。

#
# Beginning Sensor Networks 2nd Edition
#
# XBee Sensor Node Example: Reading a BMP280 sensor.
# This demonstrates how to use an I2C driver.
#
# Dr. Charles Bell
#
from machine import I2C
from bmp280 import BMP280

import xbee

# BMP280 address
BMP_ADDR = 0x77
# Target address to send data
TARGET_64BIT_ADDR = b'\x00\x13\xA2\x00\x40\x8C\xCD\x0F'
wait_time = 15 # seconds between measurements
cycles = 10 # number of repeats
bmp280 = BMP280(I2C(1, freq=100000), BMP_ADDR)

for x in range(cycles):
    # Read temperature & barometric pressure
    temp_c = bmp280.temperature
    pressure = bmp280.pressure

    # Convert temperature to proper units

    print("Temperature: %.2f Celsius" % temp_c)
    temp_f = (temp_c * 9.0 / 5.0) + 32.0
    print("Temperature: %.2f Fahrenheit" % temp_f)
    print("Barometric Pressure: %.4f" % pressure)

    # Send data to coordinator
    message = "C: %.2f, F: %.2f, B: %.4f" % (temp_c, temp_f, pressure)
    print("Sending: %s" % message)
    try:
        xbee.transmit(TARGET_64BIT_ADDR, message)
        print("Data sent successfully")
    except Exception as e:
        print("Transmit failure: %s" % str(e))

    # Wait between cycles
    sleep(wait_time)

Listing 4-5Reading a BMP280 Sensor

如果您已经从 Apress book 网站下载了示例代码,您可以提取该文件并将其复制到您的 PC 上。将其重命名为 main.py,然后像前面一样使用文件系统管理器将其复制到 XBee 中。务必将其复制到 XBee 上的根文件夹中,如图 4-14 所示。

img/313992_2_En_4_Fig14_HTML.jpg

图 4-14

使用文件系统管理器复制 main.py

现在我们准备测试我们的传感器节点。

测试 XBee 传感器节点

要测试 XBee 传感器节点,您需要使用 XBee 协调器,API 固件安装在连接到 PC 的 USB 适配器上。首先这样做,以便协调器可以在您启动 XBee 传感器节点时启动并运行。将其插入您的电脑,并打开 XCTU 应用程序。使用 XCTU 发现 XBee 模块,然后打开一个终端。参见第二章了解如何操作的说明。

接下来,将电源连接到 XBee 传感器节点。XBee 需要一些时间来连接到协调器并加入网络。一旦完成,您就会看到协调器正在接收数据,如图 4-15 所示。

img/313992_2_En_4_Fig15_HTML.jpg

图 4-15

串行监视器输出

您应该看到一个或多个接收包包。我们得到这个包而不是广播,因为我们通过地址将包直接传输给协调器。注意图像中第一行以7E(十六进制)开始。这是包的起始定界符。您应该看到如下所示的正确数据。这是一系列十六进制值。

7E 00 2E 90 00 13 A2 00 41 92 DB A4 94 CC 01 43 3A 20 31 39 2E 36 39 2C 20 46 3A 20 36 37 2E 34 34 2C 20 42 3A 20 31 30 32 33 37 38 2E 32 34 39 32 58

你可能想知道我们把信息传送到了哪里。它就在那里,但是很难在十六进制输出中看到。它出现在邮件的末尾。下面显示了示例中的消息:

43 3A 20 31 39 2E 36 39 2C 20 46 3A 20 36 37 2E 34 34 2C 20 42 3A 20 31 30 32 33 37 38 2E 32 34 39 32

如果您将十六进制值转换为 ASCII,您将看到如下消息:

C: 19.69, F: 61.44, B: 102378.2492

现在,让我们仔细看看这个包。所有 ZigBee 分组都有特定的格式或布局。表 4-8 显示了接收包的布局。

表 4-8

接收数据包数据包

|

价值

|

字段名

|

笔记

|
| --- | --- | --- |
| 7E | 开始分隔符 |   |
| 00 2E | 包长度 | 47 字节校验和 |
| 90 | 框架类型 | 接收数据包指示器 |
| 00 13 A2 00 41 92 DB A4 | 64 位地址 | XBee 传感器节点的地址 |
| 94 CC | 内向的; 寡言少语的; 矜持的 |   |
| 01 | 选择 | 0x01 =数据包是广播数据包 |
| N bytes | 接收日期 | 示例:34 |
| N+1 byte | 校验和 | 例如:0x58 |

花一些时间研究示例中的其他样本,并检查温度读数的数据样本。如果你真的很小心,你可以把你的手指放在 BMP280 上,观察温度变化(它应该在一两个样本后开始增加)。一旦您确信 XBee 传感器节点正在发送类似的数据,您就可以断定传感器节点工作正常。

部件购物清单

你需要一些组件来完成本章中的项目。表 4-9 列出了它们。请注意,如果您不打算实现最后一个示例,可以考虑 Grove 组件。

表 4-9

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| XBee-ZB (ZB)系列 2、2.5 或 3 | www.sparkfun.com | $25.00–48.00 | Two |
| www.adafruit.com |
| BMP280 分线板 | www.adafruit.com/product/2651 | $9.95–14.95 | one |
| www.sparkfun.com/products/15440 |
| BMP280 Grove 传感器(可选) | www.seeedstudio.com/catalogsearch/result/?q=bmp280 | $8.95 | one |
| Grove 至母跳线(可选) | www.seeedstudio.com/Grove-4-pin-Female-Jumper-to-Grove-4-pin-Conversion-Cable-5-PCs-per-PAck.html | $3.90 | one |
| XBee Grove 开发板(可选) | www.digikey.com/products/en?mpart=76000956&v=602 | $25.00 | one |
| 试验板(非迷你) | www.sparkfun.com/products/9567 | $4.95 | one |
| 试验板跳线 | www.sparkfun.com/products/8431 | $3.95 | one |
| XBee Explorer 由集管调节 | www.sparkfun.com/products/11373 | $10.95 | one |
| TMP36 传感器 | www.sparkfun.com/products/10988 | $1.50 | one |
| www.adafruit.com/products/165 |
| 0.10uF 电容 | www.sparkfun.com/products/8375 | $0.25 | one |

摘要

XBee 模块是一种将数据从一个设备无线传输到另一个设备的廉价方式。它们还可以以托管(连接)一个或多个传感器的形式用于收集数据。

我们可以配置 XBee 从传感器收集原始数据,并将其传输(广播)到网络中的其他节点,而无需编程。或者,我们可以使用健壮的 MicroPython 编程语言编写一个脚本来读取数据并对其进行格式化,或者在将数据发送到另一个节点之前执行计算。

在本章中,我们看到了两种形式的托管传感器的示例。我们学习了如何使用模拟温度传感器(TMP36)和 I2C 数字传感器(BMP280)将传感器连接到 XBee 模块。

我们还看到了 Seeed Studio 提供的 Grove 原型开发平台。Grove 消除了在试验板上构建电路的需要,使连接传感器变得更加容易。我们将在后面的章节中看到更多关于 Grove 传感器的内容。

在下一章中,我们将探索 Raspberry Pi,包括如何使用 Raspberry Pi 的简短教程以及如何托管传感器的示例项目。

五、基于 Raspberry Pi 的传感器节点

使用 XBee 模块和微控制器托管传感器是构建传感器网络的一种经济方式。但是,当您需要的计算能力超过微控制器所能提供的能力时,该怎么办呢?如果您需要将数据转换为不同的格式,将数据合并到应用程序中,或者打印传感器数据的硬拷贝,该怎么办?在这些情况下,您可能需要一台处理能力更强、能够使用常见应用程序、允许使用脚本语言并能访问外围设备的计算机。

虽然个人电脑相对便宜,但在传感器网络中使用个人电脑有一些明显的缺点,尤其是作为传感器节点。如果传感器位于主电源不可靠或不可用的区域,或者存在过热风险的区域,或者根本没有安装个人计算机的空间的区域,则必须将数据传输到另一个节点进行处理,或者将其存储在本地,以后再进行处理。

然而,使用个人计算机作为传感器节点还有另一个限制:个人计算机没有通用输入/输出(I/O)端口。您可以购买扩展卡来收集数据,但这些扩展卡通常是为服务器或台式计算机而设计的。如果考虑计算机和数据采集卡的成本,传感器节点的成本就变得不经济了。

那么,在这些情况下你会怎么做?要是有一台低成本的计算机就好了,它具有足够的处理能力和内存,使用标准外围设备,支持可编程 I/O 端口、,并且外形小巧。这正是树莓派能做到的。

本章探讨如何开始使用 Raspberry Pi,包括如何使用系统以及如何使用 I/O 端口读取传感器。您还将探索几种类型的传感器,并研究如何从它们读取数据的差异。

Note

我们将涵盖树莓 Pi 3B 和最新的树莓 Pi 4B。你可以在这本书的项目中使用任何一个,但大多数数字描绘的是树莓派 3B 和 3B+。幸运的是,所有的 GPIO 连接在 3B、3B+和 4B 板上是相同的。

什么是树莓派?

Raspberry Pi 是一款小型廉价的个人电脑。虽然它缺乏 RAM(随机存取存储器)扩展的能力,也没有 CD、DVD 和硬盘等板载设备,但它拥有简单个人电脑所需的一切。也就是说,它有四个 USB 端口(Raspberry Pi 3 有 2.0 端口,Raspberry Pi 4B 有两个 USB 2.0 和两个 USB 3 端口),一个以太网端口,HDMI 视频,甚至还有一个用于声音的音频连接器。

Raspberry Pi 有一个 SD 驱动器 3 ,你可以用它来引导电脑进入几个 Linux 操作系统中的任何一个。您只需要一根 HDMI 电缆和显示器(或带有 HDMI 转 DVI 适配器的 DVI 电缆和显示器)、一个 USB 键盘和鼠标,以及一个 5V 电源,就可以开始运行了。

Note

在撰写本文时,还不能在 Raspberry Pi 4B 上从 USB 驱动器启动。博客暗示这一功能将很快可用。当它可用时,您可以通过将操作系统移动到 USB 驱动器来轻松创建更快的引导系统。

您也可以使用电脑上的 USB 端口为您的 Raspberry Pi 供电。在这种情况下,您需要一根 USB A 型公头转微型 USB B 型公头电缆(3B Raspberry Pi)或 USB A 型公头转 USB-C 公头电缆。将 type A 端插入电脑上的 USB 端口,将 micro-USB type B/USB-C 端插入 Raspberry Pi 电源端口。

该板有多种版本,裸板价格低至 35.00 美元。较新的 Raspberry Pi 4B 有 1GB、2GB、4GB 或 8GB 版本(只有内存不同),价格从 35 美元到 65 美元不等。可以从 SparkFun 和 Adafruit 等电子产品供应商那里在线购买。一些百思买零售商已经开始销售 Raspberry Pi 4B 2GB 主板以及基本配件(如外壳、电源)。大多数在线供应商都有一系列附件,这些附件已经过测试和验证,可以与 Raspberry Pi 一起使用。这些设备包括小型显示器、微型键盘,甚至用于安装主板的机箱。

在本节中,您将探索 Raspberry Pi 的起源,浏览硬件连接,并了解开始使用 Raspberry Pi 需要哪些附件。

高贵的出身

Raspberry Pi 旨在成为一个探索计算机科学主题的平台。设计者看到了提供廉价、易操作的计算机的需要,这种计算机可以被编程以与诸如伺服电机、显示设备和传感器之类的硬件交互。他们还想打破在个人电脑上花费数百美元的模式,从而让更多的人可以使用电脑。

设计者观察到学生进入计算机科学课程的体验下降。学生们在进入他们的学年时,对计算机系统、硬件或编程很少或没有经验,而不是在编程或硬件方面有一些经验。相反,学生们精通互联网技术和应用。其中一个因素是个人电脑的成本更高,更复杂,这意味着父母不愿意让他们的孩子在家用电脑上做实验。

这给学术机构带来了挑战,它们不得不调整课程,让计算机科学迎合学生的口味。由于学生缺乏兴趣或能力,他们不得不放弃较低级别的硬件和软件课题。学生们不再希望学习计算机科学的基础知识,如汇编语言、操作系统、计算理论和并发编程。相反,他们希望学习更高级的语言来开发应用程序和 web 服务。因此,一些学术机构不再提供基础计算机科学的课程。这可能会导致未来几代计算机专业人员知识和技能的流失。

为了应对这种趋势,树莓派的设计者认为,配备了正确的平台,年轻人可以回到个人电脑的实验中,因为在那个时代,个人电脑需要更大的承诺来学习系统和编程,以满足你的需求。例如,久负盛名的 Commodore 64、Amiga 以及早期的苹果和 IBM 个人电脑提供的软件非常有限。拥有许多这样的机器后,我在很小的时候就接触到了编程的奇迹和发现。 5

Why is it Called Raspberry Pi?

这个名字部分来源于设计委员会的贡献,部分是为了延续以水果命名新计算平台的传统(想想吧)。Pi 部分来自 Python,因为设计者希望 Python 成为计算机编程的首选语言。但是,也有其他编程语言可供选择。

Raspberry Pi 试图提供一个鼓励实验的廉价平台。以下部分将探讨更多有关 Raspberry Pi 的信息,包括可用的型号、所需的附件以及在哪里购买主板。

模型

Raspberry Pi 板目前有两种型号分类:型号 A 和型号 b。早期的型号 A 板是第一批大规模生产的板,具有 256MB 的 RAM,一个 USB 端口,没有以太网端口。紧随其后的是第一个 B 型板,它有 512MB 的内存,两个 USB 端口和一个以太网端口。图 5-1 显示了型号 A 板的版本 3 变体,命名为 Raspberry Pi 3A+。

img/313992_2_En_5_Fig1_HTML.jpg

图 5-1

树莓派 3A+(由树莓派基金会提供)

What Does The “+” Mean?

型号名称中的“+”符号表示它是同一版本的更新版本,只是做了一些改进。例如,3B+包括一个稍快的处理器和一系列细微的改进。通常,这些主板实际上是相同的,您可能不会注意到差异,但是如果您想要“最新”或“更好”的主板,您会想要带有“+”标志的主板。

图 5-2 显示了被指定为树莓派 3B+的版本 3 模型 B 板。请注意,电路板稍微大了一点,连接也多了。

img/313992_2_En_5_Fig2_HTML.jpg

图 5-2

树莓派 3B+(由树莓派基金会提供)

图 5-3 显示了被指定为 Raspberry Pi 4B 的最新型号 B 板。该图描述了 3B+型号的一些改进,包括更多 RAM、USB-C 电源、两个 HDMI 端口和 USB 3 支持。另外,它是迄今为止最快的树莓派电脑!

img/313992_2_En_5_Fig3_HTML.jpg

图 5-3

Raspberry Pi 4B(由 Raspberry Pi 基金会提供)

你经常可以在网上零售商和拍卖网站上找到树莓派 3A+的产品,价格比树莓派 3B+板略低。最新的 Raspberry Pi 4B 仍然需求量很大,因此您可能需要支付更多的价格来购买这些主板,但请货比三家,寻找建议零售价为 35 美元(1GB)、45 美元(2GB)、55 美元(4GB)或 91 美元(8GB)的零售商。如果您计划使用 Raspberry Pi 进行实验,并且不需要额外的内存来运行内存密集型应用程序,您可以使用 Raspberry Pi 3A+。

Tip

对于本书中的项目,建议使用树莓 Pi 3B+或最新的树莓 4B。本章和其余章节中的示例使用 B 型变型——树莓派 3B+或 4B。

董事会之旅

不比一副扑克牌大多少,Raspberry Pi 板包含许多用于连接设备的端口。本节介绍电路板的概况。如果你想跟着你的冲浪板走,拿着它,让树莓派的标志朝上。我将顺时针绕着棋盘工作。图 5-4 描绘了带有所有主要连接器标签的电路板图。

img/313992_2_En_5_Fig4_HTML.jpg

图 5-4

树莓派 3B+(由树莓派基金会提供)

在近侧的中央,您可以看到一个 HDMI 连接器。HDMI 连接器的左侧是 micro-USB 电源连接器,右侧是摄像头连接器和音频连接器。众所周知,某些主板上的电源连接器有点脆弱,所以插拔时要小心。使用您的树莓派时,请务必避免给这根电缆带来额外的压力。

在电路板的左侧,我们可以看到 WiFi 芯片的位置。microSD 卡连接器位于电路板的下侧。有趣的是,大多数保护套都不是为了保护 microSD 卡而设计的。安装后,microSD 卡会突出主板几厘米。

电路板的另一侧是通用输入/输出(GPIO)接头(双排 20 个引脚),可用于连接传感器和其它器件。在本章的后面,您将使用该连接器。

电路板的右侧是四个 USB 连接器和一个以太网连接器。连接到 Raspberry Pi 上的 USB 端口的外部供电 USB 集线器可以为一些电路板供电,但建议您使用连接到 micro-USB 连接器的专用电源。

Caution

因为板子很小,所以很容易把它用在不稳定的地方,比如在行驶中的汽车上或凌乱的桌子上。确保您的 Raspberry Pi 放在安全的地方。micro-USB 电源和 microSD 卡插槽似乎最容易损坏。 6

花点时间检查一下电路板的顶面和底面。如您所见,元件安装在两侧。这与大多数只在一面有元件的电路板不同。Raspberry Pi 两面都有组件的主要原因是它使用多层进行跟踪。这使得电路板可以更小,并且能够使用两个表面来安装其元件。这可能是考虑使用外壳的最有说服力的理由——保护电路板底部的元件,从而避免短路和电路板故障。

所需附件

Raspberry Pi 作为裸系统板出售,没有机箱、电源或外围设备。根据您计划如何使用 Raspberry Pi,您需要一些常见的附件。如果你像我一样一直在积累备用物品,快速翻一翻你的商店可能会找到你需要的大部分物品。

如果你想在控制台模式下使用 Raspberry Pi(没有图形用户界面),你需要一个 USB 电源(Raspberry Pi 4B 的 USB-C),一个键盘,一根 HDMI 线和显示器。树莓 Pi 3B、3B+板的电源最低额定值应为 2500mA 或更高,树莓 Pi 4B 的电源最低额定值应为 3000mA USB-C (15 瓦)或更高。如果你想使用带有图形用户界面的 Raspberry Pi,你还需要一个鼠标。

如果你不得不购买这些物品,坚持使用没有额外功能的普通品牌和型号。例如,避免使用最新的多功能键盘和鼠标。很可能他们需要的驱动程序并不适用于 Raspberry Pi 的各种操作系统。

你还必须有一张 microSD 卡。我推荐 16GB 或者更高的版本。回想一下,microSD 是唯一可用的板载存储介质。您需要将操作系统安装到卡上,并且您创建的任何文件都将存储在卡上。

如果您想在应用程序中使用声音,您还需要一套支持标准 3.5 毫米音频插孔的有源扬声器。最后,如果你想将你的 Raspberry Pi 连接到互联网,你需要接入 WiFi 接入端口或以太网集线器。

推荐配件

我强烈建议至少添加小的橡胶或硅胶自粘缓冲垫,以使电路板远离您的办公桌。电路板底部有许多尖尖的插脚,可能会接触到导电材料,从而导致短路,更糟糕的是,可能会出现树莓皮。这些保险杠在大多数家装和五金店都有售。

如果您计划将板从一个房间移动到另一个房间,或者您想要确保您的 Raspberry Pi 受到良好的保护以防意外损坏,您应该考虑购买一个盒子来放置板。有许多箱子可供选择,从简单的卡扣模型到由激光切割丙烯酸树脂甚至轧制铝制成的模型。下面的列表包括几个很好的选择,并附有供应商链接。

别住它

SparkFun 的 Pi Tin 是一款简单、透明的两件式表壳,可按扣在一起。它使用光导管使读取状态指示灯更容易,并有用于 GPIO 接头的切口。它不贵,是精打细算的人的绝佳选择。它是为树莓派 3B+制作的。图 5-5 显示了来自 SparkFun ( www.sparkfun.com/products/13103 )的 Pi Tin。

img/313992_2_En_5_Fig5_HTML.jpg

图 5-5

Pi Tin(由 SparkFun 提供)

用于 Raspberry Pi 4B 的铝制散热器外壳

SparkFun 的 Raspberry Pi 4B 的铝制散热器外壳是一个为 Raspberry Pi 4B 制造的带有散热器的良好金属外壳的例子。它由两块内置散热垫的金属制成,散热垫通过螺栓连接在一起。这是一种极简设计,不会使电路板变得更大(但更重)。它比基本的塑料外壳贵一点,但能提供更好的冷却和保护,而且对我来说,它看起来也比其他的好。图 5-6 展示了 SparkFun ( www.sparkfun.com/products/15773 )的铝制散热器外壳。我还见过这种设计的版本,如果你需要额外的冷却,顶部有一个或两个风扇。

img/313992_2_En_5_Fig6_HTML.jpg

图 5-6

用于 Raspberry Pi 4B 的铝制散热器外壳(由 SparkFun 提供)

Pibow 切

Pibow Coupé ( www.adafruit.com/products/2083 )可从 Adafruit 和其他供应商处获得,并为树莓 Pi 3B+和 4B 提供各种颜色(也可用于其他树莓 Pi 板)。它是由丙烯酸片制成的,但它们以一种新颖的切片图案排列。组装 Pibow 时,将树莓皮放在底板上,堆叠各层,最后放在顶板上。关键层为包括 GPIO 接头在内的所有端口提供切口。四个尼龙扣件把箱子固定成一个整体。组装后,外壳看起来很棒,而且非常坚固。图 5-7 显示了 Adafruit 生产的 3B 覆盆子酱的皮鲍 Coupé。

img/313992_2_En_5_Fig7_HTML.jpg

图 5-7

Pibow Coupé(由 Adafruit 提供)

Tip

如果这些案例都不符合你的需求或审美选择,你可以在 www.adafruit.com/category/395 的 Adafruit 找到一大堆选项。你一定会在那里找到你想要的东西!

除了机箱之外,您还应该考虑购买(或从备件中取出)一个带电源的 USB 集线器。USB 集线器电源模块应该为 2500mA 或更大(有些人建议您只需要 1500mA,但如果您想将 USB 设备连接到您的主板,则更大更好)。如果您计划使用消耗大量电力的 USB 设备,如 USB 硬盘驱动器或 USB 软导弹发射器,则需要一个供电集线器。

去哪里买

树莓派在欧洲已经有一段时间了。越来越容易找到,但很少有实体店有树莓酱。幸运的是,许多在线零售商都有它的存货,还有许多已知与树莓派配合使用的配件。以下是一些比较受欢迎的在线零售商,提供了他们的 Raspberry Pi 目录条目链接:

下一节将介绍一个关于开始使用 Raspberry Pi 的简短教程。如果您已经学会如何使用 Raspberry Pi,您可以跳到下一节,开始学习如何将传感器连接到您的电路板。

树莓派教程

Raspberry Pi 是一台拥有惊人的功能和通用性的个人电脑。您可能会认为它是一个玩具或一个严重受限的平台,但这与事实相差甚远。通过添加 USB、WiFi、以太网和 HDMI 视频等板载外围设备,Raspberry Pi 拥有了轻量级台式计算机所需的一切。对于配有 2GB、4GB 或 8GB 内存的 Raspberry Pi 4B 来说尤其如此——这些内存构成了不错的台式计算机!

此外,如果您考虑添加 GPIO 头,Raspberry Pi 就不仅仅是一台简单的桌面计算机,它还履行了作为一个旨在促进硬件实验的计算系统的职责。

以下部分提供了一个简短的教程,介绍如何开始使用新的 Raspberry Pi,从裸板到完全可操作的平台。许多优秀的作品更详细地讨论了这个主题。如果你发现自己陷入困境,或者想知道更多关于开始使用 Raspberry Pi 和 Raspbian 操作系统的信息,请参见 B. Schell 的使用 Raspberry Pi 进行计算(a press,2019)。如果你想了解更多关于在硬件项目中使用 Raspberry Pi 的信息,一个极好的深度资源是 W. Gay 的高级 Raspberry Pi(a press,2018)。

入门指南

如“所需附件”一节所述,您需要一个 microSD 卡(建议使用 16GB)、一个额定电流为 2500mA 或更高的 USB 电源(带有公 micro-USB 连接器(或用于 Raspberry Pi 4B 的 USB-C)、一个键盘、一个鼠标(可选)以及一个 HDMI 电缆和显示器或一个带有 HDMI 适配器的 DVI 显示器。然而,在您将这些东西插入您的 Raspberry Pi 并享受它的光辉之前,您需要为您的 microSD 卡创建一个引导映像。

安装启动映像包

安装启动映像的过程包括选择映像、下载它,然后将其复制到您的 microSD(因此简称为 SD)卡。以下部分详细介绍了相关步骤。

选择图像

你需要做的第一件事是决定你想要使用哪个操作系统版本。有几个很好的选择,包括标准的 Raspbian“buster”变体。每一个都可以作为一个称为图像或卡片图像的压缩文件。你可以在 Raspberry Pi foundation 下载页面上找到推荐图片的列表以及下载每张图片的链接: www.raspberrypi.org/downloads 。以下图片是该网站提供的部分图片:

  • Raspbian Buster :基本或默认图像。它基于 Debian,包含图形用户界面、开发工具和基本的多媒体功能。

  • Ubuntu MATE :为基本的桌面计算提供一个完整的、熟悉的(如果你知道 Ubuntu 的话)桌面环境。

  • Ubuntu Core:一个强化的 Ubuntu 核心操作系统,适用于安全性非常重要的场合。

  • Ubuntu Server :运行服务器应用的 Ubuntu Server 缩小版。

如果您刚刚开始使用 Raspberry Pi,您应该使用 Raspbian 图像。本书中的例子也推荐使用这个图像。

有两种方法可以为你的树莓派制作图像。最简单的方法是使用一个特殊的引导加载所谓的新的开箱即用软件(NOOBS),这是用来帮助简化您的板设置。另一种是下载特定的镜像,用镜像格式化 SD 卡。在接下来的部分中,我将向您展示这两种方法。

使用 NOOBS 安装

这是迄今为止最简单的方法来建立你的 SD 卡。你需要做的就是从 www.raspberrypi.org/downloads/noobs/ 下载并解压 NOOBS,格式化 SD 卡,然后复制文件。一旦您从 NOOBS 启动,您将被引导安装默认操作系统(Raspbian)或您选择的另一个系统(需要额外的下载)。

有一些学习如何用 NOOBS 安装 Raspbian 的优秀资源。 www.raspberrypi.org/help/videos/#noobs-setup 有一段很好听的视频。

还有一个完整的安装指南,引导您完成整个过程。参见projects.raspberrypi.org/en/projects/raspberry-pi-setting-up。对于那些完全不熟悉使用 Raspberry Pi 或者从未做过任何格式化或 SD 卡设置的人来说,这些链接是一个不错的选择。但是,我将在这里向那些对使用电脑感到舒适的人总结这些步骤:

  1. www.raspberrypi.org/downloads/noobs/ 下载 NOOBS 二进制。有两个选项:一个较小的在安装过程中使用互联网下载,另一个较大的有 Raspbian 映像。如果您的互联网连接速度较慢,或者在安装期间无法将您的 Raspberry Pi 连接到互联网,您应该选择带有 Raspbian 的版本(而不是 Lite 版本)。这是一个 Zip 文件,您可以下载并解压缩。

  2. 您必须格式化 SD 卡。为了获得最佳效果,请使用 16GB 或 32GB 的卡。你可以使用任何你想要的应用程序,但我发现最好的是从 www.sdcard.org/downloads/formatter/index.html 下载 SD Formatter,它适用于大多数平台。

  3. 接下来,从 NOOBS 档案中找到您之前解压缩的文件,并将它们全部复制到 SD 卡上。

  4. 将 SD 卡插入您的树莓派;连接鼠标、键盘和显示器。打开电源。

  5. 按照屏幕指示安装 Raspbian。

就这样!同样,在线教程和视频要详细得多,但是现在你已经了解了这个过程,跟随在线教程将会非常容易。

安装 Raspbian

如果想把 Raspbian 安装到 SD 卡或者不想用 NOOBS,可以从 www.raspberrypi.org/downloads/raspbian/ 下载 Raspbian。对于这本书,你应该选择名为“Raspbian Buster 带桌面和推荐软件”的那本。与 NOOBS 选件不同,该文件是一个可引导映像文件,需要通过特殊过程在 SD 卡上构建新的可引导映像。

Tip

参见 www.raspberrypi.org/documentation/installation/installing-images/README.md 获取树莓派安装图像的教程。

下载完映像后,首先解压缩文件,然后将映像传输(有时称为“写入”)到 SD 卡。有多种方法可以做到这一点。以下部分描述了一些适用于各种平台的简化方法。您的计算机上必须连接有 SD 卡读写器。有些系统内置了 SD 卡驱动器(联想笔记本电脑、苹果笔记本电脑和台式机等等)。

Windows 操作系统

要在 Windows 上创建 SD 卡映像,您可以使用来自 Launchpad ( https://launchpad.net/win32-image-writer )的 Win32 磁盘映像软件。下载该文件,并将其安装在您的系统上。如果还没有解压缩映像,请将其解压缩,然后将 SD 卡插入 SD 卡读写器。启动 Win32 Disk Imager 应用程序,选择顶部框中的图像,然后单击 WRITE 将图像复制到 SD。

Caution

复制过程会覆盖 SD 卡上已有的任何内容,因此请务必先将这些照片复制到您的硬盘上!

Mac OS X

要在 Mac 上创建 SD 卡映像,请下载映像并将其解压缩。将您的 SD 卡插入 SD 卡读写器。确保卡是用 FAT32 格式化的。接下来,打开系统报告。(提示:使用苹果菜单➤ 关于这台 Mac。)

如果您有内置读卡器,请单击读卡器,或者浏览 USB 菜单并找到 SD 卡。记下磁盘编号。比如可以是disk4

接下来,打开磁盘工具,卸载 SD 卡。你需要这样做来允许磁盘工具安装并连接到卡上。现在事情变得有点混乱。打开终端,运行以下命令,将磁盘号替换为 n,将镜像文件的路径和名称替换为<image_file>:

sudo dd if=<image_file> of=/dev/diskn bs=1m

此时,您应该会看到磁盘驱动器指示灯闪烁(如果有),您需要耐心等待。这个步骤可以在没有用户反馈的情况下运行一段时间。当命令提示符再次显示时,您就知道它完成了。

Linux 操作系统

要使用 Linux 创建 SD 卡映像,您需要知道 SD 卡读卡器的设备名称。执行以下命令查看当前安装的设备:

df -h

接下来,插入 SD 卡或连接读卡器,等待系统识别。再次运行该命令:

df -h

花点时间检查这个列表,并将其与第一次执行进行比较。“额外”设备是您的 SD 读卡器。记下设备名称,例如/dev/sdc1。该数字是分区号。所以,/dev/sdc1是分区 1,设备是/dev/sdc。接下来,卸载设备(我将使用前面的例子):

umount /dev/sdc1

使用以下命令写入映像,将设备名称替换为<device>,将映像文件的路径和名称替换为<image_file>(例如/dev/sdcmy_image.img):

sudo dd bs=4M if=<image_file> of=<device>

此时,您应该会看到磁盘驱动器指示灯闪烁(如果有),您可能需要耐心等待。这个步骤可以在没有用户反馈的情况下运行一段时间。当命令提示符再次显示时,您就知道它完成了。

启动

要启动您的 Raspberry Pi,请插入带有新图像的 SD 卡,并插入您的外围设备。等到最后插上 USB 电源。因为树莓派没有开/关开关,所以一通电就会启动。下面描述了您将看到的首次引导 Raspbian 的过程。设置步骤仅执行一次(但是如果需要,您可以在以后更改设置)。

当您打开 Raspberry Pi 时,系统会启动,然后开始加载操作系统。您会看到一个很长的语句列表,这些语句传达了每个子系统在加载时的状态,后面是一个欢迎标语。你不必试图去阅读甚至理解所有出现的行, 7 但是你应该注意任何错误或警告。

您可能还会看到一条关于调整启动设备大小的消息,您的 Raspberry Pi 可能会重新启动。这是自动的,没什么好担心的。事实上,它可以确保启动卷扩展到 microSD 支持的最大大小。当引导序列完成时,您将看到 Raspbian 桌面,如图 5-8 所示。

img/313992_2_En_5_Fig8_HTML.jpg

图 5-8

Raspbian 桌面

请注意,桌面中央有一个打开的对话框。同样,这些步骤只会在第一次引导时执行一次。这些步骤包括以下内容:

  • 欢迎来到树莓派:点击下一步开始设置。您可以取消,稍后再运行安装程序。

  • 设置国家:选择你的国家、语言和时区。点击下一步继续。

  • 设置密码:选择默认用户的密码。点击下一步继续。

  • 设置屏幕:如果你的屏幕边缘显示一个黑色矩形,你可以勾选复选框,让视频适配器在下次启动时正确同步。点击下一步继续。

  • 选择 WiFi 网络:选择您的 WiFi 接入点连接互联网。您可以点击跳过跳过该步骤,或者点击下一步继续。

  • 更新软件:如果你已经连接到互联网,你可以选择下载并安装 Raspbian 的更新。这是强烈推荐的,当您选择这个选项时,您将经历几个更多的信息对话框,向您显示更新的进度。您可以点击跳过跳过该步骤。完成后点击下一步继续。

  • 设置完成:设置完成。点击下一步继续,如果您选择了任何需要重启的选项,系统将立即重启。

图 5-9 显示了从左上开始从左到右工作的每个步骤。

img/313992_2_En_5_Fig9_HTML.jpg

图 5-9

首次启动设置序列

当系统下一次启动时,您将看到 Raspbian 桌面,其中配置了您的设置。如果您设置了 WiFi 连接,它会自动重新连接。不错。

SD 卡的维护和进给

想象一下这个场景。您正在创建文件、下载文档等等。您的生产力很高,并且您正在享受您的新的低成本、超级酷的树莓派。现在想象一下电源线不小心被踢出墙外,你的树莓 Pi 没电了。没什么大不了的,对吧?嗯,大部分时间。

SD 卡没有你的硬盘坚固。您可能已经知道突然关闭 Linux 系统是不明智的,因为这样做会导致文件损坏。嗯,在 Raspberry Pi 上,它会导致您的磁盘映像完全丢失。症状包括从轻微的读取错误到无法在引导程序上引导或加载映像。这是有可能发生的——而且据其他人报道,这种情况已经发生了不止一次。

这并不是说所有的 SD 卡都是坏的,也不是说 Raspberry Pi 有问题。意外断电时的损坏是这种介质的副作用。有人报告说,某些 SD 卡比其他 sd 卡更容易出现这种情况。你能做的保护自己的最好的事情是使用一个已知的可以与 Raspberry Pi 一起工作的 SD 卡,并确保使用sudo shutdown -h now命令关闭系统——并且永远不要以任何其他方式关闭系统。

你也可以备份你的 SD 卡。详见 http://elinux.org/RPi_Beginners#Backup_your_SD_card

Tip

如果您在使用您的树莓派时需要任何帮助,在 www.raspberrypi.org/help/ 有非常有用的文章,官方文档在 www.raspberrypi.org/documentation/

GPIO 引脚映射

Raspberry Pi 有一个特殊的硬件特性,称为通用 I/O (GPIO)头。它位于主板的左上方,类似于软盘驱动器的磁头。 8 接头由两排 20 个公插脚组成。

所有 GPIO 引脚都可以配置为输入(读取)或输出(写入)。电压读数可用于数字 I/O。具体来说,当电压小于 1.7V 时,值为 0;大于 1.7V 的值为 1。对于输出引脚,您可以设置 0 至 3.3V 的电压。

图 5-10 显示了树莓 Pi 3B+(和 4B)的 GPIO 头的布局。

img/313992_2_En_5_Fig10_HTML.jpg

图 5-10

GPIO 引脚分配(raspberrypi.org 提供)

请注意,引脚没有按顺序命名。例如,有 GPIO 1 和 GPIO 2,但它们在接头上并不相邻(GPIO 1 在接头位置或引脚 28 的右侧,GPIO 2 在接头位置或引脚 3 的左侧)。这种命名可能会引起混淆,因为它不符合您的预期,也不反映 Arduino 等微控制器的整洁布局。因此,当使用 GPIO 接头时,您应该仔细检查您的引脚选择。

Caution

不要将 pin 码误认为 GPIO 号。务必用接头上使用的位置或引脚号仔细检查要使用的连接名称。例如,GPIO 16 不在管脚 16,而是在管脚 36。

还要注意,有些引脚有两个名称。例如,GPIO 14 和 GPIO 15 也被分别命名为 TXD(发送)和 RXD(接收)。这些引脚可用于串行通信。GPIO 18 和 GPIO 21 标记为 PWM(脉冲波调制),用于为 led、电机和类似设备供电。GPIO 0 和 GPIO 1 也分别命名为 SDA 和 SCL,用于 I2C 通信。I2C 是一种快速数字协议,使用两条线(加上电源和地)从电路(或设备)读取数据。最后,GPIO 9、GPIO 10 和 GPIO 11 也分别命名为 MISO、MOSI 和 SCKL,用于 SPI 通信。

Caution

所有引脚都被限制在 3.3V。试图发送超过 3.3V 可能会损坏您的 Raspberry Pi。在连接到您的 Raspberry Pi 之前,请始终测试您的电路的最大电压。您还应该将电流限制在不超过 5mA。

Adafruit 有一个漂亮的附件,使得使用 GPIO 更加容易。这是一个小 PCB,上面印有 GPIO 的布局。它叫做 GPIO 参考卡,可以在 https://www.adafruit.com/product/2263 找到。图 5-11 显示了卡片的样子(为简洁起见旋转,为细节放大)。我建议你下次从 Adafruit 订购时买一个。

img/313992_2_En_5_Fig11_HTML.jpg

图 5-11

GPIO 参考卡(Adafruit 提供)

如果您想确保您的 Raspberry Pi 免受更高电压和电流的影响,大多数扩展板都有额外的电源保护电路。有许多扩展板可用,包括格特板( www.element14.com/community/docs/DOC-51726?ICID=raspberrypi-gert-banner )。这本书不包括扩展板的使用,但如果您的传感器涉及复杂的电路,需要更多的端口或额外的功能,如电机控制器或继电器,您可能会考虑使用扩展板。

这里使用的不是扩展板,而是一个简单的原型板。我选择的这个叫做 Pi T-Cobbler Plus 分线板,可以从 Adafruit ( www.adafruit.com/products/2028 )买到。它具有一根带状电缆和一个兼容试验板的连接器,其引脚排列顺序与 Raspberry Pi 上的相同。图 5-12 显示了圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆头圆除了通过将 Raspberry Pi 连接到试验板来简化 GPIO 工作之外,该板不提供任何其他功能。

img/313992_2_En_5_Fig12_HTML.jpg

图 5-12

Pi T-Cobbler Plus 分线板(由 Adafruit 提供)

Caution

无论何时想要将传感器或电路连接到 GPIO 接头,无论是直接连接(不推荐)还是通过分线板连接(推荐),都应该先关闭 Raspberry Pi。这听起来可能很不方便,甚至在你完成一个项目时会觉得很痛苦,但这是确保你不会意外短路或连接错误的最好方法。

我发现最好在关闭 Raspberry Pi 电源的情况下进行连接,并通过几次验证连接是否连接到正确的引脚。很容易连接到错误的引脚——数量太多,而且非常接近。奇怪的密码排列也没有帮助。经过适当的告诫,让我们开始使用您的 Raspberry Pi GPIO 并连接一些传感器吧!

Pi T-Cobbler 的一个更小的替代方案是将带状连接器放在中间,在试验板上占用更少的空间。这个版本叫做圆周率补鞋匠加版,在 www.adafruit.com/products/2029 有售。图 5-13 所示为圆周率补鞋匠加号。

img/313992_2_En_5_Fig13_HTML.jpg

图 5-13

Pi 鞋匠加分线板(由 Adafruit 提供)

Note

Pi T-Cobbler Plus 和 Cobbler Plus 可能会部分组装。您可能需要将试验板接头焊接(或让人焊接)到分线板。

你还可以在流行的在线拍卖和电子产品折扣网站上找到 Pi T-Cobbler Plus 和 Cobbler Plus 的几种变体。一个很好的例子就是来自 SparkFun ( www.sparkfun.com/products/13717 )的 SparkFun Pi Wedge。该板的布局与 Pi T-Cobbler Plus 相似,但 GPIO 引脚的排列顺序略有不同(但为了便于参考,该板印制在 PCB 上)。

无论您选择哪种分线板,它都可以让您将您的 Raspberry Pi 连接到试验板上,从而使电子设备(和传感器)的试验更加容易。它不会保护你免受意外的电力过载,所以要注意这一点。以下项目使用试验板;所以,如果你有一个馅饼,把你的覆盆子馅饼连到你的面包板上。

现在,您已经知道如何将硬件连接到 GPIO 引脚,您需要知道需要什么软件才能让您编写程序来读写这些引脚。

所需软件

您需要安装一些软件包来使用 GPIO 头。本节研究使用 Python 编程语言所需的软件。您可以使用 C 和 Scratch 语言扩展,但 Python 是最好学的,因为它在语法上易于阅读和掌握。此外,Raspberry Pi 的设计者最初选择 Python 作为其唯一的语言,所以您可能会在互联网上找到更多的例子来参考想法和帮助。我们已经看过了关于 MicroPython 的教程,我们将在 Raspberry Pi 上使用的 Python 几乎是相同的,至少在我们如何编写代码方面是如此。

默认情况下,Raspbian 包含 Python 和许多支持库。但它不包括你需要的一切。为了完全访问所有 GPIO 特性,您还需要 Raspberry Pi Python GPIO 模块(RPi.GPIO)用于与 GPIO 引脚通信,pySerial用于连接串行设备,以及python-smbus用于访问 I2C 总线。如果你用的是扩展板,厂商可能也有你需要安装的特殊模块。Pi T-Cobbler 分线板不需要特殊模块。如果你对写游戏感兴趣,你可能也想安装python-game包。

但是首先,你需要一些先决条件。您必须使用以下命令安装额外的 Python 模块。您的 Raspberry Pi 需要连接到互联网才能执行这些命令,因为它们从互联网下载模块:

sudo apt-get update
sudo apt-get install python-dev

要安装RPi.GPIO模块、pySerialpython-smbus模块,发出以下命令:

sudo apt-get install python-rpi.gpio
sudo apt-get install python-serial
sudo apt-get install python-smbus

既然您已经加载了软件,是时候进行实验了!如果你没有插上分线板,关闭你的 Raspberry Pi(从终端使用sudo shutdown –h now或从 Raspbian 菜单关闭)并连接分线板,然后重启你的 Raspberry Pi。

项目:硬件“你好,世界!”

在这个项目中,您将构建一个“Hello,World!”树莓派的项目。这个项目使用了一个 LED,Raspberry Pi 通过调用 Python 库函数来打开和关闭它。这是一个很好的开始项目,但它与如何使用传感器无关。

因此,在本节中,您将使用一个经过修改的 LED 项目,我们只需通过添加一个传感器来触发 LED。在这种情况下,您仍然可以通过使用可以说是最基本的传感器来简化事情:按钮。目标是每当按钮被按下时,LED 就会亮起。

硬件连接

让我们首先组装一个树莓派、来自 Adafruit 的派 T-Cobbler(可选)、试验板、一个 LED 和一个按钮。你从关掉树莓派开始。

将分线板插入试验板。将 3.3V 引脚(而非 5V 引脚)连接到试验板电源轨,将接地引脚连接到接地轨,并在电路板的另一侧绕一圈。此连接为分线板供电。因此,您不需要试验板电源。

将 LED 和按钮放在试验板的一侧,如图 5-14 所示。请记住,LED 上最长的边是正极。注意,为了简洁起见,我展示了 Raspberry Pi 和 Pi Cobbler 分线板,但没有展示 Raspberry Pi 的电缆。Raspberry Pi 通过带状电缆连接到 Pi Cobbler Plus(您可以以同样的方式使用 T-Cobbler Plus)。确保对齐电缆,使彩色条纹(指示针脚 1)与连接器上的针脚 1 对齐。对树莓派和分线板都这样做。

img/313992_2_En_5_Fig14_HTML.jpg

图 5-14

带按钮的 LED 图示

Caution

将 5V 引脚连接到 GPIO 接头上的任何其他引脚都会损坏您的 Raspberry Pi。如果您使用需要 5V 输入的传感器,请确保仔细检查其最大输出为 3.3V 或更低。

你就快到了。现在,将一根跳线从电源轨连接到按钮的一侧,并将按钮的另一侧连接到分线板上的引脚 GPIO 17(左侧的引脚 6)。将 LED 连接到试验板上的地线和一个 150 欧姆的电阻(颜色:棕色、绿色、棕色、金色)。LED 的另一侧应连接到分线板上的 GPIO 7 引脚(参见图 5-13 )。

你还需要一个电阻,在按钮没按下的时候把按钮拉低。将一个 10K 欧姆电阻器(颜色:棕色、黑色、橙色、金色)放在按钮一侧,用导线连接到 GPIO 17 引脚和地。LED 最短的一侧是接地侧。这一侧应该是连接到电阻的一侧。你连接电阻的方向无关紧要。它用于限制 LED 的电流。再次检查图纸,以确保您有一个类似的设置。

A Cool Gadget

使用树莓酱最酷的小工具之一是 Adafruit ( www.adafruit.com/products/942 )的树莓酱碟安装板。这个小亚克力板有足够的空间放一个全尺寸的面包板和一个覆盆子馅饼。它甚至有安装孔,用于将树莓酱固定在盘子上,还有小橡胶脚,用于保持盘子远离工作表面。下图显示了工作中的安装板。请注意,这张图片显示的是一个旧的 Raspberry Pi 板,但所有最新的 Raspberry Pi 板都适合。

img/313992_2_En_5_Figa_HTML.jpg

虽然你可以用 Lexan 或 Plexiglas 制作你自己的覆盆子 Pi 安装板,但 Adafruit 产品比你自己制作的要好一点。大约 23.00 美元,你就可以把你的树莓派和实验板放在一起,避免树莓派底部的组件与导电材料接触而导致你的桌子划伤和短路。

写剧本

这个项目需要的脚本需要两个引脚:一个输出和一个输入。输出引脚将点亮 LED,输入引脚将检测按钮接合。将正电压连接到按钮的一侧,另一侧连接到输入引脚。当检测到输入引脚上的电压时,告诉 Raspberry Pi 处理器向输出引脚发送正电压。这种情况下,LED 的正极连接到输出引脚。

现在,用下面的命令打开一个文本编辑器,创建一个新的 Python 模块(或者使用您选择的编辑器,比如从编程菜单中选择的 Thonny Python IDE):

nano hello_raspi.py

当编辑器打开时,键入以下代码来设置 GPIO 模块并建立引脚分配:

import RPi.GPIO as GPIO   # GPIO library
LED_PIN = 7
BUTTON_PIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BUTTON_PIN, GPIO.IN)

如图 5-13 所示,输入引脚为 GPIO 17 引脚,输出引脚为 GPIO 7 引脚。让我们使用一个变量来存储这些数字,这样您就不必担心重复硬编码的数字(并冒着出错的风险)。使用GPIO.setup(方法设置每个管脚的模式(GPIO.INGPIO.OUT)。

你还需要放置代码在输入引脚状态为 ( ==1)时打开 LED。在这种情况下,当输入引脚状态为 ( 1)时,使用GPIO.output()方法将输出引脚设置为,同样,当输入引脚状态为 ( 0)时,将输出引脚设置为。我们将代码封装在一个try…except…finally块中,以捕获键盘中断,并在最后清理 GPIO 分配(关闭输出引脚)。以下代码显示了所需的语句:

GPIO.output(LED_PIN, GPIO.LOW)
while 1:
    if GPIO.input(BUTTON_PIN) == 1:
        GPIO.output(LED_PIN, GPIO.HIGH)
    else:
        GPIO.output(LED_PIN, GPIO.LOW)

Tip

回想一下,缩进在 Python 中很重要。缩进的语句形成一个代码块。例如,要对一个 if 语句执行多个语句,当条件被评估为 true 时,缩进所有要执行的行。

现在让我们看看清单 5-1 中的完整脚本,以及适当的文档。

#
# RasPi Simple Sensor - Beginning Sensor Networks 2nd Edition
#
# For this script, we explore a simple sensor (a pushbutton) and a simple response to sensor input (a LED). When the
  sensor is activated (the button is pushed), the LED
  is illuminated.

import RPi.GPIO as GPIO

# Pin assignments
LED_PIN = 7
BUTTON_PIN = 17

# Setup GPIO module and pins
GPIO.setmode(GPIO.BCM)
GPIO.setup(LED_PIN, GPIO.OUT)
GPIO.setup(BUTTON_PIN, GPIO.IN)

# Set LED pin to OFF (no voltage)
GPIO.output(LED_PIN, GPIO.LOW)
try:
    # Loop forever
    while 1:
        # Detect voltage on button pin
        if GPIO.input(BUTTON_PIN) == 1:
            # Turn on the LED
            GPIO.output(LED_PIN, GPIO.HIGH)
        else:
            # Turn off the LED
            GPIO.output(LED_PIN, GPIO.LOW)
except KeyboardInterrupt:
    print("Done!")
finally:
    GPIO.cleanup()

Listing 5-1Simple Sensor Script

Tip

为了节省大量的打字时间,您可以从 Apress 网站下载本章或书中任何示例的代码。

一旦您输入了脚本或将它下载到您的 Raspberry Pi 上,您就可以运行它了。要运行 Python 脚本,请按如下方式启动它:

python3 hello_raspi.py

Note

我使用python3和后来的pip3,它们是为 Python 版本 3 执行的。在最新版本的 Raspbian 上使用这个。旧版本或其他发行版可能不支持 Python3,在这种情况下,您可以省略命令中的“3”。

测试传感器

一旦脚本启动,您会在树莓 Pi 上看到什么?如果你做对了每一件事,答案是“什么都没有”它只是用一个黑暗的 LED 回望着你——几乎是嘲弄地。现在,按下按钮。LED 是否点亮?如果是这样,那么恭喜你:你是一名 Raspberry Pi Python GPIO 程序员!

如果 LED 不亮,按住按钮一两秒钟。如果这不起作用,请检查您的所有连接,以确保您插入了试验板上的正确线路,并且您的 LED 正确就位,长边连接到电阻器和 GPIO 7 引脚。

另一方面,如果 LED 灯一直亮着,尝试将按钮重新调整 90 度。您可能将按钮设置在了错误的方向。

尝试这个项目几次,直到兴奋过去。如果你是覆盆子酱的老手,那可能是很短的一段时间。如果这一切对你来说都是新的,那就按下那个按钮,享受建造第一个传感器节点的荣耀吧!

现在,你怎么阻止它?因为你编码了一个死循环(故意的),你需要使用Ctrl+C来取消脚本。这不会损害你的树莓皮或 GPIO 或电路。

下一节研究一个使用温度和湿度传感器的更复杂的传感器节点。

为了更多乐趣

为了让这个脚本更加用户友好,您可以修改代码以更优雅地退出。以下是一些有趣的建议:

  • 循环不超过 10,000 次迭代。提示:使用一个变量并递增它。

  • 使用第二个 LED,并设置代码来切换两个 LED,以便当按下按钮时,一个点亮,另一个熄灭。

  • 使用一个第二个按钮,这样当按下第二个按钮时,循环终止。提示:使用sys.exit()break

用 Raspberry Pi 托管传感器

Raspberry Pi 的 GPIO 引脚使其成为托管传感器的理想平台。因为大多数传感器只需要很少的支持组件,所以您通常可以在一个 Raspberry Pi 上托管多个传感器。例如,可以托管一个温度传感器,甚至多个温度、气压、湿度和其他传感器,用于从给定地点采样天气状况。

Analog Only?

Raspberry Pi GPIO 引脚不支持数字信号,它们都是模拟引脚。这是有助于压低价格的许多小成本因素之一。要访问数字信号,您需要一个模数控制器。如果遇到需要使用数字传感器的情况,可以看看 Adafruit ( www.adafruit.com/products/1083 )的带可编程增益放大器的 12 位 ADC—4 通道。

正如我在第一章中所讨论的,有许多传感器可供选择。SparkFun 和 Adafruit 都有很好的网站,提供大量关于他们销售的产品的信息。你也可以谷歌一下 Raspberry Pi 使用模拟传感器的例子。

虽然本章演示了如何使用连接到试验板的分线板在 Raspberry Pi 中托管传感器,但仅限使用模拟和 3.3V 最大电压的限制使得 Raspberry Pi 不如 Arduino 通用。此外,您必须使用 root 运行 Python 脚本,在 Raspberry Pi 上托管传感器比使用 Arduino 更难(但也不过分)也更麻烦。

您仍然可以将传感器直接连接到 Raspberry Pi,如下一节所示。但是,您可能想考虑使用 Raspberry Pi 作为聚合节点,使用 XBee 连接到 XBee 托管的传感器,甚至 Arduino 托管的传感器。但首先,让我们看看如何将传感器连接到树莓 Pi 并读取一些数据。

为了使事情变得简单,你使用一个类似于你在第三章中使用的项目。更具体地说,您用一个 Raspberry Pi 和一个温度传感器构建一个传感器节点。在开始之前,让我们讨论一些与使用 Raspberry Pi GPIO 头相关的安全因素。

项目:建立一个树莓温度传感器节点

您要探索的下一个项目是另一个温度传感器示例。这一次,你使用一个温度传感器,利用一个特殊的数字协议来发送。如前所述,Raspberry Pi 没有模数转换器。

虽然这可能是另一个温度传感器节点,但该项目还会让您体验如何读取使用单线协议的数字传感器,该协议内置于 Raspberry Pi GPIO 中。具体来说,您可以使用 SparkFun 和 Adafruit 提供的 DS18B20 数字温度传感器。

在某些方面,这个项目的硬件部分比之前的项目更容易,因为零件更少;但是代码更复杂。让我们从硬件设置开始。

硬件设置

这个项目需要的硬件是一个试验板、一个用于 Raspberry Pi 的分线板(如 Pi Cobbler+)、一个 DS18B20 数字温度传感器、一个 0.10mF 电容和一些跳线。将分线板插入试验板,将引脚 1 (3.3V)对准试验板上的位置 1。这将有助于您通过使用试验板上的数字更容易地确定引脚的方向,从而快速找到引脚,而不用数(和数错)它们。图 5-15 显示了分线板的正确方向。为了简洁,我省略了带状电缆。

img/313992_2_En_5_Fig15_HTML.jpg

图 5-15

将温度传感器连接到树莓皮

接下来,将温度传感器安装到分线板的右侧,引脚 1 位于左侧。如果您拿着传感器,使平面朝向您,则针 1 位于传感器平面的左侧。如图 5-15 所示,在引脚 3(右侧)和引脚 2(中间)之间连接 0.10 毫伏的电容器或使用跳线。

将分线板的电源连接到试验板的电源轨,并将分线板的地线连接到试验板的地线轨,如图 5-14 所示。接下来,将电源连接到传感器的引脚 3,将接地连接到引脚 1。最后,将传感器的引脚 2 连接到 GPIO 4。为什么选择 GPIO 4?因为传感器是数字传感器,您可以使用单线设施(因为它只使用一根数据线)来读取数据。酷吧。

What About The Waterproof Version?

如果您有 DS18B20 数字温度传感器的防水版本,该传感器有四根导线。通常,电线被涂上红色、黑色、白色或黄色或橙色,以及铜色或银色。不使用铜线或银线;这是屏蔽的一部分。红线连接电源,黑线接地,白色或黄色或橙色是数据线。

测试硬件

一旦你仔细检查了你的连接,继续启动你的 Raspberry Pi。因为传感器使用单线协议读取数据,所以您可以使用 Raspberry Pi 内置的功能从传感器读取数据。这远不如编写 Python 脚本优雅,但它允许您在开始编程之前看到一切都正常工作。

您将使用一个名为modprobe的特殊实用程序。这个实用程序将模块加载(或卸载)到 Linux 内核中。在其他操作系统的术语中,它加载设备驱动程序。modprobe 实用程序可以做的远不止加载模块(驱动程序);要了解更多信息,请参见 http://linux.die.net/man/8/modprobe

您想要加载的模块被命名为w1-gpiow1-thermw1-gpio模块注册并加载连接到 GPIO 4 引脚的新传感器。w1-therm模块注册并加载支持温度传感器的模块。

要使用这些模块,我们必须在重启您的 Pi 之前,通过编辑添加下面一行到/boot/config.txt来启用它们。使用命令sudo nano /boot/config.txt并在文件中的任意位置添加下面一行(对于其他示例,最好是靠近末尾)。请注意,这使引脚 4 成为默认引脚。您可以通过添加选项gpiopin=N到该行来指定您想要使用的 pin。编辑文件,然后在连接传感器之前关闭您的 Raspberry Pi。有关编辑文件包括添加多个传感器的更多信息,请参见 https://pinout.xyz/pinout/1_wire#

dtoverlay=w1-gpio

当您使用modprobe加载这些模块中的每一个模块时(首先是w1-gpio,Raspberry Pi 启用引脚 GPIO 4 上的数据收集,并从传感器读取数据并将其存储在文件中。该文件以28开头,后跟一个唯一的文件名。如果您有其他传感器,每个传感器都会有一个文件。

Note

无论是否创建了传感器,都会创建该文件,但要查看有意义的数据,您应该关机,连接传感器,然后在检查该文件之前重新启动。

该文件包含从传感器读取的原始数据。您可以打开该文件并检查其内容以查看原始数据。你可能会看到没有什么意义的数据,但我们会破译代码中的数据。清单 5-2 显示了用于加载模块的命令,然后检查文件以及显示所创建文件名称的输出。

pi@raspberrypi:~ $ sudo modprobe w1-gpio
pi@raspberrypi:~ $ sudo modprobe w1-therm
pi@raspberrypi:~ $ cd /sys/bus/w1/devices/28-1a1970a65dff
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ ls
driver  hwmon  id  name  power  subsystem  uevent  w1_slave
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ cat w1_slave
3e 01 55 00 7f ff 0c 10 8d : crc=8d YES
3e 01 55 00 7f ff 0c 10 8d t=19875
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ cat w1_slave
3f 01 55 00 7f ff 0c 10 ce : crc=ce YES
3f 01 55 00 7f ff 0c 10 ce t=19937
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ cat w1_slave
3f 01 55 00 7f ff 0c 10 ce : crc=ce YES
3f 01 55 00 7f ff 0c 10 ce t=19937
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ cat w1_slave
3e 01 55 00 7f ff 0c 10 8d : crc=8d YES
3e 01 55 00 7f ff 0c 10 8d t=19875
pi@raspberrypi:/sys/bus/w1/devices/28-1a1970a65dff $ cat w1_slave
5e 01 55 00 7f ff 0c 10 6c : crc=6c YES
5e 01 55 00 7f ff 0c 10 6c t=21875

Listing 5-2Testing the Temperature Sensor Hardware

注意,在这个例子中,我运行了cat 9 (连接并打印)实用程序多次打印出文件中的数据。在运行实用程序时,我将手放在传感器上,以模拟温度的升高。你能看出这些值是如何变化的吗?

软件设置

此项目所需的软件已经安装。您将编写一个简短的 Python 脚本来读取来自传感器的数据,并将其显示到标准输出(终端窗口)。首先导入所需的模块,如下所示:

import glob
import os
import time

接下来,使用名为os的 Python 模块进行系统调用,运行前面示例中的两个modprob e 命令。在这种情况下,你使用os.system()方法:

os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

在您进入代码读取文件之前,让我们通过声明几个变量来包含目录和文件名,使它变得更容易。在这种情况下,您不知道文件名,但知道目录。您可以使用 glob 模块在特定目录中搜索与通配符匹配的文件。您可以使用以下代码来实现这一点:

base_dir = '/sys/bus/w1/devices/'
datadir = glob.glob(base_dir + '28*')[0]
datafile = datadir + '/w1_slave'

请注意,您知道父目录和目录的开始部分。glob 模块为您完成所有工作。如果有多个目录匹配通配符,调用将返回一个列表。在这种情况下,您只有一个传感器,因此您只能看到一个目录。

现在您已经准备好从文件中读取数据了。您可以随心所欲地设计自己的代码,但是我选择编写两个方法(用 def 指令定义)。我将使用一种方法打开文件并读取文件中的所有行(数据),使用另一种方法使用读取的数据计算摄氏和华氏温度。我们来看第一种方法。我把它命名为read_data():

def read_data():
    f = open(datafile, 'r')
    lines = f.readlines()
    f.close()
    return lines

如您所见,它非常简单,读起来就像您想象的步骤。具体来说,您打开文件,读取文件中的所有行,关闭文件,并返回您读取的内容。

现在我们来看第二种方法。我把它命名为get_temp():

def get_temp():
    temp_c = None
    temp_f = None
    lines = read_data()

    while not lines[0].strip().endswith('YES'):
        time.sleep(0.25)
        lines = read_data()
    pos = lines[1].find('t=')
    if pos != -1:
        temp_string = lines[1][pos+2:]
        temp_c = float(temp_string) / 1000.00
        temp_f = temp_c * 9.00 / 5.00 + 32.00
    return temp_c, temp_f

这个方法有两个部分。第一部分使用前面的方法从文件中读取数据,并检查第一行(Python 中数组和列表从索引 0 开始)以查看状态是否为YES。如果不是,您再次读取文件中的行,并重复,直到您找到一个具有正确、有效状态的文件。

下一部分在文件中查找读取的数据。在这种情况下,您查找以 t=开头的子字符串,然后读取其后的数据,并将其转换为摄氏度和华氏度。您返回这些值用于打印数据。

让我们把它们放在一起。清单 5-3 显示了完整的脚本,包括文档。打开一个编辑器,创建一个名为 pi_temp.py 的文件,并输入所示的源代码。随意修改它,以适应你的心情或特定的幽默。

花一些时间来研究这个完整的代码,直到您理解它是如何工作的。在这个文件中有几个 python ms10,所以如果有些代码不是很清楚,不要被吓倒。例如,看看倒数第二行的 print 语句。这个语句可以用不同的方式编写,但是这里显示的是大多数 Python 程序员采用的公认标准。

# RasPi Temperature Sensor - Beginning Sensor Networks Second Edition
#
# For this script, we explore connecting a digital temperature
  sensor to the Raspberry Pi and reading the data. We display
  the temperature in Celsius and Fahrenheit.

# Import Python modules (always list in alphabetical order)
import glob
import os
import time

# Issue the modprobe statements to initialize the GPIO and
# temperature sensor modules
os.system('modprobe w1-gpio')
os.system('modprobe w1-therm')

# Use glob to search the file system for files that match the prefix.
base_dir = '/sys/bus/w1/devices/'
# Save the directory to the file.
datadir = glob.glob(base_dir + '28*')[0]
# Create the full path to the file
datafile = datadir + '/w1_slave'

# Procedure for reading the raw data from the file.
# Open the file and read all of the lines then close it.
def read_data():
    f = open(datafile, 'r')
    lines = f.readlines()
    f.close()
    return lines

# Read the temperature and return the values found.
def get_temp():
    # Initialize the variables.
    temp_c = None
    temp_f = None
    lines = read_data()

    # If the end of the first line ends with something other than 'YES'
    # Try reading the file again until 'YES' is found.
    while not lines[0].strip().endswith('YES'):
        time.sleep(0.25)
        lines = read_data()

    # Search the second line for the data prefixed with 't='
    pos = lines[1].find('t=')

    # A return code of -1 means it wasn't found

.
    if pos != -1:

        # Get the raw data located after the 't=' until the end of the line.
        temp_string = lines[1][pos+2:]

        # Convert the scale for printing
        temp_c = float(temp_string) / 1000.00

        # Convert to Fahrenheit
        temp_f = temp_c * 9.00 / 5.00 + 32.00

    # Return the values read
    return temp_c, temp_f

# Main loop. Read data then sleep 1 second until cancelled with CTRL+C.
while True:
    temp_c, temp_f = get_temp()
    print("Temperature is {0} degrees Celsius, "
          "{1} degrees Fahrenheit.".format(temp_c, temp_f))
    time.sleep(1)

Listing 5-3The pi_temp.py Script

花几分钟时间仔细检查您的文件,以确保您正确输入了所有语句。如果您的台式机或笔记本电脑上有编辑器,您可能希望使用它来创建和编辑文件,并使用语法检查功能来捕捉任何错误。该脚本无法在您的台式机或笔记本电脑上正确运行,但检查语法会有很大帮助。

现在软件写好了,让我们看看它做什么。

测试传感器

和前面的项目一样,您需要使用sudo python ./pi_temp.py以 root 用户身份运行脚本。当您这样做时,您可能不会立即看到任何输出,但是在一两秒钟内,您应该会开始看到如下所示的输出:

$ python ./pi_temp.py
Temperature is 20.062 degrees Celsius, 68.11160000000001 degrees Fahrenheit.
Temperature is 20.187 degrees Celsius, 68.3366 degrees Fahrenheit.
Temperature is 21.25 degrees Celsius, 70.25 degrees Fahrenheit.
Temperature is 21.437 degrees Celsius, 70.5866 degrees Fahrenheit.
Temperature is 21.875 degrees Celsius, 71.375 degrees Fahrenheit.
Temperature is 21.687 degrees Celsius, 71.0366 degrees Fahrenheit.
Temperature is 21.5 degrees Celsius, 70.7 degrees Fahrenheit.
Temperature is 21.187 degrees Celsius, 70.1366 degrees Fahrenheit.
Temperature is 21.0 degrees Celsius, 69.8 degrees Fahrenheit.

如果您遇到语法错误,请返回并检查您是否完全按照清单 5-3 中所示输入了每一行。Python 非常擅长提供足够的信息来修复大多数语法错误。如果遇到任何错误,您不仅会看到错误是什么,还会看到发生错误的文件的行号。修复错误后,再次尝试该脚本,直到看到正确的输出。

下一节将探讨一个更复杂的项目,在该项目中,Raspberry Pi 与使用 I2C 协议的数字传感器进行通信。

为了更多乐趣

为了让这个项目更有趣一点,尝试连接第二个传感器(同类型),并打印出数据,包括读取数据的传感器。提示:您可以使用文件中嵌入的序列号来识别传感器,并且您应该将它们并联。也就是说,每个传感器连接到相同的地(引脚 1)和电源连接(引脚 3)。每个传感器的数据输出(引脚 2)连接到同一个 GPIO 引脚。

对于额外的特殊乐趣,修改代码以检测何时传感器读取失败并打印适当的错误消息。你能发现这在哪里是可能的吗? 11 给你一个提示:如果没有找到t=,在get_temp()方法中会发生什么?

项目:建立一个树莓气压传感器节点

这个项目演示了如何使用不同类型的传感器——使用 I2C 总线的传感器。为此,您需要四根电线来连接传感器,并需要一些设备来与传感器通信。幸运的是,Raspberry Pi 有这样的功能,但是要使它可用还需要一些工作。您将使用 Adafruit ( www.adafruit.com/products/2651 )的 BMP280 传感器模块。图 5-16 显示了 Adafruit 的模块。

img/313992_2_En_5_Fig16_HTML.jpg

图 5-16

BMP280 I2C 传感器(Adafruit 提供)

默认情况下,Raspberry Pi 上的 I2C 功能是禁用的。在查看该项目的硬件设置之前,让我们启用 I2C 特性。你可以通过打开首选项菜单下的 Raspberry Pi 配置工具轻松完成。点击界面选项卡,勾选 I2C 复选框,如图 5-17 所示。

img/313992_2_En_5_Fig17_HTML.jpg

图 5-17

启用 I2C 接口

接下来,我们需要安装 I2C 工具和实用程序。为此,您可以打开终端并输入以下命令:

$ sudo apt-get install i2ctools

就这样!现在,您可以开始连接硬件了。这个项目的硬件部分比上一个项目要简单,因为部件少,但代码更复杂。让我们从硬件设置开始。

硬件设置

这个项目需要的硬件是一个试验板、一个用于 Raspberry Pi 的分线板(如 Pi Cobbler+)、一个 BMP280 传感器模块和一些跳线。将分线板插入试验板,将引脚 1 (3.3V)对准试验板上的位置 1。

将 Raspberry Pi Cobbler+板上的 5V 引脚连接到传感器模块上。将接地线连接到传感器上的接地线。Raspberry Pi 上的 I2C 引脚是 GPIO 0 (SDA)和 GPIO 1 (SCL)。将这些引脚连接到传感器模块上的相应引脚(在 BMP280 上,SDA 连接到 SCK,在 BMP280 上,SCL 连接到 SDI)。图 5-18 显示了物理连接。

img/313992_2_En_5_Fig18_HTML.jpg

图 5-18

将 BMP280 传感器连接到树莓 Pi

测试硬件

一旦你仔细检查了你的连接,继续启动你的 Raspberry Pi。登录后,运行以下命令:

sudo i2cdetect -y 1

该命令使用实用程序来检测 I2C 总线上的任何传感器。我说传感器是因为你可以使用 I2C 协议连接多个传感器。每个传感器都有自己的地址。以下代码显示了运行命令的输出:

$ sudo i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- --
10: 10 -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- 2a -- -- -- -- --

40: -- -- -- 43 -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- 77

请注意,在示例中,图表显示第 7 列第 70 行的数据,值为 77。这意味着传感器位于地址0x77(十六进制)。如果安装了其他传感器,它们也会出现在图表中。记住这个地址,因为代码需要它。

Note

如果您在该命令的输出中看不到任何设备,请用–y 0试试看是否会产生任何输出。例如,使用 sudo i2cdetect -y 0

软件设置

该项目所需的软件需要您之前安装的 Python 库,以及一个用于与 BMP280 通信的特殊库。您需要一个特殊的模块,因为 I2C 协议是双向的,并且大多数 I2C 组件被设计为响应一个或多个命令来调用数据生成。在这种情况下,您需要一个支持 BMP280 传感器模块的 Python 模块。

这个库是由 Adafruit 的好心人创建的,可以从 Python 包库(PyPi 12 )下载。您可以在 https://readthedocs.org/projects/adafruit-circuitpython-bmp280/downloads/pdf/latest/ 找到该库的综合文档。

要下载并安装模块,在终端中发出sudo pip3 install adafruit-circuitpython-bmp280命令。此命令将为您下载并安装库及其所有依赖库。这有多酷?注意有些 Raspbian 发行版可能不需要sudo。下面显示了安装的摘录。

$ sudo pip3 install adafruit-circuitpython-bmp280
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting adafruit-circuitpython-bmp280
  Downloading https://www.piwheels.org/simple/adafruit-circuitpython-bmp280/adafruit_circuitpython_bmp280-3.1.2-py3-none-any.whl
...
Successfully installed Adafruit-Blinka-3.9.0 Adafruit-PlatformDetect-2.2.0 Adafruit-PureIO-1.0.4 adafruit-circuitpython-bmp280-3.1.2 adafruit-circuitpython-busdevice-4.1.4

I2C Sensor Libraries

有许多 I2C 传感器模块可供选择。然而,相应的 Python(或其他语言)库并没有为它们全部构建。在决定在网络中使用传感器之前,您应该研究是否有支持传感器的库。如果您是一名程序员,您可以通过检查数据手册并编写适当的命令与传感器交互,修改现有代码(库)以增加对新传感器的支持。

您将使用 Adafruit_BMP280 代码模块从 I2C 总线读取数据。Python 模块支持许多 I2C 模块,包括 BMP280,并且基于也在这个目录中的Adafruit_I2C模块。要使用Adafruit_BMP280库,您需要导入 BMP280 模块的类及其依赖项,如下所示:

import board
import busio
import time
import adafruit_bmp280

接下来,您需要初始化该类。在这种情况下,您使用来自busio模块的名为I2C的助手类来配置对 I2C 接口的使用。接下来,我们使用该实例初始化 BMP280 类,如下所示。您将该实例分配给一个变量,以便以后可以使用它来调用库。默认地址是0x77。如果您从i2cdetect实用程序中看到一个不同的地址,将十六进制地址和参数address=<hexadecimal>传递给 BMP280 类的构造函数。

i2c = busio.I2C(board.SCL, board.SDA)
bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)

还有一个选项我们可能想做:校准模块在我们的位置使用。我们通过为我们的位置传递 hPa 值来实现这一点。在这种情况下,我们使用美国大部分地区的标准(但在某些地区可能会有所不同)。

bmp280.sea_level_pressure = 1013.25

一旦完成,您只需要使用库提供的属性读取值,然后打印出信息。您使用的主要属性如下所示:

bmp280.temperature
bmp280.pressure
bmp280.altitude

现在让我们把它们放在一起。清单 5-4 显示了脚本的完整清单。打开一个编辑器,创建一个名为pi_bmp280.py的文件,输入显示的源代码。

正如您所看到的,在新库的帮助下,您的 Python 脚本变得非常短并且非常容易编写。这是一个很好的例子,展示了 Python 社区的成员如何自由地(好吧,至少是大多数)交流思想,并为常见和不常见的任务共享代码。

#
# RasPi I2C Sensor - Beginning Sensor Networks 2nd Edition
#
# For this script, we connect to and read data from an
# I2C sensor. We use the BMP280 sensor module from Adafruit
# or Sparkfun to read barometric pressure and altitude
# using the Adafruit I2C Python code.

import board
import busio
import time

import adafruit_bmp280

# First, we configure the BMP280 class instance for our use.
i2c = busio.I2C(board.SCL, board.SDA)
bmp280 = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)

# Calibrate the pressure (hPa) at sea level for our location
# in this case the East coast US
bmp280.sea_level_pressure = 1013.25

# Read data until cancelled
while True:
    try:
        # Read the data
        pressure = float(bmp280.pressure)
        altitude = bmp280.altitude

        # Display the data
        print("The barometric pressure at altitude {0:.2f} "
              "is {1:.2f} hPa.".format(pressure, altitude))

        # Wait for a bit to allow sensor to stabilize
        time.sleep(3)

    # Catch keyboard interrupt (CTRL-C) keypress
    except KeyboardInterrupt:
        break

Listing 5-4The pi_bmp280.py Script

现在软件写好了,让我们看看它做什么。

测试传感器

和前面的项目一样,您需要使用python3 ./pi_bmp280.py以 root 用户身份运行脚本。当您这样做时,可能不会马上看到任何输出;但是在一两秒钟内,您应该会看到如下所示的输出:

$ python3 ./pi_bmp280.py
The barometric pressure at altitude 1007.25 is 50.37 hPa.
The barometric pressure at altitude 1007.28 is 50.24 hPa.
The barometric pressure at altitude 1007.24 is 50.40 hPa.
The barometric pressure at altitude 1007.24 is 50.74 hPa.
The barometric pressure at altitude 1007.22 is 50.45 hPa.
The barometric pressure at altitude 1007.25 is 50.27 hPa.
The barometric pressure at altitude 1007.25 is 49.83 hPa.
The barometric pressure at altitude 1007.26 is 50.19 hPa.
The barometric pressure at altitude 1007.27 is 50.16 hPa.

下一节将探索一个更复杂的项目,在这个项目中,Raspberry Pi 是一个数据收集器(一个聚合节点),通过 XBee 无线连接到传感器节点来托管传感器数据。您将重用在第四章中创建的传感器节点。如果您没有通读并成功构建第四章中的项目,您可能想要在继续之前返回并完成最后一个项目。

为了更多乐趣

如你所见,BMP280 传感器读取大气压力,但它也读取温度。更改前面的代码以读取温度数据和大气压力。

项目:为 XBee 传感器节点创建 Raspberry Pi 数据收集器

这个项目结合了你在本章中所学的树莓 Pi,第二章中所学的 XBee,以及第四章中所学的 XBee 传感器节点。更具体地说,您使用一个 Raspberry Pi 和一个使用 XBee 模块连接传感器和 Raspberry Pi 的远程传感器。你已经从第四章了解了基本知识,所以让我们开始吧。

XBee 传感器节点

按照第四章的内容创建 XBee 传感器节点。提醒一下,该节点的构造如图 5-19 所示。

img/313992_2_En_5_Fig19_HTML.jpg

图 5-19

XBee 传感器节点

如果您尚未配置第四章中的传感器节点,或者如果您需要重置模块,您应该首先确保加载了最新的固件,并使用表 5-1 中所示的设置。请注意,您不需要第四章中的 IR 设置,但是如果您想要重用您在该章中使用的模块,这是可以的。

表 5-1

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| D3 | a3/上帝 3 | 触发模拟或数字数据记录 | 2—ADC |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | TMP36 |
| V+ | 电源电压阈值 | 电源电压 | FFFF(总是发送) |

Tip

确保使用“TMP36”作为节点 id ( NI)。除非节点 id 与以下代码中的值匹配,否则项目不会返回结果。

协调器节点应使用最新加载的固件和表 5-2 中所示的设置进行类似配置。

表 5-2

XBee 协调器选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | 协调者 |

五金器具

这个项目的硬件设置非常简单。你需要做的就是使用 GPIO 头的串行接口来连接 XBee 的串行接口。就这么简单!在所有硬件连接完成并确认正确之前,不要打开 Raspberry Pi 或传感器节点的电源。我将在本节稍后告诉您何时通电。

你需要一个试验板和一个 XBee 试验板适配器,就像你在第四章中使用的那个;将其插入试验板。然后插入您的 Raspberry Pi 试验板适配器。现在,将 3.3V 和地线连接到 XBee 适配器上的引脚。如果你使用的是 SparkFun ( www.sparkfun.com/products/11373 调节的 XBee Explorer,可以连接 5V 电源,因为 XBee Explorer 可以调节电源(因此得名)。如图所示,SparkFun 板一侧的接头中排列有串行接口引脚。它还具有板载电压调节功能,以便在您意外将 5V 引脚而不是 3.3V 引脚连接到 explorer 时保护 XBee。

Note

如果您已经将试验板接头焊接到 XBee 适配器,但没有焊接串行 I/O 接头,请花一点时间来完成这项工作。您可以通过另一个接头连接 XBee,但统一的接头使它更容易一些。

接下来,将 Raspberry Pi Cobber+上的 TXD(输出)引脚 GPIO 14 连接到 XBee Explorer 上的 DIN 引脚。然后将 Raspberry Pi Cobble+上的 RXD(输入)引脚 GPIO 15 连接到 XBee Explorer 上的 DOUT 引脚。图 5-20 显示了完成的连接。

img/313992_2_En_5_Fig20_HTML.jpg

图 5-20

将 XBee 连接到树莓 Pi

如果您没有使用 SparkFun 适配器,请务必查看适配器上的文档,以确保您连接了正确的引脚。将您的协调器 XBee 模块插入 XBee。

你还需要做一件事。Raspberry Pi 的设计者包括了在启动时将串行终端连接到 Raspberry Pi 的功能。首选项中有一个设置允许您启用此界面。默认情况下,它是禁用的。

要打开串行接口,打开终端并输入命令sudo raspi-config。这将打开 Raspberry Pi 配置工具。要启用串行接口,选择raspi-config中的接口选项。最简单的方法是使用箭头键并突出显示选择,然后按下键进入

在下一个屏幕上,选择 Seria l 选项,然后按照接下来的三个屏幕禁用通过串口登录 shell(选择,启用硬件串口(选择,然后在确认页面上按回车

当控制返回到主屏幕时,使用选项卡键选择完成,然后选择以允许树莓派重新启动。图 5-21 显示了屏幕顺序和您应该选择的选项。

img/313992_2_En_5_Fig21_HTML.jpg

图 5-21

启用串行接口(raspi-config)

在你的机器重新启动后,我们可以安装项目所需的软件。

软件

在编写脚本之前,您需要下载并安装一个特殊的库。这个项目需要的软件是由 Digi 提供的一个特殊的 Python 模块,它是专门为封装 XBee 协议和帧处理机制(使之易于使用)而开发的。要安装该模块,请发出以下命令:

$ sudo pip3 install digi-xbee
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting digi-xbee
  Downloading https://www.piwheels.org/simple/digi-xbee/digi_xbee-1.3.0-py3-none-any.whl (224kB)
    100% |████████████████| 225kB 10kB/s
Collecting srp (from digi-xbee)
  Downloading https://www.piwheels.org/simple/srp/srp-1.0.15-py3-none-any.whl
Requirement already satisfied: pyserial>=3 in /usr/lib/python3/dist-packages (from digi-xbee) (3.4)
Requirement already satisfied: six in /usr/lib/python3/dist-packages (from srp->digi-xbee) (1.12.0)
Installing collected packages: srp, digi-xbee
Successfully installed digi-xbee-1.3.0 srp-1.0.15

这个库有许多用于 XBee 模块的类,并且有许多例子可以用于您自己的项目。我们将会看到使用图书馆的一种方式。关于该库中其他特性的更多细节,您可以使用以下 URL 下载 Digi-Python 程序员指南: https://xbplib.readthedocs.io/en/latest/getting_started_with_xbee_python_library.html

如果你想看库本身的代码,你可以在 GitHub 的 https://github.com/digidotcom/xbee-python 看到。

一旦安装了库,您就可以开始使用它了。对于这个例子,我们将使用基本的XBeeDevice库以及助手库IOLineIOMode,如下所示:

from digi.xbee.devices import XBeeDevice
from digi.xbee.io import IOLine, IOMode

在实例化这些类之前,让我们做一些定义,使维护更容易,并提高源代码的可读性。在这种情况下,您创建一个对串行端口、波特率、远程节点名称(NI来自传感器节点设置)、传感器在远程节点上连接的模拟线路以及采样速率的引用:

SERIAL_PORT = "/dev/ttyS0"
BAUD_RATE = 9600
# Analog pin we want to monitor/request data
ANALOG_LINE = IOLine.DIO3_AD3
SAMPLING_RATE = 15

Note

如果您已经更改了 XBee 模块的波特率,则必须在此处使用该波特率。

现在您可以实例化 XBeeDevice 类,如下所示。传入我们之前定义的串口和波特率。

device = XBeeDevice(SERIAL_PORT, BAUD_RATE)

现在我们准备进入程序的核心部分。我们将使用两种你在使用方法之前可能没见过的技术。首先,我们将创建一个方法来连接到 ZigBee 网络,并检索我们可以用来读取数据的远程 XBee 模块类的实例。其次,我们将创建第二种方法,用于以采样率读取数据。这被称为回调方法,因为每当采样率要求读取数据时,库就用它来“回调”你的程序。

下面的代码显示了我们用来设置名为get_remote_device()的远程设备的方法。在这里,我们连接到网络,然后请求网络通过节点 id ( NI)搜索我们的远程节点,如果找到,我们捕获它的地址,请求模拟线路,最后设置采样率。如果你按照第四章中的示例操作,这应该看起来很熟悉。

def get_remote_device():
    """Get the remote node from the network
    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    remote_device = xbee_network.discover_device(REMOTE_NODE_ID)
    if remote_device is None:
        print("ERROR: Remove node id {0} not found.".format(REMOVE_NODE_ID))
        exit(1)
    remote_device.set_dest_address(device.get_64bit_addr())
    remote_device.set_io_configuration(ANALOG_LINE, IOMode.ADC)
    remote_device.set_io_sampling_rate(SAMPLING_RATE)

下面显示了名为io_sample_callback()的回调方法。在这个方法中,我们在名为sample的参数中获得以特殊格式传递的样本(参见下面的代码)。我们还将远程节点的实例作为remote,将样本的时间作为time。因此,您不仅可以看到样本的来源,还可以看到样本的日期和时间。酷!

def io_sample_callback(sample, remote, time):
    print("Reading from {0} at {1}:".format(REMOTE_NODE_ID,
                                            remote.get_64bit_addr()))
    # Get the temperature in Celsius
    temp_c = ((sample.get_analog_value(ANALOG_LINE) * 1200.0 / 1024.0) – 500.0) / 10.0
    # Calculate temperature in Fahrenheit
    temp_f = ((temp_c * 9.0) / 5.0) + 32.0
    print("\tTemperature is {0}C. {1}F".format(temp_c, temp_f))
    # Calculate supply voltage
    volts = (sample.power_supply_value * (1200.0 / 1024.0)) / 1000.0
    print("\tSupply voltage = {0}v".format(volts))

注意,我们还在这个方法中放置了解密数据的代码。具体来说,我们用摄氏度计算温度,将其转换为华氏度,然后计算节点处的电压。我将代码的细节作为练习,但是我们已经在前面的例子中使用了公式。

好了,现在剩下的就是把所有的东西放在一起。我们遵循 Digi-Python 示例中提出的模式,将代码封装在一个try...except块中来捕获错误。在块内部,我们简单地打开设备类实例(启动实例),获取远程设备,并注册回调方法。我们使用无限循环配置和键盘上的 trapCtrl+C命令来退出脚本。

try:
    print("Welcome to example of reading a remote TMP36 sensor!")

    device.open()  # Open the device class
    # Setup the remote device
    get_remote_device()
    # Register a listener to handle the samples received by the local device.
    device.add_io_sample_received_callback(io_sample_callback)
    while True:
        pass
except KeyboardInterrupt:
    if device is not None and device.is_open():
        device.close()

在我们把代码看作一个单元之前,让我们讨论一下参数sample中返回的数据的格式。这里,由于我们请求模拟线路 DIO3_AD3,我们看到样本是一个包含两个字典的元组:第一个是数据样本,第二个是电源电压。如果您简单地打印出sample参数的值,您将会看到这是如何形成的。下面的代码显示了测试运行中返回的一些数据的示例:

> {[IOLine.DIO3_AD3: 563], [Power supply voltage: 3277]}
> {[IOLine.DIO3_AD3: 565], [Power supply voltage: 3269]}
> {[IOLine.DIO3_AD3: 565], [Power supply voltage: 3273]}
> {[IOLine.DIO3_AD3: 564], [Power supply voltage: 3272]}
> {[IOLine.DIO3_AD3: 563], [Power supply voltage: 3273]}
> {[IOLine.DIO3_AD3: 569], [Power supply voltage: 3269]}
> {[IOLine.DIO3_AD3: 566], [Power supply voltage: 3269]}
> {[IOLine.DIO3_AD3: 564], [Power supply voltage: 3268]}

现在让我们把所有这些放在一起,看看完整的代码是什么样子的。打开一个编辑器,创建文件pi_xbee.py,输入清单 5-5 中的代码。

#
# Raspberry Pi Data Aggregator - Beginning Sensor Networks Second Edition
#
# For this script. we read data from an XBee remote data mode
# from a ZigBee Coordinator connected to a Raspberry Pi via a
# serial interface.
#
# The data read includes an analog value from DIO3/AD3 and the current voltage value.
#
from digi.xbee.devices import XBeeDevice
from digi.xbee.io import IOLine, IOMode

# Serial port on Raspberry Pi
SERIAL_PORT = "/dev/ttyS0"
# BAUD rate for the XBee module connected to the Raspberry Pi
BAUD_RATE = 9600
# The name of the remote node (NI)
REMOTE_NODE_ID = "TMP36"
# Analog pin we want to monitor/request data
ANALOG_LINE = IOLine.DIO3_AD3
# Sampling rate
SAMPLING_RATE = 15

# Get an instance of the XBee device class
device = XBeeDevice(SERIAL_PORT, BAUD_RATE)

# Method to connect to the network and get the remote node by id
def get_remote_device():
    """Get the remote node from the network

    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    remote_device = xbee_network.discover_device(REMOTE_NODE_ID)
    if remote_device is None:
        print("ERROR: Remove node id {0} not found.".format(REMOVE_NODE_ID))
        exit(1)
    remote_device.set_dest_address(device.get_64bit_addr())
    remote_device.set_io_configuration(ANALOG_LINE, IOMode.ADC)
    remote_device.set_io_sampling_rate(SAMPLING_RATE)

def io_sample_callback(sample, remote, time):
    print("Reading from {0} at {1}:".format(REMOTE_NODE_ID,
                                            remote.get_64bit_addr()))
    # Get the temperature in Celsius
    temp_c = ((sample.get_analog_value(ANALOG_LINE) * 1200.0 / 1024.0) – 500.0) / 10.0

    # Calculate temperature in Fahrenheit
    temp_f = ((temp_c * 9.0) / 5.0) + 32.0
    print("\tTemperature is {0}C. {1}F".format(temp_c, temp_f))

    # Calculate supply voltage
    volts = (sample.power_supply_value * (1200.0 / 1024.0)) / 1000.0
    print("\tSupply voltage = {0}v".format(volts))

try:
    print("Welcome to example of reading a remote TMP36 sensor!")

    device.open()  # Open the device class

    # Setup the remote device
    get_remote_device()

    # Register a listener to handle the samples received by the local device.
    device.add_io_sample_received_callback(io_sample_callback)
    while True:
        pass
except KeyboardInterrupt:
    if device is not None and device.is_open():
        device.close()

Listing 5-5Reading Data from an XBee Module

测试最终项目

现在您可以运行您的脚本并观察输出。以python3 ./pi_xbee.py开始脚本。清单 5-6 显示了该脚本的示例输出。为了看到数据的变化,我只需触摸传感器(小心不要短路引脚),让我的体温增加读数。

$ python3 ./pi_xbee.py
Welcome to example of reading a remote TMP36 sensor!
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 15.9765625C. 60.7578125F
    Supply voltage = 3.840234375v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.2109375C. 61.1796875F
    Supply voltage = 3.830859375v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.2109375C. 61.1796875F
    Supply voltage = 3.835546875v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.09375C. 60.96875F
    Supply voltage = 3.834375v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 15.9765625C. 60.7578125F
    Supply voltage = 3.835546875v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.6796875C. 62.0234375F
    Supply voltage = 3.830859375v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.328125C. 61.390625F
    Supply voltage = 3.830859375v
Reading from TMP36 at 0013A2004192DB79:
    Temperature is 16.09375C. 60.96875F
    Supply voltage = 3.8296875v

Listing 5-6Output of the XBee Aggregate Node Script (pi_xbee.py)

你看到类似的东西了吗?如果是这样,那么您做得很好,现在已经具备了构建传感器节点和基于 Raspberry Pi 的传感器数据聚合器所需的知识。

如果您根本看不到任何数据,请返回第四章,并遵循本章最后一个项目的故障排除提示。您可以随时将协调器模块插入 USB explorer,并使用个人计算机上的终端程序来查看是否正在从 XBee 传感器节点接收数据。

Tip

如果您没有看到任何数据,请关闭您的传感器节点和 Raspberry Pi。从 Raspberry Pi 中移除协调器模块,将其插入 USB XBee Explorer,将其插入您的个人计算机,并将串行程序连接到端口,然后给传感器节点加电。过一会儿,您应该会看到协调器节点正在接收数据。

为了更多乐趣

如果您想扩展项目,可以添加第二个 XBee 传感器节点,并修改代码以指定数据来自哪个节点。例如,脚本应该记录(写入标准输出)数据源以及来自 XBee 的传感器数据。

部件购物清单

完成本章中的项目需要一些组件;它们在表 5-3 中列出。其中一些,比如 XBee 模块和支持硬件,也包含在第四章的购物清单中。

表 5-3

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| 树莓 Pi 型号 B | 大多数在线商店,如 Adafruit、SparkFun 和 Mouser | $35.00 及以上 13 | one |
| 5V 电源(3A,3B,3B+) | www.pishop.us/product/wall-adapter-power-supply-micro-usb-2-4a-5-25v/ | $9.95 | one |
| 5V 电源(4B) | www.raspberrypi.org/products/type-c-power-supply/ | $8.00 | one |
| HDMI 或 HDMI 转 DVI 电缆 | 大多数在线和零售商店 | 变化 | one |
| HDMI 或 DVI 监视器 | 大多数在线和零售商店 | 变化 | one |
| USB 键盘 | 大多数在线和零售商店 | 变化 | one |
| USB A 型到微型 USB 插头 | 大多数在线和零售商店 | 变化 | one |
| SD 卡,2GB 或更大 | 大多数在线和零售商店 | 变化 | one |
| 树莓派补鞋匠+(你也可以用 T-补鞋匠+) | www.adafruit.com/products/2028 | $7.95 | one |
|   | www.adafruit.com/products/2029 |   |   |
| 150 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| 0.10 毫伏电容 | 大多数在线和零售商店 | 变化 | one |
| 10K 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| 发光二极管 | 大多数在线和零售商店 | 变化 | one |
| DS18B20 数字温度传感器 | www.adafruit.com/product/374 | $3.95 | one |
| 按钮 | 大多数在线和零售商店 | 变化 | one |
| BMP280 传感器 | www.adafruit.com/products/2651 | $9.95 | one |
| 试验板(非迷你) | www.sparkfun.com/products/9567 | $5.95 | Two |
| www.adafruit.com/product/64 |
| XBee Explorer 受管制 | www.sparkfun.com/products/11373 | $9.95 | Two |

摘要

在本章中,您探索了 Raspberry Pi 的起源,包括硬件之旅和可用操作系统列表。您了解了如何创建 SD 引导映像,并学习了如何开始使用 Raspberry Pi。

您还了解了如何使用 GPIO 头点亮 LED,从传感器读取数据,以及通过 XBee 从 XBee 传感器节点读取数据。通过执行这些项目,您对 Raspberry Pi 的了解要比大多数人多得多。

到目前为止,您应该开始看到构建传感器网络的各个部分开始融合在一起。您已经探索了用于无线通信的 XBee 模块、使用 XBee 和 Raspberry Pi 的主机传感器,甚至是如何使用这两个平台构建聚合传感器节点。

我们通过检查 Arduino 平台来完成我们的传感器节点之旅。在下一章,我们将学习什么是 Arduino,以及如何用它来安装传感器。酷!

六、基于 Arduino 的传感器节点

物理计算的最大进步之一是微控制器的激增。微控制器由一个处理器、一个小指令集、存储器和可编程输入/输出电路组成,包含在单个芯片上。微控制器通常与支持电路和连接一起封装在一个小的印刷电路板上。

微控制器用于嵌入式系统中,可以定制小型软件程序来控制和监控硬件设备,使其成为传感器网络的理想选择。Arduino 平台是最成功和最受欢迎的微控制器之一。

在本章中,您将探索 Arduino 平台,目标是使用 Arduino 管理传感器节点。您会看到一个关于 Arduino 的简短教程和几个帮助您开始使用 Arduino 的项目。

什么是 Arduino?

Arduino 是一个由开源软件环境支持的开源硬件原型平台。它于 2005 年首次推出,设计目标是使硬件和软件易于使用,并尽可能提供给最广泛的受众。因此,使用 Arduino 并不需要成为电子专家。

最初的目标受众包括艺术家和爱好者,他们需要一个微控制器来使他们的设计和创作更有趣。然而,由于其易用性和多功能性,Arduino 很快成为更广泛的受众和更广泛的项目的选择。

这意味着您可以将 Arduino 用于各种项目,从对环境条件做出反应到控制复杂的机器人功能。Arduino 还通过实际应用使学习电子学变得更加容易。

帮助 Arduino 平台快速采用的另一个方面是通过 Arduino 官方网站( http://arduino.cc/en/ )提供的丰富信息的贡献者社区不断增长。当您访问该网站时,您会发现一个优秀的“入门”教程,以及一个有用的项目想法列表和一个完整的类似 C 语言的参考指南,用于编写控制 Arduino 的代码(称为草图)。

Arduino 还提供了一个名为 Arduino IDE 的集成开发环境。IDE 在您的计算机(称为主机)上运行,在那里您可以编写和编译草图,然后通过 USB 连接将它们上传到 Arduino。IDE 可用于 Linux、Mac 和 Windows。它是围绕一个专门为编写代码而设计的文本编辑器和一组支持编译和加载草图的有限功能而设计的。

草图是以一种特殊的格式编写的,只包含两个必需的方法——一个在 Arduino 复位或通电时执行,另一个持续执行。因此,您的初始化代码放在setup()中,控制 Arduino 的代码放在loop()中。这种语言类似于 C 语言,你可以定义自己的变量和函数。关于写草图的完整指南,见 http://arduino.cc/en/Tutorial/Sketch

您可以通过编写封装了某些功能(如联网、使用存储卡、连接数据库、做数学等)的库来扩展草图的功能并提供重用。

Arduino 支持许多模拟和数字引脚,您可以使用这些引脚来连接各种设备和组件并与之交互。主流主板有特定的引脚布局或接头,允许使用称为屏蔽的扩展板。Shields 允许您为 Arduino 添加额外的硬件功能,如以太网、蓝牙和 XBee 支持。Arduino 和盾牌的物理布局允许你堆叠盾牌。因此,您可以拥有以太网屏蔽和 XBee 屏蔽,因为两者使用不同的 I/O 引脚。在探索 Arduino 在传感器网络中的应用时,您将学习插针和屏蔽的使用。

接下来的部分将研究各种 Arduino 板,并简要描述它们的功能。我按上市时间列出这些主板,从最新型号开始。有更多的电路板和变体可供使用,在本书出版时可能会有一些新的电路板问世,但这些都是传感器网络项目中通常使用的电路板。

Arduino 型号

越来越多的 Arduino 板可供使用。一些是为特殊应用而配置的,而另一些是为不同的处理器和内存配置而设计的。一些主板被认为是官方的 Arduino 主板,因为它们是由 Arduino.cc 标记和认可的。由于 Arduino 是使用知识共享署名共享许可进行许可的,任何人只要遵守许可,都可以构建 Arduino 兼容的主板。本节研究一些更受欢迎的 Arduino 品牌主板。

Arduino 板的基本布局包括一个 USB 连接、一个电源连接器、一个复位开关、用于电源和串行通信的 led 以及一组用于连接屏蔽的标准间隔接头。官方电路板采用独特的蓝色印刷电路板,上面印有白色字样。除了一个型号之外,所有官方主板都可以安装在一个机箱中(它们在 PCB 上有孔,用于安装螺钉)。例外情况是 Arduino 设计用于安装在试验板上。

优诺牌

Uno 板是大多数 Arduino 新手都会选择的标准 Arduino 板。它采用了 ATmega328P 处理器;14 个数字 I/O 引脚,其中 6 个可作为脉宽调制 1 输出;和 6 个模拟输入引脚。Uno 板有 32KB 的闪存和 2KB 的 SRAM。

Uno 有表面贴装器件(SMD)和标准 IC 插座两种形式。如果您希望使用外部 IC 编程器来构建定制解决方案,IC 插座版本允许您更换处理器。详情和完整的数据表可在 https://store.arduino.cc/usa/arduino-uno-rev3 获得。它有一个标准的 USB 型连接器,支持所有屏蔽。图 6-1 显示了 Arduino Uno 板卡。

img/313992_2_En_6_Fig1_HTML.jpg

图 6-1

Arduino Uno Rev3(由 Arduino.cc 提供)

还有一种版本的主板内置 Wi-Fi 芯片,可以用于传感器网络或使用 Wi-Fi 屏蔽有问题的情况(缺乏空间,与其他屏蔽冲突等)。).虽然名称相同,但它在几个方面与标准的 Uno 不同。除了 Wi-Fi 芯片,它还有一个不同的处理器和一个更少的 PWM 引脚。你可以在 https://store.arduino.cc/usa/arduino-uno-WiFi-rev2 了解更多关于 Uno Wi-Fi 板的信息。图 6-2 显示了 Arduino Uno Wi-Fi 板。

img/313992_2_En_6_Fig2_HTML.jpg

图 6-2

Arduino Uno Wi-Fi 版本 2(由 Arduino.cc 提供)

【男性名字】利奥纳多

Leonardo 板代表 Arduino 平台中的另一种标准板。略有不同的是,虽然它支持标准接头布局,但它还有一个 USB 控制器,允许电路板作为 USB 设备(如鼠标或键盘)出现在主机上。该板使用较新的 ATmega32u4 处理器,具有 20 个数字 I/O 引脚,其中 12 个可用作模拟引脚,7 个可用作脉宽调制(PWM)输出。它有 32KB 的闪存和 2.5KB 的 SRAM。

莱昂纳多比乌诺有更多的数字引脚,但继续支持大多数盾牌。USB 连接使用较小的 USB 连接器。该板有带接头和不带接头两种。图 6-3 描绘了一个官方的莱昂纳多董事会。详情和完整的数据表可在 https://store.arduino.cc/usa/leonardo 找到。

img/313992_2_En_6_Fig3_HTML.jpg

图 6-3

Arduino Leonardo(由 Arduino.cc 提供)

由于

Arduino Due 是一款基于 Atmel SAM3X8E ARM Cortex-M3 处理器的新型、更大、更快的主板。处理器为 32 位处理器,板卡支持海量的 54 个数字 I/O 端口,其中 14 个可用于 PWM 输出;12 路模拟输入;4 个 UART 芯片(串行端口)以及 2 个数模转换器(DAC)和 2 个双线接口(TWI)引脚。新处理器有几个优点:

  • 32 位寄存器

  • DMA 控制器(允许独立于 CPU 的内存任务)

  • 512KB 闪存

  • 96KB SRAM

  • 84 兆赫时钟

Due 具有更大的外形尺寸(称为 mega footprint ),但仍然支持使用标准屏蔽和 mega 格式屏蔽。这种新板有一个明显的限制:不像其他板在 I/O 引脚上可以接受高达 5V 的电压,在 I/O 引脚上的 Due 限制为 3.3V。详情和完整的数据表可在 https://store.arduino.cc/usa/due 找到。

Arduino Due 旨在用于需要更多处理能力、更多内存和更多 I/O 引脚的项目。尽管新板的功能非常强大,但它仍然是开源的,价格与以前的板相当。寻找您的项目需要最大硬件性能的原因。图 6-4 显示了一个 Arduino Due 板。

img/313992_2_En_6_Fig4_HTML.jpg

图 6-4

arduino Due(arduino . cc 提供)

Mega 2560

Arduino Mega 2560 是 Due 的旧版本。它基于 ATmega2560 处理器(因此得名)。与 Due 一样,该板支持大量的 54 个数字 I/O 端口,其中 14 个可用作 PWM 输出、16 个模拟输入和 4 个 UARTs(硬件串行端口)。它使用 16MHz 时钟和 256KB 闪存。详情和完整的数据表可在 https://store.arduino.cc/usa/mega-2560-r3 找到。

Mega 2560 本质上是标准 Arduino Uno 和 Leonardo 的更大形式,但支持标准盾牌(以及“Mega”盾牌)。图 6-5 显示了 Arduino Mega 2560 板。

img/313992_2_En_6_Fig5_HTML.jpg

图 6-5

Arduino Mega(由 Arduino.cc 提供)

有趣的是,Arduino Mega 256 是 Prusa Mendel 和类似 3D 打印机的首选主板,这些打印机需要使用名为 rep rap Arduino Mega polo Lu Shield(RAMPS)的控制板。

Tip

请注意 Due 比 Uno 大多少。如果您选择安装 Due、Mega 或类似的电路板,您可能需要留出更多空间来安装电路板。

微处理器

Arduino Micro 是 Leonardo 板的一种特殊形式,使用带有 20 个数字 I/O 引脚的相同处理器,其中 12 个可用作模拟引脚,7 个可用作 PWM 输出。它有 32KB 的闪存和 2.5KB 的 SRAM。详情和完整的数据表可在 https://store.arduino.cc/usa/arduino-micro 找到。

这款 Micro 与 Mini 一样,是为在试验板上使用而设计的,但形式更新颖、更新。但与 Mini 不同的是,Micro 是一个全功能的主板,配有 USB 连接器。和 Leonardo 一样,它有内置的 USB 通信,允许电路板作为鼠标或键盘连接到计算机。图 6-6 显示了 Arduino 微板。

img/313992_2_En_6_Fig6_HTML.jpg

图 6-6

Arduino Micro(由 Arduino.cc 提供)

虽然品牌是官方的 Arduino 板,但 Arduino Micro 是与 Adafruit 合作生产的。

NANOTECHNOLOGY 简称

Arduino Nano 是 Arduino Micro 的旧版本。在这种情况下,它基于 Duemilanove4 的功能,具有 ATmega328 处理器(旧型号使用 ATmega168)和 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,8 个可用作模拟输入。mini 有 32KB 的闪存,使用 16MHz 时钟。详情和完整的数据表可在 https://store.arduino.cc/usa/arduino-nano 找到。

像 Micro 一样,它具有通过 USB 连接进行连接和编程所需的所有功能。图 6-7 显示了一个 Arduino 纳米板。

img/313992_2_En_6_Fig7_HTML.jpg

图 6-7

Arduino Nano(由 Arduino.cc 提供)

MKR 系列主板

Arduino 还有另一种形式,叫做 MKR(意为“制造者”)系列。MKR 系列包括基于(现已退役的 Zero)板的各种板,具有各种通信功能,如 Wi-Fi、LoRa、LoRaWAN 和 GSM。

它们基于 Atmel ATSAMW25 SoC(片上系统),专为物联网项目和设备而设计。它还支持加密认证。对于那些在需要电池端口的项目上工作的人来说,MKR 系列电路板包括一个 LiPo 充电电路,用于在使用外部电源运行时为 LiPo 电池充电。详情和完整的数据表可在 https://store.arduino.cc/usa/arduino-mkr1000 找到。

这些板不使用与 Uno 兼容的基于屏蔽的板相同的引脚布局(但是你可以得到一个适配器)。相反,它们的设计类似于 Nano 和 Mini(但稍大一点),以最小化电路板的尺寸,从而更容易集成到您的项目中。事实上,它们是物联网(IoT)项目的首选板之一,是传感器网络项目的绝佳选择。由于它们相对较新,并且有些具有专门的通信选项,大多数 Arduino 新手最好从支持 Uno 兼容屏蔽的 Arduino 板开始。

img/313992_2_En_6_Fig8_HTML.jpg

图 6-8

mkr 1000(arduino . cc 提供)

Caution

MKR 板采用 3.3V 电源供电,GPIO 引脚上的最大输入为 3.3V

arduino 翻制

越来越多的 Arduino 板可从大量来源获得。因为 Arduino 是开放的硬件,所以发现世界各地的厂商生产的 Arduino 板并不罕见,甚至有点不合法。

虽然有些人会坚持认为真正的 Arduinos 是那些品牌的,但事实是,只要构建质量良好,组件质量高,选择使用品牌还是副本,也就是克隆,是个人偏好之一。我已经从许多来源对 Arduino 板进行了采样,除了少数例外,它们都出色地执行了预期的功能。

除了 Arduino Mini,Arduino 克隆板的硬件配置种类更多。有些 Arduinos 是为嵌入式系统或试验板设计的,有些是为原型设计的。在接下来的几节中,我将研究一些更流行的克隆板。

迷你版 Arduino

Arduino Pro Mini 是 SparkFun 的另一款主板。它基于 ATmega168 处理器(旧型号使用 ATmega168 ),有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,8 个模拟输入。Pro Mini 有 16KB 的闪存和 1KB 的 SRAM,它使用 16MHz 的时钟。详情和完整的数据表可在 www.sparkfun.com/products/11113 找到。

Arduino Pro Mini 以 Arduino Mini 为模型,也用于试验板,但不带有接头。这使得 Arduino Pro Mini 成为半永久性安装的理想选择,在这种安装中,引脚可以焊接到组件或电路上,空间非常宝贵。图 6-9 显示了一个 Arduino Pro 迷你板。它真的那么小。

img/313992_2_En_6_Fig9_HTML.jpg

图 6-9

Arduino Pro Mini(由 SparkFun 提供)

此外,Pro Mini 不包括 USB 连接器,因此必须连接到 FTDI 电缆或类似的分线板并进行编程。它有两种型号,一种是带 8MHz 时钟的 3.3V 型号,另一种是带 16MHz 时钟的 5V 型号。

菲欧

Arduino Fio 是 SparkFun 制作的另一款主板。它是为无线项目设计的。它基于 ATmega32U4 处理器,具有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,8 个模拟引脚。详情和完整的数据表可在 www.sparkfun.com/products/11520 找到。

Fio 需要一个 3.3V 电源,允许使用锂聚合物(LiPo)电池,该电池可以通过板上的 USB 连接器充电。

在主板底部的 XBee 插座中可以看到它的无线血统。虽然 USB 连接可以让您给电池充电,但您必须使用 FTDI 电缆或分线适配器来连接和编程 Fio。与 Pro 型号类似,Fio 不带有接头,允许电路板用于半永久性安装,连接焊接到位。图 6-10 显示了一个 Arduino Fio 板。

img/313992_2_En_6_Fig10_HTML.jpg

图 6-10

Arduino Fio(由 SparkFun 提供)

西迪耶诺

Seeeduino 是 Seeed Studio ( www.seeedstudio.com )制作的 Arduino 克隆体。它基于 ATmega328P 处理器,具有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,8 个模拟引脚。它有 32KB 的闪存和 2KB 的 SRAM。详情和完整的数据表可在 www.seeedstudio.com/Seeeduino-V4-2-p-2517.html 找到。

该板的尺寸与 Arduino Uno 相似,支持所有标准接头。它支持许多增强功能,如 I2C 和串行 Grove 连接器和迷你 USB 连接器,并使用 SMD 组件。这也是一个醒目的红色黄色标题。图 6-11 显示了一个 Seeeduino 板。

img/313992_2_En_6_Fig11_HTML.jpg

图 6-11

Seeeduino(由 Seeed Studio 提供)

Seeed Studio 还制作了该板的“迷你”版本()。

SpikenzieLabs ( www.spikenzielabs.com )的 Sippino 设计用于无焊料试验板。它的成本更低,因为它的元件更少,占地面积更小。它是未组装的,如果你正在学习焊接,可以成为一个非常愉快的下午项目。

它基于 ATmega328 处理器,有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,6 个模拟输入引脚。Sippino 板有 32KB 的闪存和 2KB 的 SRAM。详情和完整的数据表可在 www.spikenzielabs.com/Catalog/arduino/sippino-prototino-8482/sippino-kit?cPath=1& 找到。

Sippino 没有 USB 连接,因此您必须使用 FTDI 电缆对其进行编程。好消息是,无论您的项目中有多少个 Sippinos,您都只需要一根电缆。我有许多 Sippinos,并在我的许多 Arduino 项目中使用它们,这些项目的空间非常宝贵。图 6-12 显示了安装在试验板上的 Sippino。

img/313992_2_En_6_Fig12_HTML.jpg

图 6-12

西皮诺(SpikenzieLabs 提供)

SpikenzieLabs 还做了一个带 USB 接口的版本( www.spikenzielabs.com/Catalog/arduino/sippino-prototino-8482/sippino8482-usb-kit?cPath=1& )。

虽然您不能在 si Pino 上使用普通屏蔽,但 SpikenzieLabs 还提供了一种称为屏蔽坞的特殊适配器,允许您将 si Pino 与标准 Arduino 屏蔽配合使用。shield dock 是一个令人惊叹的附件,它可以让您像使用标准的 Uno 或 Duemilanove 一样使用 Sippino。图 6-13 显示了安装在屏蔽坞上的西皮诺。详情和完整的数据表可在 www.spikenzielabs.com/Catalog/spikenzielabs/the-shield-dock 找到。

img/313992_2_En_6_Fig13_HTML.jpg

图 6-13

盾牌码头上的西皮诺(SpikenzieLabs 提供)

原型

Prototino 是 SpikenzieLabs 的另一个产品。它具有与 Sippino 相同的元件,但它不是一个友好的试验板布局,而是安装在一个包含完整原型制作区域的 PCB 上。与 Sippino 一样,它基于 ATmega328 处理器,具有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,6 个模拟输入引脚。Prototino 板有 32KB 的闪存和 2KB 的 SRAM。详情和完整的数据表可在 www.spikenzielabs.com/Catalog/arduino/sippino-prototino-8482/prototino?cPath=1& 找到。

Prototino 是构建具有支持组件和电路的解决方案的理想选择。在某些方面,它类似于 Nano、Mini 和类似的板,因为您可以将其用于永久安装。但与这些电路板(甚至 Arduino Pro)不同的是,Prototino 为您提供了一个直接将组件添加到电路板的空间。我在项目中使用了许多 Prototino 板,在这些项目中,我将组件添加到 Prototino 并将其安装在机箱中。这让我可以使用一块板创建一个解决方案,甚至可以快速轻松地构建几个副本。

像 Sippino 一样,Prototino 没有 USB 连接,所以你必须使用 FTDI 电缆对其进行编程。图 6-14 显示了一个 Prototino 板。

img/313992_2_En_6_Fig14_HTML.jpg

图 6-14

Prototino(由 SpikenzieLabs 提供)

阿达福罗地铁站

Adafruit 的 Metro 是一组 Arduino 兼容板,支持多种格式,包括几种支持 Arduino 屏蔽的格式。我喜欢的版本是 Metro 328 ( www.adafruit.com/product/2488 )。图 6-15 显示了 Adafruit 的 Metro 328 板。该板使用 16MHz 的 ATmega328P 和许多小的改进,使 Metro 成为 Arduino Uno 的优秀替代产品。查看产品页面了解更多详情。

img/313992_2_En_6_Fig15_HTML.jpg

图 6-15

地铁 328 号线(由 Adafruit 提供)

Note

Adafruit 也有几个 CircuitPython 模型。有些更小,因此如果您需要最小化节点的占用空间,这可能是一个选项。详见 www.adafruit.com/category/966

There is Even One Made of Paper

时不时地,你会遇到一些平凡的事情,因为媒介的改变而变得非常有趣。古伊列梅·马丁斯创立的 PAPERduino 就是这样一个例子。PAPERduino 是一个最小的 Arduino,它使用纸质模板代替 PCB。您只需下载并打印模板,购买一份常用分立元件清单,然后按照模板上打印的连接图将元件焊接到短导线上。您可以访问以下网站了解更多信息: http://lab.guilhermemartins.net/2009/05/06/paperduino-prints/

那么,我该买哪个呢?

如果你想知道该买哪个 Arduino,答案取决于你想做什么。对于本书中的大多数项目,任何 Arduino Uno 或支持标准 shield headers 的类似克隆都可以。您不需要购买更大的 Due 或其前身,因为不需要增加内存和 I/O 引脚。

我使用 Arduino Uno、Uno Wi-Fi 或 Leonardo 来完成本书中的所有项目。虽然您可以使用旧的板没有问题,但使用莱昂纳多板有一些问题。当你遇到这些问题时,我会指出来。大多数问题都与列奥纳多板上重新定位的图钉有关。例如,在 Leonardo 上,SPI 接头引脚(在图 6-3 的左上角)已经被移动。

对于未来的项目,在选择 Arduino 之前,您应该考虑一些事情。例如,如果您的项目主要基于试验板,或者您想要将项目的物理大小保持在最小,并且您不打算使用任何屏蔽,Arduino Mini 可能是更好的选择。相反,如果您计划进行大量的编程来实现复杂的数据操作或分析算法,您可能需要考虑它所增加的处理能力和内存。

底线是,大多数情况下,您的选择将基于物理特性(大小、屏蔽支持等),很少基于处理能力或内存。SparkFun 有一个优秀的买家指南,在其中你可以看到每种选择的利弊。详见 www.sparkfun.com/pages/arduino_guide

去哪里买

由于 Arduino 平台的流行,许多供应商出售 Arduino 和 Arduino 克隆板、屏蔽和附件。Arduino.cc 网站( https://store.arduino.cc/usa )也有一个专门介绍授权经销商的页面。如果这里列出的资源都不适合你,你可以在这个页面上找到你附近的零售商。

在线零售商

有越来越多的在线零售商,你可以在那里购买 Arduino 板和配件。下面列出了一些比较受欢迎的网站:

  • SparkFun :从分立元件到公司自有品牌的 Arduino 克隆和盾牌,SparkFun 几乎拥有你可能想要的任何 Arduino 平台( www.sparkfun.com/ ))。

  • 阿达果:携带越来越多的组件、小工具等等。它为电子爱好者提供越来越多的产品,包括 Arduino 产品的完整系列。Adafruit 还拥有出色的文档库和 wiki 来支持其销售的所有产品( www.adafruit.com/ ))。

你也可以拜访一些克隆板的制造商。以下是领先的克隆制造商及其店面链接:

零售店(美国)

也有实体商店销售 Arduino 产品。虽然没有在线零售商那么多,而且他们的库存通常也有限,但是如果你很快需要一个新的 Arduino 板,你可以在以下零售商那里找到。你可能会在你所在的地区找到更多的零售商。寻找受欢迎的业余电子商店:

  • Fry's :一家电子超市,有一个巨大的仓库,里面存放着电子产品、组件、微控制器、电脑零件等,可供订购。Fry's 出售 Arduino 品牌的电路板、盾牌和配件,以及 Parallax、SparkFun 和更多( http://frys.com/ ).的产品

  • 微中心:微中心类似于 Fry 的,提供大量的产品库存。然而,大多数微型中心商店的电子元件库存比弗莱氏( www.microcenter.com/ ).)要少

现在,您已经对硬件细节和各种可用的 Arduino 板有了更好的了解,让我们深入了解如何使用和编程 Arduino。下一节提供安装 Arduino 编程环境和 Arduino 编程的教程。后面的章节将介绍一些项目,以培养你开发传感器网络的技能。

Arduino 教程

本节是一个简短的教程,介绍如何开始使用 Arduino。它涵盖了获取和安装 IDE 以及编写示例草图。我没有重复本书之前的优秀作品,而是涵盖了重点,并向不太熟悉 Arduino 的读者推荐在线资源和其他提供更深入介绍的书籍。此外,Arduino IDE 有许多示例草图,您可以使用它们自己探索 Arduino。大多数在 Arduino.cc 网站上都有相应的教程。

学习资源

有很多关于 Arduino 平台的信息。如果您刚刚开始使用 Arduino,Apress 提供了一系列令人印象深刻的书籍,涵盖了与 Arduino 相关的各种主题,从开始使用微控制器到学习其设计和实现的细节。以下是一些比较受欢迎的书籍。有些比你想象的要老一些,但是仍然非常有用。

  • 迈克尔·麦克罗伯茨的《Arduino 入门》

  • 实用 Arduino:开源硬件的酷项目(技术在行动)作者 Jonathan Oxer 和 Hugh bl mings(a press,2009)

  • 戴尔·麦特(2011 年出版)的《Arduino 内部原理》

还有一些优秀的在线资源,可以帮助您了解更多关于 Arduino、Arduino 库和示例项目的信息。以下是一些最好的:

Arduino IDE

Arduino IDE 可供 Mac、Linux (32 位和 64 位版本)和 Windows 平台下载。你可以从 http://arduino.cc/en/Main/Software 下载 IDE。每个平台都有链接,如果需要为不同的平台编译 IDE,还可以链接到源代码。

Tip

有趣的是,有一个网页版的 IDE,你可以不用安装在电脑上就可以使用。如果您想在不想(或不能)安装 IDE 的 PC 上使用它,这可能会有所帮助。

安装 IDE 非常简单。为了简洁起见,我省略了安装 IDE 的实际步骤,但是如果您需要安装 IDE 的演示,您可以在下载页面上看到入门链接,或者阅读 Michael McRoberts 的开始 Arduino 中的更多内容(Apress,2010)。

IDE 启动后,您会看到一个简单的界面,其中包含一个文本编辑器区域(默认为白色背景),编辑器下方的一个消息区域(默认为黑色背景),以及顶部的一个简单的按钮栏。按钮(从左至右)编译编译上传新建打开保存。右边还有一个打开串行监视器的按钮。您可以使用串行监视器查看通过串行库发送(或打印)的 Arduino 消息。你可以在你的第一个项目中看到这一点。图 6-16 显示了 Arduino IDE。

img/313992_2_En_6_Fig16_HTML.jpg

图 6-16

Arduino IDE

注意,在图 6-16 中,你可以看到一个样本草图(称为blink)和一个成功编译操作的结果。我通过点击文件例子基本眨眼来加载这个草图。注意底部,它告诉你你正在一个特定的串行端口上编程一个 Arduino Leonardo 板。

由于处理器和支持架构的不同,编译器构建程序的方式(以及 IDE 上传程序的方式)也有所不同。因此,当您启动 IDE 时,首先要做的事情之一就是从工具电路板菜单中选择您的电路板。图 6-17 显示了在 Mac 上选择板卡的示例。

img/313992_2_En_6_Fig17_HTML.jpg

图 6-17

选择 Arduino 板

请注意可用的电路板数量。请务必选择与您的主板相匹配的产品。如果您使用的是克隆板,请查看制造商网站,了解推荐使用的设置。如果你选择了错误的板,你通常会在上传时得到一个错误,但是你选择了错误的板可能并不明显。因为我有很多不同的主板,所以我养成了每次启动 IDE 时都选择主板的习惯。

接下来您需要做的是选择 Arduino 板连接的串行端口。要连接到板上,使用工具端口菜单选项。图 6-18 显示了 Mac 上的一个例子。在这种情况下,没有列出串行端口。如果您没有将 Arduino 插入电脑的 USB 端口(或集线器),您已经将它插入但在某个时候将其断开,或者您没有加载 Arduino 的驱动程序(Windows ),就会发生这种情况。通常,这可以通过简单地拔出 Arduino 并将其插回,然后等待直到计算机识别该端口来解决。

Note

如果你用的是 Mac,选择哪个端口并不重要:无论是以tty开头的端口还是以cu开头的端口都可以。

img/313992_2_En_6_Fig18_HTML.jpg

图 6-18

选择串行端口

Tip

如果您需要在 Windows 上安装驱动程序的帮助,请参见 www.arduino.cc/en/Guide/HomePage?from=Guide.Howto

好了,现在你已经安装了你的 Arduino IDE,你可以连接你的 Arduino 并设置板和串行端口。您会看到 Arduino 上的 led 亮起。这是因为 Arduino 从 USB 获得电源。因此,当 Arduino 连接到您的电脑时,您不需要提供外部电源。接下来,您将深入一个简单的项目来演示 Arduino IDE,并学习如何构建、编译和上传基本草图。

项目:硬件“你好,世界!”

无处不在的“你好,世界!”Arduino 的项目是闪烁的灯。该项目使用一个 LED,一个试验板,和一些跳线。Arduino 在loop()迭代过程中打开和关闭。这是一个很好的开始项目,但它与如何使用传感器无关。

因此,在本节中,您将通过添加传感器来扩展闪光灯项目。在这种情况下,您仍然可以通过使用可以说是最基本的传感器来简化事情:按钮。目标是每当按钮被按下时,LED 就会亮起。

硬件连接

让我们从组装 Arduino 开始。确保首先断开(关闭)Arduino。您可以使用任何带有 I/O 引脚的 Arduino 变体。在试验板上放置一个 LED 和一个按钮。将 5V 引脚连接到试验板电源轨,将接地引脚连接到接地轨,并将按钮放在试验板的中心。将 LED 放在试验板的一侧,如图 6-19 所示。

img/313992_2_En_6_Fig19_HTML.jpg

图 6-19

带按钮的 LED 图示

你就快到了。现在,将一根跳线从电源轨连接到按钮的一侧,并将按钮的另一侧连接到 Arduino 上的(DIGITAL)引脚 2(位于带有 USB 连接器的一侧)。接下来,将 LED 连接到试验板上的地和一个 150 欧姆的电阻(颜色:棕色、绿色、棕色、金色)。电阻的另一端应连接到 Arduino 上的第 13 号针脚。你还需要一个电阻,在按钮没按下的时候把按钮拉低。将一个 10K 欧姆电阻器(颜色:棕色、黑色、橙色、金色)放在按钮的侧面,导线连接到引脚 2 和接地。

LED 最长的一边是正极。正极应该是连接到电阻的一侧。电阻接哪个方向都没关系;它用于限制 LED 的电流。再次检查图纸,以确保您有一个类似的设置。

Note

大多数 Arduino 板都有一个连接到引脚 13 的 LED。您将重复使用该引脚来演示如何使用模拟输出。因此,您可能会看到引脚 13 附近的一个小 LED 与试验板上的 LED 同时亮起。

Cool Gadget

使用 Arduino 的最酷的小工具之一是 Adafruit ( www.adafruit.com/products/275 )的 Arduino 安装板。

这个小小的丙烯酸板有足够的空间放半块面包板和一个 Arduino。它甚至有安装孔,用于将 Arduino 固定在平板上,还有小橡胶脚,用于保持平板远离工作表面。下图(由 Adafruit 提供)显示了安装板的作用。

img/313992_2_En_6_Figa_HTML.jpg

虽然你可以用 Lexan 或 Plexiglas(我有)制作自己的 Arduino 安装板,但 Adafruit 产品比你自己制作的产品好一点。只需大约 5.00 美元,您就可以将 Arduino 和试验板放在一起,避免在桌面上留下划痕(来自 Arduino 底部的尖头)——更好的是,避免不小心将通电的 Arduino 放在导电表面上带来的严重副作用(这绝不是一个好主意)。

写素描

这个项目需要的草图在 Arduino 上使用了两个 I/O 引脚:一个输出和一个输入。输出引脚将用于点亮 LED,输入引脚将检测按钮接合。将正电压连接到按钮的一侧,另一侧连接到输入引脚。当您检测到输入引脚上的电压时,您告诉 Arduino 处理器向输出引脚发送正电压。这种情况下,LED 的正极连接到输出引脚。

如图 6-18 所示,输入引脚为 2 号引脚,输出引脚为 13 号引脚。让我们使用一个变量来存储这些数字,这样您就不必担心重复硬编码的数字(并冒着出错的风险)。使用pinMode()方法设置每个引脚的模式(INPUTOUTPUT)。您将变量语句放在setup()方法之前,并在setup()方法中设置pinMode()调用,如下所示:

int led = 13;     // LED on pin 13
int button = 2;   // button on pin 2

void setup() {
  pinMode(led, OUTPUT);
  pinMode(button, INPUT);
}

loop(方法中,您放置代码来检测按钮按压。使用digitalRead()方法读取管脚的状态(LOWHIGH),其中LOW表示管脚上没有电压,HIGH表示管脚上检测到正电压。

您还可以在loop()方法中放置当输入引脚状态为HIGH时打开 LED 的代码。在这种情况下,当输入引脚状态为HIGH时,使用digitalWrite()方法将输出引脚设置为HIGH,同样,当输入引脚状态为LOW时,将输出引脚设置为LOW。以下代码显示了所需的语句:

void loop() {
  int state = digitalRead(button);
  if (state == HIGH) {
    digitalWrite(led, HIGH);
  }
  else {
    digitalWrite(led, LOW);
  }
}

现在让我们看看完整的草图,并附上适当的文档。清单 6-1 显示了完成的草图。

/*
  Simple Sensor - Beginning Sensor Networks Second Edition
  For this sketch, we explore a simple sensor (a pushbutton) and a simple response to sensor input (a LED). When the sensor
  is activated (the button is pushed), the LED is illuminated.
*/
int led = 13; // LED on pin 13
int button = 2;   // button on pin 2
// the setup routine runs once when you press reset:
void setup() {
  // initialize pin 13 as an output.
  pinMode(led, OUTPUT);
  pinMode(button, INPUT);
}
// the loop routine runs over and over again forever:
void loop() {
  // read the state of the sensor
  int state = digitalRead(button);
  // if sensor engaged (button is pressed), turn on LED
  if (state == HIGH) {
    digitalWrite(led, HIGH);
  }
  // else turn off LED
  else {
    digitalWrite(led, LOW);
  }
}

Listing 6-1Simple Sensor Sketch

当你已经输入了草图,你就可以编译和运行它了。将草图命名为basic_sensor.ino

Tip

想要避免手动输入所有这些内容吗?你可以在 Apress 网站上找到这本书的源代码。

编译和上传

写好草图后,使用 IDE 左上角的编译按钮测试编译。修复消息窗口中出现的任何编译错误。典型的错误包括变量或方法的拼写错误或大小写改变(编译器区分大小写)。

修复任何编译错误后,点击上传按钮。IDE 编译草图,并将编译后的草图上传到 Arduino 板上。您可以通过消息窗口上方右下角的进度条来跟踪进度。当编译好的草图上传后,进度条会消失。

测试传感器

上传完成后,你会在 Arduino 上看到什么?如果你做对了每一件事,答案是什么都没有。它只是用一个黑暗的 LED 回望着你——几乎是嘲弄地。现在,按下按钮。LED 是否点亮?如果是这样,那么恭喜你:你是一名 Arduino 程序员!

如果 LED 不亮,按住按钮一两秒钟。如果不起作用,请检查您的所有连接,以确保您插入了试验板上的正确走线,并且您的 LED 正确就位,长边连接到电阻器,电阻器连接到引脚 13。

另一方面,如果 LED 灯一直亮着,尝试将按钮重新调整 90 度。您可能将按钮设置在了错误的方向。

尝试几次这个项目,直到兴奋过去。如果你是 Arduino 的老手,那可能是很短的一段时间。如果这一切对你来说都是新的,那就按下那个按钮,享受建造第一个传感器节点的荣耀吧!

下一节研究一个更复杂的传感器节点,使用一个发送数字数据的温度和湿度传感器。正如您将看到的,还有很多事情要做。

使用 Arduino 托管传感器

Arduino 的数字和模拟引脚使其成为托管传感器的理想平台。由于大多数传感器只需要很少的支持组件,所以您通常可以在一个 Arduino 上安装多个传感器。例如,可以安装一个温度传感器,甚至多个温度传感器、气压传感器、湿度传感器等等,用于从给定的地点采集天气状况。

SparkFun 和 Adafruit 有很好的网站,提供大量关于他们销售的产品的信息。通常,传感器产品页面包括示例链接和更多关于使用传感器的信息。如果你是电子产品的新手,你应该坚持使用能提供使用示例的传感器。这听起来可能像作弊,但除非你有很好的电子知识,否则不正确地使用传感器会付出很高的代价,因为你在正确使用之前已经烧坏了几个组件。

然而,当您想使用另一种传感器时,您应该查看其数据手册。大多数制造商和供应商通过产品页面上的链接提供数据手册。数据手册提供了使用传感器所需的所有信息,但可能没有实际的使用示例。如果你熟悉电子学,这是你可能需要的全部。

如果你是电子产品的业余爱好者或新手,可以查看 Arduino.cc、SparkFun 和 Adafruit 上的维基和论坛。这些站点有丰富的信息和大量的示例,并附有完整的示例代码。如果你找不到任何例子,你可以试着谷歌一下。使用类似“ Arduino <传感器名称>示例的术语。如果你找不到任何例子,并且不是一个有经验的电子技术人员,你可能要重新考虑使用传感器。

另一件需要考虑的事情是如何将传感器连接到 Arduino。回想一下,根据您选择的 Arduino,有许多不同的物理布局。因此,在规划 Arduino 托管的传感器节点时,您应该熟悉 Arduino 的引脚布局。如果您使用 Arduino 托管单个传感器,这可能不是问题。举例来说,图 6-20 显示了一个 Arduino Leonardo 板,突出显示了 I/O 引脚。如果你仔细观察你的 Arduino 板,你会在每个管脚旁边看到缩写的文字来表示它的用途。一些小尺寸的 Arduino 板可能没有放置标签的空间。在这种情况下,请查阅供应商的产品页面,并将其打印出来以供将来参考。

img/313992_2_En_6_Fig20_HTML.jpg

图 6-20

识别 Arduino 板上的 I/O 引脚

现在,让我们将您从学习 Arduino 中学到的知识用于构建一个带有 Arduino 和传感器的传感器节点。

项目:构建 Arduino 温度传感器

在这个项目中,您将构建一个更加复杂的 Arduino 托管的传感器节点。这个项目不仅演示了如何用 Arduino 托管传感器,还提供了一个例子,说明为什么需要微控制器来托管某些类型的传感器。在这种情况下,DHT22 传感器是一个数字传感器,它有自己的协议,这需要一点逻辑来正确解释,从而使它与 XBee 一起使用更加复杂。 2 稍后,您将看到一个简单模拟传感器的示例,您可以将它直接连接到 XBee 模块。

该项目使用 DHT22 温度和湿度传感器,通过试验板连接到 Arduino。DHT22 是一种产生数字信号的简单数字传感器。它需要一个电阻将数据引脚上拉至电压。这种情况下的上拉确保数据值被“上拉”到电压电平,以确保线路上的逻辑电平有效。

让我们直接进入并连接硬件。

Note

这个例子改编自 Adafruit 网站上的一个例子( http://learn.adafruit.com/dht )。

硬件设置

本项目所需的硬件包括 Arduino、DHT22 湿度和温度传感器、试验板、4.7K 欧姆电阻(颜色:黄色、紫色、红色、金色)和试验板跳线。

Tip

如果你被卡住了或者想要更多的信息,在 Adafruit 的网站上有一个极好的教程。

首先将 Arduino 放在试验板旁边。将 DHT22 传感器插入试验板的一侧,如图 6-21 所示。请经常参考此图,并在打开 Arduino 电源(或将其连接到笔记本电脑)之前仔细检查您的连接。你要避免电混沌理论中的意外实验。

img/313992_2_En_6_Fig21_HTML.jpg

图 6-21

DHT22 接线

接下来,将 Arduino 的电源连接到试验板。用一根跳线将 Arduino 上的 5V 引脚连接到试验板电源轨,用另一根跳线将 Arduino 上的接地(GND)引脚连接到试验板的接地轨。这些电线就位后,您就可以为传感器布线了。您使用四个引脚中的三个,如表 6-1 所示。

表 6-1

DHT22 连接

|

别针

|

连接到

|
| --- | --- |
| one | 电源和数据引脚之间的+5V 4.7K 电阻(强上拉电阻) |
| Two | Arduino 上的引脚 7,4.7K 电阻 |
| three | 不连接 |
| four | 地面 |

接下来,将传感器的地和电源连接到试验板电源和接地轨。然后将一根电线从传感器上的数据引脚连接到 Arduino 的引脚 7。还有最后一个连接:使用一个 4.7K 欧姆的上拉电阻连接到数据线和试验板的电源轨。

软件设置

要将 DHT22 与 Arduino 配合使用,您需要拥有最新的 DHT22 库。您可以通过搜索库管理器直接从 Arduino IDE 安装库。打开 Arduino IDE,然后打开一个新的草图,从菜单中选择草图包含库管理库……。图 6-22 显示了库管理器。

img/313992_2_En_6_Fig22_HTML.jpg

图 6-22

图书馆经理

库管理器可能需要一些时间来连接到服务器并下载最新的目录。完成后,您可以在右上角的文本框中键入DHT22并按下ENTER。这将在库目录中搜索所有匹配的库。

从 Adafruit 选择 DHT 传感器库,点击Install。如果系统提示您安装支持库,请点击Install all以确保所有必备软件都已安装,如图 6-23 所示。

img/313992_2_En_6_Fig23_HTML.jpg

图 6-23

安装所有库

现在您已经配置好了硬件并设置好了 DHT22 库,让我们写一些代码吧!

写素描

像任何草图一样,我们需要包含一些库,定义一些常量,并在序言中实例化 DHT 对象。这里包含了DHT库头,将传感器的数据引脚定义为 Arduino 上的引脚 7,添加 5 秒的延迟常数,并实例化 DHT 类的一个实例。由于该库支持多种类型的 DHT 传感器,我们还必须使用库中声明的特殊类型。例如,为了告诉库使用 DHT22 传感器,我们相应地将类型设置为DHT22。以下是草图的序言:

#include "DHT.h"
#define DHTPIN 7          // DHT2 data is on pin 7
#define read_delay 5000   // 5 seconds
#define DHTTYPE DHT22     // DHT 22 (AM2302)

接下来,我们需要实例化 DHT 类。我们通过传递想要使用的 pin 和 DHT 类型来实现这一点。

DHT dht(DHTPIN, DHTTYPE);

DHT22 有自己的数据通信协议。幸运的是,Adafruit 的库使得从传感器读取变得容易。要读取数据,您只需调用适当的方法,如表 6-2 所示,该方法将返回值保存在变量中。

表 6-2

DHT 库中的数据方法

|

方法

|

描述

|
| --- | --- |
| dht.readHumidity() | 读取湿度 |
| dht.readTemperature() | 读取摄氏温度 |
| dht.readTemperature(true) | 读取华氏温度 |
| dht.computeHeatIndex(temp_c, humidity, false) | 获取以摄氏度为单位的热量指数 |
| dht.computeHeatIndex(temp_c, humidity, true) | 获取华氏温度的热量指数 |

知道了这些,我们需要做的就是调用这些方法来读取数据,将数据保存到变量中,然后打印出来。为了使草图简单而整洁,我们可以将这个逻辑放在一个名为read_data()的方法中。清单 6-2 显示了完整的read_data()方法。

void read_data() {
  // Read humidity
  float humidity = dht.readHumidity();
  // Read temperature as Celsius
  float temp_c = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  float temp_f = dht.readTemperature(true);

  // Check for errors and return if any variable has no value
  if (isnan(temp_c) || isnan(temp_f) || isnan(humidity)) {
    Serial.println("ERROR: Cannot read all data from DHT-22.");
    return;
  }
  // Calculate heat index for Celsius
  float hi_c = dht.computeHeatIndex(temp_c, humidity, false);
  // Calculate heat index for temperature in Fahrenheit
  float hi_f = dht.computeHeatIndex(temp_f, humidity, true);
  Serial.print("Humidity: ");
  Serial.print(humidity);
  Serial.print("%, ");
  Serial.print(temp_c);
  Serial.print("C, ");
  Serial.print(temp_f);
  Serial.println("F ");
  Serial.print("      Heat Index: ");
  Serial.print(hi_c);
  Serial.print("C, ");
  Serial.print(hi_f);
  Serial.println("F ");
}

Listing 6-2The read_data() Method

请注意,代码非常简单。我们只是读取这些值,然后使用Serial.print()Serial.println()方法将数据写入串行监视器。

唯一剩下的就是setup()loop()方法。setup()方法简单地初始化了Serialdht类。loop()方法使用延迟并调用read_data()方法。清单 6-3 显示了完成的草图。

/*
  Beginning Sensor Networks Second Edition
  Sensor Networks Example Arduino Hosted Sensor Node
  This sensor node uses a DHT22 sensor to read temperature and humidity printing the results in the serial monitor.
*/
#include "DHT.h"

#define DHTPIN 7          // DHT2 data is on pin 7
#define read_delay 5000   // 5 seconds
#define DHTTYPE DHT22     // DHT 22 (AM2302)

DHT dht(DHTPIN, DHTTYPE);

void read_data() {
  // Read humidity
  float humidity = dht.readHumidity();
  // Read temperature as Celsius
  float temp_c = dht.readTemperature();
  // Read temperature as Fahrenheit (isFahrenheit = true)
  float temp_f = dht.readTemperature(true);

  // Check for errors and return if any variable has no value
  if (isnan(temp_c) || isnan(temp_f) || isnan(humidity)) {
    Serial.println("ERROR: Cannot read all data from DHT-22.");
    return;
  }
  // Calculate heat index for Celsius
  float hi_c = dht.computeHeatIndex(temp_c, humidity, false);
  // Calculate heat index for temperature in Fahrenheit
  float hi_f = dht.computeHeatIndex(temp_f, humidity, true);
  Serial.print("Humidity: ");
  Serial.print(humidity);
  Serial.print("%, ");
  Serial.print(temp_c);
  Serial.print("C, ");
  Serial.print(temp_f);
  Serial.println("F ");
  Serial.print("      Heat Index: ");
  Serial.print(hi_c);
  Serial.print("C, ");
  Serial.print(hi_f);
  Serial.println("F ")

;
}

void setup() {
  Serial.begin(115200);  // Set the serial port speed
  dht.begin();
  delay(1000);
  Serial.println("Welcome to the DHT-22 Arduino example!\n");
}

void loop() {
  delay(read_delay);
  read_data();
}

Listing 6-3Completed Sketch: Reading a DHT-22 Sensor

如果您尚未这样做,请通过单击新菜单按钮或选择文件来打开一个新的 Arduino 草图。现在您可以编译、上传和测试项目了。你可以给它起任何你喜欢的名字,比如dht22_example.ino

测试执行

执行草图意味着将它上传到你的 Arduino 并观看它运行。如果您尚未连接 Arduino,现在就可以连接。

我喜欢从画草图开始。单击 Arduino 应用程序左侧的复选标记,观察底部信息屏幕中的输出。如果您看到错误,请修复它们并重试编译。常见错误包括丢失 DHT22 库(这可能需要重新启动 Arduino 应用程序)、键入错误、语法错误等。一旦一切编译正确,你就可以点击工具栏上的上传按钮来上传你的草图了。

上传完成后,单击工具栏右侧的按钮打开串行监视器。观察 Arduino 消息。清单 6-4 显示了您应该看到的典型输出。

Welcome to the DHT-22 Arduino example!

Humidity: 48.00%, 18.20C, 64.76F
      Heat Index: 17.33C, 63.19F
Humidity: 50.00%, 18.30C, 64.94F
      Heat Index: 17.49C, 63.48F
Humidity: 51.80%, 19.10C, 66.38F
      Heat Index: 18.42C, 65.15F
Humidity: 53.60%, 20.20C, 68.36F
      Heat Index: 19.67C, 67.42F
Humidity: 53.20%, 21.40C, 70.52F
      Heat Index: 20.98C, 69.77F
Humidity: 51.50%, 22.10C, 71.78F
      Heat Index: 21.71C, 71.08F
Humidity: 50.00%, 22.50C, 72.50F
      Heat Index: 22.11C, 71.80F
Humidity: 48.50%, 22.60C, 72.68F
      Heat Index: 22.18C, 71.93F

Listing 6-4Output of the DHT22 Sensor Sketch

如果你看到类似的输出,恭喜你!您刚刚构建了第一个 Arduino 托管的传感器节点。这是构建传感器网络的重要一步,因为您现在已经拥有了开始构建更复杂的无线传感器节点和用于记录传感器数据的聚合节点所需的工具。

让我们进一步体验 Arduino 传感器,添加 XBee 模块,使传感器能够远离 Arduino 放置。这有效地展示了 Arduino 如何远程托管多个传感器节点,从而成为传感器网络中的聚合节点。

项目:使用 Arduino 作为 XBee 传感器节点的数据收集器

这个项目结合了你在本章学到的 Arduino 和在第 2 和 4 章学到的 XBee。更具体地说,您使用一个 Arduino 和一个使用 XBee 模块将传感器与 Arduino 连接起来的远程传感器。我们将重用第四章中的 XBee 传感器节点,并使用 Arduino 读取数据。

XBee 传感器节点

按照第四章的内容创建 XBee 传感器节点。提醒一下,该节点的构造如图 6-24 所示。

img/313992_2_En_6_Fig24_HTML.jpg

图 6-24

XBee 传感器节点

如果您尚未配置第四章中的传感器节点,或者如果您需要重置模块,您应该首先确保加载了最新的固件,并使用表 6-3 中所示的设置。请注意,您不需要第四章中的 IR 设置,但是如果您想要重用您在该章中使用的模块,这是可以的。

表 6-3

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| D3 | a3/上帝 3 | 触发模拟或数字数据记录 | 2—ADC |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 红外的 | 输入输出采样率 | 等待发送数据的时间 | 3a 98—15000 毫秒 |
| 镍 | 节点标识符 | 节点的名称 | TMP36 |
| V+ | 电源电压阈值 | 电源电压 | FFFF(总是发送) |

注意,与第五章中的项目不同,我们必须设置 I/O 采样率。这是因为我们将使用的库不具备在 ZigBee 网络中搜索远程节点的能力。相反,在这个项目中,Arduino 将进行轮询,直到 IO 样本被发送到协调器节点。因此,我们已经看到了从 ZigBee 网络获取数据的两种不同方式——直接从节点请求数据(第五章)和轮询从节点发送的数据(本章)。

协调节点

协调器节点应使用最新加载的固件和表 6-4 中所示的设置进行类似配置。

表 6-4

XBee 协调器选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | 协调者 |

现在还不需要安装 XBee 模块。您需要配置其设置。您将在下一节中执行该操作。

现在,让我们设置 Arduino 和 XBee。

带 XBee 盾的 Arduino

您可以使用 Arduino 从 XBee 传感器节点读取数据。这是一个使用 Arduino 作为来自 XBee 传感器节点的传感器数据的数据聚合器(收集器)的例子。让我们用 XBee 设置一个 Arduino。这个项目演示了使用 Arduino 通过 XBee 接收数据,但是你也可以通过 XBee 发送数据。

硬件设置

本节中的示例设置使用典型的 Arduino (Uno、Leonardo 等。)支持标准屏蔽。虽然没有明确要求必须使用设计用于接受 XBee 模块的屏蔽,但是大多数 XBee 屏蔽都是为了使 XBee 的使用更容易。换句话说,你不必担心如何将 XBee 连接到 Arduino。图 6-25 显示了一个来自 SparkFun 的带有 XBee 盾的 Arduino。

img/313992_2_En_6_Fig25_HTML.jpg

图 6-25

Arduino XBee 盾(SparkFun 提供)

我使用这个屏蔽来演示如何与 XBee 模块通信。如果您决定使用另一个 shield,请务必查看该 shield 的文档,查看如何使用它的示例,并将其与本项目中的代码进行比较。进行适当的修改(硬件连接和对草图的更改),以便您的项目可以正确地与您的盾一起工作。

shield 允许您选择通过数字引脚 0 和 1 与 Arduino 的板载串行电路(UART 3 )进行通信。但这些也是从 Arduino IDE 通过 USB 与 Arduino 通信时使用的引脚。幸运的是,SparkFun XBee shield 有一个小开关,允许你选择使用引脚 2 和 3 来代替。使用此选项,您可以编写一个脚本,通过 XBee 从 shield 读取数据,同时仍然连接到 Arduino IDE 并使用串行监视器。但是有一个问题:只有一个 UART 可用。您必须使用软件串行库来模拟第二个串行连接。Arduino IDE 中包含软件串行库。您可以在“软件设置”一节中看到如何操作。

Tip

如果您使用不同的 XBee 屏蔽,您应该参考屏蔽上的文档,并按照说明使用引脚。有些屏蔽是硬连线的。

如果您不想使用屏蔽,您可以像前面一样将 XBee 连接到 Arduino。在本例中,您使用 SparkFun 的 XBee 分线板来连接试验板。图 6-26 显示了将 XBee Explorer 调节分线板连接到 Arduino 的接线图。请注意,您使用的是 Arduino 的 5V 引脚。如果您使用的是非调节分线板,您应该使用 3.3V 引脚。在给项目通电之前,一定要仔细检查您使用的任何组件的最大电压。

img/313992_2_En_6_Fig26_HTML.jpg

图 6-26

通过 SparkFun XBee 分线板将 XBee 连接到 Arduino

Note

这两种方法都适用于这个项目。

无论您选择哪种方法,请将 XBee 协调器模块从 USB 适配器上取下,并将其插入 XBee shield 或 XBee Explorer 调节分线板。现在硬件已经准备好了,让我们设置您的 Arduino 环境,并编写一个草图来从 XBee 传感器节点读取数据。

软件设置

像大多数传感器一样,我们需要一个库来连接和读取数据。幸运的是,Arduino 有一个很好的 XBee 串行库。我们像安装 DHT22 库一样,打开一个新的草图,从菜单中选择草图包含库管理库… ,然后搜索 XBee,从 Andrew Rapp 安装库。图 6-27 显示了选择了正确库的库管理器。点击安装进行安装。

img/313992_2_En_6_Fig27_HTML.jpg

图 6-27

加载 XBee Arduino 库

一旦安装了库并且重启了 Arduino IDE,就可以编写脚本从 XBee 中读取数据。该库为每个流行的 XBee 数据包提供了类,用于向 XBee 发送数据或从 XBee 接收数据。这个项目使用 IO sample 类,因为您知道这是我们在这个项目中唯一感兴趣的包。

你需要创建草图的几个部分。使用 XBee 库比编写自己的通信方法更容易,但是这个库有一些设置步骤和方法,您需要使用它们来读取数据包。

首先,让我们包括 XBee 和软件串行库的库头。回想一下,软件串行库是 Arduino IDE 的一部分:

#include <XBee.h>
#include <SoftwareSerial.h>

现在,您必须定义用于与 XBee 模块通信的引脚。您将串行监视器用作输出设备,因此需要使用替代引脚。在这种情况下,使用引脚 2 和 3 进行接收和发射连接。您需要定义这些并初始化软件串行库,并使用它与 XBee 通信。下面显示了所需的定义:

uint8_t recv = 8;
uint8_t trans = 9;
SoftwareSerial soft_serial(recv, trans);

接下来,您必须实例化 XBee 库和助手类。在这种情况下,您需要 I/O 数据示例包的 helper 类:

XBee xbee = XBee();
ZBRxIoSampleResponse ioSample = ZBRxIoSampleResponse();

现在我们准备写启动代码。对于这个项目,您必须启动软件串行库,并将其传递给 XBee 库,以便在与 XBee 模块通信时使用。您还需要初始化默认的 serial 类,这样您就可以使用print()语句在代码的后面部分显示读取的数据。下面显示了完整的setup()方法:

void setup() {
  Serial.begin(9600);
  while (!Serial);   // Leonardo boards need to wait for Serial to start
  soft_serial.begin(9600);
  xbee.setSerial(soft_serial);
}

注意带有 while 循环的那一行。你需要把这个加到达芬奇的画板上。如果你忽略了这一点,并在莱昂纳多板上运行草图,XBee 可能无法工作。添加此循环以允许 Leonardo 有时间启动串行实例。

现在,让我们编写用于从数据包中读取数据的方法。稍后您将学习如何从 XBee 读取数据包。首先,让我们看看如何获得数据包的源地址。下面显示了这样做的代码:

void get_address(ZBRxIoSampleResponse *ioSample) {
  Serial.print("Received data from address: ");
  Serial.print(ioSample->getRemoteAddress64().getMsb(), HEX);
  Serial.print(ioSample->getRemoteAddress64().getLsb(), HEX);
  Serial.println("");
}

注意,您只需使用ioSample类实例并调用方法getRemoteAddress64().getMsb()。实际上,这是对一个子类(RemoteAddress64)及其方法getMsb()的调用。这将返回 64 位地址的最高有效字节(高 16 位)。对于getRemoteAddress64().getLsb()调用,您对最低有效位做同样的操作。然后打印这些值,指定要以十六进制打印它们。如果您从多个 XBee 节点读取数据,为每个地址应用一个名称会很方便,比如“卧室”或“起居室”。我把这个留给你做练习。

接下来,您想要读取数据有效负载。在这种情况下,您想要读取从 XBee 传感器节点发送到 XBee 协调器的温度数据。下面显示了执行此操作所需的代码。您可以使用前面讨论的公式将传感器读取的毫伏值转换为摄氏温度,然后再转换为华氏温度。

void get_temperature(ZBRxIoSampleResponse *ioSample) {
  float adc_data = ioSample->getAnalog(3);
  Serial.print("Temperature is ");
  float temperatureC = ((adc_data * 1200.0 / 1024.0) - 500.0) / 10.0;
  Serial.print(temperatureC);
  Serial.print("c, ");
  float temperatureF = ((temperatureC * 9.0)/5.0) + 32.0;
  Serial.print(temperatureF);
  Serial.println("f");
}

最后,您需要从数据包中读取电源电压。这种情况下,电源电压出现在数据采样之后。因为您知道只有一个数据样本(通过模拟样本掩码),所以您知道模拟电压正好出现在校验和之前。遗憾是,目前还没有从 XBee 库中的 I/O sample 包获取信息的方法。但是,并没有全部丢失,因为库的作者将数据存储在一个数组中,并提供了一个子类供您用来获取原始数据。在这种情况下,您需要数据中的字节 17(最高有效字节)和 18(最低有效字节)。你知道这些是从零开始从帧类型之后的字节计数所需的索引。详见表 6-5 。

与温度数据一样,您必须使用前面讨论的公式将读数转换为伏特。下面显示了读取、转换和显示 XBee 传感器节点的电源电压所需的代码。请注意,您将最高有效字节移动了 8 位,以便可以保留 16 字节的浮点值。

void get_supply_voltage() {
  Serial.print("Supply voltage is ");
  int ref = xbee.getResponse().getFrameData()[17] << 8;
  ref += xbee.getResponse().getFrameData()[18];
  float volts = (float(ref) * float(1200.0 / 1024.0))/1000.0;
  Serial.print(" = ");
  Serial.print(volts);
  Serial.println(" volts.");
}

花些时间检查一下计算结果。在本例中,您将 XBee 传感器节点读取和发送的电压转换为摄氏度,然后再转换为华氏度。为了便于阅读,您还将电源电压转换为伏特。测试期间,所有这些值都被发送到串行监视器进行反馈。

一旦实现了这些方法,就可以将从 XBee 读取数据的代码放在loop()方法中,调用这些方法来解密数据并将其打印到串行监视器上。

因为这个loop()方法被重复调用,所以您使用 XBee 类方法来读取包,然后确定包是否是 I/O 数据样本包。如果是,就从包中读取数据。如果不是,您添加一些简单的错误处理,以便 Arduino 可以继续读取数据而不是停止。下图显示了完成的loop()方法:

void loop() {
  xbee.readPacket();
  if (xbee.getResponse().isAvailable()) {
    if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {
      xbee.getResponse().getZBRxIoSampleResponse(ioSample);
      // Read and display data
      get_address(&ioSample);
      get_temperature(&ioSample);
      get_supply_voltage();
    }
    else {
      Serial.print("Expected I/O Sample, but got ");
      Serial.print(xbee.getResponse().getApiId(), HEX);
    }
  } else if (xbee.getResponse().isError()) {
    Serial.print("Error reading packet.  Error code: ");
    Serial.println(xbee.getResponse().getErrorCode());
  }
}

请注意,在代码中,您检查数据包是否可用;如果是的话,你读一下。如果读取的数据包是正确的帧类型,在本例中为ZB_IO_SAMPLE_RESPONSE,则从数据包中读取数据并显示。如果它不是正确的包,你把收到的包的帧类型打印到串行监视器上。如果读取数据包时出现错误,您可以在最后一个 else 中捕获该错误,并在串行监视器上显示该错误。

注意ZB_IO_SAMPLE_RESPONSE条件代码块的内容。首先用读取的数据初始化 I/O 数据样本类,然后读取发送数据包的 XBee 的地址,然后计算温度和参考电压。

到目前为止,一旦你理解了代码,创建一个新的文件,并在新的草图窗口中输入信息。清单 6-5 显示了 Arduino XBee 接收器项目的完整草图。该代码也可以在本书的源代码链接中的 Apress 站点上获得。

/**
  Beginning Sensor Networks Second Edition
  Sensor Networks Example Arduino Receiver Node

  This project demonstrates how to receive sensor data from
  an XBee sensor node. It uses an Arduino with an XBee shield
  with an XBee coordinator installed.

  Note: This sketch was adapted from the examples in the XBee
  library created by Andrew Rapp.
*/

#include <XBee.h>
#include <SoftwareSerial.h>

// Setup pin definitions for XBee shield
uint8_t recv = 2;
uint8_t trans = 3;
SoftwareSerial soft_serial(recv, trans);

// Instantiate an instance of the XBee library
XBee xbee = XBee();

// Instantiate an instance of the IO sample class
ZBRxIoSampleResponse ioSample = ZBRxIoSampleResponse();

void setup() {
  Serial.begin(9600);
  while (!Serial);   // Leonardo boards need to wait for Serial to start
  soft_serial.begin(9600);
  xbee.setSerial(soft_serial);
  Serial.println("Hello. Welcome to the Arduino XBee Data Aggregator.");
}

// Get address and print it

void get_address(ZBRxIoSampleResponse *ioSample) {
  Serial.print("Received data from address: ");
  Serial.print(ioSample->getRemoteAddress64().getMsb(), HEX);
  Serial.print(ioSample->getRemoteAddress64().getLsb(), HEX);
  Serial.println("");
}

// Get temperature and print it
void get_temperature(ZBRxIoSampleResponse *ioSample) {
  float adc_data = ioSample->getAnalog(3);

  Serial.print("Temperature is ");
  float temperatureC = ((adc_data * 1200.0 / 1024.0) - 500.0) / 10.0;
  Serial.print(temperatureC);
  Serial.print("c, ");
  float temperatureF = ((temperatureC * 9.0)/5.0) + 32.0;
  Serial.print(temperatureF);
  Serial.println("f");
}

// Get supply voltage and print it
void get_supply_voltage() {
  Serial.print("Supply voltage is ");
  int ref = xbee.getResponse().getFrameData()[17] << 8;
  ref += xbee.getResponse().getFrameData()[18];
  float volts = (float(ref) * float(1200.0 / 1024.0))/1000.0;
  Serial.print(" = ");
  Serial.print(volts);
  Serial.println(" volts.");
}

void loop() {
  //attempt to read a packet
  xbee.readPacket();

  if (xbee.getResponse().isAvailable()) {
    // got something

    if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {

      // Get the packet
      xbee.getResponse().getZBRxIoSampleResponse(ioSample);

      // Read and display data
      get_address(&ioSample);
      get_temperature(&ioSample);
      get_supply_voltage();
    }
    else {
      Serial.print("Expected I/O Sample, but got ");
      Serial.print(xbee.getResponse().getApiId(), HEX);
    }
  } else if (xbee.getResponse().isError()) {
    Serial.print("Error reading packet.  Error code: ");
    Serial.println(xbee.getResponse().getErrorCode());
  }
}

Listing 6-5Arduino XBee Receiver

在将草图上传到 Arduino 之前,请花些时间确保它已经编译好。记住,草图一旦上传,就开始运行了。另存为xbee_sensor.ino

测试最终项目

要测试该项目,请确保首先启动 Arduino,然后启动 XBee 传感器节点。启动 Arduino,上传草图,然后打开串行监视器。当 XBee 节点被 Arduino 上的协调器接受并添加到网络中时,您应该会观察到 XBee 调节分线板上的链路灯闪烁。在大约 5 秒钟内,XBee 传感器节点开始发送数据。发生这种情况时,Arduino 草图应该开始将语句打印到您的串行监视器。清单 6-6 显示了您应该在串行监视器中看到的输出示例。

Hello. Welcome to the Arduino XBee Data Aggregator.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.83 volts.
Received data from address: 13A2004192DB79
Temperature is 11.76c, 53.16f
Supply voltage is  = 3.83 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.82 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.83 volts.
Received data from address: 13A2004192DB79
Temperature is 12.34c, 54.22f
Supply voltage is  = 3.83 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.82 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.82 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.82 volts.
Received data from address: 13A2004192DB79
Temperature is 12.46c, 54.43f
Supply voltage is  = 3.82 volts.

Listing 6-6Sample Output of the XBee Arduino Sketch

你看到类似的东西了吗?如果是这样,那么您已经做了大量的工作,现在已经拥有了构建传感器节点和基于 Arduino 的传感器数据聚合器的基本组件。

如果您在串行监视器中看不到任何输出,请不要惊慌。相反,请仔细检查 Arduino 上的 XBee 是否正确插入,以及您是否使用了草图中与您正在使用的 XBee shield 如何连接到 Arduino 相对应的正确引脚(并非所有 shield 都像 SparkFun shield 一样使用引脚 2 和 3)。提示:查看你的盾的文档。

如果所有这些都是正确的,请确保在连接到 Arduino 的 XBee 上使用协调器 API 固件,在 XBee 传感器节点上使用路由器 API 固件。如果仍有问题,请返回到上一个项目,以确保传感器节点仍在工作。

您也可以尝试关闭 Arduino 和 XBee 传感器节点;然后打开 Arduino,等待大约 10 秒钟,并再次打开 XBee 传感器节点。有时握手过程和网络加入可能会停止,一段时间内什么都不会发生。按此顺序关闭再打开 XBee 可确保它会重新尝试配置。

另一方面,也许你正在获得数据,但它是不正确的——温度读数对于实际环境来说太低了。我曾经遇到过这种情况,当时我用来连接 TMP36 上数据引脚的电线被意外拔下。底线是总是检查,再检查你的线路。

为了更多乐趣

如果您想要扩展项目,您可以添加第二个 XBee 传感器节点,并修改 Arduino 草图以提供每个节点的位置。例如,您可以将一个节点标记为“办公室”,将另一个节点标记为“厨房”。草图应记录(写入串行监视器)传感器的位置以及来自 XBee 的传感器数据。

部件购物清单

完成本章中的项目需要一些组件。它们在表 6-5 中列出。

表 6-5

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| 发光二极管(任何颜色) | www.sparkfun.com/products/9592 | $0.35 | one |
| 按钮(试验板安装) | www.sparkfun.com/products/97 | $0.35 | one |
| 试验板(非迷你) | www.sparkfun.com/products/9567 | $5.95 | one |
| 试验板跳线 | www.sparkfun.com/products/8431 | $3.95 | one |
| www.adafruit.com/product/758 |
| DHT22 | www.sparkfun.com/products/10167 | $9.95 | one |
| www.adafruit.com/products/385 |
| 150 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| 4.7K 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| 10K 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| Arduino XBee 神盾局 | www.sparkfun.com/products/10854 | $24.95 | one |
| XBee-ZB (ZB)系列 2、2.5 或 3 | www.sparkfun.com | $25.00+ | Two |
| www.adafruit.com |
| www.makershed.com |
| TMP36 传感器 | www.sparkfun.com/products/10988 | $1.50 | one |
| www.adafruit.com/products/165 |
| 试验板电源 | www.sparkfun.com/products/10804 | $14.95 | one |
| 壁式电源(6V–12V) | www.sparkfun.com/products/15314 | $5.95 | one |
| 0.10 毫伏电容 | www.sparkfun.com/products/8375 | $0.25 | one |
| XBee Explorer 由集管调节 | www.sparkfun.com/products/11373 | $9.95 | one |
| 分离式公接头(可选) | www.adafruit.com/products/392 | $4.95 | one |
| Arduino Uno(任何支持盾牌的) | 各种各样的 | 25 美元及以上 | one |
| 斯帕克芬 XBee 盾 | www.sparkfun.com/products/10854 | $24.95 | one |
| 烙铁和焊料(可选) | 大多数在线和零售商店 | 变化 | one |

摘要

这一章涵盖了很多内容。您探索了 Arduino 平台,包括许多可用的表单以及如何编写草图(程序)来控制 Arduino。我还向您展示了如何使用温度和湿度传感器在 Arduino 中托管传感器。

您应用了在第 2 和 4 章中学到的关于 XBee 的信息,创建了一个 XBee 传感器节点来读取温度数据。然后,使用 XBee 协调器设置一个 Arduino,从 XBee 传感器节点接收传感器数据,并将其显示在串行监视器中。

在下一章中,您将发现各种用于存储传感器数据的机制,无论是板载的还是云中的。

七、存储传感器数据的方法

如果你已经成功完成了本书中的项目,那么你已经有了几种形式的传感器和数据聚合节点。本质上,您已经具备了构建传感器网络来监控和记录温度数据的基本构建模块。为湿度或气压等其他环境传感器添加节点并不需要太多工作。事实上,您构建的基本传感器节点可以托管各种传感器。

如果您已经运行了示例项目并尝试了这些挑战,毫无疑问,您已经注意到生成了大量数据。你用这些数据做什么?它是仅在生成时有意义,还是您认为更有可能希望存储数据并在以后检查它?例如,如果你想知道你的车间全年每月的温度范围,逻辑上你需要一整年的数据 1 来制表和平均。

Arduino 主板一般没有内置存储设备(但一些专门的变体有)。Raspberry Pi 板配有一个安全数字(SD)驱动器,可以接受基于 USB 的存储设备来存储数据,但如何处理来自基于 Arduino 的节点的数据呢?

本章分析了可用的存储方法,并举例说明了如何使用这些方法存储数据。提供了示例项目来说明机制和代码,但是为了简洁起见,我省略了特定于传感器的代码。

存储方法

传感器数据可以有几种形式。传感器可以产生由浮点数或整数组成的数字数据。一些传感器产生更复杂的信息,这些信息被组合在一起,可能包含多种形式的数据。知道如何解释读取的值通常是使用传感器最困难的部分。事实上,您可以在许多传感器节点示例中看到这一点。例如,温度传感器产生的值必须转换成刻度才有意义。

虽然可以将所有数据存储为文本,但如果您希望在另一个应用程序中使用数据,或者在电子表格或统计应用程序中使用数据,您可能需要考虑以二进制形式或易于转换的文本形式存储数据。例如,大多数电子表格应用程序可以轻松地将文本字符串(如"123.45)转换为浮点型,但它们可能无法将"12E236"转换为浮点型。另一方面,如果您计划为 Arduino 草图或 Raspberry Pi Python 脚本编写额外的代码来处理数据,您可能希望以二进制形式存储数据,以避免必须编写成本高昂(并且可能很慢)的转换例程。

但这只是问题的一部分。你在哪里存储数据是一个更大的问题。您希望以您需要的形式存储数据,但也希望将数据存储在一个位置(在设备上),您可以从该位置检索数据,并且在主机重新启动时不会被擦除。例如,将数据存储在 Arduino 的主内存中并不是一个好主意。它不仅消耗了宝贵的程序空间,而且是易失性的,并且在 Arduino 断电时会丢失。

树莓派提供了更好的选择。您可以轻松地创建一个文件,并将数据存储在根分区或 SD 卡上的主目录中。这是非易失性的,不会影响 Raspberry Pi 操作系统的运行。唯一的缺点是,如果数据显著增长,它可能会导致磁盘空间过少。但是在威胁到操作系统的稳定性之前,数据必须增长到近 2GB(对于 2GB 的 SD 卡而言)(尽管这是可能发生的)。

那么,你有哪些用 Arduino 存储数据的选择呢?树莓派还有其他的可能性吗?有两种类型的存储需要考虑:本地和远程。本地存储包括导致数据与节点一起存储的任何方法,例如,将数据存储在 Raspberry Pi 上的 SD 卡上。远程存储包括将数据存储在不直接连接到节点的设备或介质上的任何方法,例如,将数据存储在不同的节点上,甚至存储在连接到互联网的服务器上。

Storing Date And Time With Samples

Arduino 和 Raspberry Pi 都没有内置实时时钟(RTC)。如果您想在本地存储传感器数据,您必须存储带有大致日期和时间戳的数据,或者使用 RTC 模块读取准确的日期/时间值。

幸运的是,有 RTC 模块可以与 Arduino 或 Raspberry Pi 一起使用。如果您的 Raspberry Pi 连接到互联网,并且您已启用网络时间同步功能,则不需要 RTC 模块。但是,如果您的 Raspberry Pi 没有连接到互联网,并且您希望存储准确的时间数据,您应该考虑使用 RTC 模块。

以下部分介绍了 Arduino 和 Raspberry Pi 可用的各种本地和远程存储选项。

Arduino 的本地存储选项

虽然 Arduino 确实没有板载存储设备,但有两种方法可以在本地为 Arduino 存储数据。您可以将数据存储在一种特殊形式的非易失性存储器中,或者通过特殊的 SD 卡盾或以太网盾(大多数以太网盾都有内置的 SD 卡驱动器)托管在 SD 卡上。

如果你真的很有创造力(或者无法抗拒挑战),你可以使用一些通信协议向其他设备发送数据。例如,您可以使用串行接口将数据写入串行设备。

以下部分将更详细地讨论每个选项。后面的小节介绍了一些小项目,您可以用它们来学习如何使用这些设备来存储数据。

非易失存储器

Arduino 最常见的非易失性存储器是电可擦除可编程只读存储器(EEPROM,读作“e-e-prom”或“double-e prom”)。EEPROMs 被封装成芯片(集成电路)。顾名思义,数据可以写入芯片,即使上电后也是可读的,但可以被擦除或覆盖。

大多数 Arduino 板都有一个小 EEPROM,用于存储草图,并在上电时读取。如果你曾经想知道 Arduino 是如何做到这一点的,现在你知道了。如果愿意,您可以写入该内存中未使用的部分,但是可用的内存量很小(某些主板为 512KB)。您也可以使用 EEPROM,并通过 I2C 协议将其直接连接到 Arduino,以克服这一限制。

Arduino IDE 中包含一个特殊的库,支持对 EEPROM 的读写。由于可用存储器的数量有限,将数据存储在 EEPROM 存储器中对于大多数传感器节点来说并不理想。如果存储的数据很大或者每个样本有许多数据项,则可能会超出可用的内存。

您还会遇到从 EEPROM 获取数据以用于其它应用的问题。在这种情况下,您不仅要构建写入数据的方法,还要构建读取数据并将其导出到其他介质(本地或远程)的方法。

这并不是说你不应该使用 EEPROM 来存储数据。几个可能的原因证明了在 EEPROM 中存储数据的合理性。例如,如果您的传感器节点可能被隔离,或者与其他节点的连接受到限制,您可能希望在节点离线时使用 EEPROM 临时存储数据。事实上,您可以构建草图来检测节点何时离线,并在那时切换到 EEPROM。这样,基于 Arduino 的传感器节点可以继续记录传感器数据。一旦节点重新联机,您可以编写草图将 EEPROM 的内容转储到另一个节点(远程存储)。

sdcard

您还可以在 SD 卡上存储(和检索)数据。Arduino IDE 有一个用于与 SD 驱动器交互的库。在这种情况下,您可以使用库通过 SD shield 或以太网 shield 访问 SD 驱动器。

在 SD 卡上存储数据是通过文件完成的。您打开一个文件,以最适合下一阶段数据分析的格式将数据写入其中。Arduino IDE 和其他地方的示例演示了如何为 Arduino 创建 web 服务器界面,以显示 SD 卡上可用文件的列表。

与 EEPROMs 相比,SD 卡存储的数据要多很多倍。可以购买超过 128GB 存储空间的高密度 SD 卡。这是大量的传感器数据!

在传感器节点被设计为不与其他节点连接的远程传感器的情况下,您可以选择将数据存储到 SD 卡,或者在传感器节点断开连接或数据聚合器节点关闭的情况下,您可以将它用作备份日志记录设备。由于该卡可在其他设备上移动和读取,当您想要使用数据时,可以在另一个设备上读取它。

使用 SD 卡意味着您可以将数据从传感器节点移动到计算机,只需从 Arduino 中拔出 SD 卡,然后将其插入计算机中的 SD 读卡器即可。

项目:在非易失性存储器中保存数据

回想一下,您可以使用 Arduino 上的本地 EEPROM 来存储数据。Arduino IDE 中有一些优秀的例子,我鼓励您在闲暇时尝试一下。它们位于 EEPROM 子菜单下的示例菜单下。您只需要一台 Arduino 和一台笔记本电脑,就可以在 Arduino 上尝试读写 EEPROM。

本节不再重复使用内置 EEPROM 的示例草图,而是概述一个使用外部 EEPROM 存储数据的项目。与使用专用库进行交互的本地 EEPROM 不同,外部 EEPROM 使用 I2C 通信协议。

硬件设置

这个项目的硬件包括一个 24LC256 或 24LC512 EEPROM 芯片,如 SparkFun ( www.sparkfun.com/products/525 )、一个按钮、跳线和一个 Arduino。图 7-1 显示了一个典型的 24LC256 引脚安装 EEPROM 芯片。

img/313992_2_En_7_Fig1_HTML.jpg

图 7-1

I2C EEPROM 芯片(SparkFun 提供)

该按钮将允许您重置芯片上的存储器。这样做会擦除存储的数据值,重新设置内存配置以备再次使用。当第一次使用草图、调试问题以及在存储器被读取并存储在另一个介质上后重用芯片时,您会发现该功能特别方便。

该芯片通过 I2C 总线进行通信。如图 7-2 所示,您可以通过将接地或电源连接到引脚 A0–A2 来设置芯片的地址。您可以将此视为一个二进制数,其中将地连接到所有三个引脚是可用的最低地址(0x50),将电源连接到所有三个引脚是可用的最高地址(0x57)。表 7-1 显示了所需的可能地址和连接。通过将地连接到所有三个引脚,可以使用最低地址(0x50)。

表 7-1

设置 I2C EEPROM 的地址

|

地址

|

A0

|

一流的

|

主动脉第二声

|
| --- | --- | --- | --- |
| 0x50 | 地面 | 地面 | 地面 |
| 0x51 | 地面 | 地面 | +5V |
| 0x52 | 地面 | +5V | 地面 |
| 0x53 | 地面 | +5V | +5V |
| 0x54 | +5V | 地面 | 地面 |
| 0x55 | +5V | 地面 | +5V |
| 0x56 | +5V | +5V | 地面 |
| 0x57 | +5V | +5V | +5V |

img/313992_2_En_7_Fig2_HTML.jpg

图 7-2

I2C EEPROM 的引脚排列

现在您已经了解了如何寻址芯片,让我们将它连接到您的 Arduino。首先将芯片放在试验板上,半圆指向左侧。这将引脚 1 设置为右上引脚。将接地线连接到芯片顶部的所有四个引脚。这些是引脚 1–4,如图 7-2 所示。

接下来,将引脚 5 (SDA)连接到 Arduino 上的引脚 4,将引脚 6 (SCL)连接到 Arduino 上的引脚 5。将接地线连接到针脚 7。然后将正电压(+5V)连接到针脚 8。我们还在 I2C 线上使用 4.7K 欧姆的电阻来降低噪声。最后,将按钮一端连接到引脚 2,另一端接通电源。使用 10K 欧姆电阻器将按钮拉高(将其连接到正电压),就像您在之前的项目中所做的那样。详细接线图见图 7-3 。一定要仔细检查你的连接。

img/313992_2_En_7_Fig3_HTML.jpg

图 7-3

将 EEPROM 连接到 Arduino

Tip

如果您使用的是莱昂纳多板,您需要使用 USB 端口附近的 SDC 和 SCL 引脚。对于 Uno 板,它们位于 A4 和 A5,在 Mega 2560 上,它们位于 20 号和 21 号引脚。检查主板的硬件引脚排列,确保使用正确的 I2C 接口连接。

软件设置

布线就绪后,您就可以开始写草图来读写数据了。在本例中,您不是编写一个脚本来简单地存储数据,而是编写一个草图来允许您将数据写入芯片和从芯片中读取数据。您还包括一个复位操作,允许您覆盖任何内存。

添加读取方法,以便您可以创建额外的草图来读取数据,如果您希望查看数据,将芯片(数据)移动到另一个 Arduino,或者使用另一个草图来处理数据。

我们开始吧。您使用 I2C 库(称为Wire)与 EEPROM 交互。打开新草图,并输入以下内容:

#include <Wire.h>

#define FIRST_SAMPLE 0x02  // First position of first sample
#define MEM_ADDR 0x50      // EEPROM address
#define BUTTON_PIN 0x02    // Button pin
#define EEPROM_SIZE 32768  // Size of 24LC256
#define SAMPLE_BYTES 2     // Size of sample in bytes

int next_index = 0;        // Address of first sample

这些语句包括导线库,并定义了一些在草图中使用的常数。请注意,您有第一个样本的地址(芯片内存中的位置)、芯片地址、按钮引脚、最大尺寸(256 芯片)以及每个样本的字节数。

你需要很多方法。您需要将单个字节写入内存、存储样本、读取字节和读取样本的能力。让我们看看这些方法的最简单的形式——读字节方法。在下面的代码中,address 指的是 EEPROM 芯片的地址,index 指的是要访问的内存位置:

byte read_byte(int address, unsigned int index)  {
  byte data = 0xFF;

  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.endTransmission();

  Wire.requestFrom(address,1);

  if (Wire.available()) {
    data = Wire.read();
  }
  return data;
}

请注意与芯片通信的过程。首先,你用芯片开始一次传输,发送你想要读取的地址,然后结束传输。地址是一个两字节的值,这些语句向您展示了如何操作这些字节来形成一个字(两个字节)。下一个方法requestFrom(),告诉芯片你想读取一个字节。如果芯片准备好了,你就可以读取数据。最后,将值返回给调用者。

您可以对芯片的每个操作使用相同的格式。让我们看看将单个字节写入芯片的写入方法:

void write_byte(int address, unsigned int index, byte data) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.write(data);
  Wire.endTransmission();

  delay(5);
}

请注意,您有相同的设置—您开始传输并在指定的索引处设置值。不同的是,您在结束传输之前发送数据(写入数据)。

但是你怎么知道什么被写到哪个地址(或者索引)?让我们使用索引 0 处的第一个字节来存储数据样本(或行)的数量,使用第二个字节来存储每个样本消耗的字节(或列)的数量,而不是随意地或以某种不合逻辑的顺序写入数据。这样,您可以使数据更容易阅读,因为它是统一的,并且在重新启动时更容易管理。

实际上,让我们添加一个名为sample_data()的新方法来写入一些数据,并在启动时显示 EEPROM 中数据的内容。回想一下 Arduino,如果您想在启动时执行一次方法,您可以将它放在setup()方法中。下面显示了如何使用现有的读取方法从 EEPROM 读取数据,并在串行监视器中显示信息:

void sample_data(void) {
  int bytes_per_sample = SAMPLE_BYTES;
  byte buffer[SAMPLE_BYTES];

  next_index = read_byte(MEM_ADDR, 0);
  bytes_per_sample = read_byte(MEM_ADDR, 1);
  Serial.print("Byte pointer: ");
  Serial.println(next_index, DEC);
  Serial.print("Bytes per sample: ");
  Serial.println(bytes_per_sample, DEC);
  Serial.print("Number of samples:");
  Serial.println((next_index/bytes_per_sample)-1, DEC);

  // Add some sample data
  record_sample(MEM_ADDR, 6011);
  record_sample(MEM_ADDR, 8088);

  // Example of how to read sample data - read last 2 values
  read_sample(MEM_ADDR, next_index-(SAMPLE_BYTES * 2), buffer);
  Serial.print("First value: ");
  Serial.println((int)(buffer[0] << 8) + (int)buffer[1]);
  read_sample(MEM_ADDR, next_index-SAMPLE_BYTES, buffer);
  Serial.print("Second value: ");
  Serial.println((int)(buffer[0] << 8) + (int)buffer[1]);
}

这种技术使得通过在启动时运行 dump 方法来验证代码正在工作变得容易,如下所示。本质上,您创建了一个简单的自我诊断机制,可以用来检查数据的状态。如果您在启动时看到有效数据之外的任何东西,您就知道出了问题:

void setup(void) {
  Serial.begin(115200);
  while (!Serial);
  Wire.begin();
  Serial.println("Welcome to the Arduino external EEPROM project.");
  initialize(MEM_ADDR);
  sample_data();
}

但是等等!如果遇到未初始化的 EEPROM,这段代码会做什么?在这种情况下,您可以创建一种特殊的方法来初始化 EEPROM。下面的代码显示了initialize()方法:

void initialize(int address) {
  // Clear memory
  // NOTE: replace '10' with EEPROM_SIZE to erase all data
  for (int i = 0; i < 10; i++) {
    write_byte(address, i, 0xFF);
  }
  write_byte(address, 0, FIRST_SAMPLE);
  write_byte(address, 1, SAMPLE_BYTES);
  Serial.print("EEPROM at address 0x");
  Serial.print(address, HEX);
  Serial.println(" has been initialized.");
}

使用write_byte()方法为字节数写 0,为每个样本的字节数写前面定义的常数。该方法首先将 0xff 写入前 10 个字节,以确保没有存储任何数据;然后将字节数写入索引 0,将每个样本的字节数写入索引 1。您添加了一些反馈的打印报表。

但是这个方法是如何被调用的呢?一种方法是将它放在 setup()方法中,作为初始化 Wire 库的调用之后的第一个调用,但这意味着您必须注释掉其他方法,加载草图,执行它,删除该方法,然后重新加载。那似乎是许多额外的工作。一个更好的方法是用按钮触发这个方法。完成这项工作的代码放在loop()方法中,如下所示:

if (digitalRead(BUTTON_PIN) == LOW) {
  initialize(MEM_ADDR);
  delay(500); // debounce
}

现在,您可以读写一个字节并初始化芯片,您还需要能够读取一个样本,以防您想在另一个草图中使用芯片来处理数据。以下代码显示了读取样本的方法:

void read_sample(int address, unsigned int index, byte *buffer) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.endTransmission();
  Wire.requestFrom(address, SAMPLE_BYTES);
  for (int i = 0; i < SAMPLE_BYTES; i++) {
    if (Wire.available()) {
      buffer[i] = Wire.read();
    }
  }
}

请注意,您形成了一个类似于read_byte()的事件序列。但是,不是读取一个字节,而是使用一个循环来读取样本的字节数。您还需要一种将样本存储(写入)到芯片的方法:

void write_sample(int address, unsigned int index, byte *data) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Serial.print("START: ");
  Serial.println(index);
  for (int i = 0; i < SAMPLE_BYTES; i++) {
    Wire.write(data[i]);
  }
  Wire.endTransmission();
  delay(5); // wait for chip to write data
}

同样,该方法类似于write_byte()方法,但是您使用一个循环来写入样本的字节。请注意,您包含了一个 debug 语句来显示所使用的起始索引。这样做是为了在多次运行草图时可以看到值的增加。

Note

您可能已经注意到,我在*_ byte()和*_sample()方法中复制了代码。我这样做是为了代码的清晰,但这并不是绝对必要的。例如,如果您更改了*_sample()方法,使用一个额外的参数来指示读/写多少字节,那么您可以合并代码。我把这个优化留给你作为练习。

还有一个方法可以考虑。回想一下,您使用存储在索引 0 中的计数器来记录写入的样本数。write_sample()方法只是在特定的索引处写一个样本。您需要的是一个管理样本计数器和存储样本的方法。因此,您创建了一个record_sample()方法来处理更高级别的操作:

void record_sample(int address, int data) {
  byte sample[SAMPLE_BYTES];
  sample[0] = data >> 8;
  sample[1] = (byte)data;
  write_sample(address, next_index, sample);
  next_index += SAMPLE_BYTES;
  write_byte(address, 0, next_index);
}

请注意您是如何跟踪样本数量和下一个样本的下一个索引的。您使用之前创建的变量,并按照示例中的字节数对其进行递增。这样,您总是知道下一个地址是什么,而无需先读取样本数并计算索引。最后一个方法更新样本数值。

现在你已经有了所有的构建模块,清单 7-1 显示了这个草图的完整代码。将草图另存为external_eeprom.ino。请注意,在草图中,您没有包括任何要从传感器读取的代码。为了简洁起见,我省略了这一点,而是在setup()方法中包含了一些调试语句(以粗体显示),以展示如何记录样本。在修改草图以供传感器使用时,请务必删除这些陈述。

/**
  Beginning Sensor Networks Second Edition
  Sensor Networks Example Arduino External EEPROM data store

  This project demonstrates how to save and retrieve sensor data
  to/from an external EEPROM chip.
*/

#include <Wire.h>

#define FIRST_SAMPLE 0x02 // First position of fist sample
#define MEM_ADDR 0x50     // EEPROM address
#define BUTTON_PIN 0x02   // Button pin
#define EEPROM_SIZE 32768 // Size of 24LC256
#define SAMPLE_BYTES 2    // Size of sample in bytes

int next_index = 0;       // Address of first sample

/* Initialize the chip erasing data */
void initialize(int address) {
  // Clear memory
  // NOTE: replace '100' with EEPROM_SIZE to erase all data
  for (int i = 0; i < 100; i++) {
    write_byte(address, i, 0x00);
  }
  write_byte(address, 0, FIRST_SAMPLE);
  write_byte(address, 1, SAMPLE_BYTES);
  Serial.print("EEPROM at address 0x");
  Serial.print(address, HEX);
  Serial.println(" has been initialized.");
}

/* Write a sample to the chip. */
void write_sample(int address, unsigned int index, byte *data) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Serial.print("START: ");
  Serial.println(index);
  for (int i = 0; i < SAMPLE_BYTES; i++) {
    Wire.write(data[i]);
  }
  Wire.endTransmission();

  delay(5); // wait for chip to write data
}

/* Write a byte to the chip at specific index (offset). */
void write_byte(int address, unsigned int index, byte data) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.write(data);
  Wire.endTransmission();

  delay(5);
}

/* Read a sample from an index (offset). */
void read_sample(int address, unsigned int index, byte *buffer) {
  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.endTransmission();

  Wire.requestFrom(address, SAMPLE_BYTES);
  for (int i = 0; i < SAMPLE_BYTES; i++) {
    if (Wire.available()) {
      buffer[i] = Wire.read();
    }
  }
}

/* Read a byte from an index (offset). */
byte read_byte(int address, unsigned int index)  {
  byte data = 0xFF;

  Wire.beginTransmission(address);
  Wire.write((int)(index >> 8));   // MSB
  Wire.write((int)(index & 0xFF)); // LSB
  Wire.endTransmission();

  Wire.requestFrom(address,1);

  if (Wire.available()) {
    data = Wire.read();
  }
  return data;
}

/* Save a sample to the data chip and increment next address counter. */
void record_sample(int address, int data) {
  byte sample[SAMPLE_BYTES];

  sample[0] = data >> 8;
  sample[1] = (byte)data;
  write_sample(address, next_index, sample);
  next_index += SAMPLE_BYTES;
  write_byte(address, 0, next_index);
}

/* Example write data sample */
void sample_data(void) {
  int bytes_per_sample = SAMPLE_BYTES;
  byte buffer[SAMPLE_BYTES];

  next_index = read_byte(MEM_ADDR, 0);
  bytes_per_sample = read_byte(MEM_ADDR, 1);
  Serial.print("Byte pointer: ");
  Serial.println(next_index, DEC);
  Serial.print("Bytes per sample: ");
  Serial.println(bytes_per_sample, DEC);
  Serial.print("Number of samples:");
  Serial.println((next_index/bytes_per_sample)-1, DEC);

  // Add some sample data
  record_sample(MEM_ADDR, 6011);
  record_sample(MEM_ADDR, 8088);

  // Example of how to read sample data - read last 2 values
  read_sample(MEM_ADDR, next_index-(SAMPLE_BYTES * 2), buffer);
  Serial.print("First value: ");
  Serial.println((int)(buffer[0] << 8) + (int)buffer[1]);
  read_sample(MEM_ADDR, next_index-SAMPLE_BYTES, buffer);
  Serial.print("Second value: ");
  Serial.println((int)(buffer[0] << 8) + (int)buffer[1]);
}

void setup(void) {
  Serial.begin(115200);
  while (!Serial);
  Wire.begin();
  Serial.println("Welcome to the Arduino external EEPROM project.");
  initialize(MEM_ADDR);
  sample_data();
}

void loop() {
  delay(2000);
  if (digitalRead(BUTTON_PIN) == LOW) {
    initialize(MEM_ADDR);
    delay(500); // debounce
  }
  //
  // Read sensor data and record sample here
  //
  sample_data();
}

Listing 7-1Storing and Retrieving Data on an External EEPROM

请注意,您包括了一些通过串行监视器传达草图进度的附加语句。花一些时间来检查这些,这样你就可以熟悉草图运行时会发生什么。

Tip

如果你想写保护芯片,断开 WP 引脚。这样做使芯片成为只读的。

测试草图

要测试草图,请确保代码可以编译,并且您的硬件设置正确。当你有了一个可以编译的草图,把它上传到你的 Arduino 上,然后启动一个串口监视器。

第一次加载草图时,需要按下按钮来初始化 EEPROM。这是因为芯片上的值对于新芯片来说是未初始化的。您只需在第一次运行草图时执行此操作。完成后,您应该会看到类似于清单 7-2 中的输出。

Welcome to the Arduino external EEPROM project.
EEPROM at address 0x50 has been initialized.
Byte pointer: 2
Bytes per sample: 2
Number of samples: 0
START: 2
START: 4
First value: 6011
Second value: 8088
Byte pointer: 6
Bytes per sample: 2
Number of samples: 2
START: 6
START: 8
First value: 6011
Second value: 8088
Byte pointer: 10
Bytes per sample: 2
Number of samples: 4
START: 10
START: 12
First value: 6011
Second value: 8088
EEPROM at address 0x50 has been initialized.
Byte pointer: 2
Bytes per sample: 2
Number of samples: 0
START: 2
START: 4
First value: 6011
Second value: 8088
Byte pointer: 6
Bytes per sample: 2
Number of samples: 2
START: 6
START: 8
First value: 6011
Second value: 8088

Listing 7-2Serial Monitor Output for EEPROM Example

你看到类似的东西了吗?如果再次运行草图(例如,按下重置按钮),您应该看到开始索引值(从write_sample()方法)增加。去试一试吧。

当你在非易失性存储器中保存数据几次后,按下按钮,注意会发生什么。正如您在清单 7-2 中看到的,开始索引被重置,接下来的样本被存储在内存的开始处。

为了更多乐趣

这个项目的草图很有前途。毫无疑问,您可以用这段代码做很多事情。以下是改进代码和尝试使用外部 EEPROM 的一些建议:

  • 添加一些在嵌入式项目中使用的视觉帮助(没有串行监视器功能的情况)。您可以添加一个 LED,当芯片上有数据时它就会发光。您还可以添加一组七段 led 来显示存储的数据样本数。

  • 改进代码以便重用。首先删除前面在 read 和 write 方法中描述的冗余,然后将代码移到一个类中,以便更容易在其他草图中使用 EEPROM。

  • 添加第二个 EEPROM 芯片以扩展可用存储量。提示:您需要将每个芯片设置为不同的地址,但使用的方法是相同的。

  • 也许更简单、更符合 Arduino 硬件入侵原理的方法是将 EEPROM 转移到另一个 Arduino 并读取所有存储的值。这证明了 EEPROM 芯片的非易失性。

Caution

使用适当的接地来避免静电放电(ESD)对芯片的损坏。

项目:将数据写入 SD 卡

除了 EEPROM 芯片,您还可以通过将数据写入 SD 驱动器,在 Arduino 上本地存储数据。SD 驱动器是存储数据的好选择,因为数据存储在文件中,其他设备可以读取(和写入)。

例如,虽然将数据写入 EEPROM 芯片并不困难,但在个人电脑上读取该芯片需要为 Arduino 编写一个草图来传输数据。但是,SD 卡可以从 Arduino 中取出(一旦文件关闭),并插入连接到个人电脑的 SD 驱动器中,这样您就可以直接读取文件。因此,SD 卡是传感器网络的更好选择,在传感器网络中,传感器节点不通过网络或其他无线连接进行连接。

为 Arduino 添加 SD 读卡器有多种选择。最受欢迎的两个是来自 SparkFun ( www.sparkfun.com/categories/240 )的 Arduino Ethernet shield 和 microSD shield。如果您使用 Arduino Ethernet shield,您可以同时使用网络功能和 SD 卡。许多类似的设备可以从不同的供应商处获得。

Adafruit 还为 Arduino 提供了一个数据记录屏蔽,内置 SD 驱动器( www.adafruit.com/product/1141 )。数据记录屏蔽还包括一个 RTC,可以存储样品的日期和时间。我将讨论在下一个项目中使用 RTC。

Tip

microSD shield 和 Data Logging shield 都提供了一个原型制作区域,您可以使用它来安装传感器组件甚至 XBee 模块。

SD 驱动器允许您创建一个混合节点,在本地存储数据,并将其传输到网络中的另一个节点。这种冗余是您可以在传感器网络中构建耐用性的方法之一。例如,如果一个节点失去了通过网络与另一个节点的连接,它仍然可以在本地记录其数据。尽管这是一个手动恢复数据的过程(您必须去拿 SD 卡),但数据完全可以恢复的事实意味着网络可以在网络故障中生存而不会丢失数据。

可以使用 EEPROM 作为本地存储备份选项,但 EEPROM 更难使用,不如 SD 卡耐用,不具有相同的存储容量,也不容易在其他设备中使用。

关于构建耐用的传感器节点,还有一件非常重要的事情需要考虑。如果您不知道数据是何时存储的,拥有数据的本地备份可能没有帮助。除了有限的精确周期时间,Arduino 没有任何计时功能。因此,如果您在本地存储数据,而没有可以与其他数据关联的任何类型的时间戳,则所获取的样本可能没有超出序列本身(值的顺序)的意义。

为了减轻这种情况,您可以向 Arduino 添加一个 RTC 模块。RTC 允许您存储取样的日期和时间。如果您试图绘制一段时间内的值,或者想知道虚假或有趣的事件何时发生,此信息可能非常重要。

硬件设置

这个项目的硬件使用 Arduino Ethernet shield、SparkFun 的 microSD shield(安装了 SD 卡)或 Adafruit 的数据记录 shield。为了简单起见,我使用了 Arduino 以太网屏蔽,并展示了使用 microSD 屏蔽或数据记录屏蔽所需的代码更改(通过#define语句)。

您还需要 RTC 模块。Adafruit 有一款出色的产品,性能非常好,包括一个板载电池,即使在 Arduino 断电时也能为时钟供电。Adafruit 的 DS1307 实时时钟分线板套件( www.adafruit.com/product/3296 )是添加到您的项目中的优秀模块。图 7-4 显示了 Adafruit RTC 模块。

img/313992_2_En_7_Fig4_HTML.jpg

图 7-4

DS1307 实时时钟分线板(Adafruit 提供)

SparkFun 还有一个名为实时时钟模块( www.sparkfun.com/products/99 )的产品,它使用与 Adafruit 产品相同的 DS1307 芯片和接口。你可以在这个项目中使用任何一个。

Note

Adafruit RTC 模块需要装配。SparkFun 的 RTC 模块没有。

RTC 模块使用易于连接到 Arduino 的 I2C 接口。只需将 5V 电源连接到 5V 引脚,将地连接到 GND 引脚,将 SDA 引脚连接到 Arduino 的引脚 4,将 SCL 引脚连接到 Arduino 的引脚 5。图 7-5 显示了连接 RTC 模块的接线图。

img/313992_2_En_7_Fig5_HTML.jpg

图 7-5

带以太网屏蔽和 RTC 模块的 Arduino

请注意,以太网屏蔽安装在 Arduino 上。如果您使用的是 SparkFun microSD shield,接线也是一样的。

对于这个项目,我们将使用 Adafruit 数据记录屏蔽和 Arduino Uno Wi-Fi,以尽量减少布线。事实上,你所需要做的就是把盾牌插到你的 Uno 上,然后你就可以出发了!图 7-6 显示了数据记录屏蔽的样子。

img/313992_2_En_7_Fig6_HTML.jpg

图 7-6

Adafruit 数据记录屏蔽(Adafruit 提供)

Note

如果您没有数据记录屏蔽,您可以使用前面描述的 RTC 模块。

软件设置

有了数据记录屏蔽,您就可以开始写草图,将数据写入 SD 卡。打开主板电源之前,请务必插入已格式化的 SD 卡。但是首先,你必须从 Adafruit ( https://github.com/adafruit/RTClib )下载并安装 RTC 库。

回想一下,要安装一个库,我们打开草图包含库库管理器,一旦它加载了所有的头文件,键入 RTCLib 并从 Adafruit 中选择库,然后单击安装来安装它。图 7-7 显示了用于本项目的库。

img/313992_2_En_7_Fig7_HTML.jpg

图 7-7

在库管理器中安装 Adafruit RTCLib

一旦下载并安装了这个库(并且重启了 Arduino IDE),就可以开始一个名为sd_file_example.ino的新草图了。输入以下代码,指定您需要在草图中使用的模块。您需要 Wire、RTC、SD 和 String 库:

#include <Wire.h>
#include "RTClib.h"
#include <SD.h>
#include <String.h>

接下来,您需要定义用于与 SD 驱动器通信的 pin。以下是前面描述的所有三个 SD 驱动器选项的定义。我在这个例子中使用了以太网屏蔽;但是如果您没有使用以太网屏蔽,您可以注释掉该行,并取消注释掉与您正在使用的屏蔽相对应的行。还包括用于存储样本的文件名的定义。请注意,在使用 FAT 格式的 microSD 时,我们必须使用 8.3 文件名格式。 2

// Pin assignment for Arduino Ethernet shield
// #define SD_PIN 4
// Pin assignment for SparkFun microSD shield
//#define SD_PIN 8
// Pin assignment for Adafruit Data Logging shield
#define SD_PIN 10
// Sensor data file - require 8.3 file name
#define SENSOR_DATA "sensdata.txt"

现在你声明一些变量。您需要一个用于 RTC 模块,一个用于您在 SD 驱动器上使用的文件。请注意,我使用了 RTC_PCF8523 模块,因为数据记录屏蔽具有该 RTC 模块。确保使用与您的 RTC 芯片匹配的 RTC 模块。

RTC_PCF8523 rtc;
File sensor_data;

准备工作完成后,您需要一种将传感器样本保存到 SD 卡的方法。该方法必须从 RTC 模块读取日期和时间,接受样本作为参数,并存储数据。在本例中,首先放置日期和时间,然后放置样本值。将此方法命名为record_sample()

使用 RTC 库可以轻松读取 RTC 模块。您只需使用这个库,通过 now()方法获取当前日期和时间。从那里,您可以调用方法来获取月、日、年、小时等等。可以通过多种方式形成要写入文件的字符串。我使用 string 类来构造字符串。请随意使用您喜欢的任何其他方法:

// Capture the date and time
DateTime now = rtc.now();

写入文件非常容易。您只需以写模式(FILE_WRITE)打开文件,这将自动允许任何写操作被写到文件的末尾(追加)。这很好,因为您不必担心寻找或找出文件指针在文件中的位置。打开文件会返回一个 file 对象实例,您可以使用它来写入数据。写入文件(一旦打开)只需要一次方法调用。以下代码显示了使用 SD 库打开文件和写入数据的一组简化调用。我把 record_sample()方法的细节留给您在清单 7-2 中探索:

// Open the file
sensor_data = SD.open(SENSOR_DATA, FILE_WRITE);
// Save the data
sensor_data.write(1234);
sensor_data.write("\n");
// Close the file
sensor_data.close();

当然,您需要一些东西来正确地设置组件和库。setup()方法至少应包含串行、有线和 RTC 库的初始化(通过调用它们的begin()方法)以及对 SD 库的调用,以启动与 SD 驱动器的通信。以下是这些步骤所需代码的摘录。请注意,您还可以根据草图的最后编译日期和时间(实际上是上传的日期和时间)来初始化 RTC 的日期和时间:

void setup () {
  Serial.begin(115200);
  while(!Serial);
  Wire.begin();
  rtc.begin();

  if (!rtc.initialized()) {
    Serial.println("RTC is NOT running!");
    // Set time to date and time of compilation
    rtc.adjust(DateTime(__DATE__, __TIME__));
  }

  // disable w5100 SPI (if needed)
  // pinMode(10,OUTPUT);
  // digitalWrite(10,HIGH);

  // Initialize the SD card.
  Serial.print("Initializing SD card...");
  if (!SD.begin(SD_PIN)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");
...
}

请注意,您还有关闭以太网 W5100 SPI 接口的代码。这仅在以太网屏蔽时需要,并且仅在您不打算使用网络功能时才需要。

还有一件事你可能想补充。你可能需要检查一下是否能读取 SD 卡上的文件。仅仅初始化 SD 库是不够的。SD 驱动器可能会正常通信,但您无法在卡上打开或创建文件。将以下代码添加到 setup()方法中,作为额外的检查。在这种情况下,您检查文件是否存在,如果不存在,则尝试创建该文件。如果在打开呼叫时出现错误,您将打印一条消息:

// Check for file. Create if not present
if (!SD.exists(SENSOR_DATA)) {
  Serial.print("Sensor data file does not exit. Creating file...");
  sensor_data = SD.open(SENSOR_DATA, FILE_WRITE);
  if (!sensor_data) {
    Serial.println("ERROR: Cannot create file.");
  }
  else {
    sensor_data.close();
    Serial.println("done.");
  }
}

loop()方法是调用record_sample()方法的地方。在这种情况下,为了简洁起见,将loop()方法留空。您可以在这里随意添加自己的代码来读取传感器,并为每个传感器调用record_sample()方法。

清单 7-3 显示了这个项目的完整代码。虽然到目前为止的解释都是关于草图的关键部分,但是请注意,清单中添加了额外的错误处理代码,以确保 SD 驱动器正确初始化,并且文件存在并且可以写入。

/**
  Beginning Sensor Networks Second Edition
  Sensor Networks Example Arduino SD card data store

  This project demonstrates how to save sensor data to a
  microSD card.
*/

#include <Wire.h>
#include "RTClib.h"
#include <SD.h>
#include <String.h>

// Pin assignment for Arduino Ethernet shield
//#define SD_PIN 4
// Pin assignment for SparkFun microSD shield
//#define SD_PIN 8
// Pin assignment for Adafruit Data Logging shield
#define SD_PIN 10

// Sensor data file - require 8.3 file name
#define SENSOR_DATA "sensdata.txt"

RTC_PCF8523 rtc;
File sensor_data;

void record_sample(int data) {
  // Open the file
  sensor_data = SD.open(SENSOR_DATA, FILE_WRITE);
  if (!sensor_data) {
    Serial.println("ERROR: Cannot open file. Data not saved!");
    return;
  }

  // Capture the date and time

  DateTime now = rtc.now();

  String timestamp(now.month(), DEC);
  timestamp += ("/");
  timestamp += now.day();
  timestamp += ("/");
  timestamp += now.year();
  timestamp += (" ");
  timestamp += now.hour();
  timestamp += (":");
  timestamp += now.minute();
  timestamp += (":");
  timestamp += now.second();
  timestamp += (" ");

  // Save the sensor data
  sensor_data.write(&timestamp[0]);

  String sample(data, DEC);
  sensor_data.write(&sample[0]);
  sensor_data.write("\n");

  // Echo the data
  Serial.print("Sample: ");
  Serial.print(timestamp);
  Serial.print(data, DEC);
  Serial.println();

  // Close the file

  sensor_data.close();
}

void setup () {
  Serial.begin(115200);
  while(!Serial);
  Wire.begin();
  rtc.begin();

  if (!rtc.initialized()) {
    Serial.println("RTC is NOT running!");
    // Set time to date and time of compilation
    rtc.adjust(DateTime(__DATE__, __TIME__));
  }

  // disable w5100 SPI
  // pinMode(10,OUTPUT);
  // digitalWrite(10,HIGH);

  // Initialize the SD card.
  Serial.print("Initializing SD card...");
  if (!SD.begin(SD_PIN)) {
    Serial.println("initialization failed!");
    return;
  }
  Serial.println("initialization done.");

  // Check for file. Create if not present
  if (!SD.exists(SENSOR_DATA)) {
    Serial.print("Sensor data file does not exit. Creating file...");
    sensor_data = SD.open(SENSOR_DATA, FILE_WRITE);
    if (!sensor_data) {
      Serial.println("ERROR: Cannot create file.");
    }
    else {
      sensor_data.close();
      Serial.println("done.");
    }
  }

  // Record some test samples

.
  record_sample(1);
  record_sample(2);
  record_sample(3);
}

void loop () {
  // Read sensor data here and record with record_sample()
}

Listing 7-3Storing Data on an SD Card

出于演示的目的,我在setup()方法中添加了调试语句,以确保草图正常工作。在setup()方法中放置这些调用允许您加载草图(或重启 Arduino)并检查 SD 卡的内容,看看代码是否工作。如果您将语句放在loop()方法中,那么根据您关闭 Arduino 的时间(拔掉插头),您可能不知道添加了多少行,甚至不知道文件是否正确关闭。将record_sample()语句放在setup()方法中意味着您希望检查输出。

Tip

如果您收到 SD 驱动器初始化错误,请检查定义部分中使用的 pin 分配,以确保您对 SD 驱动器/屏蔽使用了正确的 pin。

如果遇到文件写入或打开错误,请确保 SD 卡被格式化为 FAT 分区,SD 卡没有写保护,并且您可以使用个人电脑在驱动器上创建和读取文件。

测试草图

要测试草图,请确保代码可以编译,并且您的硬件设置正确。一旦你有了一个可以编译的草图,把它上传到你的 Arduino 并启动一个串行监视器。以下代码显示了串行监视器中的预期输出:

Initializing SD card...initialization done.
Sample: 2/29/2020 16:34:27 1
Sample: 2/29/2020 16:34:27 2
Sample: 2/29/2020 16:34:28 3

Note

第一次运行草图时,您可能会看到一条关于初始化 SD 卡和创建文件的消息。这很正常。后续运行(Arduino 重新启动)可能不会显示消息。

如果在写入草图时多次运行草图,那么每次初始化草图时,都会在文件末尾插入三行。这是因为出于调试目的,您在setup()方法中放置了对record_sample()的示例调用。在您读取传感器之后,这些调用自然会放在 loop()方法中。以下代码显示了运行草图(启动 Arduino)四次后的文件内容示例:

2/29/2020 16:31:8 1
2/29/2020 16:31:8 2
2/29/2020 16:31:8 3
2/29/2020 16:34:27 1
2/29/2020 16:34:27 2
2/29/2020 16:34:28 3

如果您检查文件并发现比预期更多的条目集,请尝试从文件中删除数据,启动 Arduino,然后按两次重置按钮。当您查看内容时,您应该正好看到三组条目(一组用于初始启动,因为草图一开始就在内存中,另一组用于每次重新启动 Arduino)。

如果您只看到部分集合(每个集合少于三行),请检查以确保您在关闭 Arduino 之前允许它启动。最好在关闭 Arduino 之前,使用串行监视器,并等待所有三个语句都在监视器上出现。

如果您的草图编译成功,串行监视器中没有显示错误,但数据文件为空,请检查以确保卡可用且未损坏。尝试用 FAT 文件格式重新格式化该卡。

Handle With Care

MicroSD 卡非常易碎。如果处理不当或受到 ESD 或磁场的影响,它们很容易损坏。如果您的卡不能正常工作,并且您不能重新格式化它,它可能已经损坏而无法使用。您可以尝试使用 sdcard.org 的格式化程序,但如果失败,您的卡将不再有效。到目前为止,这种情况只在我身上发生过一次。

既然您已经研究了在 Arduino 上本地存储数据的两种主要方法,那么让我们看看 Raspberry Pi 的可用选项。

树莓酱的本地存储选项

因为 Raspberry Pi 是一台个人电脑,它具有创建、读取和写入文件的能力。虽然可以使用通过 GPIO 头连接的 EEPROM,但为什么要这样做呢?鉴于编程的简易性和使用文件的便利性,很少需要另一种形式的存储。

你也知道 Raspberry Pi 可以用多种方式编程,并且是用最流行的语言之一 Python。 3 在 Python 中处理文件非常容易,并且是默认库的原生功能。这意味着您不需要添加任何东西来使用文件。

以下项目演示了使用 Python 处理文件的简易性。在线 Python 文档详细解释了读写文件( https://docs.python.org/2/tutorial/inputoutput.html#reading-and-writing-files )。

你会注意到,文件放在哪里并不重要——是放在 SD 卡上还是连接的 USB 驱动器上。您只需要知道您想要存储数据的位置(文件夹)的路径,并将其传递给 open()方法。

精明的 Python 程序员 4 知道 Python 库包含了额外的库和类,用于操作文件夹、导航路径等等。有关更多信息,请查看 OS 和 Sys 库的 Python 文档。比如找normpath()Path 5 类。

项目:将数据写入文件

这个项目展示了使用 Python 在 Raspberry Pi 上使用文件是多么容易。因为不需要额外的硬件或软件库,所以我可以跳过这些部分,直接进入代码。

启动您的 Raspberry Pi,并登录。使用以下命令(或类似命令)打开一个新文件:

nano file_io_example.py

使用. py 扩展名命名该文件,以表明它是一个 Python 脚本。在文件中输入以下代码:

import datetime
with open("/home/pi/sample_data.txt", "a+") as my_file:
    my_file.write("{0} {1}\n".format(datetime.datetime.now(), 101))

在本例中,首先导入datetime库。您使用datetime来获取当前的日期和时间。接下来,使用较新的with子句打开文件(注意,您使用的是 Pi 用户的主目录),并向文件中写入一行(您不需要关闭文件——当 execute 离开with子句的范围时,已经为您完成了)。如果你觉得使用显式打开和关闭更好,请随意这样做。

注意open()方法。它需要两个参数——文件路径和名称以及打开文件的模式。您使用“a+”添加到文件中(a),如果文件不存在(+),则创建该文件。其他值包括 r 表示读取,w 表示写入。其中一些可以结合起来:例如,“rw+”创建一个不存在的文件,并允许读写数据。

Note

使用写入模式会截断文件。对于大多数需要存储传感器样本的情况,可以使用追加模式。

对于每次执行,您应该会看到一行时间值略有不同,对应于脚本运行的时间。要执行该文件,请使用以下命令:

python ./file_io_example.py

继续并尝试运行脚本。如果出现错误,请检查代码并更正任何语法错误。如果打开文件时遇到问题(运行脚本时出现 I/O 错误),请尝试检查您正在使用的文件夹的权限。尝试多次运行该脚本,然后显示文件的内容。以下代码显示了该项目的完整命令序列:

$ nano file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ python ./file_io_example.py
$ more sample_data.txt
2020-02-29 16:50:34.076657 101
2020-02-29 16:53:23.252384 101
2020-02-29 16:53:24.078429 101
2020-02-29 16:53:24.680599 101
2020-02-29 16:53:25.676225 101
2020-02-29 16:53:26.324482 101

有没有得到类似的结果?如果没有,请更正任何错误,然后重试,直到成功为止。从这个简单的例子中可以看出,在 Raspberry Pi 上使用 Python 将数据写入文件非常容易。

远程存储选项

远程存储意味着数据被发送到另一个节点或系统进行记录。这通常需要与远程系统进行某种形式的通信或网络连接。传感器网络本质上是相互连接的,因此可以利用远程存储。

为了让您了解我正在讨论的内容,请考虑一个 Arduino 传感器节点,它带有一个连接到基于 Raspberry Pi 的节点的 XBee 模块。假设您想将样本数据写入文件。您可以将数据发送到基于 Raspberry Pi 的节点并将数据存储在那里的文件中,而不是在 Arduino 节点上使用 SD 卡来存储数据。主要动机是在 Raspberry Pi 上通过 Python 使用文件要容易得多。如果您还考虑到使用 XBee 模块的多个 Arduino 传感器节点的可能性,您可以使用基于 Raspberry Pi 的节点作为数据集合,将所有数据存储在一个文件中。

Single File or Multiple Files?

在讨论存储聚合数据时,我有时会遇到这个问题。如果您的数据相似(例如温度),您可以考虑将来自相似传感器的数据存储到同一个文件中。但是,如果数据不同(例如一个节点的温度和另一个节点的湿度),您应该考虑使用不同的文件。这使得读取文件更容易,因为您不必编写代码(或使用工具)来分离数据。

但是,您真的只是在谈论将数据存储在文件中吗?答案是否定的。有许多远程存储数据的机制。虽然将数据存储在文件中是最简单的形式,但是您也可以将数据存储在云中,甚至存储在远程数据库服务器上。

如果您有使用数据库存储和检索数据的经验,这种方法会对您有吸引力——特别是如果您计划以后使用其他工具来处理数据。例如,您可能希望执行统计分析或创建图表来跟踪一段时间内的样本。因为使用数据库是一个复杂的主题,所以我将在接下来的几章中研究这种形式的远程存储。

您已经看到了使用文件是多么容易,但是如何在云中存储数据呢?这是怎么回事?简而言之,在云中存储数据涉及使用基于云的数据存储服务来接收数据并以某种方式托管数据。最流行的形式是将数据呈现给互联网上的其他人,供他们自己查看或消费。

下一节讨论如何使用 MathWorks 提供的一种流行的、易于使用的、基于云的物联网数据托管服务 ThingSpeak ( www.thingspeak.com )将样本数据存储在云中。您将看到在 Arduino 和 Raspberry Pi 上使用 ThingSpeak 的示例项目。

在云中存储数据

除非你住在一个非常偏僻的地方,否则你很可能已经被关于云和物联网的讨论轰炸了。也许你在杂志和电视上看到过广告,或者在其他书上读到过,或者参加过研讨会或会议。除非你花时间了解云的含义,否则你可能会想知道这一切有什么大惊小怪的。

简单来说, 6 云是一个通过互联网提供的服务的名称。这些可以是您可以访问的服务器(在更大的服务器上作为虚拟机运行),提供对特定软件或环境的访问的系统,或者是您可以附加到其他资源的资源,如磁盘或 IP 地址。云背后的技术包括网格计算(分布式处理)、虚拟化和网络。正确的科学术语是云计算。虽然对云计算的深入探讨超出了本书的范围,但是理解您可以使用云计算服务来存储您的传感器数据就足够了。

有许多物联网云供应商提供各种各样的产品、容量和功能,以满足您对物联网项目的任何需求。有这么多供应商提供物联网解决方案,很难选择一个。以下是云行业顶级供应商提供的更受欢迎的物联网产品的简短列表:

大多数供应商提供商业产品,但少数如谷歌,Azure,Arduino 和 ThingSpeak 提供有限的免费帐户。正如你所猜测的,有些产品是复杂的解决方案,需要很高的学习曲线,但是 Arduino 和 ThingSpeak 产品简单易用。因为我们想要一个支持 Arduino 和 Raspberry Pi(以及其他平台)的解决方案,所以我们将在本章中使用 ThingSpeak 作为在云中存储数据的例子。

Tip

如果您想要或需要使用其他供应商的产品,请确保在开始编写代码之前通读所有教程。

ThingSpeak 为每年产生少于 300 万条消息(或数据元素)或每天大约 8000 条消息的非商业项目提供免费账户。免费账户也限定为 7 个通道(一个通道相当于一个项目,最多可以保存 8 个数据项)。如果您需要存储或处理更多的数据,您可以购买四个类别之一的商业许可证,每个类别都有特定的产品、功能和限制:标准、学术、学生和家庭。请参见 https://thingspeak.com/prices 并单击每个许可选项,了解有关功能和定价的更多信息。

Note

除非你使用工作或学校账户,否则你可能需要付费使用一些产品,如 MatLab。

ThingSpeak 的工作原理是从包含您想要保存或绘制的数据的设备接收消息。有一些库可以用于某些平台或编程语言,如 Arduino 或 Python。这是迄今为止连接到 ThingSpeak 和传输数据最简单的方式。

但是,您也可以使用机器对机器(M2M)连接协议(称为 MQTT 7 )或表述性状态传输(REST 8 ) API,该 API 被设计为通过 HTTP 进行通信的请求-响应模型,用于向 ThingSpeak 发送数据或从中读取数据。是的,你甚至可以从其他设备读取数据。

Tip

参见 www.mathworks.com/help/thingspeak/channels-and-charts-api.html 了解更多关于 ThingSpeak MQTT 和 REST API 的细节。

当您想要读取或编写 ThingSpeak 通道时,您可以发布 MQTT 消息,通过 HTTP 向 REST API 发送请求,或者使用一个特定于平台的库来为您封装这些机制。一个通道最多可以有八个表示为字符串或数字数据的数据字段。您还可以使用一些复杂的过程来处理数字数据,如求和、平均、舍入等。

我们不会深入讨论这些协议的细节;相反,我们将看到如何使用 ThingSpeak 作为快速入门指南。MathWorks 提供了一整套教程、文档和示例。因此,如果你需要更多关于 ThingSpeak 如何工作的信息,请查看位于 www.mathworks.com/help/thingspeak/ 的文档。

The Cloud: Isn’t That Just Marketing Hype?

不要相信任何名称中包含“云”的产品的宣传或销售。云计算服务和资源应该可以通过互联网从任何地方访问,通过订阅(收费或免费)提供给你,并允许你消费或生产和分享所涉及的数据。此外,请考虑这样一个事实,即您必须能够访问云才能访问您的数据。因此,如果服务不可达(或关闭),您别无选择。

开始使用 ThingSpeak

要使用 ThingSpeak,您必须先注册一个帐户。幸运的是,他们提供了免费账户的选择。事实上,您可以先获得一个免费帐户,然后再添加(购买)许可证。要创建一个免费帐户,请访问 https://thingspeak.com/users/sign_up ,填写您的电子邮件地址、位置(通用地理信息)以及姓名,然后点击继续。然后,您将收到一封确认电子邮件。打开并按照说明验证您的电子邮件,并通过选择密码和完成一个简短的问卷来完成您的免费帐户。

创建频道

登录 ThingSpeak 后,您可以创建一个通道来保存您的数据。回想一下,每个通道最多可以有八个数据项(字段)。在你的登录首页,点击新频道,如图 7-8 所示。

img/313992_2_En_7_Fig8_HTML.jpg

图 7-8

在 ThingSpeak 中创建频道

你会看到一个很长的表格,里面有很多你可以填写的字段。图 7-9 显示了表格的示例。

img/313992_2_En_7_Fig9_HTML.jpg

图 7-9

新渠道形式

至少,您只需要命名频道,输入描述(不严格要求,但建议如此),然后选择(打勾)一个或多个字段来命名每个频道。就这样。点击保存频道完成该过程。

那么,这些频道设置是什么?下面给出了每种方法的简要概述。当您使用 ThingSpeak 时,您可能想要开始使用以下一些字段:

  • 完成百分比:根据频道中名称、描述、位置、URL、视频和标签的完成情况计算的字段。

  • 通道名称:通道的唯一名称。

  • 描述:通道的描述。

  • 字段# :勾选各框,启用该字段。

  • 元数据:JSON、XML 或 CSV 格式的频道附加数据。

  • 标签:逗号分隔的搜索关键字列表。

  • 链接到外部网站:如果你有一个关于你的项目的网站,你可以在这里提供网址,在频道上发布。

  • 显示频道位置:勾选此框,包括以下字段:

    • 纬度:项目或数据源传感器的纬度

    • 经度:项目或数据源传感器的经度

    • 标高:标高,以米为单位,用于受标高影响的项目

  • 视频 URL :如果您有一个与您的项目相关的视频,您可以在此处提供 URL 以在频道上发布。

  • 链接到 GitHub :如果你的项目托管在 GitHub,你可以提供要在频道上发布的 URL。

哇,这么多东西都是免费的!正如你将看到的,这不是一个简单的玩具或严重受限的产品。有了这些设置,你可以完成很多事情。请注意,有些地方可以放置视频、网站和 GitHub 的链接。这是因为通道可以是私有的(只有你的登录名或 API 密匙可以访问),也可以是公共的。公开频道允许您与任何人共享数据,因此这些 URL 字段可以方便地记录您的项目。酷毙了。

一旦你创建了你的通道,是时候写一些数据了。对于大多数项目来说,有两条信息是您需要的:通道的 API 键和一些库的通道号(显示在通道页面上的整数值)。许多平台都有可用的库,在某些平台上,可能有几种方法(库或技术)将数据写入 ThingSpeak 通道。

您可以通过点击 API 密钥选项卡在渠道页面上找到 API 密钥。当您创建一个新通道时,您将拥有一个 write 和一个 read API 密钥。如果需要,您可以添加更多密钥,以便每个设备、位置、客户等可以使用一个密钥。图 7-10 显示了之前在图 7-9 中创建的通道的 API 键选项卡。

img/313992_2_En_7_Fig10_HTML.jpg

图 7-10

ThingSpeak 通道的 API 键

注意,我遮住了钥匙。如果您公开您的频道,请不要与您不想允许写入您的频道的任何人共享写入密钥。您可以通过点击生成新的写 API 密钥添加新的读 API 密钥按钮来创建新的密钥。点击删除 API 键按钮可以删除读取的键。

我们在代码中使用密钥来允许设备连接到通道并向通道写入数据。因此,我们通常从频道页面复制这个字符串,并将其作为字符串粘贴到我们的代码中。回想一下,我们可能使用封装了 HTTP 或 MQTT 机制的库,或者在 Raspberry Pi Python 库的情况下,我们使用 Python 库和 HTTP 协议。我们将在即将到来的 Arduino 和 Raspberry Pi 示例项目中看到这两者。

既然你已经理解了向 ThingSpeak 写入数据的基础,那么让我们来看看如何为 Arduino 更详细地做这件事。接下来是树莓派的例子。

项目:用 Arduino 向 ThingSpeak 写入数据

这个项目演示了如何将传感器数据写入 ThingSpeak 通道。与本章前面的项目不同,您将使用一个传感器并生成一些样本数据。在这种情况下,您在 Arduino MKR1000 上监控温度,并将摄氏和华氏温度值保存到您的 ThingSpeak 中。如果您还没有为 Arduino 创建 ThingSpeak 通道,现在就创建,并记录通道 ID 和生成的 API 密钥。使用以下通道数据,并将其命名为MKR1000_TMP36,如图 7-11 所示。

img/313992_2_En_7_Fig11_HTML.jpg

图 7-11

为 MKR1000 和 TMP36 传感器设置通道

点击保存频道按钮创建频道。然后,在 API Key 选项卡上,复制 write key 并粘贴到一个新文件中以备后用。

现在我们已经创建了一个通道,让我们设置硬件。

硬件设置

本项目的硬件是 Arduino MKR1000、试验板、试验板电线、TMP36 温度传感器和 0.10uF 电容。如图 7-12 所示连接传感器和电容器。将传感器的第 1 针连接到 Arduino 上的 5V 针,将传感器的第 2 针连接到 MKR1000 上的 A1 针,将第 3 针连接到 MKR1000 上的地。电容器也连接到传感器的引脚 1 和 3(方向无关紧要)。

Tip

你可以使用较新的 MKR 型号,只要它们是 Wi-Fi 类型的。

img/313992_2_En_7_Fig12_HTML.jpg

图 7-12

Arduino 的 ThingSpeak 温度馈送的布线设置

若要使用“连接到互联网”,您还需要 Wi-Fi 接入点或路由器来连接。您将需要连接的 SSID 和密码。

现在,让我们看看如何设置软件和项目草图。

配置 Arduino IDE

为了创建草图,我们需要在 Arduino IDE 中设置一些东西。我们需要确保 MKR 板得到支持,ThingSpeak 和 Wi-Fi 101 库得到安装。让我们从 MKR 董事会的支持开始。

在 Arduino IDE 中,打开一个新草图,点击工具板卡 XXXX板卡管理器……(其中 XXXX 代表你最后使用的板卡)。当表单加载时,在搜索框中键入 Arduino & MKR1000,并安装 Arduino SAMD 支持,如图 7-13 所示。

img/313992_2_En_7_Fig13_HTML.jpg

图 7-13

安装 SAMD 板支架

点击安装按钮,安装板卡支撑模块。在某些 PC 平台上,当您第一次使用连接到 PC 的主板启动 IDE 时,Arduino IDE 可能会提示您安装主板支持。

现在,让我们安装我们需要的两个库。你可以按任何顺序做。在 Arduino IDE 中,选择草图包含库……管理库。在搜索框中输入 ThingSpeak,然后点击安装按钮,如图 7-14 所示。再次点击安装安装库。

img/313992_2_En_7_Fig14_HTML.jpg

图 7-14

安装 ThingSpeak 库

我们还需要 Wi-Fi 101 图书馆,以允许使用互联网通信。在 Arduino IDE 中,选择草图包含库……管理库。在搜索框中输入 Wi-Fi 101,然后点击安装按钮,如图 7-15 所示。再次点击安装安装库。

img/313992_2_En_7_Fig15_HTML.jpg

图 7-15

安装 WiFi 101 库

写草图

现在您已经安装了必要的库和板模块,打开一个新的 Arduino 项目并将其命名为arduino_thingspeak.ino。回想一下,我们将使用 TMP36 并从 MKR1000 上的引脚 A1 读取值。我们已经在第六章中看到了读取 TMP36 的代码,所以我们将跳过解释,直接进入如何与 ThingSpeak 交互。

在这个例子中,我们将看到如何在一个单独的头文件(.h)中存储我们的 API 密钥和其他关键数据,该文件将是草图的一部分,并保存在同一个文件夹中。要添加一个头文件,点击草图右侧的小向下箭头按钮,选择新建标签,如图 7-16 所示。在提示中,输入secrets.h并按键输入。这将打开一个新的标签。单击该选项卡打开文件。

img/313992_2_En_7_Fig22_HTML.jpg

图 7-22

树莓派的示例 ThingSpeak 饲料

img/313992_2_En_7_Fig21_HTML.jpg

图 7-21

验证 ADC 模块

img/313992_2_En_7_Fig20_HTML.jpg

图 7-20

将 TMP36 和 ADC 连接到 Raspberry Pi

img/313992_2_En_7_Fig19_HTML.jpg

图 7-19

12 位 ADC 模块(Adafruit 提供)

img/313992_2_En_7_Fig18_HTML.jpg

图 7-18

为 Raspberry Pi 和 TMP36 传感器设置通道

img/313992_2_En_7_Fig17_HTML.jpg

图 7-17

通道数据示例(MKR1000_TMP36)

img/313992_2_En_7_Fig16_HTML.jpg

图 7-16

添加新选项卡

我们将把 Wi-Fi 和我们的 ThingSpeak 频道数据放在这个文件中。使用#define指令创建我们将在主草图中使用的新字符串。以下代码显示了文件所需的行和数据。输入这些并保存文件。

#define SECRET_SSID "YOUR_SSID"                 // SSID
#define SECRET_PASS "SSID_PASS"                 // WiFi Password
#define SECRET_CH_ID 0000000000                 // Channel number
#define SECRET_WRITE_APIKEY "ABCDEFGHIJKLMNOP"  // Write API Key

现在,返回到主草图选项卡。从以下内容开始草图。您需要我们刚刚创建的ThingSpeak.hWiFi101.hsecrets.h文件。

#include "ThingSpeak.h"
#include <WiFi101.h>
#include "secrets.h"

接下来,我们需要声明几个变量,并实例化 Wi-Fi 客户机,如下面的代码所示。我们还添加了一个变量来存储传感器的 pin 号。注意,我们使用存储在secrets.h文件中的那些#defines

char ssid[] = SECRET_SSID;   // your network SSID (name)
char pass[] = SECRET_PASS;   // your network password
WiFiClient  client;

unsigned long myChannelNumber = SECRET_CH_ID;
const char * myWriteAPIKey = SECRET_WRITE_APIKEY;

int SENSOR_PIN = 1;

要使用以太网屏蔽,您还必须声明一个 MAC 地址。以太网屏蔽的 IP 地址是通过 DHCP 请求的。您将 MAC 地址定义为数组。这可以是一组随机值,只要它们在 0x 00–0x ff 范围内。您可以使用此处显示的内容:

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

接下来,定义 ThingSpeak API 键和提要 ID。您还可以定义 TMP36 传感器的引脚编号。本例使用引脚 0。您可以选择想要使用的 pin——只需更改这个定义,剩下的代码就会指向正确的 pin:

char ThingSpeakKey[] = "<YOUR_KEY_HERE>";
#define FEED_NUMBER <YOUR_FEED_HERE>
#define SENSOR_PIN 0

现在我们准备编写setup()方法。您必须初始化串行类(这样您就可以使用串行监视器)并初始化 Wi-Fi 和 ThingSpeak 客户端。您对这些操作中的每一个都使用了begin()方法。对于 Wi-Fi 类,您传入前面定义的 SSID 和密码。完整的setup()方法如下:

void setup() {
  Serial.begin(115200);      // Initialize serial
  while (!Serial);
  // Connect to WiFi
  Serial.println("Welcome to the MKR1000 + TMP36 ThingSpeak Example!");
  while(WiFi.status() != WL_CONNECTED){
    WiFi.begin(ssid, pass);
    delay(5000);
  }
  Serial.println(" Connected.");
  ThingSpeak.begin(client);  // Initialize ThingSpeak
}

最后,loop()方法包含读取传感器的代码,计算摄氏和华氏温度,并将这些数据发送到我们的 ThingSpeak 通道。为此,我们首先调用 ThingSpeak 库的setField()方法来设置我们想要更新的每个字段(字段编号从 1 开始)。然后我们使用writeFields()方法将数据发送给 ThingSpeak。我们可以检查该调用的结果,以确保返回的代码是200,这意味着成功(OK)。这里显示了loop()方法的简化版本:

void loop() {
  // Read TMP36 here

  // Set the fields with the values
  ThingSpeak.setField(1, temperatureC);
  ThingSpeak.setField(2, temperatureF);

  // Write to the ThingSpeak channel
  int res = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
  if (res == 200) {
    Serial.println("Channel update successful.");
  } else {
    Serial.print("Problem updating channel. HTTP error code ");
    Serial.println(res);
  }
  Serial.println("sleeping...");
  delay(20000); // Wait 20 seconds to update the channel again
}

请注意,如果没有返回代码 200,我们将显示实际结果。还要注意,我们在末尾添加了一个 sleep ( delay())来休眠 20 秒。我们这样做是因为 ThingSpeak 免费帐户仅限于每 15 秒更新一次。

现在您已经理解了草图的流程和内容,您可以完成缺失的部分并开始测试。清单 7-4 显示了该项目的完整草图。

/**
  Beginning Sensor Networks Second Edition
  Sensor Networks Arduino ThingSpeak Write Example

  This project demonstrates how to write data to a ThingSpeak channel.
*/

#include "ThingSpeak.h"
#include <WiFi101.h>
#include "secrets.h"

char ssid[] = SECRET_SSID;   // your network SSID (name)
char pass[] = SECRET_PASS;   // your network password
WiFiClient  client;

unsigned long myChannelNumber = SECRET_CH_ID;
const char * myWriteAPIKey = SECRET_WRITE_APIKEY;

int SENSOR_PIN = 1;

void setup() {
  Serial.begin(115200);      // Initialize serial
  while (!Serial);
  // Connect to WiFi
  Serial.println("Welcome to the MKR1000 + TMP36 ThingSpeak Example!");
  Serial.print("Attempting to connect to SSID: ");
  Serial.print(SECRET_SSID);
  Serial.print(" ");
  while(WiFi.status() != WL_CONNECTED){
    WiFi.begin(ssid, pass);
    Serial.print(".");
    delay(5000);
  }
  Serial.println(" Connected.");
  ThingSpeak.begin(client);  // Initialize ThingSpeak

}

void loop() {
  // Read TMP36
  float adc_data = analogRead(A1);
  float voltage = adc_data *  (3.3 / 1024.0);
  Serial.print("Temperature is ");
  float temperatureC = (voltage - 0.525) / 0.01;
  Serial.print(temperatureC);
  Serial.print("C, ");
  float temperatureF = ((temperatureC * 9.0)/5.0) + 32.0;
  Serial.print(temperatureF);
  Serial.println("F");

  // Set the fields with the values
  ThingSpeak.setField(1, temperatureC);
  ThingSpeak.setField(2, temperatureF);

  // Write to the ThingSpeak channel
  int res = ThingSpeak.writeFields(myChannelNumber, myWriteAPIKey);
  if (res == 200) {
    Serial.println("Channel update successful.");
  } else {
    Serial.print("Problem updating channel. HTTP error code ");
    Serial.println(res);
  }
  Serial.println("sleeping...");
  delay(20000); // Wait 20 seconds to update the channel again
}

Listing 7-4Arduino-Based ThingSpeak Channel Write

Note

确保在secrets.h文件中替换您的 API 密钥和通道号。否则会导致编译错误。

花一些时间来确保所有的代码都输入正确,并且草图编译无误。一旦到了这个阶段,就可以上传草图,进行试用了。

测试草图

要测试草图,请确保代码可以编译,并且您的硬件设置正确。一旦你有了一个可以编译的草图,把它上传到你的 Arduino MKR1000 并启动一个串行监视器。以下代码显示了您应该看到的输出示例:

Attempting to connect to SSID: ATT-WiFi-0059 . Connected.
Temperature is 15.50C, 59.90F
Channel update successful.
sleeping...
Temperature is 16.14C, 61.06F
Channel update successful.
sleeping...
Temperature is 15.82C, 60.48F
Channel update successful.
sleeping...
Temperature is 16.14C, 61.06F
Channel update successful.
sleeping...
Temperature is 16.46C, 61.64F
Channel update successful.
sleeping...

你看到类似的输出了吗?如果没有,请检查串行监视器中显示的返回代码。您应该会看到返回代码 200(表示成功)。如果返回代码是一位数(1、2、3 等等),您可能会遇到连接到 ThingSpeak 的问题。如果出现这种情况,请将您的笔记本电脑连接到同一根网线,并尝试访问 ThingSpeak。

如果连接速度非常慢,您可能会遇到每隔一次或每 N 次尝试都得到 200 以外的错误代码的情况。如果是这种情况,您可以在loop()方法中增加超时来进一步延迟处理。这可能有助于一些非常慢的连接,但它不是一个坏的或间歇性的连接治愈。

在访问 ThingSpeak 之前,让草图运行大约 3 分钟。草图运行一段时间后,导航到 ThingSpeak,登录并点击您的渠道页面。您应该会看到类似于图 7-17 所示的结果。

请注意图表开头和结尾附近的峰值。我通过在 TMP36(我的手指)上按压一个温暖的物体设备来模拟数据中的尖峰。如果你尝试这样做,小心不要碰到任何电线!

为了更多乐趣

您可以从这个脚本中获得很多乐趣。尝试连接其他传感器,并在 ThingSpeak 中创建其他通道。你也可以尝试阅读你保存在 ThingSpeak 中的数据。

既然您已经知道如何在 Arduino 上将数据保存到 ThingSpeak,那么让我们来探索如何在 Raspberry Pi 上做同样的事情。

项目:用 Raspberry Pi 向 ThingSpeak 写入数据

这个项目演示了在 Raspberry Pi 上通过 HTTP 使用 ThingSpeak REST API 将传感器数据写入 ThingSpeak 通道的简单性。回想一下,要读取模拟温度传感器(TMP36),您需要使用一个提供 12 位读数精度的 I2C 模块。

Tip

这个例子演示了如何使用 HTTP 接口向 ThingSpeak 写数据。然而,这也是一个 ThingSpeak Python 库,如果你愿意,你可以使用它。你可以用pip3 install thingspeak命令安装它。ThingSpeak Python 库的文档可以在 https://thingspeak.readthedocs.io/en/latest/ 找到。

如果您还没有为 Raspberry Pi 创建 ThingSpeak 通道,现在就创建,并记录通道 ID 和生成的 API 键。使用以下通道数据并命名为RASPI_TMP36,如图 7-18 所示。

点击保存频道按钮创建频道。然后,在 API Key 选项卡上,复制 write key 并粘贴到一个新文件中以备后用。

现在我们已经创建了一个通道,让我们设置硬件。

硬件设置

本项目的硬件包括一个 Raspberry Pi、一个 Raspberry Pi Cobbler+(可选)、一个试验板、TMP36 传感器、跳线和一个 ADC 模块。

我提到过,Raspberry Pi 不包含任何 ADC,因此不能使用模拟传感器。在本项目中,您将探索如何将多通道 ADC 与 Raspberry Pi 配合使用,以便使用 TMP36 模拟温度传感器。图 7-19 显示了 Adafruit ( www.adafruit.com/products/1083 )的 12 位 ADC。该模块最多支持四个传感器(通道)。在图中,您可以看到引脚 A0–A3;这些是用于支持的每个通道的引脚。

Tip

您正在探索将 ADC 模块与 Raspberry Pi 一起使用,因为它支持 I2C 协议,但是您也可以将该模块与 Arduino 一起使用。详见 http://learn.adafruit.com/adafruit-4-channel-adc-breakouts

您还需要通过 Raspberry Pi 上的网络连接连接到互联网。互联网连接可以通过有线以太网连接或无线连接。像 Arduino 一样,对连接没有具体要求。

图 7-20 显示了您需要进行的连接。如果你已经完成了前几章中的项目,这些对你来说应该很熟悉。对于 TMP36,将引脚 1 连接到与 ADC 模块相同的 5V 连接,将引脚 3 连接到 ADC 模块的 GND 连接。传感器上的引脚 2 连接到 ADC 模块上的 A0 引脚。

按如下方式连接 TMP36 传感器(再次参见图 7-20 )。

Caution

务必仔细检查您的连接,并将其与图 7-20 进行比较。树莓派上的东西连接不当会导致电路板损坏。

完成这些连接后,打开 Raspberry Pi 并发出以下命令:

$ sudo i2cdetect –y 1

您应该会看到 ADC 模块在输出中显示为地址 0x48,如图 7-21 所示。

写代码

现在您已经有了所需的库,是时候编写一个脚本来从 TMP36 传感器读取样本(通过 ADC 模块)并将数据保存到您的 ThingSpeak 通道了。由于我们已经编写了读取 TMP36 传感器的代码,因此我们将专注于向 ThingSpeak 写入数据的代码。

首先在树莓 Pi 上打开一个名为raspi_tmp36.py的新文件。您可以使用 Thonny IDE 或文本编辑器或终端中的nano来创建文件。

让我们从进口开始。我们需要为 ADC 模块导入 http.client、time、urllib、board、busio 和 Adafruit 库,如下所示:

import http.client
import time
import urllib

# import the Raspberry Pi libraries
import board
import busio

# Import the ADC Adafruit libraries
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

接下来,我们需要为 API 键声明一个变量,并实例化 I2C 接口,如下所示:

# API KEY
THINGSPEAK_APIKEY = 'YOUR_API_KEY'

# Instantiate (start/configure the I2C protocol)
i2c = busio.I2C(board.SCL, board.SDA)
# Instantiate the ADS1115 ADC board
ads = ADS.ADS1115(i2c)
# Setup the channel from Pin 0 on the ADS1115
channel0 = AnalogIn(ads, ADS.P0)

接下来是脚本的核心代码。我们将使用一个try…except块来捕获键盘中断( Ctrl+C )。在其中,我们准备了一个特殊的 URL 来编码我们通道的字段数据,然后打开这个 URL。

更具体地说,我们使用urllib.parseurlencode()方法以包含字段数据的字典的形式对数据进行编码。这确保了创建的字符串在 URL 中有效使用。接下来,我们创建一个标题字典,并将其传递给http.clientHttpConnection()方法,以打开到 ThingSpeak 的连接。最后,我们以对 update REST API 端点的POST命令的形式将数据发送给 ThingSpeak。哇哦!下面的代码显示了这些步骤。花点时间通读它们。它们应该容易理解。记住,你可以。

params = urllib.parse.urlencode(
    {
        'field1': temp_c,
        'field2': temp_f,
        'key': THINGSPEAK_APIKEY,
    }
)
# Create the header
headers = {
    "Content-type": "application/x-www-form-urlencoded",
    'Accept': "text/plain"
}
# Create a connection over HTTP
conn = http.client.HTTPConnection("api.thingspeak.com:80")
# Execute the post (or update) request to upload the data
conn.request("POST", "/update", params, headers)

清单 7-5 显示了该项目的完整代码。你会注意到我们跳过了print()语句和错误处理代码,但是这些都是我们在之前的项目中看到的。在运行代码之前,请务必通读一遍,这样您就可以看到它是如何工作的。此外,您可以从图书网站下载这些代码,而不是键入所有代码。

#
# Beginning Sensor Networks Second Edition
#
# IoT Example - Publish temperature data from a Raspberry Pi
# with TMP36 and ADC.
#
# Dr. Charles A. Bell
# March 2020
#
from __future__ import print_function

# Python imports
import http.client
import time
import urllib

# import the Raspberry Pi libraries
import board
import busio

# Import the ADC Adafruit libraries
import adafruit_ads1x15.ads1115 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

# API KEY
THINGSPEAK_APIKEY = 'YOUR_API_KEY'

# Instantiate (start/configure the I2C protocol)
i2c = busio.I2C(board.SCL, board.SDA)
# Instantiate the ADS1115 ADC board
ads = ADS.ADS1115(i2c)
# Setup the channel from Pin 0 on the ADS1115
channel0 = AnalogIn(ads, ADS.P0)

# Run the program to upload temperature data to ThingSpeak

print("Welcome to the ThingSpeak Raspberry Pi temperature sensor! Press CTRL+C to stop.")
try:
    while 1:
        # Get temperature in Celsius
        temp_c = ((channel0.voltage * 3.30) - 0.5) * 10
        # Calculate temperature in Fahrenheit
        temp_f = (temp_c * 9.0 / 5.0) + 32.0
        # Display the results for diagnostics
        print("Uploading {0:.2f} C, {1:.2f} F"
              "".format(temp_c, temp_f), end=' ... ')
        # Setup the data to send in a JSON (dictionary)
        params = urllib.parse.urlencode(
            {
                'field1': temp_c,
                'field2': temp_f,
                'key': THINGSPEAK_APIKEY,
            }
        )
        # Create the header
        headers = {
            "Content-type": "application/x-www-form-urlencoded",
            'Accept': "text/plain"
        }
        # Create a connection over HTTP
        conn = http.client.HTTPConnection("api.thingspeak.com:80")
        try:
            # Execute the post (or update) request to upload the data
            conn.request("POST", "/update", params, headers)
            # Check response from server (200 is success)
            response = conn.getresponse()
            # Display response (should be 200)
            print("Response: {0} {1}".format(response.status,
                                             response.reason))
            # Read the data for diagnostics
            data = response.read()
            conn.close()
        except Exception as err:
            print("WARNING: ThingSpeak connection failed: {0}, "
                  "data: {1}".format(err, data))

        # Sleep for 20 seconds

        time.sleep(20)
except KeyboardInterrupt:
    print("Thanks, bye!")
exit(0)

Listing 7-5Complete Code for the raspi_thingspeak.py Script

Note

确保在标记的位置替换您的 API 密钥。否则将导致运行时错误。

现在您已经输入了所有的代码,让我们测试脚本,看看它是否工作。

测试脚本

Python 脚本是解释程序。尽管在脚本开始时有大量的语法检查,但是直到执行语句时才发现逻辑错误。因此,如果没有正确输入脚本,您可能会遇到错误或异常(例如,如果您拼错了方法或变量名称)。如果您未能替换 API 键和提要编号的占位符,也可能会发生这种情况。

要运行该脚本,请输入以下命令。在使用 Ctrl+C 中断主循环之前,让脚本运行几次迭代。

$ python3 ./raspi_thingspeak.py

以下代码显示了您应该看到的输出示例:

Welcome to the ThingSpeak Raspberry Pi temperature sensor! Press CTRL+C to stop.
Uploading 18.46 C, 65.23 F ... Response: 200 OK
Uploading 18.49 C, 65.28 F ... Response: 200 OK
Uploading 19.20 C, 66.56 F ... Response: 200 OK
Uploading 18.41 C, 65.13 F ... Response: 200 OK
Uploading 18.24 C, 64.83 F ... Response: 200 OK
Uploading 18.25 C, 64.85 F ... Response: 200 OK
Uploading 18.31 C, 64.96 F ... Response: 200 OK
Uploading 18.32 C, 64.97 F ... Response: 200 OK
Uploading 18.29 C, 64.93 F ... Response: 200 OK
Uploading 18.35 C, 65.03 F ... Response: 200 OK
Uploading 18.24 C, 64.83 F ... Response: 200 OK
Uploading 18.39 C, 65.09 F ... Response: 200 OK
Uploading 18.25 C, 64.84 F ... Response: 200 OK
Thanks, bye!

让脚本运行 3 分钟左右,然后在 ThingSpeak 上导航到你的 Raspberry Pi 频道。您应该会看到您的传感器数据显示出来,类似于图 7-22 所示。

如果您没有看到类似的数据,请返回并检查上一个项目中讨论的返回代码。您应该会看到返回代码 200(成功)。检查并更正网络连接中的任何错误或脚本中的语法或逻辑错误,直到它成功运行几次(存储的所有样本都返回代码 200)。

如果你看到类似的数据,恭喜你!您现在知道了如何使用两个不同的平台生成数据并将其保存到云中。

为了更多乐趣

您可以从这个脚本中获得很多乐趣。尝试连接其他传感器,并在 ThingSpeak 中为它们创建其他通道。你也可以尝试阅读你保存在 ThingSpeak 中的数据。

将传感器数据存储在数据库中

正如您可能已经猜到的,可以将传感器数据存储到 Raspberry Pi 上的数据库中。您可以使用 MySQL 作为您的数据库服务器,并使用 Connector/Python 库来编写 Python 脚本,该脚本读取传感器数据并将数据存储在表中以供以后处理。因为涉及的内容比几十行代码要多得多(比如在 Raspberry Pi 上设置 MySQL),所以您将在第 8 和 9 章中更详细地探讨这个主题。

部件购物清单

完成本章中的项目需要一些组件,如表 7-2 中所列。其中一些,如 XBee 模块和支持硬件,也包含在其他章节的购物清单中。这些如表 7-3 所示。

表 7-3

以前章节中重复使用的组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| 按钮 | www.sparkfun.com/products/97 | $0.35 | one |
| 试验板(非迷你) | www.sparkfun.com/products/9567 | $4.95 | one |
| 试验板跳线 | www.sparkfun.com/products/8431 | $3.95 | one |
| TMP36 传感器 | www.sparkfun.com/products/10988 | $1.50 | one |
| www.adafruit.com/products/165 |
| 0.10uF 电容 | www.sparkfun.com/products/8375 | $0.25 | one |
| 树莓派 3B,3B+或 4B | 大多数在线和零售商店 | 35 美元及以上 | one |
| HDMI 或 HDMI 转 DVI 电缆 | 大多数在线和零售商店 | 变化 | one |
| HDMI 或 DVI 监视器 | 大多数在线和零售商店 | 变化 | one |
| USB 键盘 | 大多数在线和零售商店 | 变化 | one |
| USB 电源 | 大多数在线和零售商店 | 变化 | one |
| USB A 型至 USB 微型插头 | 大多数在线和零售商店 | 变化 | one |
| SD 卡,2GB 或更大 | 大多数在线和零售商店 | 变化 | one |
| 鞋匠+ | www.adafruit.com/products/914 | $7.95 | one |
| 10K 欧姆电阻器 | 大多数在线和零售商店 | 变化 | one |
| 4.7K 欧姆电阻器 | 大多数在线和零售商店 | 变化 | Two |

表 7-2

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| I2C EEPROM | www.sparkfun.com/products/525 | $1.95 | one |
| Arduino 以太网盾 | www.sparkfun.com/products/9026 | $24.95 | 1 |
| 微型防护罩 | www.sparkfun.com/products/9802 | $14.95 |
|
| Arduino 的数据记录屏蔽 | www.adafruit.com/products/1141 | $19.95 | * |
| DS1307 实时时钟分线板 | www.adafruit.com/product/3296 | $7.50 | 1 |
| 实时时钟模块 | www.sparkfun.com/products/99 | $14.95 |
|
| 12 位 ADC 模块 | www.adafruit.com/products/1083 | $9.95 | one |

* 你只需要这些选项中的一个。

这两种方法都可以。

摘要

本章探讨了 Arduino 和 Raspberry Pi 的本地存储选项。您完成了许多小型项目,展示了每种可能的储物方案。我还讨论了使用 MathWorks 的 ThingSpeak 物联网网站将传感器数据存储在云中。在那里,我们学习了如何创建通道和向通道发送数据。

在接下来的两章中,我将暂停对传感器项目的探索,开始讨论另一种形式的远程存储:数据库服务器。第八章着重于设置 MySQL 服务器,第九章着重于通过为 Arduino 编写的特殊数据库连接器(库)将 MySQL 服务器与 Arduino 一起使用。

八、将您的 Raspberry Pi 变成数据库服务器

既然您已经知道了什么是传感器网络,甚至如何使用 Arduino 和 Raspberry Pi 构建传感器节点,那么是时候用您的 Raspberry Pi 做一些真正酷的事情了。上一章讨论了存储传感器数据的各种方法。最可靠和最通用的方法之一是将您的传感器数据存储在数据库中。本章探索了如何使用 Raspberry Pi 作为数据库服务器。

虽然这一直是从旧版本的 2B 板开始的树莓 Pi 的一个选项,但现在树莓 Pi 4B 板出来了,它甚至更是一个选项。他们有足够的处理能力和更多的内存来处理繁重的数据库工作。酷!

您从简单介绍 MySQL 开始,然后开始在 Raspberry Pi 上运行 MySQL。如果你有安装和使用 MySQL 的经验,你可以跳到“构建一个 Raspberry Pi MySQL 服务器”一节。

什么是 MySQL?

MySQL 是世界上最受欢迎的开源数据库系统,原因有很多。首先,它是开源的,这意味着任何人都可以免费使用它来完成各种各样的任务。 2 最棒的是,MySQL 被包含在许多平台仓库中,使其易于获取和安装。如果你的平台在资源库中没有包含 MySQL(比如 aptitude),可以从 MySQL 网站( http://dev.mysql.com )下载。

甲骨文公司拥有 MySQL。Oracle 通过收购 Sun Microsystems 获得了 MySQL,Sun Microsystems 从其原始所有者 MySQL AB 获得了 MySQL。尽管担心会出现相反的情况,但 Oracle 通过继续投资于新功能的演进和开发以及忠实地维护其开源遗产,表现出了对 MySQL 的出色管理。尽管 Oracle 也提供 MySQL 的商业许可——就像它以前的所有者过去做的那样——MySQL 仍然是开源的,每个人都可以使用。

What is Open Source? Is it Really Free?

开源软件是从对公司财产心态的有意识抵制中成长起来的。在为麻省理工工作时,自由软件运动之父理查德·斯托尔曼抵制了软件私有(封闭)的趋势,离开了麻省理工,创办了 GNU (GNU 的非 Unix)项目和自由软件基金会(FSF)。

斯托曼的目标是重建一个合作的开发者社区。然而,他有先见之明,意识到这个系统需要版权许可来保证某些自由。(有些人把斯托曼对版权的理解称为“左版权”,因为它保障了自由,而不是限制了自由。)为了解决这个问题,斯托曼创建了 GNU 公共许可证(GPL)。GPL 是一个巧妙的法律许可作品,它允许代码不受限制地被复制和修改,规定衍生作品(修改后的副本)必须在与原始版本相同的许可下发布,没有任何附加限制。

自由软件运动有一个问题。自由一词旨在保证使用、修改和发布的自由;这并不意味着“没有成本”或“免费到一个好的家。”为了消除这种误解,开放源码倡议(OSI)成立了,后来采用并推广了“开放源码”一词来描述 GPL 许可证所保证的自由。有关开源软件的更多信息,请访问 www.opensource.org

MySQL 在你的系统上作为后台进程运行(或者作为前台进程运行,如果你从命令行 3 )。像大多数数据库系统一样,MySQL 支持结构化查询语言(SQL)。您可以使用 SQL 创建数据库和对象(使用数据定义语言[DDL]),写入或更改数据(使用数据操作语言[DML]),以及执行各种命令来管理服务器。

要发出这些命令,必须首先连接到数据库服务器。MySQL 提供了一个客户端应用程序,使您能够连接到服务器并在其上运行命令。该应用程序被命名为 MySQL Shell ( mysqlsh),与旧客户端相比有许多改进,包括更好的界面以及 SQL、Python 和 JavaScript 模式。如果你过去用过 MySQL,你可能会对老一点的 MySQL 客户端(mysql)比较熟悉,也可以用,但是 MySQL Shell 就好用多了。请参阅 MySQL Shell 的在线参考手册( https://dev.mysql.com/doc/mysql-shell/8.0/en/ )以了解更多关于如何使用它的信息,但是那些使用过旧客户端的人或者跟随教程的人会很快上手。

Tip

在 Raspberry Pi 上工作时最好使用旧的mysql客户端,因为它需要的编译和安装步骤更少,但是您可以在 Raspberry Pi 上构建和安装 MySQL Shell。

如果您还没有安装 MySQL Shell,请访问 https://dev.mysql.com/downloads/shell/ 并下载,然后安装在您的系统上。对于 macOS 和 Linux,请遵循您用于任何其他软件的特定于平台的安装过程。对于 Windows,您可以下载单独的 MySQL Shell 安装(.msi)或者下载 Windows Installer,其中包含所有 MySQL 应用程序、工具和驱动程序。在这种情况下,您只需在安装开始时选择想要的组件。

当然,你还需要访问运行在某个地方的 MySQL 服务器。好消息是你可以在你的电脑上安装它!只需从 https://dev.mysql.com/downloads/mysql/ 网站(社区版)下载正确的安装程序,安装在你的系统上即可。它非常容易安装,但如果你想要一步一步的指导,请参见在线参考手册获得帮助( https://dev.mysql.com/doc/refman/8.0/en/ )。

一旦 MySQL Shell 安装到您的系统上,您就可以启动它,如清单 8-1 所示,该清单显示了前面讨论的每种类型命令的示例。请注意,这些命令在旧客户端中的工作方式是相同的。

$ mysqlsh --uri root@localhost:33060
Please provide the password for 'root@localhost:33060':
Save password for 'root@localhost:33060'? [Y]es/[N]o/Ne[v]er (default No): y
MySQL Shell 8.0.18

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:33060'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 8 (X protocol)
Server version: 8.0.18 MySQL Community Server – GPL

> CREATE DATABASE testme;
Query OK, 1 row affected (0.0012 sec)

> CREATE TABLE testme.table1 (sensor_node char(30), sensor_value int, sensor_event timestamp);
Query OK, 0 rows affected (0.0059 sec)

> INSERT INTO testme.table1 VALUES ('living room', 23, NULL);
Query OK, 1 row affected (0.0051 sec)

> SELECT ∗ FROM testme.table1;
+-------------+--------------+--------------+
| sensor_node | sensor_value | sensor_event |
+-------------+--------------+--------------+
| living room |           23 | NULL         |
+-------------+--------------+--------------+
1 row in set (0.0003 sec)

> SET @@global.server_id = 111;
Query OK, 0 rows affected (0.0002 sec)

> \q
Bye!

Listing 8-1Commands Using the MySQL Shell

如果您还没有使用过 MySQL Shell,请看看我是如何启动这个 Shell 的。注意,我以不同的格式输入了用户凭证,这非常直观,比单独的选项要简单一些。还要注意,shell 允许我保存密码,以便以后更快地登录。很好!

在本例中,您看到 DML 以CREATE DATABASECREATE TABLE语句的形式出现,DDL 以INSERTSELECT语句的形式出现,还有一个简单的管理命令来设置全局服务器变量。接下来,创建一个数据库和一个表来存储数据,在表中添加一行,最后检索表中的数据。

MySQL 中有很多可用的命令。幸运的是,你只需要掌握几个比较常见的。以下是您最常使用的命令。<>中包含的部分表示用户提供的命令组件,而[...]表示需要额外的选项:

Tip

必须用分号(;\G结束每个命令。

  • CREATE DATABASE <database_name>:创建数据库

  • USE <database>:设置默认数据库

  • CREATE TABLE <table_name> [...]:创建一个表格或结构来存储数据

  • INSERT INTO <table_name> [...]:向表格中添加数据

  • UPDATE [...]:更改特定行的一个或多个值

  • DELETE FROM <table_name> [... ]:从表格中删除数据

  • SELECT [...]:从表格中检索数据(行)

虽然这个列表只是一个简短的介绍,并不像一个完整的语法指南,但有一个很好的在线参考手册,它非常详细地解释了每个命令(以及更多)。当你对 MySQL 有任何疑问时,你应该参考在线参考手册。你可以在 https://dev.mysql.com/doc/refman/8.0/en/ 找到它。

如果您认为 MySQL 不仅仅是几个简单的命令,那么您绝对是正确的。尽管 MySQL 易于使用且启动时间快,但它是一个成熟的关系数据库管理系统(RDBMS)。比你在这里看到的要多得多。有关 MySQL 的更多信息,包括所有高级特性,请参见参考手册。

MYSQL—What Does it Mean?

MySQL 这个名字是一个专有名称和一个缩写的组合。SQL 是结构化查询语言。“我的部分”不是所有格形式——它是一个名称。在这种情况下,My 是创始人的一个女儿的名字。至于发音,MySQL 专家发音为“My-S-Q-L”而不是“my sequel”事实上,一个精明的 MySQL 用户的标志在于他们对产品的正确发音。

MySQL 入门

既然您已经知道了 MySQL 是什么以及它是如何使用的,那么在开始构建您的第一个数据库服务器之前,您需要对 RDBMSs 和 MySQL 有更多的了解。本节讨论 MySQL 如何存储数据(以及存储在哪里),如何与其他系统通信,以及管理新 MySQL 服务器所需的一些基本管理任务。

Note

我将这些信息作为 MySQL 的教程或入门介绍。在后面的小节中,您将在 Raspberry Pi 上安装 MySQL。

但是首先,让我们回顾一下什么是关系数据库系统以及它为什么重要。

什么是关系数据库管理系统?

RDBMS 是一种基于数据关系模型的数据存储和检索服务,由 E. F. Codd 于 1970 年提出。这些系统是结构化数据的标准存储机制。大量的研究致力于改进 Codd 提出的基本模型,正如 C. J. Date 在数据库关系模型:回顾和分析中所讨论的。 4 这种理论和实践的演变最好地记录在第三个宣言中。 5

关系模型是存储库(数据库)的直观概念,可以通过使用一种称为查询语言的机制来检索、更新和插入数据,从而方便地查询存储库。关系模型已经被许多厂商实现,因为它具有完善的系统理论、坚实的数学基础和简单的结构。最常用的查询机制是 SQL,它类似于自然语言。虽然关系模型中不包括 SQL,但它提供了关系模型在 RDBMSs 中的实际应用的一个组成部分。

数据被表示为关于特定事件或实体的相关信息(属性或列)。属性值集以元组的形式形成(有时称为记录或行)。元组存储在具有相同属性集的表中。然后,表可以通过键、属性和元组的约束与其他表相关。

表可以有称为索引的列的特殊映射,允许您以特定的顺序读取数据。索引对于快速检索与索引列的值相匹配的行也非常有用。

现在我们已经了解了一些理论,让我们看看 MySQL 是如何存储我们的数据的。

MySQL 存储数据的方式和位置

MySQL 数据库系统通过一种有趣的编程隔离机制存储数据,这种机制称为存储引擎,由处理程序接口控制。处理程序接口允许在 MySQL 服务器中使用可互换的存储组件,以便解析器、优化器和各种组件可以使用公共机制在磁盘上存储数据时进行交互。这也称为可插拔存储引擎。 6 虽然 MySQL 支持几种存储引擎,但默认的存储引擎被称为 InnoDB,这是一种事务存储引擎。

这对你意味着什么?这意味着您可以选择不同的数据存储机制,但对于大多数应用程序,您不需要更改存储引擎。如果您确实想更改存储引擎,可以在下面的代码示例所示的CREATE TABLE语句中指定存储引擎。请注意命令中的最后一行:这是如何指定存储引擎的。去掉这个子句会导致 MySQL 使用默认的存储引擎(InnoDB)。

CREATE TABLE `books` (
  `ISBN` varchar(15) DEFAULT NULL,
  `Title` varchar(125) DEFAULT NULL,
  `Authors` varchar(100) DEFAULT NULL,
  `Quantity` int(11) DEFAULT NULL,
  `Slot` int(11) DEFAULT NULL,
  `Thumbnail` varchar(100) DEFAULT NULL,
  `Description` text
) ENGINE=MyISAM;

太好了。现在,MySQL 上存在哪些存储引擎?您可以通过发出以下命令来发现支持哪些存储引擎。如你所见,有很多可供选择。我将介绍一些可能与传感器网络规划相关的内容。

> SELECT engine, support, transactions FROM information_schema.engines;
+--------------------+---------+--------------+
| engine             | support | transactions |
+--------------------+---------+--------------+
| ARCHIVE            | YES     | NO           |
| BLACKHOLE          | YES     | NO           |
| MRG_MYISAM         | YES     | NO           |
| FEDERATED          | NO      | NULL         |
| MyISAM             | YES     | NO           |
| PERFORMANCE_SCHEMA | YES     | NO           |
| InnoDB             | DEFAULT | YES          |
| MEMORY             | YES     | NO           |
| CSV                | YES     | NO           |
+--------------------+---------+--------------+
9 rows in set (0.0005 sec)

通用存储引擎

从 5.6 版本开始,MySQL 默认使用 InnoDB 存储引擎。以前的版本默认使用 MyISAM。InnoDB 是一个完全事务性的,ACID 7 存储引擎。事务是一批语句,在将任何更改写入磁盘之前,这些语句必须全部成功。典型的例子是银行转账。如果您考虑一个需要从一个帐户中扣除一笔金额,然后将该金额存入另一个帐户以完成资金转移的系统,您不会希望第一个帐户成功,第二个帐户失败,反之亦然!

将语句包装在一个事务中可以确保在所有语句都正确无误地完成之前,不会将任何数据写入磁盘。在这种情况下,事务由 BEGIN 语句指定,并以保存更改的COMMIT或撤销更改的ROLLBACK结束。InnoDB 将其数据存储在一个文件中(还有一些用于管理索引和事务的附加文件)。

MyISAM 存储引擎针对读取进行了优化。MyISAM 作为默认引擎已经有一段时间了,并且是第一批可用的存储引擎之一。事实上,服务器的很大一部分是专用于支持 MyISAM 的。它与 InnoDB 的不同之处在于,它不支持事务,并且以索引顺序访问方法格式存储数据。这意味着它支持快速索引。如果您不需要事务,并且希望能够移动或备份单个表,那么您应该选择 MyISAM 而不是 InnoDB。

您可能需要考虑的另一个存储引擎是归档,尤其是对于传感器网络。该引擎不支持删除(但是您可以删除整个表),并且针对磁盘上的最小存储进行了优化。很明显,如果你在一个像 Raspberry Pi 这样的小系统上运行 MySQL,最小化磁盘使用可能是一个目标。无法删除数据可能会限制更高级的应用,但大多数传感器网络只是存储数据,很少删除数据。在这种情况下,您可以考虑使用归档存储引擎。

还有 CSV 存储引擎(其中 CSV 代表逗号分隔值)。此存储引擎创建文本文件,以纯文本形式存储数据,其他应用程序(如电子表格应用程序)可以读取这些数据。如果您将传感器数据用于统计分析,CSV 存储引擎可能会使获取数据的过程更容易。

我的数据存储在哪里?

那么这些数据都在哪里呢?如果您查询 MySQL 服务器并发出命令SHOW VARIABLES LIKE 'datadir';,您会看到所有存储引擎用来存储数据的磁盘位置的路径。对于 InnoDB,这是位于数据目录中的磁盘上的一个文件。InnoDB 也创建一些管理文件,但是数据存储在单个文件中。对于除 NDB 和内存之外的大多数其他存储引擎,表的数据存储在 data 目录下一个以数据库名称命名的文件夹中。清单 8-2 给出了一个例子。数据库文件夹以粗体显示。为了简洁起见,省略了一些文件。

Tip

当您第一次使用 sudo 时,您需要输入 root 用户的密码。

> SHOW VARIABLES LIKE 'datadir';
+---------------+------------------------+
| Variable_name | Value                  |
+---------------+------------------------+
| datadir       | /usr/local/mysql/data/ |
+---------------+------------------------+
1 row in set (0.0037 sec)
> \q
Bye!

$ sudo ls -lsa /usr/local/mysql/data
total 336248
    0 drwxr-x---   12 _mysql  _mysql       384 Nov  4 16:28 #innodb_temp
    0 drwxr-x---   30 _mysql  _mysql       960 Nov  4 17:05 .
    0 drwxr-xr-x   17 root    wheel        544 Nov  4 16:28 ..
    8 -rw-r-----    1 _mysql  _mysql        56 Nov  4 16:28 auto.cnf
    8 -rw-r-----    1 _mysql  _mysql       665 Nov  4 16:28 binlog.000001
  264 -rw-r-----    1 _mysql  _mysql     84608 Nov  4 17:05 binlog.000002
    8 -rw-r-----    1 _mysql  _mysql        32 Nov  4 16:28 binlog.index
    0 drwxr-x---    8 _mysql  _mysql       256 Nov  4 17:05 bvm
    8 -rw-r-----    1 _mysql  _mysql      3513 Nov  4 16:28 ib_buffer_pool
98304 -rw-r-----    1 _mysql  _mysql  50331648 Nov  4 17:05 ib_logfile0
98304 -rw-r-----    1 _mysql  _mysql  50331648 Nov  4 16:28 ib_logfile1
24576 -rw-r-----    1 _mysql  _mysql  12582912 Nov  4 17:05 ibdata1
24576 -rw-r-----    1 _mysql  _mysql  12582912 Nov  4 16:28 ibtmp1
    0 drwxr-x---    8 _mysql  _mysql       256 Nov  4 16:28 mysql
49152 -rw-r-----    1 _mysql  _mysql  25165824 Nov  4 17:05 mysql.ibd
    8 -rw-r-----    1 _mysql  _mysql       739 Nov  4 16:28 mysqld.local.err
    8 -rw-r-----    1 _mysql  _mysql         5 Nov  4 16:28 mysqld.local.pid
    0 drwxr-x---  105 _mysql  _mysql      3360 Nov  4 16:28 performance_schema
    0 drwxr-x---    3 _mysql  _mysql        96 Nov  4 16:28 sys
    0 drwxr-x---    3 _mysql  _mysql        96 Nov  4 16:36 testme
20480 -rw-r-----    1 _mysql  _mysql  10485760 Nov  4 17:05 undo_001
20480 -rw-r-----    1 _mysql  _mysql  10485760 Nov  4 17:05 undo_002

$ sudo ls -lsa /usr/local/mysql/data/bvm
total 64
 0 drwxr-x---   8 _mysql  _mysql   256 Nov  4 17:05 .
 0 drwxr-x---  30 _mysql  _mysql   960 Nov  4 17:05 ..
16 -rw-r-----   1 _mysql  _mysql  5324 Nov  4 17:05 books.MYD
 8 -rw-r-----   1 _mysql  _mysql  1024 Nov  4 17:05 books.MYI
16 -rw-r-----   1 _mysql  _mysql  8012 Nov  4 17:05 books_354.sdi
 8 -rw-r-----   1 _mysql  _mysql   281 Nov  4 17:05 settings.MYD
 8 -rw-r-----   1 _mysql  _mysql  1024 Nov  4 17:05 settings.MYI
 8 -rw-r-----   1 _mysql  _mysql  2250 Nov  4 17:05 settings_355.sdi

Listing 8-2Finding Where Your Data Is Located

该示例首先向数据库服务器查询数据目录的位置(它位于该计算机上受保护的文件夹中)。如果您发出一个列表命令,您可以看到由前缀ibibd标识的 InnoDB 文件。您还可以看到许多目录,所有这些目录都是该服务器上的数据库。下面是一个数据库文件夹的列表。注意到扩展名为.MY的文件吗?:这些是 MyISAM 文件(数据和索引)。

有关存储引擎及其选择和特性的更多信息,请参见在线 MySQL 参考手册“存储引擎”( https://dev.mysql.com/doc/refman/8.0/en/storage-engines.html )一节。

MySQL 配置文件

MySQL 服务器可以使用配置文件进行配置,类似于您配置 Raspberry Pi 的方式。在 Raspberry Pi 上,MySQL 配置文件位于/etc/mysql文件夹中,命名为my.cnf。该文件包含几个部分,其中一部分被标记为[mysqld]。该列表中的项目是键-值对:等号左边的名称是选项,它的值在右边。以下是一个典型的配置文件(为简洁起见,省略了许多行):

[mysqld]
port = 3306
basedir = /usr/local/mysql
datadir = /usr/local/mysql/data
server_id = 5
general_log

正如您所看到的,这是一种配置系统的简单方法。本示例设置 TCP 端口、基本目录(MySQL 安装的根目录,包括数据以及二进制和辅助文件)、数据目录和服务器 ID(用于复制,稍后将讨论)并打开常规日志(当包含布尔开关时,它打开日志)。您可以为 MySQL 设置许多这样的变量。有关使用配置文件的详细信息,请参阅在线 MySQL 参考手册。当你在覆盆子 Pi 上设置 MySQL 时,你会改变这个文件。

如何启动、停止和重启 MySQL

在 Raspberry Pi 上使用数据库和配置 MySQL 时,您可能需要控制 MySQL 服务器的启动和关闭。安装 MySQL 的默认模式是在启动时自动启动,在关机时自动停止,但是您可能希望更改这一模式,或者您可能需要在更改参数后停止并启动服务器。此外,当您更改配置文件时,需要重新启动服务器才能看到更改的效果。

您可以使用位于/etc/init.d/mysql中的脚本启动、停止和重启 MySQL 服务器。以下是它的选项列表:

$ /etc/init.d/mysql --help
Usage: mysql.server  {start|stop|restart|reload|force-reload|status}  [ MySQL server options ]

该脚本可以启动、停止和重启服务器,并获取其状态。您还可以将配置(如启动)选项传递给服务器。这对于打开临时使用的功能以替代修改配置文件非常有用。例如,如果要在一段时间内打开常规日志,可以使用以下命令:

/etc/init.d/mysql restart --general-log
/etc/init.d/mysql restart

第一次重启以一般登录方式重启服务器,第二次重启不启用日志(假设日志不在配置文件中)。重新启动服务器时,最好确保没有人在使用它。

然而,在最新版本的 Raspbian 上启动和停止 MySQL 的更好方法是使用如下的systemctl命令。你可以使用任何一种方法。

  • 开始 : sudo systemctl start mysqld

  • 停止 : sudo systemctl stop mysqld

  • 重启 : sudo systemctl restart mysqld

  • 状态 : sudo systemctl status mysqld

Shutting Down Correctly

您可能会像关闭 Arduino 传感器节点一样关闭 Raspberry Pi 数据库服务器,但是您应该避免这种诱惑。Raspberry Pi 是一台真正的计算机,具有需要同步关机的活动文件系统。断电前,您应该始终执行受控关机。

要关闭 Raspberry Pi,回想一下您发出了sudo shutdown –h now命令。要重新启动,您可以使用sudo shutdown –r now命令。

创建用户和授予访问权限

在使用 MySQL 之前,您需要了解另外两个管理操作:创建用户帐户和授予数据库访问权限。MySQL 可以用CREATE USER和一个或多个GRANT语句来执行这两个任务。例如,下面显示了名为 sensor1 的用户的创建,并授予该用户对数据库room_temp的访问权限:

CREATE USER 'sensor1'@'%' IDENTIFIED BY 'secret';
GRANT SELECT, INSERT, UPDATE ON room_temp.∗ TO 'sensor1'@'%';

第一个命令创建名为sensor1的用户,但是该名称也有一个@后跟另一个字符串。第二个字符串是与用户相关联的机器的主机名。也就是说,MySQL 中的每个用户都有一个用户名和一个主机名,以user@host的形式唯一地标识他们。这意味着用户和主机sensor1@10.0.1.16以及用户和主机sensor1@10.0.1.17是不同的。但是,%符号可以用作通配符,将用户与任何主机关联起来。IDENTIFIED BY子句为用户设置密码。

A Note About Security

为您的应用程序创建一个对 MySQL 系统没有完全访问权限的用户总是一个好主意。这是为了最大限度地减少任何意外更改,也是为了防止被利用。对于传感器网络,建议您创建一个只能访问存储(或检索)数据的数据库的用户。您可以使用以下命令更改 MySQL 用户密码:

ALTER USER sensor1@"%" IDENTIFIED BY 'super_secret';

对于主机使用通配符%也要小心。虽然创建单个用户并让用户从任何主机访问数据库服务器变得更加容易,但这也使得恶意用户更容易访问您的服务器(一旦他们发现了密码)。

另一个考虑是连接性。与 Raspberry Pi 一样,如果您将一个数据库连接到您的网络,而该网络又连接到 Internet,那么您的网络或 Internet 上的其他用户就有可能访问该数据库。不要让他们轻易得逞——更改您的 root 用户密码,并为您的应用程序创建用户。

第二个命令允许访问数据库。您可以授予用户许多权限。该示例显示了您想要给传感器网络数据库的用户的最可能的集合:读取(SELECT)、、、、添加数据(INSERT)和改变数据(UPDATE)。有关安全性和帐户访问权限的更多信息,请参见在线参考手册。

该命令还指定要授予权限的数据库和对象。因此,可以给用户一些表的读(SELECT)权限,给另一些表的写(INSERTUPDATE)权限。这个例子让用户可以访问room_temp数据库中的所有对象(表、视图等等)。

现在您已经对 MySQL 有了一个简短的介绍,让我们从 MySQL Raspberry Pi 数据库服务器开始吧。

构建 Raspberry Pi MySQL 服务器

是时候弄脏你的手,在你毫无戒心的树莓派上施展魔法了!让我们从给它添加一个 USB 驱动器开始。具有快速读/写速度的闪存驱动器可以特别好地工作,因为它不需要像传统的外部硬盘驱动器那样多的功率。根据数据的大小,您可能需要认真考虑这样做。

如果您的数据很小(从不超过几兆字节),您可以从您的启动映像 SD 卡使用 MySQL。但是,如果您想确保不会耗尽空间并保持数据与启动映像分开,您应该安装一个在启动时自动连接的 USB 驱动器。本节详细解释了如何做到这一点。

如果您计划使用外置硬盘,请确保使用高质量的 USB 集线器来存放外置硬盘。如果你使用的是传统的主轴驱动,这一点尤其重要,因为它会消耗更多的能量。将外部驱动器直接连接到 Raspberry Pi 可能会剥夺它的电源并导致无尽的沮丧。症状包括随机重启(总是令人惊喜)、命令失败、数据丢失等等。请务必为您的外围设备和 Raspberry Pi 提供充足的电源。

使用什么样的磁盘由您决定。你可以使用 USB 闪存驱动器,如果它有足够的空间和足够的速度(大多数新型号都很快),应该可以正常工作。如果您有额外的固态硬盘或者想要将功耗和热量降至最低,您也可以使用固态硬盘(SSD)。另一方面,你可能有一个额外的硬盘可以使用。本节的示例使用安装在典型 USB 硬盘驱动器外壳中的剩余 250GB 笔记本电脑硬盘驱动器。

Tip

使用外部硬盘驱动器(SSD 或传统的主轴驱动器)比访问闪存驱动器上的数据要快得多。它通常每单位(千兆字节)更便宜,或者,正如我提到的,可以很容易地从盈余。

对驱动器进行分区和格式化

在使用与 Raspberry Pi 不兼容的文件系统的新驱动器或现有驱动器之前,必须对驱动器进行分区和格式化。因为这个例子中的剩余驱动器上有一个旧的 Windows 分区,所以我必须遵循这些步骤。你的 Raspberry 操作系统可能能够读取你的旧驱动器的格式,但是你应该使用ext4文件系统以获得最佳性能。本节向您展示如何对您的驱动器进行分区和格式化。

首先,将驱动器连接到树莓派。然后使用fdisk命令确定连接了哪些驱动器,如下所示:

$ sudo fdisk -l
...
Disk /dev/sda: 59.2 GiB, 63518539776 bytes, 124059648 sectors
Disk model: Cruzer Fit
Units: sectors of 1 ∗ 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xde217a25

Device     Boot   Start      End  Sectors  Size  Id  Type
/dev/sda1  ∗         64  6691199  6691136  3.2G  17  Hidden HPFS/NTFS
/dev/sda2       6691200  6692671     1472  736K   1  FAT12

你在这里看到的是所有连接到树莓派的设备。如果您是 Linux 或分区驱动器的新手,这可能看起来像是一派胡言。我用粗体突出显示了有趣的行。注意,输出标识了位于指定为/dev/sda的设备上的 64GB 驱动器。所有关于硬盘的有趣数据也会显示出来。

正如我提到的,这个驱动器上已经有一个分区,由带有设备名称和分区号的行来表示。因此,/dev/sda1是该驱动器上唯一的分区。让我们删除该分区并创建一个新分区。您使用清单 8-3 中所示的fdisk应用程序来执行这两个操作。

Caution

如果您的驱动器上有一个分区包含您想要保留的数据,请立即中止,并首先将数据复制到另一个驱动器。以下步骤将擦除驱动器上的所有数据!

$ sudo fdisk /dev/sda

Welcome to fdisk (util-linux 2.33.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): p

Disk /dev/sda: 59.2 GiB, 63518539776 bytes, 124059648 sectors
Disk model: Cruzer Fit
Units: sectors of 1 ∗ 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xde217a25

Device     Boot   Start     End Sectors  Size Id Type
/dev/sda1  ∗         64 6691199 6691136  3.2G 17 Hidden HPFS/NTFS
/dev/sda2       6691200 6692671    1472  736K  1 FAT12

Command (m for help): d
Partition number (1,2, default 2):

Partition 2 has been deleted

.

Command (m for help): d
Selected partition 1
Partition 1 has been deleted.

Command (m for help): n
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1):
First sector (2048-124059647, default 2048):
Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-124059647, default 124059647):

Created a new partition 1 of type 'Linux' and of size 59.2 GiB.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

Listing 8-3Partitioning the USB Drive

第一个命令d,删除一个分区。在这种情况下,只有一个分区,所以通过输入1来选择它。然后使用命令 n 创建一个新的分区,并接受缺省值,使用所有的可用空间。为了检查你的工作,你可以使用p命令来打印设备分区表和元数据。它显示(并确认)新的分区。

如果你担心你可能犯了一个错误,不要惊慌!关于fdisk的伟大之处在于,它不会写或改变磁盘,直到你用w或写命令告诉它。在示例中,您发出w命令来写入分区表。要查看可用命令的完整列表,您可以使用h命令或运行man fdisk

Tip

对于所有的 Linux 命令,您可以使用命令man <application>查看手册文件。

下一步是用ext4文件系统格式化驱动器。这很简单,只需要一个命令:mkfs (make file system)。你把设备名传给它。如果你记得的话,这是/dev/sda1。即使您创建了一个新分区,它仍然是第一个分区,因为驱动器上只有一个分区。如果您尝试使用不同的分区,请确保使用正确的编号!该命令可能需要几分钟时间来运行,具体取决于驱动器的大小。清单 8-4 展示了运行中的命令。

$ sudo mkfs.ext4 /dev/sda
mke2fs 1.44.5 (15-Dec-2018)
/dev/sda contains an iso9660 file system labelled 'Backup'
Proceed anyway? (y,N) y
Creating filesystem with 15507456 4k blocks and 3883008 inodes
Filesystem UUID: d370c755-18be-4c7f-bf66-4dd666ade676
Superblock backups stored on blocks:
    32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 4096000, 7962624, 11239424

Allocating group tables: done
Writing inode tables: done
Creating journal (65536 blocks): done
Writing superblocks and filesystem accounting information: done

Listing 8-4Formatting the Drive

现在您有了一个新的分区,并且已经正确格式化了。下一步是将驱动器与引导映像上的挂载点相关联,然后在引导时连接该驱动器,这样每次启动 Raspberry Pi 时,您都不必做任何事情来使用该驱动器。

设置自动驱动安装

Linux 中的外置驱动器通过mount连接(挂载),通过umount断开(卸载)。与某些操作系统不同,不先卸载就拔掉 USB 驱动器通常不是个好主意。同样,您必须先安装驱动器,然后才能使用它。本节说明了安装驱动器并使驱动器在每次引导时自动安装所需的步骤。

我首先讨论安装驱动器并为自动安装做好准备的预备步骤。这些包括在/media文件夹下创建一个文件夹来挂载驱动器(称为挂载点),更改文件夹的权限以允许访问,以及执行一些可选步骤来调整驱动器:

$ sudo mkdir /media/mysql
$ sudo chmod 755 /media/mysql
$ sudo tune2fs -m 0 /dev/sda
tune2fs 1.44.5 (15-Dec-2018)
Setting reserved blocks percentage to 0% (0 blocks)
$ sudo tune2fs -L MySQL /dev/sda
tune2fs 1.44.5 (15-Dec-2018)
$ sudo mount /dev/sda /media/mysql
$ sudo ls -lsa /media/mysql
total 24
 4 drwxr-xr-x 3 root root  4096 Nov 27 13:44 .
 4 drwxr-xr-x 4 root root  4096 Nov 27 13:55 ..
16 drwx------ 2 root root 16384 Nov 27 13:44 lost+found

这些命令很容易识别,是基本的文件和文件夹命令。但是,使用tune2fs(调整文件系统)的调整步骤用于首先重置用于特权访问的块数(这样可以节省一点空间),然后将驱动器标记为MYSQL。同样,这些是可选的,如果你愿意,你可以跳过它们。

Tip

可以用sudo umount /dev/sda1卸载驱动器。

此时,驱动器可以访问并准备好使用。你可以切换到/media/HDD文件夹,创建文件或者做任何你想做的事情。现在,让我们来完成为自动挂载设置驱动器的任务。

最好的方法是通过驱动器的通用唯一标识符(UUID)来引用它。这被分配给这个驱动器,并且只分配给这个驱动器。您可以告诉操作系统将具有特定 UUID 的驱动器挂载到特定的挂载点(/media/HDD)。

还记得之前的/dev/sda设备名称吗?如果您将驱动器插入另一个集线器端口,或者更好的情况是,如果有其他驱动器连接到您的设备,并且您卸载然后再装载它们,则下次引导时设备名称可能会不同!UUID 帮助您确定哪个驱动器是您的数据驱动器,使您不必将驱动器插入特定的端口,并允许您使用其他驱动器,而不必担心如果驱动器被赋予不同的设备名称会破坏您的 MySQL 安装。

要获得 UUID,使用blkid(块 ID)应用程序:

$ sudo blkid
...
/dev/sda: LABEL="MySQL" UUID="d370c755-18be-4c7f-bf66-4dd666ade676" TYPE="ext4"
...

注意粗体的那一行。哇哦!那是一大串。UUID 是一个 128 字节(字符)的字符串。为下一步复制它。

要设置自动驱动器映射,您可以使用一个称为文件系统静态信息的特性(fstab)。这包括位于系统上的/etc文件夹中的一个文件。你可以随意编辑这个文件。如果你来自 Linux 或 Unix 的老学校,你可能会选择使用vi9 由此产生的文件如下:

$ sudo nano /etc/fstab
proc    /proc   proc    defaults  0       0
/dev/mmcblk0p1  /boot   vfat    defaults  0       0
/dev/mmcblk0p2  /       ext4    defaults,noatime  0       0
UUID= d370c755-18be-4c7f-bf66-4dd666ade676 /media/mysql   ext4   defaults,noatime   0   0

您添加的行以粗体显示。在这里,您只需添加 UUID、挂载点、文件系统和选项。就这样!您可以使用以下命令重启您的 Raspberry Pi,并在消息滚动时观察屏幕。最终,您会看到驱动器已安装。如果出现错误,您可以在启动序列中看到它:

$ sudo shutdown –r now

现在您已经准备好构建 MySQL 数据库服务器了!下一节详细介绍了使用 Raspberry Pi 实现这一点所需的步骤。

项目:在 Raspberry Pi 上安装 MySQL 服务器

将 Raspberry Pi 转变成 MySQL 数据库服务器很容易。嗯,差不多了。最新版本的 MySQL (8.0)不适用于 Raspberry Pi。 10 然而,由于 MySQL 是开源的,我们可以在我们的 Raspberry Pi 上从源代码构建(编译和链接)MySQL。多酷啊。本节将向您展示如何获取 MySQL 的源代码、构建和安装它。然后,我们将了解如何将其默认数据目录从您的引导映像移动到您在上一节中连接的新外部驱动器。

What about Other MYSQL Variants?

精明的读者可能已经知道其他供应商提供的 MySQL 的变体。虽然大多数都声称与 Oracle 的 MySQL(源代码所有者)100%兼容,但仍有一些差异会使开发更加困难。例如,已知用于 Arduino 的 MySQL 数据库连接器(称为连接器/Arduino)在某些版本的某些变体中存在问题。因此,作者认为你应该总是使用 Oracle 发布的 MySQL,而不是它的变体。

在本节中,我们将使用 Raspberry Pi 计算机,而不是更昂贵的主流服务器硬件。如果您想继续使用更传统的服务器硬件,您可以这样做,但是请记住,Raspberry Pi 上使用的一些命令与您在典型的基于 Linux 的平台上使用的命令非常相似。您可能需要替换特定于平台的版本,以便在您的 PC 上使用以下软件。

回想一下,由于 MySQL 是开源的,我们可以自己下载源代码、编译和安装。事实上,我们将在本演练中做到这一点。下面列出了准备使用 MySQL 的 Raspberry Pi 计算机的必要步骤:

  • 构建 MySQL。

  • 手动安装 MySQL。

  • 配置 MySQL。

这个列表类似于您在商用硬件上设置 MySQL 的过程,但是构建和配置步骤是使 MySQL 在 Raspbian 上工作所必需的(因为没有安装包)。值得注意的是,这些额外的步骤并不是 Raspbian 独有的。

事实上,从源代码构建、安装和配置 MySQL 是使用安装包的可行替代方案。您可以在在线参考手册( https://dev.mysql.com/doc/refman/8.0/en/source-installation.html )的“从源代码安装 MySQL”一节中找到为各种平台构建 MySQL 的说明。

这个过程很简单,包括一些次要的系统配置项来准备我们的系统和两个命令:cmakemake。这一节将通过大量的例子和每一步的文档来引导你完成所有这些步骤。

从源代码构建 MySQL 的任务对于那些从来没有编程过的人或者已经有一段时间没有编写过程序的人来说可能会令人望而生畏,但是不要绝望。在 Raspberry Pi 上编译 MySQL 最困难的部分是等待过程完成。也就是说,可能要花一个小时左右的时间来编译所有的东西。但是对于能够使用 Raspberry Pi 计算机来试验 MySQL 来说,这是一个很小的代价!

让我们从先决条件开始,深入研究在 Raspbian 上编译 MySQL。

先决条件

你需要安装一些东西来准备你的 Raspberry Pi 来编译 MySQL,包括硬件和软件先决条件。

硬件要求是 MySQL 的最新版本(撰写本文时是 8.0.18)要求使用 2GB 或 4GB (4GB 更快)主板的 Raspberry Pi 4B。因此,您应该考虑是否要在安装 MySQL 的同一个 Raspberry Pi 4B 上构建它。为什么这很重要?这很重要,因为你可能想在旧的 Raspberry Pi 板上运行 MySQL。更具体地说,虽然最好在 4B 上编译 MySQL,但你可以在 3B+上安装和运行它,不会有任何问题。我们将在后面看到如何做到这一点。我们需要使用 4B 的主要原因是内存。MySQL 只需要超过 3B 主板上 1GB 的内存。 11

除了需要 Raspberry Pi 4B 之外,软件先决条件还包括以下软件:

  • 你需要安装诅咒 5 ( libncurses5-dev)。

  • 你需要安装 Bison。

  • 需要安装 OpenSSL ( libssl-dev)。

  • 您需要安装 CMake。

要一次安装所有这些库,请在终端窗口中使用以下命令。这将下载并安装必要的文件。请注意,我们必须使用提升的权限来安装库。

$ sudo apt-get install libncurses5-dev bison libssl-dev cmake

唯一的其他先决条件是,我们必须下载 MySQL 服务器的源代码。进入 https://dev.mysql.com/downloads/mysql/ ,在选择操作系统下拉框中选择源代码,在选择 OS 版本下拉框中选择通用 Linux ,然后点击列表底部的通用 Linux(架构无关),压缩的 TAR 存档包含 Boost 头文件下载链接,如图 8-1 所示。这个文件包含我们需要的另一个库(boost)以及服务器源代码。这是最容易开始构建的下载。一旦你下载了文件,把它复制到你的 Raspberry Pi。

img/313992_2_En_8_Fig1_HTML.jpg

图 8-1

下载 MySQL 服务器源代码

好了,现在我们准备好构建 MySQL 服务器了。

构建 MySQL 服务器

在 Raspberry Pi 上构建 MySQL 只需要三个步骤。我们首先运行名为 CMake 的预处理器,然后用 Make 构建代码,最后用 make package 命令构建安装包。我们可以用这个包在另一个 Raspberry Pi 上安装 MySQL。让我们从 CMake 开始,看看每个步骤的细节。

CMake ( cmake.org)是另一个用于构建、测试和打包软件的开源产品。回想一下,我们在上一节安装了 CMake。您可以使用许多不同的选项来构建软件,其中许多也适用于 MySQL。事实上,您可以花费大量时间定制 CMake 命令选项,以便为几乎任何平台进行构建。由于我们下载了带有 Boost 库的普通 Linux 的 MySQL 源代码,我们已经得到了我们需要的一切。

因此,我们需要使用 CMake 的命令选项很少,包括以下内容。这里将对每一项进行更详细的解释:

  • 你应该设置-DWITH_UNIT_TESTS=OFF来节省编译时间(不需要)。

  • 您应该设置PREFIX来设置安装路径,以便于安装。

  • 我们需要关掉“黄金”连接器。

  • 我们必须用发布代码来构建(debug 对于 Raspberry Pi 来说需要太多内存)。

  • 我们必须添加额外的编译和构建标志,以确保代码在 ARM32 上正确构建。

运行 CMake(准备编译)

我们要做的第一件事是提取我们下载的 TAR 文件。您可以使用以下命令来实现这一点。这将创建一个名为 mysql-8.0.18 的文件夹。建议您将此文件解压缩到 root 用户个人文件夹中的一个文件夹中,例如/home/pi/source。解压缩过程将需要几分钟时间,因为它包含大量代码。

$ cd /home/pi
$ mkdir source
$ cd source
$ cp ~/Downloads/mysql-boost-8.0.18.tar.gz .
$ tar -xvf mysql-boost-8.0.18.tar.gz

接下来,我们将使用以下命令创建一个目录来存储所有编译后的代码。这有助于防止编译时发生意外,并保留源代码。

$ cd mysql-8.0.18
$ mkdir build
$ cd build

现在我们可以运行 CMake 命令了。清单 8-5 显示了您需要在build文件夹中使用的完整命令。请注意,该命令指定了许多选项,包括(按出现的顺序)使用 Unix makefiles、将构建设置为发布代码(而不是调试)、忽略 AIO 检查、设置 boost 文件夹(包含在我们下载的 TAR 文件中)、关闭单元测试,以及为 ARM32 上的编译设置一些神秘的设置。

$ cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=release -DBUILD_CONFIG=mysql_release -DDEBUG_EXTNAME=OFF -DIGNORE_AIO_CHECK=1 -DWITH_UNIT_TESTS=OFF -DCMAKE_C_LINK_FLAGS="-Wl,--no-keep-memory,-latomic" -DCMAKE_CXX_LINK_FLAGS="-Wl,--no-keep-memory,-latomic" -DCMAKE_C_FLAGS_RELEASE="-fPIC" -DCMAKE_CXX_FLAGS_RELEASE="-fPIC" -DCMAKE_INSTALL_PREFIX="/usr/local/mysql" -DUSE_LD_GOLD=OFF -DWITH_BOOST="../boost" ..
-- Running cmake version 3.13.4
-- Found Git: /usr/bin/git (found version "2.20.1")
-- MySQL 8.0.18
-- Source directory /media/pi/source/mysql-8.0.18
-- Binary directory /media/pi/source/mysql-8.0.18/build
-- CMAKE_GENERATOR: Unix Makefiles
...
-- CMAKE_C_FLAGS: -fno-omit-frame-pointer  -Wall -Wextra -Wformat-security -Wvla -Wundef -Wwrite-strings -Wjump-misses-init
-- CMAKE_CXX_FLAGS: -std=c++14 -fno-omit-frame-pointer  -Wall -Wextra -Wformat-security -Wvla -Wundef -Woverloaded-virtual -Wcast-qual -Wimplicit-fallthrough=2 -Wlogical-op
-- CMAKE_CXX_FLAGS_DEBUG: -DSAFE_MUTEX -DENABLED_DEBUG_SYNC -g
-- CMAKE_CXX_FLAGS_RELWITHDEBINFO: -DDBUG_OFF -ffunction-sections -fdata-sections -O2 -g -DNDEBUG
-- CMAKE_CXX_FLAGS_RELEASE: -DDBUG_OFF -ffunction-sections -fdata-sections -fPIC
-- CMAKE_CXX_FLAGS_MINSIZEREL: -DDBUG_OFF -ffunction-sections -fdata-sections -Os -DNDEBUG
-- CMAKE_C_LINK_FLAGS: -Wl,--no-keep-memory,-latomic
-- CMAKE_CXX_LINK_FLAGS: -Wl,--no-keep-memory,-latomic
-- CMAKE_EXE_LINKER_FLAGS
-- CMAKE_MODULE_LINKER_FLAGS
-- CMAKE_SHARED_LINKER_FLAGS
-- Configuring done
-- Generating done

Listing 8-5Running the CMake Command (ARM32)

如果这个命令看起来很奇怪,不要担心,也没有必要理解我们在编译和链接阶段使用的所有特殊设置。但是,如果您确实想了解更多关于这些选项的信息,您可以查看关于 GNU 编译器( http://gcc.gnu.org/onlinedocs/gcc/Option-Summary.html )和链接器( https://gcc.gnu.org/onlinedocs/gcc/Link-Options.html )选项的文档。

运行该命令可能需要几分钟时间。确保没有错误,并且最后几行表明构建文件已经写入构建文件夹。请特别注意结尾的LINK_FLAGS消息。CMake 命令中的选项不包括空格。如果您不小心添加了空格,逗号分隔的列表会在 CMake 输出中显示它们。确保没有空格。如果有空格,您可能会得到一个错误,指出--icf=safe(或其他)选项无效。如果发生这种情况,请再次运行不带空格的命令。

如果你已经走了这么远而没有错误,你几乎可以放松了。下一步,编译代码很容易,但在 Raspberry Pi 4B 上运行需要一段时间(至少 1-2 小时)。

运行 Make(编译)

下一步是编译代码。这可以通过 make 命令简单地完成。这个命令允许我们指定想要使用多少个并行线程。对于 Raspberry Pi 4B 和总共四个 CPU 内核,使用三个内核进行编译是安全的。如果您有一个正在运行的 CPU 使用监视器,您将会看到这三个内核,有时可能是所有四个内核都在 100%运行。如果你的树莓皮是装在盒子里的,确保你有足够的通风或风扇吹过电路板。

清单 8-6 显示了使用命令make -j3编译 MySQL 服务器代码的步骤。该清单是您可能会看到的消息的摘录(将有数千行),但需要注意的是最后几行。这些确保代码编译无误。

Tip

在代码编译时,您可能会看到轻微的警告,您可以忽略这些警告。但是,您应该看不到任何编译错误。如果是这样,请返回检查 CMake 命令,并在必要时重新运行它。如果所有这些都失败了,删除构建目录并重新开始。

$ make -j3
[  0%] Built target INFO_SRC
[  0%] Built target INFO_BIN
[  0%] Building C object extra/zlib/CMakeFiles/zlib_objlib.dir/gzread.o
[  0%] Building C object extra/zstd/CMakeFiles/zstd_objlib.dir/lib/common/threading.c.o
[  0%] Building C object extra/zstd/CMakeFiles/zstd_objlib.dir/lib/common/xxhash.c.o
[  0%] Building C object extra/zlib/CMakeFiles/zlib_objlib.dir/gzwrite.o
...
[100%] Building CXX object storage/innobase/CMakeFiles/innobase.dir/os/os0thread.cc.o
[100%] Building CXX object storage/innobase/CMakeFiles/innobase.dir/page/zipdecompress.cc.o
[100%] Building CXX object storage/innobase/CMakeFiles/innobase.dir/rem/rec.cc.o
[100%] Building CXX object storage/innobase/CMakeFiles/innobase.dir/ut/crc32.cc.o
[100%] Building CXX object storage/innobase/CMakeFiles/innobase.dir/ut/ut.cc.o
[100%] Linking CXX static library libinnobase.a
[100%] Built target innobase
Scanning dependencies of target mysqld
[100%] Building CXX object sql/CMakeFiles/mysqld.dir/main.cc.o
[100%] Linking CXX executable ../runtime_output_directory/mysqld
[100%] Built target mysqld

Listing 8-6Compiling MySQL Server

编译完成后,下一步是构建一个包(TAR 文件),我们可以用它在我们的服务器上安装 MySQL。

制作包装

我们需要做的最后一件事是构建安装包。在这种情况下,我们将构建一个压缩 TAR 文件,我们可以将它复制到我们的初始服务器并进行安装。我们使用清单 8-7 中所示的 make package 命令来实现这一点。

$ make package
[  0%] Built target abi_check
[  0%] Built target INFO_SRC
[  0%] Built target INFO_BIN
[  1%] Built target zlib_objlib
[  1%] Built target zlib
[  2%] Built target zstd_objlib
[  2%] Built target zstd
[  3%] Built target edit
[  4%] Built target event_core
...
[100%] Built target routing
[100%] Built target rest_routing
[100%] Built target mysqlrouter
[100%] Built target mysqlrouter_keyring
Run CPack packaging tool...
CPack: Create package using TGZ
CPack: Install projects
CPack: - Run preinstall target for: MySQL
CPack: - Install project: MySQL
CPack: Create package
CPack: - package: /home/pi/source/mysql-8.0.18/build/mysql-8.0.18-linux-armv7l.tar.gz generated.

Listing 8-7Building the TAR Package

就是这样!我们在 Raspberry Pi 上构建了 MySQL!还不算太糟,是吧?现在,让我们看看如何在我们的服务器上安装和测试 MySQL。

安装 MySQL 服务器

如果我们在不同的 Raspberry Pi 上构建 MySQL,我们需要将 TAR 文件复制到一个可移动驱动器上,以便将该文件复制到目标 Raspberry Pi 上。

一旦服务器启动,登录并切换到/usr/local目录,创建一个名为mysql的新文件夹。然后,切换到新文件夹,并将 TAR 文件复制到该文件夹。最后,使用以下命令解压文件。有很多文件,所以解压缩可能需要几分钟时间。

$ cd /usr/local/
$ mkdir mysql
$ cd mysql
$ sudo cp ~/source/mysql-8.0.11/build/mysql-8.0.18-linux-armv7l.tar.gz .
$ sudo tar -xvf mysql-8.0.11-linux-armv7l.tar.gz --strip-components=1

注意,最后一个命令使用一个选项从提取的文件目录中删除一个组件(第一个文件夹— mysql-8.0.18-linux-armv71)。这确保了 MySQL 文件被复制到/usr/local/mysql

但是,我们还需要运行一个命令。因为我们是太空良心,我们不需要 MySQL 测试文件,所以我们可以用下面的命令删除它们。一旦我们完成了 TAR 文件,我们也可以删除它,如下所示:

$ sudo rm -rf mysql-test
$ sudo rm mysql-8.0.18-linux-armv71.tar.gz

从 TAR 文件安装比从典型的特定于平台的包安装需要更多的步骤。这是因为安装包通常负责几个必需的配置步骤,所有这些都在在线参考手册中题为“使用通用二进制文件在 Unix/Linux 上安装 MySQL”(https://dev.mysql.com/doc/refman/8.0/en/binary-installation.html)的章节中有详细说明。

配置 MySQL 服务器

现在我们已经复制了文件,我们可以完成设置。这个过程并不繁琐,但确实涉及到从终端运行几个命令,所以需要一些耐心来确保所有命令都输入正确。

我们首先创建一个名为mysql的新组,然后添加一个名为mysql的用户,然后创建一个供 MySQL 使用的文件夹,并授予mysql用户对该文件夹的访问权限。以下代码显示了所需的命令。从终端运行这些命令(任何命令都不会有输出)。

$ sudo groupadd mysql
$ sudo useradd -r -g mysql -s /bin/false mysql
$ cd /usr/local/mysql
$ sudo mkdir mysql-files
$ sudo chown mysql:mysql mysql-files
$ sudo chmod 750 mysql-files

我们可以用下面的代码所示的--initialize选项轻松初始化数据目录。注意,我们使用提升的权限运行命令,并指定要使用的用户(mysql)。以下代码显示了突出显示成功消息的输出示例。如果您看到错误,请参考在线参考手册来解决错误。请注意,输出包含初始 root 用户密码。下一步您将需要它。请注意,此步骤可能需要一些时间来运行。

$ sudo ./bin/mysqld --initialize --user=mysql
2019-11-17T02:02:41.118355Z 0 [System] [MY-013169] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.18) initializing of server in progress as process 7704
2019-11-17T02:05:04.757386Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: VPw&eFjU-0z#

接下来,我们使用我们最喜欢的编辑器创建一个配置文件,如下所示:

$ sudo vi /etc/my /etc/my.cnf

将以下几行添加到配置文件中并保存(按下 Esc 然后:,然后 w,然后 q )。在下一步中,我们将使用这个配置文件来启动服务器。

[mysqld]
basedir=/usr/local/mysql/
datadir=/usr/local/mysql/data

好了,我们现在准备好第一次启动 MySQL 了。使用mysqld命令从命令行启动 MySQL。我们使用这个命令代替/etc/init.d/mysql start命令,这样我们可以检查输出中的错误。如果没有错误,您应该会看到如下所示的输出:

$ sudo bin/mysqld --defaults-file=/etc/my.cnf --user=mysql &
[1] 8745
$ 2019-11-17T02:09:41.429418Z 0 [Warning] [MY-011037] [Server] The CYCLE timer is not available. WAIT events in the performance_schema will not be timed.
2019-11-17T02:09:42.191155Z 0 [System] [MY-010116] [Server] /usr/local/mysql/bin/mysqld (mysqld 8.0.18) starting as process 8750
2019-11-17T02:09:58.600980Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
2019-11-17T02:09:59.167758Z 0 [System] [MY-010931] [Server] /usr/local/mysql/bin/mysqld: ready for connections. Version: '8.0.18'  socket: '/tmp/mysql.sock'  port: 3306  Source distribution.
2019-11-17T02:09:59.378833Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Socket: '/tmp/mysqlx.sock' bind-address: '::' port: 33060

现在我们可以使用下面的命令用 mysql 客户端测试我们的 MySQL 服务器。确保使用初始化数据目录时显示的密码。清单 8-8 展示了第一次使用mysql客户端连接到服务器的例子。我们将首先显示版本,然后更改 root 用户密码。注意,我们还使用 shutdown SQL 命令关闭了服务器。

$ bin/mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.18

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SELECT @@version;
+-----------+
| @@version |
+-----------+
| 8.0.18    |
+-----------+
1 row in set (0.00 sec)

mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'secret';
Query OK, 0 rows affected (0.11 sec)

mysql> shutdown;
Query OK, 0 rows affected (0.00 sec)

mysql> \q

Listing 8-8Connecting to MySQL for the First Time

接下来,我们必须添加 MySQL 二进制文件的路径。通过使用命令nano ~/.bashrc编辑 Bash 资源文件,我们可以很容易地做到这一点。当文件打开时,在文件的底部添加下面一行。下次打开终端时,无需指定路径就可以执行 MySQL 应用程序和工具。

export PATH=${PATH}:/usr/local/mysql/bin

还需要最后一步——我们必须复制启动和关闭脚本(服务),以便在启动时自动启动 MySQL。为此,从构建的support-files文件夹中复制mysql.server文件到/etc/init.d/mysql文件,如清单 8-9 所示。我们还将再次测试服务器连接,然后使用sudo systemctl daemon-reload命令刷新守护进程列表,使用sudo systemctl startsudo systemctl stop命令启动或停止 MySQL,从而关闭服务器连接。你也可以使用sudo systemctl status命令来查看 MySQL 的状态。如果您遇到错误或想要检查 MySQL 是否正在运行,这可能会很有帮助。请注意,使用该命令时,系统可能会提示您输入密码。另外,您希望从build目录中复制mysql.server文件,而不是源代码目录的根目录。

$ sudo cp ./support-files/mysql.server /etc/init.d/mysql
$ sudo chmod 0755 /etc/init.d/mysql
$ sudo systemctl daemon-reload
$ sudo systemctl start mysql
$ sudo systemctl status mysql
● mysql.service - LSB: start and stop MySQL
   Loaded: loaded (/etc/init.d/mysql; generated)
   Active: active (running) since Sat 2019-11-16 21:22:44 EST; 6s ago
     Docs: man:systemd-sysv-generator(8)
  Process: 11023 ExecStart=/etc/init.d/mysql start (code=exited, status=0/SUCCESS)
    Tasks: 40 (limit: 2200)
   Memory: 350.5M
   CGroup: /system.slice/mysql.service
           ├─11037 /bin/sh /usr/local/mysql//bin/mysqld_safe --datadir=/usr/local/mysql/data --
           └─11148 /usr/local/mysql/bin/mysqld --basedir=/usr/local/mysql/ --datadir=/usr/local

...
Nov 16 21:22:44 raspberrypi systemd[1]: Started LSB: start and stop MySQL.

$ mysql -uroot -p -e "select @@version"
Enter password:
+-----------+
| @@version |
+-----------+
| 8.0.18    |
+-----------+
$ sudo systemctl stop mysql
$ sudo systemctl status mysql
...
Nov 16 21:23:02 raspberrypi systemd[1]: Stopped LSB: start and stop MySQL.

Listing 8-9Starting MySQL Automatically or Manually with systemctl

就这样!我们已经安装了 MySQL 服务器,并测试了它的工作情况。在每台服务器上安装 MySQL Shell 也是一个好主意。在下一节中,您将告诉 MySQL 使用外部驱动器来存储您的数据库和数据。

将数据目录移动到外部驱动器

回想一下,您希望使用 MySQL 来存储您的传感器数据。因此,传感器数据的量可能会增长,并且随着时间的推移可能会消耗大量空间。您可以使用外部驱动器来保存数据,而不必冒填满启动映像 SD(通常只有几千兆字节)的风险。这一节将向您展示如何告诉 MySQL 更改其保存数据的默认位置。

所涉及的步骤需要停止 MySQL 服务器,更改其配置,然后重新启动服务器。最后,测试更改以确保所有新数据都保存在新位置。首先停止 MySQL 服务器:

$ sudo systemctl stop mysql

您必须为新数据目录创建一个文件夹:

$ sudo mkdir /media/mysql/mysql_data

现在,您将现有的数据目录及其内容复制到新文件夹中。请注意,您只复制数据,而不是整个 MySQL 安装,这是不必要的:

$ sudo cp -R /usr/local/mysql/data  /media/mysql/mysql_data
$ chown -R mysql mysql /media/mysql/mysql_data

Note

如果出现权限错误,请尝试将/media/mysql文件夹的所有者更改为mysql:mysql

接下来,编辑 MySQL 的配置文件。在这种情况下,您将datadir行改为datadir = /media/mysql。注释掉 bind-address 行以允许从网络上的其他系统访问 MySQL 也是一个好主意:

$ sudo vi /etc/mysql/my.cnf

还有最后一步。您必须将所有者和组更改为安装时创建的 MySQL 用户。以下是正确的命令:

$ sudo chown -R mysql:mysql /media/mysql/mysql_data

现在您重新启动 MySQL:

$ sudo systemctl start mysql

您可以通过连接到 MySQL,创建一个新的数据库,然后检查新文件夹是否是在外部驱动器上创建的,来确定这些更改是否有效,如清单 8-10 所示。

$ ./bin/mysql -uroot -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 8.0.18 Source distribution

Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> SHOW VARIABLES LIKE 'datadir';
+---------------+----------------------------------+
| Variable_name | Value                            |
+---------------+----------------------------------+
| datadir       | /media/pi/mysql/mysql_data/data/ |
+---------------+----------------------------------+
1 row in set (0.08 sec)

mysql> CREATE DATABASE testme;
Query OK, 1 row affected (0.08 sec)

mysql> SHOW DATABASES;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| testme             |
+--------------------+
5 rows in set (0.06 sec)

mysql> \q
Bye
pi@raspberrypi:/usr/local/mysql $ sudo ls -lsa /media/pi/mysql/mysql_data/data
total 168024
    4 drwxr-x--- 7 mysql mysql     4096 Nov 27 15:09  .
    4 drwxr-xr-x 3 mysql mysql     4096 Nov 27 15:03  ..
    4 -rw-r----- 1 mysql mysql       56 Nov 27 15:03  auto.cnf
    4 -rw-r----- 1 mysql mysql      499 Nov 27 15:03  binlog.000001
    4 -rw-r----- 1 mysql mysql      178 Nov 27 15:03  binlog.000002
    4 -rw-r----- 1 mysql mysql      346 Nov 27 15:09  binlog.000003
    4 -rw-r----- 1 mysql mysql       48 Nov 27 15:06  binlog.index
...
    4 -rw-r----- 1 mysql mysql     3344 Nov 27 15:03  ib_buffer_pool
12288 -rw-r----- 1 mysql mysql 12582912 Nov 27 15:09  ibdata1
49152 -rw-r----- 1 mysql mysql 50331648 Nov 27 15:09  ib_logfile0
49152 -rw-r----- 1 mysql mysql 50331648 Nov 27 15:03  ib_logfile1
12288 -rw-r----- 1 mysql mysql 12582912 Nov 27 15:06  ibtmp1
...
    4 drwxr-x--- 2 mysql mysql     4096 Nov 27 15:09  testme

Listing 8-10Testing the New Data Directory

在输出中,新的数据库名称表示为文件夹testme

现在,你有了它——一个运行在 Raspberry Pi 上的新的 MySQL 数据库服务器!

如果您想知道新的数据库服务器还能做些什么,请继续阅读。在下一节中,您将处理 MySQL 中一个非常流行的特性,称为复制。它允许两台或多台服务器拥有数据库的副本。出于您的目的,使用副本作为备份可能会很方便,所以您不必从您的 Raspberry Pi 手动复制任何文件。

高级项目:使用 MySQL 复制来备份您的传感器数据

使用外部驱动器保存 MySQL 数据的最大好处之一是,您可以随时关闭服务器,断开驱动器,将其插入另一个系统,然后复制数据。如果您的 Raspberry Pi 数据库服务器位于一个(物理上)容易到达的位置,并且有时可以关闭服务器,那么这听起来可能很棒。

然而,对于一些传感器网络来说,情况可能并非如此。将 Raspberry Pi 用于数据库服务器的一个好处是,服务器可以位于传感器节点附近。如果传感器网络位于隔离区域,您可以通过将 Raspberry Pi 放在相同的位置来收集和存储数据。但是,如果没有网络连接到数据库服务器,这可能意味着要跋涉到一个谷仓或池塘,或者步行几个足球场的长度到一个工厂的内部去得到硬件。

但是,如果您的 Raspberry Pi 连接到网络,您可以使用 MySQL 的一项名为复制的高级功能来制作数据的实时最新副本。这不仅意味着您可以有一个备份,还意味着您可以查询维护副本的服务器,从而减轻您的 Raspberry Pi 的复杂或长时间运行的查询负担。Raspberry Pi 是一台非常酷的小尺寸计算机,但它不是数据仓库。

什么是复制,它是如何工作的?

MySQL 复制是一个易于使用的特性,同时也是 MySQL 服务器的一个非常复杂的主要组件。本节提供了复制的鸟瞰图,目的是解释它是如何工作的以及如何设置一个简单的复制拓扑。有关复制及其众多特性和命令的更多信息,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/5.5/en/replication.html )。

复制需要两台或更多服务器。必须将一台服务器指定为源服务器或主服务器。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求是只读服务器。因此,当您的传感器发送数据进行存储时,它们会将数据发送给主设备。您编写的使用传感器数据的应用程序可以从从属服务器读取这些数据。

复制机制使用一种称为二进制日志的技术,该技术以一种特殊的格式存储更改,从而保留所有更改的记录。这些更改然后被运送到从设备,并在那里重新执行。因此,一旦从机重新执行更改(称为事件),从机就拥有了数据的精确副本。

主服务器维护更改的二进制日志,从服务器维护该二进制日志的副本,称为中继日志。当从设备向主设备请求数据更改时,它从主设备读取事件并将它们写入其中继日志;然后,从属线程中的另一个线程执行中继日志中的那些事件。可以想象,从主服务器上发生更改到从服务器上发生更改会有一点延迟。幸运的是,这种延迟几乎是不明显的,除非是在流量非常大的拓扑结构中(有很多变化)。出于您的目的,当您从从属服务器读取数据时,它可能是最新的。您可以使用命令SHOW SLAVE STATUS检查从设备的进度;在许多其他事情中,它向你显示了奴隶落后于主人有多远。您将在后面的小节中看到这个命令的运行。

现在您已经对复制及其工作原理有了一些了解,让我们来看看如何设置它。下一节将讨论如何将 Raspberry Pi 设置为主机,将桌面计算机设置为从机。

如何设置复制

本节演示如何设置从 Raspberry Pi(主)到桌面计算机(从)的复制。这些步骤包括通过启用二进制日志记录和创建用于读取二进制日志的用户帐户来准备主服务器,通过将从服务器连接到主服务器来准备从服务器,以及启动从服务器进程。最后,对复制系统进行测试。

准备母版

复制要求主服务器启用二进制日志记录。默认情况下它是不打开的,因此您必须编辑配置文件并将其打开。使用sudo vi /etc/mysql/my.cnf编辑配置文件,并通过取消注释和更改以下行来打开二进制日志记录:

server-id = 1
log_bin = /media/mysql/mysql_data/mysql-bin.log

第一行设置主服务器的服务器 ID。在基本复制(5.5 版)中,每台服务器必须有一个唯一的服务器 ID。在这种情况下,您将 1 分配给主服务器;从机将具有一些其他值,例如 2。富有想象力,是吗?

下一行设置二进制日志文件的位置和名称。您将它保存到您的外部驱动器,因为像数据本身一样,二进制日志会随着时间的推移而增长。幸运的是,MySQL 旨在将文件保持在一个合理的大小,并具有允许您截断它并开始一个新文件的命令(这一过程称为旋转)。有关管理二进制日志文件的更多信息,请参见在线参考手册( https://dev.mysql.com/doc/refman/8.0/en/replication.html )。一旦保存了编辑,您就可以重启 MySQL 服务器(或者简单地停止然后启动)。

接下来,您必须创建一个用户,供从服务器用来连接到主服务器并读取二进制日志。这个有一个特殊的特权叫做REPLICATION SLAVE。下面的代码显示了创建用户和添加权限的正确的GRANT语句。记住您在这里使用的用户名和密码—您需要它用于从属服务器:

mysql> CREATE USER 'rpl'@'%' IDENTIFIED BY 'secret'
Query OK, 0 rows affected (0.01 sec)
mysql> GRANT REPLICATION SLAVE ON ∗.∗ TO 'rpl'@'%';
Query OK, 0 rows affected (0.01 sec)

但是从机还需要一条信息。从机需要知道要读取的二进制日志的名称,以及从文件中的什么位置开始读取事件。您可以使用SHOW MASTER STATUS命令来确定这一点:

mysql> SHOW MASTER STATUS;
+--------------+----------+-------------+------------------+...
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB |...
+--------------+----------+-------------+------------------+...
| binlog.000003 |      878 |              |                  |...
+--------------+----------+-------------+------------------+...
1 row in set (0.00 sec)

现在您已经有了主服务器的二进制日志文件名和位置以及复制用户和密码,您可以访问您的从服务器并将其连接到主服务器。您还需要知道 Raspberry Pi 的主机名或 IP 地址,以及 MySQL 运行的端口。默认情况下,端口是 3306;但是如果你改变了它,你应该注意到新的值。记下表 8-1 中的所有信息。

表 8-1

复制所需的主服务器信息

|

主文件中的项目

|

价值

|
| --- | --- |
| IP 地址或主机名 |   |
| 港口 |   |
| 二进制日志文件 |   |
| 二进制日志文件位置 |   |
| 复制用户 ID |   |
| 复制用户密码 |   |

您想要用作从属服务器的 MySQL 服务器应该与 Raspberry Pi 上的服务器版本相同,或者至少是兼容的服务器。在线参考手册指定了哪些 MySQL 版本可以很好地协同工作。幸运的是,有问题的版本列表非常短。在本节中,您应该在台式机或服务器计算机上安装一台服务器,并确保其配置正确。

将从设备连接到主设备所需的步骤包括发出一个CHANGE MASTER命令来连接到主设备,以及发出一个START SLAVE命令来启动服务器上的从设备角色。是的,就是这么简单!回想一下,您需要来自主机的信息来完成这些命令。以下命令显示了一个从设备连接到一个运行在 Raspberry Pi 上的主设备。让我们从如下所示的CHANGE MASTER命令开始:

mysql> CHANGE MASTER TO MASTER_HOST='10.0.1.17', MASTER_PORT=3306, MASTER_LOG_FILE='mysql-bin.000003', MASTER_LOG_POS=878, MASTER_USER="rpl", MASTER_PASSWORD="secret";
Query OK, 0 rows affected (0.22 sec)

这个例子使用了 Raspberry Pi 的 IP 地址、端口号(默认为 3306)、来自SHOW MASTER STATUS命令的日志文件和位置,以及复制用户的用户和密码。如果您键入的命令正确,它应该会无错误地返回。如果有错误或警告,使用SHOW WARNINGS命令读取警告并纠正任何问题。

下一步是启动从属进程。这个命令简单来说就是START SLAVE。它通常不会报告任何错误;您必须使用SHOW SLAVE STATUS才能看到它们。清单 8-11 显示了这两个命令的运行情况。

Tip

对于宽结果,使用\G选项将列视为行(称为垂直格式)。

mysql> start slave;
Query OK, 0 rows affected (0.00 sec)

mysql> show slave status \G
∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗ 1\. row ∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗∗
       Slave_IO_State: Waiting for master to send event
  Master_Host: 10.0.1.17
  Master_User: rpl
  Master_Port: 3306
Connect_Retry: 60
      Master_Log_File: mysql-bin.000003
  Read_Master_Log_Pos: 107
       Relay_Log_File: clone-relay-bin.000003
Relay_Log_Pos: 4
Relay_Master_Log_File: mysql-bin.000001
     Slave_IO_Running: Yes
    Slave_SQL_Running: Yes
      Replicate_Do_DB:
  Replicate_Ignore_DB:
   Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
   Last_Errno: 0
   Last_Error:
 Skip_Counter: 0
  Exec_Master_Log_Pos: 107
      Relay_Log_Space: 555
      Until_Condition: None
       Until_Log_File:
Until_Log_Pos: 0
   Master_SSL_Allowed: No
   Master_SSL_CA_File:
   Master_SSL_CA_Path:
      Master_SSL_Cert:
    Master_SSL_Cipher:
       Master_SSL_Key:
Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
Last_IO_Errno: 0
Last_IO_Error:
       Last_SQL_Errno: 0
       Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
     Master_Server_Id: 1
1 row in set (0.00 sec)

mysql>

Listing 8-11Starting the Slave

花点时间费力地读完所有这些行。有几个关键字段你需要注意。这些错误包括名称和状态列中的任何错误。例如,第一行(Slave_IO_State)显示了指示从机 I/O 线程状态的文本消息。I/O 线程负责从主服务器的二进制日志中读取事件。还有一个 SQL 线程负责从中继日志中读取事件并执行它们。

对于这个例子,您只需要确保两个线程都在运行(YES)并且没有错误。关于SHOW SLAVE STATUS命令中所有字段的详细解释,请参见在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/replication-configuration.html )。

既然从属服务器已经连接并正在运行,让我们检查它上面的testme数据库:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)

mysql>

等等!它去哪里了?这个例子不是应该复制一切吗?嗯,是也不是。的确,你的奴隶与主人相连,从现在开始,它将复制主人身上发生的任何变化。回想一下,您使用了SHOW MASTER STATUS命令来获取二进制日志文件和位置。这些值是下一个事件位置的坐标,而不是任何先前事件的坐标。啊哈:您在创建了testme数据库之后设置了复制。

你怎么解决这个问题?那得看情况。如果您真的想要复制testme数据库,您必须停止复制,修复主数据库,然后重新连接从数据库。我不会详细介绍这些步骤,但是我在这里列出了它们作为您自己试验的大纲:

  1. 阻止奴隶。

  2. 转到主服务器并删除数据库。

  3. 获取新的SHOW MASTER STATUS数据。

  4. 重新连接从机。

  5. 启动从机。

明白了吗?很好。如果没有,这是一个很好的练习,回去自己尝试这些步骤。

清理主服务器并重启复制后,继续尝试在主服务器上创建一个数据库,并观察从服务器上的结果。以下是命令。我使用了不同的数据库名称,以防您选择不尝试前面的挑战,如下所示:

mysql> create database testme_again;
Query OK, 1 row affected (0.00 sec)

mysql> show databases;
+--------------------+
| Database          |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| testme             |
| testme_again       |
+--------------------+
4 rows in set (0.01 sec)

mysql>

返回到从属数据库,查看其中列出了哪些数据库,如下所示:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| testme_again       |
+--------------------+
4 rows in set (0.00 sec)

mysql>

成功!现在,您的 Raspberry Pi 数据库服务器正在由您的桌面计算机进行备份。

部件购物清单

本章唯一需要的新组件是一个多余的 USB 硬盘,列于表 8-2 中。表 8-3 显示了包含在其他章节购物清单中的支持硬件列表。

表 8-3

以前章节中重复使用的组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| Raspberry Pi 型号 4B 2GB 或 4GB 内存 | sparkfun . com、adafruit.com、thepithut.com | 50 美元以上 | one |
| 迷你 HDMI 电缆 | 大多数在线和零售商店 | 变化 | one |
| HDMI 或 DVI 监视器 | 大多数在线和零售商店 | 变化 | one |
| USB 键盘 | 大多数在线和零售商店 | 变化 | one |
| USB-C 电源 | 大多数在线和零售商店 | 变化 | one |
| SD 卡,32GB 或更大 | 大多数在线和零售商店 | 变化 | one |

表 8-2

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| 剩余硬盘 | 任何 USB 硬盘(多余的或购买的) | 变化 | one |

摘要

这一章介绍了 MySQL,并给了你一个如何使用它的速成班。您还在 Raspberry Pi 上编译并安装了 MySQL,并了解了如何使用 MySQL 的更多高级特性,比如复制。

虽然它没有高可用性、五个九正常运行时间(99.999%)数据库服务器那么复杂,但带有附加 USB 硬盘驱动器的低成本 Raspberry Pi 是一个占用空间非常小的数据库服务器,可以放在任何地方。

这一点非常重要,因为传感器网络天生就需要体积小、成本低。必须构建一个昂贵的数据库服务器通常不是所期望的投资水平。

此外,根据您为传感器选择的主机,保存数据很困难。如果您选择 Arduino 作为主机,将数据存储到数据库需要连接到互联网,并依赖另一个服务来存储您的数据。这对于实际上可以将传感器节点连接到互联网 12 (或传感器网络的聚合器节点)的情况来说是很好的;但是如果你不能或者不想连接到互联网,从 Arduino 获取数据到数据库服务器是很困难的。

也就是说,直到最近。正如您将看到的,确实有一种方法可以保存来自传感器节点的传感器数据。在下一章中,您将构建一个传感器节点,它将数据保存在新的数据库服务器中——直接从 Arduino!

九、MySQL 和 Arduino:终于联合了!

在前几章中,我讨论了几种可以用来存储传感器数据的方法。其中一种方法是将数据存储在网络上的数据库中。如果您还记得,这有几个优点,尤其是您不必将传感器网络连接到互联网就能实现这一功能。

如果您的传感器节点连接到一个 Raspberry Pi,这并不难实现,但是如果您的传感器节点连接到一个 Arduino,您如何做到这一点呢?Arduino 本身可以是一个传感器节点,一个或多个传感器直接连接到 Arduino I/O 端口;或者,Arduino 可以是一个数据聚合器,使用 XBee 模块通过 ZigBee 无线网络从其他传感器节点收集数据,如您在第六章中所见。但是,如何在不使用第三方应用程序或基于云的解决方案的情况下将数据插入 MySQL 呢?

本章介绍了一个新的数据库连接器库,使您能够将传感器数据从 Arduino 发送到 MySQL 数据库。

介绍连接器/Arduino

恭喜你!你刚刚进入了一个 Arduino 项目的新世界。借助专门为 Arduino 设计的新数据库连接器,您可以将 Arduino 项目直接连接到 MySQL 服务器,而无需使用中间计算机或基于 web 的服务。

直接访问数据库服务器意味着您可以将从项目中获取的数据存储在数据库中。您还可以检查存储在服务器上的表中的值。该连接器允许您将传感器网络保持在本地,甚至可以与互联网或任何其他外部网络断开连接。

如果您使用了其他一些存储 Arduino 数据的方法,例如将数据写入闪存(如安全数字卡)或 EEPROM 设备,您可以完全取消手动数据复制和提取方法。同样,如果您的项目无法或不想连接到 Internet 来保存数据,那么写入本地数据库服务器的能力也可以解决这个问题。

将数据保存在数据库中不仅可以保存数据供以后分析,还意味着您的项目可以将数据提供给更复杂的应用程序。更好的是,如果您的项目使用大量数据进行计算或查找,您可以将数据存储在服务器上,只检索计算或操作所需的数据,而无需占用 Arduino 上的大量内存。显然,这为 Arduino 项目开辟了一条全新的道路!

数据库连接器被命名为 Connector/Arduino。它在为 Arduino 平台构建的库中实现了 MySQL 客户端通信协议(称为数据库连接器)。此后,当讨论一般概念和特性时,我引用 Connector/Arduino,并将实际的源代码称为 Connector/Arduino 库、连接器或简单的库。

为使用该库而编写的草图(程序)允许您对 SQL 语句进行编码以插入数据,并运行小型查询以从数据库返回数据(例如,使用查找表)。

您可能想知道,内存和处理能力有限的微控制器怎么可能支持将数据插入 MySQL 服务器的代码。您可以这样做,因为与 MySQL 服务器通信的协议不仅广为人知且有据可查,而且是专门设计为轻量级的。这是 MySQL 吸引嵌入式开发者的小细节之一。

为了与 MySQL 通信,Arduino 必须通过网络连接到 MySQL 服务器。为此,Arduino 必须使用以太网或 WiFi 屏蔽,并连接到可以连接到数据库服务器的网络或子网(您甚至可以通过互联网连接)。该库与大多数新的 Arduino 以太网、WiFi 和支持标准以太网库的兼容克隆屏蔽兼容。

Caution

如果您使用的 WiFi 或以太网屏蔽或模块不是 100%兼容 Arduino,您可能会在使用连接器时遇到问题。确保选择使用 Arduino 标准以太网系列的屏蔽和模块。如果它们有自己的库,它们可能与连接器/Arduino 不兼容,或者,最糟糕的情况是,您可能需要修改连接器代码才能使用它。

硬件要求

连接器/Arduino 需要至少 32KB 内存的 Arduino 或 Arduino 克隆。如果您使用的是 Duemilanove 之类的旧 Arduino,请确保您的版本使用的是 ATmega328P 处理器。图 9-1 和 9-2 描绘了两种最常见的 Arduino 板。

img/313992_2_En_9_Fig2_HTML.jpg

图 9-2

Arduino Leonardo(由 arduino.cc 提供)

img/313992_2_En_9_Fig1_HTML.jpg

图 9-1

Arduino Uno(由 arduino.cc 提供)

请注意,莱昂纳多和乌诺的标题是不同的。您可能看不到电路板上的细微差异,但 Leonardo 具有内置的 USB 通信功能,可以使用鼠标和键盘、四个附加的数字引脚、六个模拟引脚和一个脉冲宽度调制(PWM)引脚。有关差异和新特性的更多信息,请参见 www.arduino.cc/en/Guide/ArduinoLeonardoMicro?from=Guide.ArduinoLeonardo

连接器/Arduino 还需要 Arduino 以太网或 WiFi 屏蔽或同等设备。这是因为该库引用了为以太网屏蔽编写的以太网库。如果您有其他形式的以太网屏蔽,或者如果您正在使用的以太网屏蔽需要不同的库,您必须对该库稍加修改才能使用它。您将在后面的部分中看到这一点。图 9-3 为 Arduino 以太网盾 2,图 9-4 为 Arduino WiFi 盾。

Note

虽然 WiFi 盾在 Arduino 网站上被列为退役,但你仍然可以在大多数在线零售商网站上找到它们。也有多种 Arduino 克隆 WiFi 屏蔽可以使用。一定要找到一个与 Arduino 的以太网库兼容的。

img/313992_2_En_9_Fig4_HTML.jpg

图 9-4

arduino WiFi 101 shield(arduino . cc 提供)

img/313992_2_En_9_Fig3_HTML.jpg

图 9-3

arduino Ethernet Shield 2(arduino . cc 提供)

What about the Due?

连接器已经过测试,可与 Due 和类似的克隆板一起工作。如果你有一个 Due,你可以使用一个 Arduino 以太网屏蔽 1 与连接器。

内存呢?

连接器/Arduino 是作为 Arduino 库实现的。尽管该协议是轻量级的,但是库确实会消耗一些内存。事实上,该库需要大约 28KB 的闪存来加载。因此,它需要 ATmega328 或类似的(或更新的)带有 32KB 闪存的处理器。

这看起来似乎没有太多的空间来编程您的传感器节点,但事实证明,对于大多数传感器来说,您真的不需要那么多。如果你这样做了,你总是可以升级到一个内存更大的新 Arduino。例如,最新的 Arduino,Due,有 512KB 的内存用于程序代码。基于此,仅仅 28KB 的开销是微不足道的。

安装 MySQL 连接器/Arduino

要开始使用这个库,你只需从 Arduino IDE 安装它,就像我们在其他项目中所做的那样。回想一下,我们需要打开一个新的草图,然后选择草图➤包括库➤管理库…,当对话框打开时,在搜索框中输入 MySQL。然后点击安装按钮安装 MySQL 连接器/Arduino,如图 9-5 所示。

该库是开源的,许可为 GPLv2,归 Oracle 公司所有。因此,您打算共享的对库的任何修改都必须符合 GPLv2 许可。虽然它不是 Oracle 或 MySQL 官方支持的产品,但您可以使用 GPLv2 下的库。

img/313992_2_En_9_Fig5_HTML.jpg

图 9-5

安装 MySQL 连接器/Arduino

Database Connectors for MYSQL

MySQL 有很多数据库连接器。Oracle 为各种语言提供了许多数据库连接器,包括。你可以在 http://dev.mysql.com/downloads/connector/ 找到连接器。

  • 连接器/ODBC :符合标准 ODBC

  • 连接器/网络:视窗。Net 平台

  • 连接器/J : Java 应用程序

  • 连接器/Python : Python 应用

  • 连接器/C++ :标准化的 C++应用程序

  • Connector/node . js:JavaScript 应用

  • PHP 的 MySQL 本地驱动 (mysqlnd): PHP 连接器

如您所见,几乎所有您可能遇到的编程语言都有一个连接器——现在甚至还有一个用于 Arduino 的连接器!

既然已经安装了连接器/Arduino 库,就可以开始编写支持数据库的草图了!在您进入库源代码之前,让我们首先检查一下使用库的一些限制。

限制

鉴于目标平台是一个内存有限的小型微控制器,在 Arduino 平台上使用复杂的库有一些限制。关于 Connector/Arduino,你应该知道的第一件事是,它不是一个小库:它会消耗大量内存。虽然该库使用动态内存来将内存使用保持在最低水平,但需要多少内存取决于您如何使用连接器。

更具体地说,您需要限制创建的字符串常量的数量。如果您发出简单的数据插入命令(INSERT INTO),一个简单的计算方法是连接器使用的长度比所有字符串的长度总和多一点。如果向服务器查询数据,连接器使用的数据会比返回的一行数据的累积大小多一点。

如果您使用的是最新的 Arduino Due 或具有大量内存的类似主板,这可能不是问题。但是还有其他的考虑。以下是连接器/Arduino 的已知限制:

  • 查询字符串(SQL 语句)必须适合内存。这是因为该类使用内部缓冲区来构建要发送到服务器的数据包。建议使用 PROGMEM 将长字符串存储在程序存储器中(见cmd_query_P)。详见 www.arduino.cc/reference/en/language/variables/utilities/progmem/

  • 结果集一次读取一行,一次读取一个字段。

  • 结果集中一行的组合长度必须适合内存。

  • 立即处理服务器错误响应。连接器将错误代码和消息打印到串行监视器。

既然您已经了解了连接器的高级工作原理、所需的硬件以及如何下载和安装连接器,那么让我们开始使用连接器来编写将数据插入 MySQL 服务器的草图。

还有一个限制值得一提。该连接器被编写为支持 Oracle 公司的 MySQL 的当前和最新版本。其他供应商还维护了其他变体,但是大多数都有一些修改,引入了微妙的不兼容性。例如,已知至少有一种变体会导致连接器出现问题。 2 如果您在 MySQL 服务器上使用连接器时遇到奇怪的错误或问题,请确保您使用的是 Oracle 发布的服务器二进制文件。切换到 MySQL 的基础或“原始”源代码可以解决许多小问题和不兼容性。

Tip

如果您想了解关于连接器的最新信息,包括即将发布的版本,或者如果您需要关于连接器问题的帮助,请访问位于 https://github.com/ChuckBell/MySQL_Connector_Arduino/wiki 的连接器 GitHub 资源库。

What about the ESP8266?

由于微控制器和 WiFi 模块等小型廉价 ESP 的流行,您在规划传感器网络时可能会遇到这些问题。使用它们完全没问题,因为它们可以使用 Arduino IDE 进行编程。但是,您应该仔细阅读连接器的文档,因为它需要对您的脚本进行一些小的更改,以便能够与 ESP 模块一起使用。甚至有一个样本草图,你可以查看的想法。

支持建筑连接器/Arduino 的草图

让我们从一个简单的草图开始,这个草图设计用来在 MySQL 的一个表中插入一行。你在创造一个“你好,世界!”草图(但保存在数据库表中)。所有支持数据库的草图共享相同的公共构建块。这些包括设置要使用的数据库、使用一组特定的包含文件创建草图、连接到数据库服务器以及执行查询。本节将介绍创建和执行启用数据库的草图所需的基本步骤。

数据库设置

您首先需要的是一台数据库服务器!如果你喜欢限制未知的东西,你可以使用你的台式机或笔记本电脑(在试验嵌入式系统时,这总是一个好的实践)。为了简化示例,我使用了一台运行 MySQL 的笔记本电脑。然而,如果您在前一章中构建了一个 Raspberry Pi MySQL 数据库服务器,那么您可以自由地使用您崭新的 Raspberry Pi 数据库服务器。

我还通过仅使用setup()方法连接到 MySQL 服务器并发出查询来保持示例的简单性。这简化了事情,因为setup()方法只被调用一次。如果您想看看发出多个INSERT语句时会发生什么,可以随意将INSERT语句移到loop()方法中。确保包含delay()调用,以允许库有足够的时间来执行和协商协议。试图太快发出太多查询可能会导致奇怪的错误或丢失行。

首先创建一个数据库和一个表来存储数据。对于这个实验,您创建了一个简单的表,它有两列:一个文本列(char)用于存储消息,一个TIMESTAMP列用于记录保存行的日期和时间。我发现TIMESTAMP数据类型是存储传感器数据的绝佳选择。您很少会不想知道样本是何时采集的!最重要的是,MySQL 使它非常容易使用。事实上,您甚至不需要向服务器传递一个令牌NULL值,因为它自己生成并存储当前时间戳。 3

清单 9-1 显示了一个 MySQL 客户端(名为mysql)会话,它创建数据库和表,并手动向表中插入一行。草图将从您的 Arduino 执行一个类似的INSERT语句。通过发出一个SELECT命令,你可以看到每次表被更新。

Note

对于本章中的例子,我使用的是 MySQL Shell,但是如果您愿意,您也可以使用旧的 MySQL 客户端。

> mysqlsh --sql --uri root@localhost:33060
MySQL Shell 8.0.19

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:33060'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 18 (X protocol)
Server version: 8.0.19 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
> CREATE DATABASE test_arduino;
Query OK, 1 row affected (0.0190 sec)
> USE test_arduino;
Default schema set to `test_arduino`.
Fetching table and column names from `test_arduino` for auto-completion... Press ^C to stop.
> CREATE TABLE hello (source char(20), event_date timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP);
Query OK, 0 rows affected (0.0503 sec)
> CREATE USER 'arduino_user'@'%' IDENTIFIED WITH mysql_native_password BY 'secret';
Query OK, 0 rows affected (0.0126 sec)
> GRANT ALL ON *.* to 'arduino_user'@'%';
Query OK, 0 rows affected (0.0108 sec)
> INSERT INTO hello (source) VALUES ('From Laptop');
Query OK, 1 row affected (0.0080 sec)
> SELECT * FROM hello;
+-------------+---------------------+
| source      | event_date          |
+-------------+---------------------+
| From Laptop | 2020-03-04 13:24:14 |
+-------------+---------------------+

Listing 9-1Creating the Test Database

注意,这里我创建了数据库以及访问该数据库的用户。您可能会注意到CREATE USER命令中的一个新短语。IDENTIFIED WITH mysql_native_password子句告诉 MySQL 使用本地密码插件,而不是更新的sha256_password插件。如果您有一个较旧版本的 MySQL 服务器(5.7 和更低版本),您不需要这个子句,因为它可能会生成错误。

Tip

如果您有连接问题,请确保为您的 MySQL 服务器启用mysql_native_password插件,或者在创建您想要用来从 Arduino 连接的用户时使用前面的条款。

注意,我还创建了一个带有时间戳列的简单表,用于记录事件的日期和时间。这是一个非常简单的例子,但是您可以使用这些技术来帮助您的数据库更易于使用。也就是说,您不必计算一行的日期和时间,只需插入数据并让数据库处理它。酷。

Designing Tables for Storing Sensor Data

为传感器网络设计表格时,请务必仔细选择正确的数据类型和长度(如果适用)。几个月后,当你发现自己辛辛苦苦构建的传感器网络由于选择了错误的数据类型而被截断时,那将是一场悲剧。同样,如果在保存数据时遇到传感器节点或聚集节点失败的问题,请检查角色的长度和其他字段,以确保没有超出分配的大小(长度)。

设置 Arduino

这个例子需要的硬件是一个 Arduino 或 shield 兼容的克隆和一个 Arduino 以太网 shield。以太网屏蔽有多种形式,但我更喜欢 Arduino 品牌的屏蔽,因为它们更可靠。

Buyer Beware: Check Compatibility

在大多数情况下,被描述为“盾兼容”的 Arduino 克隆体是可以安全使用的,但是你应该经常检查。我有一次没有做到这一点,以为自己在一个“100%兼容”的以太网屏蔽上找到了很多东西,却发现它有一个恼人的缺陷,需要我移除屏蔽才能上传草图。虽然 shield 工作正常,我也经常使用,但它不是 100%兼容。

我喜欢将我的 Arduino 安装在一个平台上,以便更容易操作,并且不太可能不小心将它放在导电的表面或物体上,或者更糟糕的是,它可能会不小心划伤我的桌子!继续将以太网屏蔽安装到您的 Arduino 上。确保所有销都就位。图 9-6 显示我的 Arduino 和以太网盾安装在一个平台 4 上,旁边有一个方便的小试验板。

img/313992_2_En_9_Fig6_HTML.jpg

图 9-6

带以太网屏蔽的 Arduino

开始新的草图

是时候开始写你的草图了。打开你的 Arduino 环境,创建一个名为hello_mysql.ino的新草图。以下部分详细介绍了一个典型的支持 MySQL 数据库的草图的各个部分。您从所需的包含文件开始。

包括文件

要使用连接器/Arduino 库,请记住它需要一个以太网屏蔽,因此需要以太网库。我们还需要包括用于建立连接和发出查询的连接器头。下面显示了一个支持 MySQL 数据库的草图至少需要包含的所有库头文件。现在就开始输入这些:

#include <Ethernet.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

Note

本书中的例子不需要包含Ethernet.h头,但是如果出现编译错误,包含它也是可以的。

初步设置

设置好包含文件后,接下来必须处理一些初步声明。这些包括对以太网库和连接器/Arduino 的声明。

以太网库要求您设置服务器的 MAC 地址和 IP 地址。MAC 地址是一串十六进制数字,不需要任何特殊的东西,但是它在您网络上的机器中应该是唯一的。它使用动态主机控制协议(DHCP)来获取 IP 地址、DNS 和网关信息。服务器的 IP 地址是使用IPAddress类定义的(正如您所期望的,它将值存储为一个由四个整数组成的数组)。

另一方面,以太网类也允许您为 Arduino 提供一个 IP 地址。如果您为 Arduino 分配 IP 地址,该地址对于它所连接的网段必须是唯一的。请务必使用 IP 扫描仪,以确保您选择的 IP 地址尚未被使用。

下面显示了 10.0.1.X 网络中节点的这些语句:

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress server_addr(10,0,1,35);  // IP of the MySQL *server* here

接下来,您需要为 Connector/Arduino 设置一些变量。您需要定义对库的引用和一些字符串,用于草图中使用的数据。至少包括一个用户 ID 字符串、一个密码字符串和一个您使用的查询字符串。最后一个字符串是可选的,因为您可以在查询调用中直接使用文字字符串,但是为查询语句生成字符串是一个很好的实践。这也是使查询参数化以便重用的最佳方式。

以下是完成草图声明所需的语句示例:

EthernetClient client;
MySQL_Connection conn((Client *)&client);

char user[] = "arduino_user";      // MySQL user login username
char password[] = "secret";        // MySQL user login password
char INSERT_SQL[] = "INSERT INTO test_arduino.hello (source) VALUES ('Hello from Arduino!')";

请注意INSERT语句。您包括一个字符串来表明您正在从您的 Arduino 运行查询。

连接到 MySQL 服务器

预备工作到此结束;让我们写点代码吧!接下来,更改setup()方法。这是应该放置连接到 MySQL 服务器的代码的地方。回想一下,每次 Arduino 启动时,这个方法只被调用一次。清单 9-2 显示了所需的代码。

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect
  Ethernet.begin(mac_addr);
  Serial.println("Connecting...");
  if (conn.connect(server_addr, 3306, user, password)) {
    delay(1000);
    // insert query here //
  }
  else
    Serial.println("Connection failed.");
}

Listing 9-2Setup() Method

代码以调用Ethernet库来初始化网络连接开始。回想一下,当您使用Ethernet.begin()方法时,只传递 MAC 地址,如示例所示,这会导致Ethernet库使用 DHCP 来获取 IP 地址。如果您想手动分配 IP 地址,请参见 http://arduino.cc/en/Reference/EthernetBegin 中的Ethernet.begin()方法文档。

接下来是对串行监视器的调用。虽然不完全必要,但是包含它是一个好主意,这样您就可以看到由 Connector/Arduino 编写的消息。如果您在连接或运行查询时遇到问题,请确保使用串行监视器,以便可以看到库发送的消息。

现在调用delay()方法。您发出一秒钟的等待命令,以确保您有时间启动串行监视器,并且不会错过调试语句。如果您需要更多时间来启动串行监视器,请随意尝试更改该值。

延迟之后,您向串行监视器打印一条语句,表明您正在尝试连接到服务器。连接到服务器是对我们之前用名为connect()的方法创建的 Connector/Arduino 类的一次调用。您传递 MySQL 数据库服务器的 IP 地址、服务器监听的端口以及user namepassword。如果这个调用通过,代码将进入下一个delay()方法调用。

在发出额外的 MySQL 命令之前,需要这种延迟来减缓执行速度。与前面的延迟一样,根据您的硬件和网络延迟,您可能不需要此延迟。如果您强烈反对使用延迟来避免延迟问题,您应该进行试验。另一方面,如果连接失败,代码会通过 print 语句告诉您连接已经失败。

注意注释行,// insert query here //。这是我们放置示例查询的地方,以便 Arduino 向 MySQL 发送一次数据。如果我们将它放在loop()方法中,它将多次发送数据,对于这个例子,这不是我们想要的。

运行查询

现在是运行查询的时候了。将这段代码放在成功连接后执行的分支中。清单 9-3 显示了我们将用来运行INSERT查询的代码部分。花点时间通读一下。注意,我们创建了一个MySQL_Cursor类的新实例,执行查询,然后检查结果是否有错误(false 表示失败)。最后,我们简单地删除实例,因为我们不再需要它了。

Serial.print("Recording hello message...");
// Initiate the query class instance
MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
// Execute the query
int res = cur_mem->execute(INSERT_SQL);
if (!res) {
  Serial.println("Query failed.");
} else {
  Serial.println("Ok.");
}
// Note: since there are no results, we do not need to read any data
// Deleting the cursor also frees up memory used
delete cur_mem;

Listing 9-3Connecting and Running a Query

请注意,您只需调用一个名为cur_mem->execute()的方法,并向其传递您之前定义的查询。是的,就是这么简单!

最后,在这个例子中,loop()方法是空的,因为我们在开始时只做了一个查询。

void loop() {
}

还有一件事…

至此,您已经拥有了设置和运行连接器的基本草图所需的一切。然而,在绘制草图时,你可能需要考虑两件事。首先,您应该启用调试模式,以便在出现错误时可以看到更多信息。其次,如果您计划执行SELECT查询,启用 select 代码。默认情况下,这两种模式都是关闭的,以便通过禁用部分代码来节省几个字节。

例如,对串行监视器的许多调试打印语句会比您可能拥有的多消耗一点空间。类似地,用于处理来自服务器的数据以进行SELECT查询的代码也占用了一些空间。当使用像 Arduino 这样的小设备时,有时保存几个字节可能会使草图稳定和成功。

要打开调试模式,导航到 Arduino 库目录中的MySQL_Connector_Arduino/src文件夹下的MySQL_Packet.h文件。在版本行之后立即添加以下代码行(以粗体显示)。这将允许在出现错误时打印额外的诊断数据。

#define MYSQL_OK_PACKET     0x00
#define MYSQL_EOF_PACKET    0xfe
#define MYSQL_ERROR_PACKET  0xff
#define MYSQL_VERSION_STR   "1.2.0"
#define DEBUG

Tip

启动新草图时,始终启用调试模式。一旦你让它正常工作,你可以删除它。

要启用选择模式,请在版本行后添加以下代码行(以粗体显示)。这将启用支持选择查询的代码部分。

#define MYSQL_OK_PACKET     0x00
#define MYSQL_EOF_PACKET    0xfe
#define MYSQL_ERROR_PACKET  0xff
#define MYSQL_VERSION_STR   "1.2.0"
#define WITH_SELECT

测试草图

现在,除了 loop()方法之外,您已经拥有了完成草图所需的所有代码。在这种情况下,您让它成为一个空方法,因为您没有做任何重复的事情。清单 9-4 显示了完成的草图。

Tip

如果您在让连接器工作时遇到问题,请参阅“连接器/Arduino 故障排除”部分,然后返回到此项目。

/**
 * Beginning Sensor Networks Second Edition
 * Example: Hello, MySQL!
 *
 * This code module demonstrates how to create a simple database-enabled
 * sketch.
 *
 * Dr. Charles Bell 2020
 */
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(10,0,1,35);  // IP of the MySQL *server* here
char user[] = "arduino_user";      // MySQL user login username
char password[] = "secret";        // MySQL user login password
// Sample query
char INSERT_SQL[] = "INSERT INTO test_arduino.hello (source) VALUES ('Hello, Arduino!')";

EthernetClient client;
MySQL_Connection conn((Client *)&client);

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect
  Ethernet.begin(mac_addr);
  Serial.print("My local IP is: ");
  Serial.println(Ethernet.localIP());
  Serial.println("Connecting...");
  if (conn.connect(server_addr, 3306, user, password)) {
    delay(1000);
    Serial.print("Recording hello message...");
    // Initiate the query class instance
    MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
    // Execute the query
    int res = cur_mem->execute(INSERT_SQL);
    if (!res) {
      Serial.println("Query failed.");
    } else {
      Serial.println("Ok.");
    }
    // Note: since there are no results, we do not need to read any data
    // Deleting the cursor also frees up memory used
    delete cur_mem;
  }
  else
    Serial.println("Connection failed.");
}

void loop() {
}

Listing 9-4“Hello, MySQL!” Sketch

在你按下按钮编译和上传草图之前,让我们讨论一下可能发生的几个错误。如果 MySQL 服务器的 IP 地址或user namepassword错误,您可能会在串行监视器中看到如下所示的连接失败。注意这里它告诉我们要寻找什么;用户 id 和密码不正确。

My local IP is: 192.168.42.12
Connecting...
...trying...
Error: 84 = Access denied for user 'arduino_user'@'192.168.42.12' (using password: YES).
Connection failed.

如果您的 Arduino 连接到 MySQL 服务器,但查询失败,您会在串行监视器中看到如下所示的错误。注意它告诉我们要寻找什么;表名错误(不存在)。

My local IP is: 192.168.42.12
Connecting...
...trying...
Connected to server version: 8.0.18
Recording hello message...Error: 60 = Table 'test_arduino.hello_arduino' doesn't exist.
Query failed.

请务必仔细检查您的 MySQL 服务器的源代码和 IP 地址,以及选择的用户名和密码。如果您在连接时仍然遇到问题,请参阅“连接器/Arduino 故障排除”一节中的测试内容列表,以确保您的 MySQL 服务器配置正确。

仔细检查服务器安装和草图中的信息后,编译草图并上传到 Arduino。然后启动串口监视器,观察连接 MySQL 服务器的过程。下面显示了代码的完整和成功执行:

My local IP is: 192.168.42.12
Connecting...
...trying...
Connected to server version: 8.0.18
Recording hello message...Ok.

哇,是这个吗?不是很有趣,是吗?如果您在您的串行监视器中看到如前所示的语句,请放心,Arduino 已经连接到 MySQL 服务器并向其发出查询。要进行检查,只需返回到mysql客户端或 MySQL shell,并在表上发出一个SELECT。但是首先,多次运行草图,在表中发布几个插入。

有两种方法可以做到这一点。

首先,你可以在你的 Arduino 上按下重置。如果让串行监视器运行,Arduino 会按顺序显示消息,如下所示。第二,可以再次上传草图。在这种情况下,串行监视器关闭,您必须重新打开它。这种方法的优点是每次都可以更改查询语句,从而将不同的行插入到数据库中。现在继续尝试,并检查您的数据库的变化。

My local IP is: 192.168.42.12
Connecting...
...trying...
Connected to server version: 8.0.18
Recording hello message...Ok.
My local IP is: 192.168.42.12
Connecting...
...trying...
Connected to server version: 8.0.18
Recording hello message...Ok.
My local IP is: 192.168.42.12
Connecting...
...trying...
Connected to server version: 8.0.18
Recording hello message...Ok.

让我们检查一下试运行的结果。为此,您使用mysql客户机连接到数据库服务器,并发出一个SELECT查询。清单 9-5 显示了示例中三次运行的结果。注意每次运行的不同时间戳。正如你所看到的,我运行了一次,然后等了几分钟又运行了一次(我用了我的 Arduino 以太网盾上的重置按钮),然后马上又运行了一次。很酷,不是吗?

$ mysqlsh --uri root@localhost:33060 --sql
MySQL Shell 8.0.18

Copyright (c) 2016, 2019, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its affiliates.
Other names may be trademarks of their respective owners.

Type '\help' or '\?' for help; '\quit' to exit.
Creating a session to 'root@localhost:33060'
Fetching schema names for autocompletion... Press ^C to stop.
Your MySQL connection id is 37 (X protocol)
Server version: 8.0.18 MySQL Community Server - GPL
No default schema selected; type \use <schema> to set one.
 MySQL  localhost:33060+ ssl  SQL > SELECT * FROM test_arduino.hello;
+-----------------+---------------------+
| source          | event_date          |
+-----------------+---------------------+
| From Laptop     | 2020-03-10 14:21:21 |
| Hello, Arduino! | 2020-03-10 14:48:11 |
| Hello, Arduino! | 2020-03-10 14:54:24 |
| Hello, Arduino! | 2020-03-10 14:54:41 |
| Hello, Arduino! | 2020-03-10 14:54:56 |
+-----------------+---------------------+
5 rows in set (0.0003 sec)

Listing 9-5Verifying the Connection

以太网盾 2 怎么样?

如果你计划使用 Ethernet Shield 2,你不需要做任何改变。连接器和示例草图无需修改即可工作。

WiFi 盾呢?

如果您计划使用 WiFi shield,您需要进行一些小的更改,以启用连接器中的 WiFi 特定代码。 5 改变连接器代码非常简单。打开MySQL_Packet.h文件,添加以下代码行替换(或注释掉)#include <Ethernet.h>行:

#ifdef ARDUINO_ARCH_ESP32
    #include <Arduino.h>
#elif ARDUINO_ARCH_ESP8266
    #include <ESP8266WiFi.h>
#else
    #include <WiFi.h>
//    #include <Ethernet.h>
#endif

您需要使用一种可用的 WiFi 连接机制,并注释掉Ethernet.begin()呼叫。这里显示了一个有变化的示例setup()方法:

// WiFi card example
char ssid[] = "my_lonely_ssid";
char pass[] = "horse_with_silly_name";

Connector my_conn;        // The Connector/Arduino reference

void setup() {
  Serial.begin(115200);
  while (!Serial);        // wait for serial port to connect. Leonardo only

  // WiFi section
  int status = WiFi.begin(ssid, pass);
  // if you're not connected, stop here:
  if ( status != WL_CONNECTED) {
    Serial.println("Couldn't get a WiFi connection!");
    while(true);
  }
  // if you are connected, print out info about the connection:
  else {
    Serial.println("Connected to network");
    IPAddress ip = WiFi.localIP();
    Serial.print("My IP address is: ");
    Serial.println(ip);
  }
...

参见 http://arduino.cc/en/Guide/ArduinoWiFiShield 了解更多如何使用连接方法将 WiFi 盾连接到接入点的示例。

如果您计划将 WiFi shield 与比 Arduino Uno Rev3 更旧的 Arduino 一起使用,您需要在 IOREF 引脚上使用跳线,如此处所示(由 arduino.cc 提供),以便 WiFi shield 正常工作。WiFi shield 页面提供了这一点以及在您的项目中使用 WiFi shield 的许多其他非常重要的信息。

WiFi 101 盾呢?

也可以使用较新的 WiFi 101 盾。您必须修改 MySQL_Packet.h 文件,使其类似于 WiFi 盾,如下所示:

#ifdef ARDUINO_ARCH_ESP32
    #include <Arduino.h>
#elif ARDUINO_ARCH_ESP8266
    #include <ESP8266WiFi.h>
#else
    #include <WiFi101.h>
//    #include <Ethernet.h>
#endif

Tip

如果您的主板具有 WiFi 功能,请务必检查它需要哪个 WiFi 库,并相应地更换连接器。

连接器/Arduino 故障排除

如上所述设置和使用连接器/Arduino 通常会取得成功。然而,在有些情况下,设置并不十分正确,本章中的例子根本不起作用。很多问题都可以归咎于你选择的 Arduino 硬件,网络环境设置,甚至 MySQL 服务器配置。

例如,如果您的 Arduino 硬件不是官方的 Arduino 产品,如果它比书中使用的示例更新,或者如果您的以太网屏蔽是克隆的或有线网络屏蔽之外的东西,您可能会遇到让一切正常工作的问题。

如果这种情况发生在你身上,不要放弃! 6 本节旨在为您提供一些提示和技巧,帮助您找出硬件、软件、网络和操作系统中可能导致连接器无法工作的问题。

我没有详尽地描述所有案例所需的所有步骤,而是提出了一个分类法,您可以用它来诊断和解决您的问题。有几类问题领域。我将依次讨论这些问题:

  • MySQL 服务器配置

  • MySQL 用户帐户问题

  • 网络配置

  • 连接器安装

  • 其他人

以下部分解释了该问题,并提出了原因和解决方案。可能是这些问题中的一个或多个导致你的草图失败。使用这一部分的最好方法是从头到尾通读一遍,一路上检查你的系统。每一部分都建立在前一部分的基础上,确保所有可能的问题都以有序的方式得到解决。我确信你可以用这种技术让你的素描发挥作用。

Tip

Arduino 站点有一个非常好的 Arduino 常规帮助的疑难解答部分。除了遵循本节中的建议之外,您还应该参考本页。 http://arduino.cc/en/Guide/troubleshooting

MySQL 服务器配置

导致 MySQL 草图失败或无法正常工作的最常见问题之一与 MySQL 服务器的配置方式有关。通常,您可能会在串行监视器中看到连接到服务器的错误。本节包含导致此问题的许多原因。

服务器没有使用网络

MySQL 服务器可以配置为使用--skip-networking选项禁用网络。在这种情况下,您可以使用mysql客户端连接到本地机器(安装了 MySQL 的机器)上的 MySQL 服务器,但是从另一台主机访问该机器会失败。

不使用第二台计算机来检查这一点的最好方法(这也是一个可行的测试)是使用如下的mysql客户端。要测试到服务器的本地连接,请运行以下命令(替换您的用户名和密码):

$ mysql -uroot -psecret

如果这有效,那么您的服务器是活动的并且正在接受连接。如果这不起作用,请参考 MySQL 在线参考手册,检查您的配置和其他方法,以确保 MySQL 正常运行。

现在,让我们尝试通过网络连接。每当您使用如下所示的主机选项时,都会触发此操作。注意,这几乎是相同的命令,但是在本例中,您提供了服务器(您的机器)的 IP 地址和 MySQL 的端口:

$ mysql -uroot -psecret -h 10.0.1.23 --port 3306

这模拟了 Arduino 如何连接到 MySQL 服务器。如果工作正常,它会验证您的 MySQL 服务器是否还活着,并使用提供的 IP 和端口接受网络连接。请确保在草图中使用这些设置。

如果命令失败,找到您的mysql.cnf文件(对于某些 Windows 安装为mysql.ini),并删除跳过网络选项。您可以通过在该行的第一列放置一个#来禁用它。一旦你完成了这些,一定要重启你的 MySQL 服务器以使改变生效,然后再次尝试之前的测试。

无法连接,使用了正确的 IP 地址

--skip-networking选项密切相关的另一个问题是--bind-address选项。该选项确保 MySQL 在特定的 IP 地址上监听和通信。它主要用于多宿主系统(具有多个网络适配器的计算机)。如果启用了该选项,并且没有将其设置为与安装该选项的主机相同的 IP 地址,MySQL 服务器将表现出与上一个问题类似的行为。

测试这个问题使用与上一个例子相同的测试。解决方法就是在 MySQL 配置文件中注释掉 bind-address 选项。如果你改变了配置文件,记得重启你的 MySQL 服务器。

我可以本地连接,但不能远程连接

如果前面的问题没有解决问题,或者如果您的 MySQL 服务器配置正确,但您仍然无法连接,则您的计算机可能正在运行防火墙或类似的端口阻止应用程序。最好的测试方法是使用第二台计算机和mysql客户端应用程序来尝试连接到服务器。如果您收到与服务器没有响应相关的错误或类似错误,则服务器可能正在阻止连接。同样,从远程计算机使用的命令是

$ mysql -uroot -psecret –- 10.0.1.23 --port 3306

要解决此问题,您必须更改防火墙或端口阻止应用程序。MySQL 默认使用端口 3306。请务必检查您的防火墙应用程序,确保它允许通过端口 3306 进行连接(入站和出站)。如果没有,请启用此端口,然后再次尝试您的草图。

Tip

有关为网络访问设置 MySQL 服务器以及特定于平台的安装步骤的更多信息,请参见在线 MySQL 参考手册。

MySQL 用户帐户问题

另一个非常常见的问题是如何创建 MySQL 用户。更具体地说,它与在CREATE USERGRANT语句中选择主机名有关。例如,如果发出以下命令,从 Arduino 或第二台电脑连接时可能会出现问题:

> CREATE USER 'joe'@'10.0.1.23' IDENTIFIED BY 'secret';
Query OK, 0 rows affected (0.01 sec)
> GRANT SELECT ON test_arduino.* TO 'joe'@'10.0.1.24';
Query OK, 0 rows affected (0.01 sec)

你看到问题了吗(有三个)?首先,您创建了一个拥有特定主机(10.0.1.23)的用户,但是后来您将对test_arduino数据库的SELECT特权授予了同一个用户——或者您真的这么做了吗?

Note

对于 MySQL 服务器的新版本,示例中显示的GRANT语句可能会失败。

这是第二个问题。在GRANT语句中,您使用了主机 10.0.1.24,这意味着当用户joe从 10.0.1.24 连接时,他可以看到test_arduino数据库。

第三个问题源于第二个问题。因为您没有引用现有的用户和主机组合,所以 MySQL 不要求 joe 在从主机 10.0.1.24 连接时使用密码。通过查询mysql.user表可以看到这种情况:

> SELECT user, host, password from mysql.user WHERE user = 'joe';
+------+-----------+-------------------------------------------+
| user | host  | password  |
+------+-----------+-------------------------------------------+
| joe  | 10.0.1.23 | *14E65567ABDB5135D0CFD9A70B3032C179A49EE7 |
| joe  | 10.0.1.24 |   |
+------+-----------+-------------------------------------------+
2 rows in set (0.00 sec)

啊哈,你说。啊哈。这里的教训是始终确保您选择的用户和主机与您想要连接的机器的 IP(或主机名)相匹配。

但是你可能在想,“DHCP 呢?”如果你像大多数草图和例子一样使用 DHCP,那么你可能不知道你的 Arduino 被分配了什么 IP 地址。那你会怎么做?

减少主机名和权限问题的方法之一是使用通配符。考虑前面命令的替代方法:

> CREATE USER 'joe'@'10.0.1.%' IDENTIFIED BY 'secret';
Query OK, 0 rows affected (0.00 sec)
> GRANT SELECT ON test_arduino.* TO 'joe'@'10.0.1.%';
Query OK, 0 rows affected (0.00 sec)
> SELECT user, host, password from mysql.user WHERE user = 'joe';
+------+----------+-------------------------------------------+
| user | host | password  |
+------+----------+-------------------------------------------+
| joe  | 10.0.1.% | *14E65567ABDB5135D0CFD9A70B3032C179A49EE7 |
+------+----------+-------------------------------------------+
1 row in set (0.00 sec)

注意,这里使用了一个%作为 IP 地址的最后一部分。这有效地允许用户从该子网上的任何计算机进行连接。酷!另请注意,您的两个用户帐户的问题已经解决。

与用户帐户相关的其他问题包括忘记密码、拼写错误或在分配密码时使用大写字母(或非大写字母)。所有这些用户帐户问题都可以用mysql客户端应用程序来测试。我建议从第二台计算机尝试本地和远程连接。如果它远程工作,你知道帐户设置正确。当你连接到你的 Arduino 用户帐户时,一定要做一两个SELECT,以确保权限设置正确。

还有一个问题可能会导致连接问题。回想一下我们讨论过的mysql_native_password插件。如果您使用的是 MySQL 的最新版本,您需要在创建用户时为每个用户设置密码,或者设置密码插件默认值。

要为每个用户启用mysql_native_password插件,添加如下所示的子句:

CREATE USER 'user'@'%' IDENTIFIED WITH mysql_native_password BY 'secret';

如果您已经创建了用户,您可以使用ALTER USER命令来更改密码插件。

ALTER USER 'user'@'%' IDENTIFIED WITH mysql_native_password;

要使本机密码插件成为所有用户的默认插件,请将以下内容添加到配置文件中,然后重新启动 MySQL:

[mysqld]
...
default-authentication-plugin=mysql_native_password
...

网络配置

当网络出现问题时,问题并不总是显而易见的。我没有列出许多常见的错误情况或具体的例子,而是讨论了您需要检查以确保工作正常的事情。

当出现网络问题时,您可能会遇到或观察到无法连接到您的 MySQL 服务器。是的,您可能会以几乎相同的方式看到前面描述的相同问题。

检查您是否有网络问题的最佳方式是将第二台计算机连接到您用于 Arduino 的同一条网络电缆,并尝试连接您的朋友mysql客户端。请务必检查电脑是否设置为从 DHCP 获取其 IP 地址,以及所有其他网络设置是否与您的 Arduino 相同(无静态 DNS 等)。

这一点非常重要,因为如果你的电脑配置了静态 IP,而 Arduino sketch 使用的是 DHCP(反之亦然),这可能会掩盖问题!例如,如果没有可用的 DHCP 服务器,Arduino sketches 配置为动态获取 IP 地址将会失败。

如果您使用与 Arduino 相同的电缆将第二台计算机连接到您的网络,并且它可以工作,但草图仍然不工作,您应该考虑您的以太网屏蔽有故障或与您的硬件不兼容的可能性。查看供应商或制造商的网站,了解任何限制或兼容性解决方法。我至少在一个场合见过这种情况。另一种可能是防护罩出了故障(很少,但确实会发生)。

现在,如果您的计算机无法连接到您的 MySQL 服务器,请检查以下项目以确保您的网络配置正确。其中一些可能看起来有点愚蠢,但我可以向你保证,我个人至少遇到过一次:

  • 路由器/交换机打开了吗? 8

  • 您使用的子网是否正确?

  • 您是否按照正确的顺序为Ethernet.begin()使用了正确的选项?详见在线 Arduino 库参考页面( http://arduino.cc/en/Reference/EthernetBegin )。

  • 如果您尝试使用 DHCP,您的网络上有 DHCP 服务器吗?

  • 网络电缆是否插入交换机/路由器? 9

  • 检查交换机/路由器上的灯。是否显示线缆已连接?否则,您的电缆可能有问题。

再次检查并解决所有这些问题,回到第二台计算机,并尝试连接。当它工作时,你的草图应该连接到 MySQL 服务器。

Note

完整的网络教程或概述超出了本书的范围。然而,在谷歌搜索中键入几个关键词,会给你很多诊断网络问题的好建议。

另一个尝试是为以太网屏蔽加载一个示例草图。例如,加载、编译和运行 WebClient 草图。你应该看到的是从对 google.com 的搜索请求中返回的大量数据。如果这有效,你可以确定你的以太网屏蔽工作正常,你的数据库服务器或者你的草图仍然有问题。

连接器安装

潜在问题的最后一个主要方面与连接器的安装方式有关。如果您已经完成了这一步,并且您的草图编译和上传正确,那么您没有任何与连接器安装相关的问题。我在下面几节中描述了最常见的安装问题。

与“没有命名的模块”相关的编译错误

如果您遇到编译错误,抱怨没有名为 Connector 的模块或类似的错误,那么您没有将库安装在正确的位置。回到本章前面的“安装 MySQL 连接器/Arduino”一节,并确保您已经安装了库。

如果您从 GitHub 下载了库,您必须确保将库文件放在正确的位置。事实上,您的 libraries 文件夹可能不在您认为的位置。请务必检查首选项对话框以找到默认位置。libraries 文件夹应该是 sketchbook 位置的子文件夹。

你已经正确复制了连接器的最好标志是你可以在文件示例菜单中看到 MySQL 连接器 Arduino 子菜单。

与包含文件相关的编译错误

像这样的错误可能是由于在#include <MySQL***.h>语句中使用引号和括号造成的。如果您使用引号,并且文件没有复制到您的项目文件夹或其子文件夹中,您可能会看到编译错误。正确的方法是对位于libraries文件夹中的连接器文件使用括号。

其他人

还有一些你可能遇到的其他问题不属于前面的类别。

串行监视器中出现奇怪的字符

如果您在串行监视器输出中看到垃圾或奇怪的字符,可能是您对Serial.begin()方法的设置与串行监视器设置不匹配。从串行监视器的下拉列表中选择合适的速度,然后再次尝试您的草图。

串行监视器无输出

这个很难诊断。有时 Arduino 会挂起,或者存在硬件问题,如屏蔽未完全就位、电量不足,甚至草图太大而无法放入内存(请参见下一节)。出现这种情况时,请仔细检查您的硬件是否正确安装了所有组件,并确保您的 Arduino IDE 串行端口和主板设置正确。

我的素描太大了

因为连接器使用大量程序内存,所以在编译草图时可能会耗尽空间。当这种情况发生时,您可能会得到如下错误:

Binary sketch size: 32510 bytes (of a 32256 byte maximum).

如果发生这种情况,请尽可能删除所有不必要的变量、字符串、包含文件和代码。Arduino 网站上的故障排除部分有几个条目是关于减小草图大小的建议。

在极端情况下,您可以编辑连接器本身的源文件并删除不需要的特征。在这种情况下,您希望通过注释掉不使用的方法来移除它们。

Note

在 IDE 的某些版本中,修改文件可能需要重新加载草图以激活 IDE 中的更改。

这些都没有解决我的问题——接下来怎么办?

如果您已经尝试了前面几节中的建议,但仍有问题,请返回到顶部,再次尝试解决方案。如果这不能解决问题,请尝试不同的 Arduino(如 Uno)和不同的以太网屏蔽。测试和诊断应该已经排除了所有其他问题,只剩下 Arduino 硬件是可疑的。

现在你已经知道了 MySQL 数据库草图的基本要求,让我们简短地浏览一下 Connector/Arduino 库,了解一下有哪些方法可供你使用。

MySQL 连接器/Arduino 代码之旅

在您着手一个项目之前,让我们花点时间浏览一下这个库的源代码。本节将更详细地研究这个库及其支持方法。如果您不打算扩展或修改这个库,您可以直接跳到项目部分。

图书馆文件

MySQL 连接器 Arduino 文件夹包含许多文件和一个目录。以下列表描述了每个文件:

  • 示例:包含使用库的示例代码的目录

  • extras :包含文档和使用说明的目录

  • src :包含库源代码的目录

  • keywords.txt:为库保留的关键字列表

  • library.propertie s :包含库的 Arduino 属性的文件

  • README.md:介绍性文件

源代码分布在几个源文件中,每个文件都有自己的用途。下面列出了源代码模块(头文件.h和源文件.cpp及其用途:

  • MySQL_Connection:处理初始握手和一般客户端/服务器连接

  • MySQL_Cursor:处理查询及其结果集的执行

  • MySQL_Encrypt_Sha1:对连接握手进行 SHA1 加密

  • MySQL_Packet:处理客户端/服务器连接的低级包格式、传输和接收

如果需要更改连接器,可以将更改集中在适当的模块中。例如,如果您需要调整连接器连接到服务器的方式,请在MySQL_Connection.h / .cpp中查找。

场结构

当与服务器通信时,库使用许多结构。在返回结果集时,有一种结构是您经常使用的。它被称为field_struct,如下图所示。这个你可以在MySQL_Cursor.h里找到。

// Structure for retrieving a field (minimal implementation).
typedef struct {
  char *db;
  char *table;
  char *name;
} field_struct;

字段结构用于检索字段的元数据。请注意,您将获得数据库、表和字段名。在涉及连接的查询中,这允许您确定字段来自哪个表。用于填充该字段结构的方法get_field(),在内存中创建字符串。当您完成对数据的读取或操作时,您有责任释放这些内存(字符串)。

还有两种处理结果集的结构:column_namesrow_values。我将在下一节更详细地讨论这些问题,但是为了完整起见,在这里将它们包括在内。使用column_names获取列信息,使用row_value获取结果集中的行值。你也可以在MySQL_Cursor.h找到这些。

// Structure for storing result set metadata.
typedef struct {
  int num_fields;     // actual number of fields
  field_struct *fields[MAX_FIELDS];
} column_names;

// Structure for storing row data.
typedef struct {
  char *values[MAX_FIELDS];
} row_values;

现在您已经理解了使用库方法所涉及的结构,让我们来研究一下您可以用来与 MySQL 服务器通信的方法。

公共方法

库——或者更具体地说,类——通常有一个或多个公共方法,任何调用者(程序)都可以通过类的实例化来使用这些方法。类也有一些私有的部分,它们通常是帮助器方法,为类做一些内部的事情。这些方法可以抽象类的某些部分,或者简单地隐藏调用者不需要访问的数据和操作(想想抽象数据类型)。因此,公共方法和属性是设计者允许调用者访问的东西。

连接器/Arduino 库有许多定义库功能的公共方法。有连接、执行查询和从数据库返回结果(行)的方法。每个都在适当的代码模块中声明。我将在后面的小节中演示如何使用这些方法。

我们将看看三个最常用模块的公共方法。SHA1 模块没有适用于支持 MySQL 的草图的方法。

MySQL_Connection

下面显示了MySQL_Connection.h文件中MySQL_Connection类的公共方法的方法声明。我将在下面的段落中讨论每种方法的细节。

boolean connect(IPAddress server, int port, char *user, char *password,
                char *db=NULL);
int connected() { return client->connected(); }
const char *version() { return MYSQL_VERSION_STR; }
void close();

如您所见,connect()方法是连接 MySQL 数据库服务器时必须调用的方法。这个方法必须在 Ethernet 类初始化之后,库中任何其他方法之前调用。它需要服务器的 IP 地址、服务器的端口以及用于连接的用户名和密码。还可以指定默认数据库,这样就不必在 SQL 命令中指定数据库。

它返回一个布尔值,其中 true 表示成功,false 表示在连接到服务器时出现了一些错误。如果您在连接到服务器时遇到问题,您应该尝试使用mysql客户端和草图中定义的 IP、端口、用户和密码从网络上的另一台机器进行连接,以确保连接性,并且没有用户或密码问题。

如果 Arduino 连接到服务器,则connected()方法返回 true,否则返回 false。如果长时间不活动或出现错误,您可以使用此方法来测试连通性。

version()方法返回您连接的服务器版本。只有当连接成功时,它才有效。

close()方法断开与服务器的连接并关闭连接。如果您定期连接和断开连接,请始终调用此方法。

MySQL_Cursor

下面显示了MySQL_Cursor.h文件中MySQL_Cursor类的公共方法的方法声明。我将在下面的段落中讨论每种方法的细节。

boolean execute(const char *query, boolean progmem=false);
void show_results();
void close();
column_names *get_columns();
row_values *get_next_row();
int get_rows_affected() { return rows_affected; }
int get_last_insert_id() { return last_insert_id; }

execute()方法是您可以用来执行查询(SQL 语句)的方法。该方法采用一个常量字符串引用,其中包含您希望执行的查询。如果字符串是使用程序空间定义的,也可以传递它。在这种情况下,您将progmem参数设置为true。有关使用程序空间(称为 PROGMEM)的更多信息,请参见 Arduino 在线参考( www.arduino.cc/en/Reference/PROGMEM )。基本上,如果您需要更多的数据空间,但是能够负担得起使用程序空间来存储数据,那么您应该使用这个方法来执行程序空间中的字符串。

show_results()方法既是如何为SELECT查询从数据库中检索数据的一个例子,也是一个可以在发出execute()调用后执行的方法。该方法一次读取一行,并将其发送到串行监视器。对于测试查询和试验新草图来说,这是很方便的。

另一方面,如果您想从数据库中读取行并处理数据,您可以编写自己的方法来完成这项工作。您必须首先使用execute()执行查询;然后,如果有结果集,使用get_columns()读取列标题(服务器总是首先发送列标题),并使用迭代器get_next_row()读取行。

如果您想检索 SQL 命令返回的受影响的行数,您可以在执行查询后使用get_rows_affected()方法来获取该值。类似地,您可以使用get_last_insert_id()获得最后插入的自动增量值,但这仅在使用自动增量时有效。

MySQL _ 数据包

这个模块在大多数草图中并不常用,但是有一个方法值得一提。在游标或连接器类中可以使用print_packet()方法将数据包数据写入串行监视器。如果您尝试修改连接器以用于不同的板或客户机/服务器协议,您可以将此方法放在关键位置以显示数据包中的数据。使用方法之前,请确保打开调试模式。

示例用途

除了连接到数据库服务器,库的两个用途是发出不返回结果的查询(比如INSERT)和从返回结果集的查询中返回行(比如SELECTSHOW VARIABLES)。以下各节演示了其中的每一个选项。

没有结果的查询

您已经在“Hello,MySQL!”例子。回想一下,这只是一个对execute()的调用,将查询作为字符串传递。下面显示了一个不返回结果的查询示例:

MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
int res = cur_mem->execute(INSERT_SQL);
if (!res) {
  Serial.println("Query failed.");
} else {
  Serial.println("Ok.");
}
delete cur_mem;

返回结果的查询

从服务器返回结果(行)稍微复杂一点,但也不过分。要从服务器读取结果集,必须首先读取结果集头和字段数据包,然后读取数据行。具体来说,您必须预测、读取和解析以下数据包:

  • 结果集头包:列数

  • 字段包:列描述符

  • EOF 包:标记:场尾包

  • 行数据包:行内容

  • EOF 包:标记:数据包结束

这意味着 MySQL 服务器首先发送您必须读取的字段数量和字段(列)列表,然后行数据出现在一个或多个包中,直到没有更多行。读取结果集的算法如下:

  1. 读取结果集标题中的列数。

  2. 读取字段直到 EOF。

  3. 读取行直到 EOF。

我们来看看show_results()方法的内容;参见清单 9-6 。

void MySQL_Cursor::show_results() {
  column_names *cols;
  int rows = 0;

  // Get the columns
  cols = get_columns();
  if (cols == NULL) {
    return;
  }

  for (int f = 0; f < columns.num_fields; f++) {
    Serial.print(columns.fields[f]->name);
    if (f < columns.num_fields-1)
      Serial.print(',');
  }
  Serial.println();

  // Read the rows
  while (get_next_row()) {
    rows++;
    for (int f = 0; f < columns.num_fields; f++) {
      Serial.print(row.values[f]);
      if (f < columns.num_fields-1)
        Serial.print(',');
    }
    free_row_buffer();
    Serial.println();
  }

  // Report how many rows were read
  Serial.print(rows);
  conn->show_error(ROWS, true);
  free_columns_buffer();

  // Free any post-query messages in queue for stored procedures
  clear_ok_packet();
}

Listing 9-6Displaying Result Sets

这是怎么回事?请注意执行查询的代码结构;如果有结果(execute()不返回NULL),则读取列标题。从get_columns()方法返回的是一个包含字段结构数组的结构。结构如下所示:

// Structure for retrieving a field (minimal implementation).
Typedef struct {
  char *db;
  char *table;
  char *name;
} field_struct;

// Structure for storing result set metadata.
Typedef struct {
  int num_fields; // actual number of fields
  field_struct *fields[MAX_FIELDS];
} column_names;

注意,column_names结构有一个字段数组。使用该数组以field_struct的形式获取每个字段的信息(如前所示)。在该结构中,您可以获得数据库名、表名和列名。在代码中,您只需打印出列名和列名后的逗号。

接下来,使用一个名为get_next_row()的特殊迭代器读取行,它返回一个指向包含字段值数组的行结构的指针:

// Structure for storing row data.
typedef struct {
  char *values[MAX_FIELDS];
} row_values;

在这种情况下,当get_next_row()返回一个有效的指针(不是NULL)时,您读取每个字段并打印出值。

你可能想知道MAX_FIELDS是什么。这是确保限制列(字段)数组的一种简单方法。这在MySQL_Cursor.h中定义,并设置为 32 ( 0x20)。如果你想节省几个字节,你可以把这个值改为更低的值,但是要注意:如果你超过了这个值,你的代码将会进入 wonkyville 10 (未引用指针)。所以小心行事。

还要注意对free_row_buffer()free_columns_buffer()的调用。这些是在读取列和行值时释放任何分配的内存所需的内存清理方法(嘿,你必须把它放在某个地方!).在处理完行之后,调用free_row_buffer(),在方法结束时调用free_columns_buffer()。如果您没有将这些添加到您自己的查询处理程序方法中,您将很快耗尽内存。

为什么是手动的?嗯,就像MAX_FIELDS设定一样,我想保持简单,因此尽可能节省空间。自动垃圾收集会增加大量代码。

您可以使用此方法作为模板来构建自己的自定义查询处理程序。例如,您可以在 LCD 上显示数据,或者在草图的另一部分使用信息,而不是将数据打印到串行监视器上。

作为练习,您可以更改库以显示条形和虚线输出(称为网格)。这并不特别困难,但是需要的代码不仅仅是几个字节(这也是我没有把它放在库中的原因)。如果您想知道如何为每个字段打印多少个破折号,请记住您首先阅读字段(包括每个字段的大小)。挑战在于打印条形和破折号,以便它们在显示区域中排成一行。

既然您已经更熟悉连接器/Arduino 库,让我们重新检查第六章中的 Arduino 传感器节点——但是这一次,您添加代码来将传感器数据保存到 MySQL 服务器。

Adjusting the Speed of Query Results

该库在wait_for_client()方法(在mysql.cpp中)中包含一个延迟,可以调整该延迟以提高返回查询结果的速度。它目前被设置为适度延迟。根据您的网络延迟和与数据库服务器的接近程度(如,没有网络跃点),您可以大大降低该值。最初添加它是为了帮助防止较慢的无线网络出现问题。

项目:构建 MySQL Arduino 客户端

在前面的章节中,您了解了什么是 Connector/Arduino,以及如何使用它让 Arduino MySQL 客户端更新 MySQL 数据库服务器中的表。在本节中,您将重温上一章中的传感器节点示例,并让它将数据保存到数据库,而不是串行监视器。在这种情况下,我们将使用 WiFi 屏蔽,因为这是将 Arduino 板连接到网络的一种更常见的方式。

因为所有使用连接器/Arduino 库的例子都是一样的,所以您的进度会更快。此外,为了进一步简化示例,您没有使用 XBee 模块,而是将传感器连接到 Arduino。让我们从硬件设置开始。

Note

我重复从第章到第章的步骤,以提供完整的解释和演练。我跳过了读取 DHT22 的代码细节,因为那部分是相同的。

硬件设置

该项目所需的硬件包括 Arduino、WiFi 屏蔽、DHT22 湿度和温度传感器、试验板、4.7K 欧姆电阻(颜色:黄色、紫色、红色、金色)和试验板跳线。除了 WiFi 盾之外,这与第六章的项目设置相同。

Tip

如果你被卡住了或者想要更多的信息,在 Adafruit 的网站上有一个极好的教程。 http://learn.adafruit.com/dht

首先将 Arduino 放在试验板旁边。如果您还没有这样做,请在您的 Arduino 上安装 WiFi 盾。在继续操作之前,请确保所有引脚都已固定在其插槽中。

将 DHT22 传感器插入试验板的一侧,如图 9-7 所示。请经常参考这一点,并在打开 Arduino 电源(或将其连接到笔记本电脑)之前仔细检查您的连接。你要避免电混沌理论中的意外实验。

img/313992_2_En_9_Fig7_HTML.jpg

图 9-7

DHT22 接线

接下来,将 Arduino 的电源连接到试验板。用一根跳线将 Arduino 上的 5V 引脚连接到试验板电源轨,用另一根跳线将 Arduino 上的接地(GND)引脚连接到试验板的接地轨。这些电线就位后,您就可以为传感器布线了。您使用四个引脚中的三个,如表 9-1 所示。

表 9-1

DHT22 连接

|

别针

|

连接到

|
| --- | --- |
| one | +5V |
| Two | Arduino 上的引脚 4,VCC 和数据引脚之间的 4.7K 欧姆电阻(强上拉) |
| three | 不连接 |
| four | 地面 |

Note

我们对 DHT22 使用引脚 4,因为一些 WiFi 屏蔽使用引脚 7。

软件设置

要将 DHT22 与 Arduino 配合使用,您需要拥有最新的 DHT22 库。您可以通过搜索库管理器直接从 Arduino IDE 安装库。打开 Arduino IDE,然后打开一个新的草图,从菜单中选择草图包含库管理库……。图 9-8 显示了库管理器。

img/313992_2_En_9_Fig8_HTML.jpg

图 9-8

图书馆经理

库管理器可能需要一些时间来连接到服务器并下载最新的目录。完成后,您可以在右上角的文本框中键入DHT22并按下ENTER。这将在库目录中搜索所有匹配的库。

从 Adafruit 选择 DHT 传感器库,点击Install。如果系统提示您安装支持库,请点击Install all以确保所有必备软件都已安装,如图 9-9 所示。

img/313992_2_En_9_Fig9_HTML.jpg

图 9-9

安装所有库

设置传感器数据库

您还需要在 MySQL 服务器上创建一个表。下面显示了创建测试数据库和表所需的 SQL 语句。使用 MySQL Shell 的mysql客户端来执行这些命令。

CREATE DATABASE dht22_test;
CREATE TABLE dht22_test.temp_humid (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `temp_c` float DEFAULT NULL,
  `rel_humid` float DEFAULT NULL,
  PRIMARY KEY (`id`)
);

现在您已经配置好了硬件并建立了数据库,让我们写一些代码吧!

Tip

务必将#include <WiFi.h>添加到MySQL_Packet.h文件中,以便与 WiFi 盾一起使用。

编写代码

代码与第六章的项目非常相似,除了您添加了连接 MySQL 服务器和插入传感器数据所需的代码。这个代码模块演示了一个温度和湿度传感器节点形式的基本数据收集节点。它使用普通的 DHT22 传感器,连接到带有 WiFi 屏蔽的 Arduino。

在这个项目中,我们还将看到连接器的两个最新特性:与指定的默认数据库连接,以及检索受影响的行和上次插入的 id。我们还将看到如何组织代码以使其更易于阅读的例子。 11

更具体地说,我们将读取传感器并将数据写入 MySQL 到它自己的名为read_data()的方法中。我们还将连接 MySQL 的连接代码移动到一个名为connect_to_mysql()的方法中。

我们将集中讨论 MySQL 部分的代码,而不是遍历代码的每一个细微差别。打开一个新草图,并将其命名为dht22_mysql.ino。或者,从图书网站下载示例代码。清单 9-7 显示了完整的源代码。

/*
  Beginning Sensor Networks Second Edition
  Example: Arduino Hosted Sensor Node

  This sensor node uses a DHT22 sensor to read temperature and humidity
  printing the results in the serial monitor.
*/
#include "DHT.h"
#include <WiFi.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

#define DHTPIN 4          // DHT22 data is on pin 4
#define read_delay 5000   // 5 seconds
#define DHTTYPE DHT22     // DHT22 (AM2302)

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(192,168,42,8);  // IP of the MySQL *server* here
char user[] = "arduino_user";      // MySQL user login username
char password[] = "secret";        // MySQL user login password

// Sample query
char INSERT_DATA[] = "INSERT INTO temp_humid (temp_c, rel_humid) VALUES (%s, %s)";
char DEFAULT_DATABASE = "dht22_test";
char ssid[] = "SSID";
char pass[] = "PASSWORD";

WiFiClient client;
MySQL_Connection conn((Client *)&client);

DHT dht(DHTPIN, DHTTYPE);

/*
 * Read the data from the sensor and save it in the database.
 */
void read_data() {
  char query_buf[128];
  char temp_str[20];
  char humidity_str[20];
  int rows_affected;
  int last_insert_id;

  // Read humidity
  float humidity = dht.readHumidity();
  // Read temperature as Celsius
  float temp_c = dht.readTemperature();

  // Check for errors and return if any variable has no value
  if (isnan(temp_c) || isnan(humidity)) {
    Serial.println("ERROR: Cannot read all data from DHT-22.");
    return;
  }

  // Convert values to strings for the string buffer
  dtostrf(temp_c, 7, 2, temp_str);
  dtostrf(humidity, 7, 2, humidity_str);
  sprintf(query_buf, INSERT_DATA, temp_str, humidity_str);

  Serial.print("Humidity: ");
  Serial.print(humidity);
  Serial.print("%, ");
  Serial.print(temp_c);
  Serial.print("C ... ");

  // Initiate the query class instance
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  // Execute the query
  int res = cur_mem->execute(query_buf);
  if (!res) {
    Serial.println("Query failed.");
  } else {
    Serial.println("Ok.");
  }
  // Get the last insert id and rows affected
  rows_affected = cur_mem->get_rows_affected();
  last_insert_id = cur_mem->get_last_insert_id();
  Serial.print(rows_affected);
  Serial.print(" rows affected, last insert id = ");
  Serial.println(last_insert_id);

  delete cur_mem;
}

/*
 * Connect to MySQL
 */
void connect_to_mysql() {
  // Now connect to MySQL
  Serial.println("Connecting...");
  if (conn.connect(server_addr, 3306, user, password, DEFAULT_DATABASE)) {
    delay(1000);
  } else {
    Serial.println("Connection failed.");
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect
  Serial.println("Welcome to the DHT-22 Arduino MySQL example!");
  // WiFi section
  Serial.println("Starting WiFi.");
  int status = WiFi.begin(ssid, pass);
  // if you're not connected, stop here:
  if (status != WL_CONNECTED) {
    Serial.println("Couldn't get a WiFi connection!");
    while(true);
  }
  // if you are connected, print out info about the connection:
  else {
    Serial.println("Connected to network");
    IPAddress ip = WiFi.localIP();
    Serial.print("My IP address is: ");
    Serial.println(ip);
  }
  dht.begin();
  Serial.println("Starting Sensor Data Collection:");
  connect_to_mysql();
}

void loop() {
  delay(read_delay);
  if (conn.connected()) {
    read_data();
  } else {
    conn.close();
    Serial.println("Retrying connection.");
    connect_to_mysql();
  }
}

Listing 9-7Reading a DHT22 Sensor

请注意,setup()方法的代码与前面的例子相同,只是在这个例子中,您连接到 WiFi,连接到 MySQL,设置 DHT 库,然后退出。将数据插入 MySQL 数据库的代码在read_data()方法中。让我们看看形成查询字符串的代码。为了清楚起见,我在这里重复代码作为摘录:

char INSERT_DATA[] = "INSERT INTO temp_humid (temp_c, rel_humid) VALUES (%s, %s)";
...
char query_buf[128];
char temp_str[20];
char humidity_str[20];
...
float humidity = dht.readHumidity();
float temp_c = dht.readTemperature();
...
// Convert values to strings for the string buffer
dtostrf(temp_c, 7, 2, temp_str);
dtostrf(humidity, 7, 2, humidity_str);
sprintf(query_buf, INSERT_DATA, temp_str, humidity_str);
...

Arduino 平台上有一个关于浮点到字符串转换的特性。您必须使用dtostrf()方法,通过字符缓冲区将浮点数转换成字符串。您可以在过程中指定大小和精度。然后,我们使用这些字符串从大小为 128 的静态缓冲区构建一个字符串,使用sprintf() 12 方法来格式化和填充表的值。

Caution

注意数组大小!如果您打算保存传感器节点返回的字符串数据,请确保您的查询适合内存。

如果您的网络与这里描述的不同,您可以相应地更改IPAddress变量。同样,如果您的用户和密码不同,也要确保更改这些值。最后,如果想降低采样率,可以相应调整read_delay

一旦你将所有的代码输入到你的 Arduino 环境中,并且你的 Arduino 已经准备好了,是时候进行测试了。如果有问题,请参考前面的常见错误部分。

测试执行

执行草图意味着将它上传到你的 Arduino 并观看它运行。如果您尚未连接 Arduino,现在就可以连接。我喜欢从画草图开始。单击 Arduino 应用程序左侧的复选标记,观察底部信息屏幕中的输出。如果您看到错误,请修复它们并重试编译。常见错误包括丢失 DHT22 库(这可能需要重新启动 Arduino 应用程序)、键入错误、语法错误等。一旦一切编译正确,你就可以点击工具栏上的上传按钮来上传你的草图了。

上传完成后,单击工具栏右侧的按钮打开串行监视器。观察 Arduino 连接到 MySQL 服务器,以及每次记录传感器数据时打印的消息。让它运行几次,以生成一些数据。清单 9-8 显示了您应该看到的典型输出。

Welcome to the DHT-22 Arduino MySQL example!
Starting WiFi.
Connected to network
My IP address is: 192.168.42.13
Connecting...
...trying...
Connected to server version 8.0.18
Deleting all rows from table ... Ok.
18 rows affected.
Starting Sensor Data Collection:
Humidity: 39.80%, 23.20C ... Ok.
1 rows affected, last insert id = 462
Humidity: 39.70%, 23.20C ... Ok.
1 rows affected, last insert id = 463
Humidity: 39.70%, 23.20C ... Ok.
1 rows affected, last insert id = 464
Humidity: 39.70%, 23.20C ... Ok.
1 rows affected, last insert id = 465
Humidity: 39.70%, 23.20C ... Ok.
1 rows affected, last insert id = 466
Humidity: 39.70%, 23.20C ... Ok.
1 rows affected, last insert id = 467

Listing 9-8Example Execution of the DHT22 MySQL Example

Tip

如果出现“确认超时”错误,请尝试拔下传感器插头再插回,或者在草图运行时断开并重新连接电源线。请务必小心避免 ESD!

一旦观看 Arduino 旋转的刺激结束,请通过断开 USB 电缆来停止 Arduino。现在,您可以检查数据库,以确保记录了传感器数据。清单 9-9 显示了所需的步骤。您在这里所做的只是在桌面上发出一个SELECT命令。每次 Arduino 记录数据时,您都会看到一行。

> select * from dht22_test.temp_humid;
+-----+--------+-----------+
| id  | temp_c | rel_humid |
+-----+--------+-----------+
| 535 |   23.5 |      39.6 |
| 536 |   23.5 |      39.6 |
| 537 |   23.5 |      39.6 |
| 538 |   23.5 |      39.6 |
| 539 |   23.5 |      39.6 |
| 540 |   23.5 |      39.6 |
| 541 |   23.5 |      39.6 |
| 542 |   23.5 |      39.7 |
| 543 |   23.5 |      39.6 |
| 544 |   23.5 |      39.6 |
| 545 |   23.5 |      39.6 |
| 546 |   23.5 |      39.6 |
| 547 |   23.5 |      39.6 |
| 548 |   23.5 |      39.6 |
+-----+--------+-----------+
14 rows in set (0.0004 sec)

Listing 9-9Example Data Collected

如果你看到类似的输出,恭喜你!您刚刚构建了第一个支持数据库的基于 Arduino 的传感器节点。这是构建传感器网络的重要一步,因为您现在已经拥有了构建更复杂的无线传感器节点和聚合节点所需的工具,可以将传感器数据插入数据库。

为了更多乐趣

一旦你对这个项目的测试和实验感到满意,如果你像我一样有一颗好奇的心,你可能会开始发现你可以做些什么来改进代码和项目。我在这里列出几个供你自己考虑。不要害怕调整和修改——这是使用 Arduino 的最大乐趣之一!

  • 更改代码以存储华氏温度。

  • 将采样率更改为每 15 分钟一次或每小时一次。

  • 更改表以添加新列,并使用触发器自动将温度转换为华氏温度。提示:ALTER TABLE dht22_test.temp_humid ADD COLUMN temp_f float AFTER temp_c

  • 对于专家:与其从 DHT22 中拆分数据并将两个值存储在数据库中,不如将原始值存储在数据库中并使用视图来拆分这些值。

你认为这是一种趋势吗?最后两点建议将一些逻辑从 Arduino 移到数据库中。这是一个非常好的实践,您应该通过学习更多关于 MySQL 服务器提供的视图、函数、触发器和事件等特性来磨练它。

因为 Arduino 平台是一个容量有限的小型设备,所以将数据操作转移到数据库服务器不仅节省了处理能力,还节省了内存使用。让数据库服务器完成繁重的数据转换工作也可以让您更频繁地读取传感器读数。

要了解关于触发器、视图和事件的更多信息,请参阅在线 MySQL 参考手册。

接下来的两个部分展示了一些如何在草图中使用连接器的例子。这些都不是完整的项目;相反,它们旨在作为使用连接器编写自己草图的模板。

项目示例:从变量插入数据

在编写连接器时,我在博客上发现了一些不熟悉 C 编程的人或一般编程新手的帖子。这太棒了,因为这意味着 Arduino 正在接触到它的一些目标受众!

不断出现的一个问题是如何执行一个从传感器提供值的INSERT查询,或者如何用存储在变量中的值构造一个INSERT语句。我们在前面的 DHT22 例子中通过使用字符串看到了这一点。但是如果你想插入整数呢?简单来说,你想添加的所有东西都必须转换成一个字符串。

诀窍是知道使用哪种格式说明符。例如,我们对字符串使用一个,对整数使用另一个,等等。下面列出了一些更常用的格式说明符。更多选项参见sprintf()文档。一个这样的位置是 www.tutorialspoint.com/c_standard_library/c_function_sprintf.htm

  • %c:字符

  • %i:整数

  • %s:一串字符

  • %x:十六进制数(基数 16)

现在,让我们看一段代码来做一些前面的格式化。清单 9-10 显示了一个示例草图,演示了如何获取一个值(可能是从传感器或类似设备读取的)并创建一个INSERT语句。这恰好显示了一个INSERT语句,但是该技术也适用于填充其他语句的WHERE子句。

/*
  Beginning Sensor Networks Second Edition
  Example: Formatting data for SQL statements.
*/
void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect
  Serial.println("Welcome to the SQL string format example!");
  // SQL command formatting string
  const char INSERT_DATA[] = "INSERT INTO test_arduino.temps VALUES (%s, %i, '0x%x')";
  // Buffers for strings
  char query[128];
  char temp_buff[10];
  // Some variables
  float float_val = 26.9;
  int int_val = 13;
  int hex_val = 251;
  // Convert float to string
  dtostrf(float_val, 1, 1, temp_buff);
  // Format the string
  sprintf(query, INSERT_DATA, temp_buff, int_val, hex_val);
  // Show the result
  Serial.println(query);
}

void loop() {
}

Listing 9-10SQL String Formatting

如果你想亲自尝试一下,你可以。只需打开一个新的草图,将代码放在setup()方法中并运行它。我在sql_format.ino中命名了这个例子。您应该会看到以下输出:

Welcome to the SQL string format example!
INSERT INTO test_arduino.temps VALUES (26.9, 13, '0xfb')

在这个例子中,我还向您展示了如何处理浮点值。回想一下,Arduino sprintf()方法不支持浮点值,所以您必须首先将浮点值转换为字符串,然后在您的sprintf()方法中使用该字符串。你用来转换浮点值的方法是dtostrf()

如果通读代码示例,您会看到这个新字符串是如何形成的。这个结果字符串可以发送到数据库,并将值插入到数据库中。

项目示例:如何执行选择查询

有时,您需要从数据库服务器中获取信息,用于计算或显示(或传输)标签。例如,假设您有一个传感器,需要使用依赖于其他数据的公式进行校准或转换。与其编写所有这些东西(可能有几十个或几百个),在这个过程中消耗大量内存,为什么不将这些信息存储在数据库表中,并在需要查找值时进行查询呢?

类似地,假设您想要在 LCD 上显示文本字符串,或者甚至在串行监视器上显示,但是这些字符串取决于正在读取的传感器。也就是说,传感器可能位于不同的位置。您可以通过将这些字符串放在一个表中并在需要时获取它们来节省空间,而不是对所有这些字符串进行编码,从而占用大量空间。

在本节中,我将演示几个示例,说明如何使用连接器从数据库返回数据。

Tip

该库包含许多用于查询数据库和使用草图中的数据的方法。如果您希望在串行监视器中看到数据,它还包括显示数据的有用方法。请注意,这段代码确实给编译后的草图增加了大约 2KB。根据 Arduino 的内存大小,如果您在草图中添加了多个查询,您可能会耗尽空间。有关减小草图大小的建议,请参见“我的草图太大”故障排除部分。

在串行监视器中显示结果集

如果您想运行一个查询并在串行监视器中显示结果,您可以使用内置方法show_results()。该方法打印以逗号分隔的列名,然后遍历结果集并打印以逗号分隔的值。

代码非常简单。您只需要调用execute(),向其传递查询字符串,然后调用show_results()方法。当然,串行监视器必须打开,您才能看到结果。下面显示了一个名为 show_data()的方法示例,该方法演示了该技术:

void show_data() {
  Serial.print("Getting all rows from the table ... ");

  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  // Execute the query
  int res = cur_mem->execute(SELECT_SQL);
  if (!res) {
    Serial.println("Query failed.");
  } else {
    Serial.println("Ok.");
  }
  cur_mem->show_results();
  delete cur_mem;
}

假设您执行了前面的 DHT22 项目,那么当您运行这段代码时,您将会得到与下面类似的结果。请注意,您可以使用 DHT22 代码作为剥离 DHT 特定部分的基础,或者您可以下载该书的示例代码并打开名为select_mysql.ino的草图。

Welcome to the MySQL SELECT example!
Starting WiFi.
Connected to network
My IP address is: 192.168.42.13
Connecting...
...trying...
Connected to server version 8.0.18
Getting all rows from the table:Ok.
id,temp_c,rel_humid
540,23.5,39.6
541,23.5,39.6
542,23.5,39.7
543,23.5,39.6
544,23.5,39.6
545,23.5,39.6
546,23.5,39.6
547,23.5,39.6
548,23.5,39.6
14 rows in result.

编写自己的显示方法

有些情况下,您可能希望构建自己的迭代器来读取查询的结果集。例如,您可能希望在 LCD 上显示结果,或者将结果发送到网络中的另一个节点。幸运的是,您可以通过使用大量助手方法编写自己版本的show_results()来做到这一点。我在上一节中讨论了如何使用show_results()方法,但是我讨论的是该方法中用于编写您自己的方法的方法。正如您将看到的,帮助器方法已经可供您使用,命名这些方法是为了更容易看到代码是如何工作的。

这些包括用于检索列名的get_columns()get_next_row(),是读取行的迭代器;内存清理方法free_columns_buffer()free_row_buffer()。在处理完该行的数据后调用free_row_buffer(方法,在读取完所有行后调用free_columns_buffer()方法。

清单 9-11 显示了先前show_data()方法的修改版本,显示了读取作为参数传递的查询、执行查询,然后读取列和行的所有步骤。我们会稍微修饰一下,让事情变得更有趣一些。该代码可以在custom_results.ino草图中的示例代码中找到。

/*
 * Custom show results example
 */
void show_data(char *query) {
  column_names *cols;
  int rows = 0;
  char buffer[24];

  Serial.print("Getting all rows from the table ... ");

  // Execute the query
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  int res = cur_mem->execute(query);
  if (!res) {
    Serial.println("Query failed.");
    return;
  } else {
    Serial.println("Ok.\n");
  }

  // Fetch the columns and print them
  cols = cur_mem->get_columns();
  for (int f = 0; f < cols->num_fields; f++) {
    sprintf(buffer, COL_FORMAT, cols->fields[f]->name);
    Serial.print(buffer);
  }
  Serial.println();

  // Print a separator
  for (int f = 0; f < cols->num_fields; f++) {
    Serial.print("----------  ");
  }
  Serial.println();

  // Read the rows and print them
  row_values *row = NULL;
  do {
    row = cur_mem->get_next_row();
    if (row != NULL) {
      for (int f = 0; f < cols->num_fields; f++) {
        sprintf(buffer, COL_FORMAT, row->values[f]);
        Serial.print(buffer);
      }
      Serial.println();
    }
  } while (row != NULL);

  delete cur_mem;
}

Listing 9-11Custom Query Results Method

请注意,您首先必须阅读这些列。这是因为 MySQL 总是在发送任何行之前发送列名。一旦读取了列,就可以使用 iterator helper 方法读取行,直到没有行返回为止。

get_columns()方法返回一个指向特殊结构的指针,该结构包含字段的数量和一个字段数组,该数组也是一个特殊结构。下面显示了这两种结构;你可以在清单 9-11 中看到它们的用法。

// Structure for retrieving a field (minimal implementation).
typedef struct {
  char *db;
  char *table;
  char *name;
} field_struct;

// Structure for storing result set metadata.
typedef struct {
  int num_fields; // actual number of fields
  field_struct *fields[MAX_FIELDS];
} column_names;

方法get_next_row()返回一个指针,指向一个包含字符串数组的类似结构。这是因为从服务器返回的所有数据(行)都是作为字符串返回的。如果需要,您可以将这些值转换为其他数据类型。

下面是第二个结构:

// Structure for storing row data.
typedef struct {
  char *values[MAX_FIELDS];
} row_values;

您可能想知道为什么您必须进行内存清理。简单地说,为了使连接器尽可能轻便,一些方便的例程被有意省略了。一个典型的例子是在读取列和行数据期间清除(释放)分配的内存。前面的示例显示了这些调用的正确位置。

Caution

如果没有像图中所示的那样释放内存,将会导致草图的执行速度迅速下降,并最终冻结或挂起。

一旦创建了这样的方法,就可以在草图的其他地方使用它来执行和处理查询结果,如下所示:

const char TEST_SELECT_QUERY[] = "SELECT * FROM world.city LIMIT 10";
show_data(TEST_SELECT_QUERY);

如果您计划编写一个这样的方法来将数据发送到其他地方,请注意您使用的代码量,并消除任何不必要的字符串和转换(浮点转换需要一个名为dtostf()的库,它会给编译后的草图增加 2KB)。

如果您在 DHT22 示例之后执行此代码,您将在串行监视器中看到类似于以下代码的输出:

Welcome to the MySQL Custom SELECT example!
Starting WiFi.
Connected to network
My IP address is: 192.168.42.13
Connecting...
...trying...
Connected to server version 8.0.18
Getting all rows from the table ... Ok.

        id      temp_c   rel_humid
----------  ----------  ----------
       540        23.5        39.6
       541        23.5        39.6
       542        23.5        39.7
       543        23.5        39.6
       544        23.5        39.6
       545        23.5        39.6
       546        23.5        39.6
       547        23.5        39.6
       548        23.5        39.6

示例:从数据库获取查找值

尽管前面的示例向您展示了如何处理多行结果集以显示大量数据,但查询数据库更常见的原因是返回一个特定值或一组值以在草图中使用。通常,这是通过使用旨在返回单行的查询来完成的。例如,它可以从查找表中返回特定的值。

与前面的示例一样,您必须从列数据开始按顺序处理结果集。这种类型的查询不需要它,所以可以忽略它。您还需要对行进行迭代,因为结果集以一个特殊的包终止,并且get_next_row()方法读取该包,如果遇到它就返回NUL L(表示没有更多的行)。清单 9-12 显示了从数据库中读取并使用单个值所需的代码。如果这个例子将被多次调用或者从你的草图中的几个地方被调用,那么它可以被做成一个单独的方法。

int get_data() {
  row_values *row = NULL;
  long head_count = 0;

  // Initiate the query class instance
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  // Execute the query
  cur_mem->execute(SELECT_SQL);
  // Fetch the columns (required) but we don't use them.
  column_names *columns = cur_mem->get_columns();

  // Read the row (we are only expecting the one)
  do {
    row = cur_mem->get_next_row();
    if (row != NULL) {
      head_count = atol(row->values[0]);
    }
  } while (row != NULL);
  // Deleting the cursor also frees up memory used
  delete cur_mem;

  return head_count;
}
...
void setup() {
...
  connect_to_mysql();
  int count = get_data();
  // Show the result
  Serial.print("NYC pop = ");
  Serial.println(count);
}

Listing 9-12Getting a Lookup Value

如您所见,该库支持处理返回结果集的查询的能力。这些命令包括SELECTSHOW以及类似的命令。

如果您执行此示例,您应该会看到以下输出或类似内容:

Welcome to the MySQL lookup table example!
Starting WiFi.
Connected to network
My IP address is: 192.168.42.13
Connecting...
...trying...
Connected to server version 8.0.18
NYC pop = 12886

然而,请注意(再次)Arduino 平台的可用内存量非常有限。用几个返回大型结果集的复杂查询构建草图可能会耗尽 Arduino 板上的内存,比如 Uno 和 Leonardo。如果你的草图很大,你可能要考虑移动到 Due 板。

部件购物清单

完成本章中的项目需要一些组件。所有这些组件都在前面的章节中使用过。它们列于表 9-2 中。

表 9-2

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| Arduino Uno,莱昂纳多(任何支持盾牌的) | 各种各样的 | 25 美元及以上 | one |
| 以太网盾 arduino | www.sparkfun.com/products | 24.95 美元及以上 | one |
| Arduino WiFi 盾 | www.sparkfun.com/products/13287 | $16.95 | one |
| 试验板 | www.sparkfun.com/products/9567 | $5.95 | one |
| 试验板跳线 | www.sparkfun.com/products/8431 | $3.95 | one |
| DHT22 | www.sparkfun.com/products/10167 | $9.95 | one |
| www.adafruit.com/products/385 |
| 150 欧姆电阻器 | 各种各样的 | 变化 | one |

摘要

使用连接器/Arduino 库,您可以使您的传感器节点更加复杂。通过使您的传感器节点能够将数据保存在 MySQL 数据库中,您还可以通过使数据更容易访问并存储在非常可靠的位置(数据库服务器)来增强您的监控解决方案。

在本章中,您了解了如何编写支持数据库的 Arduino 草图,并详细浏览了连接器/Arduino 库。有了这些知识,你就可以开始创建一个真正的传感器网络了。在下一章中,您将使用前几章中积累的知识来创建您的第一个传感器网络,该网络包含 MySQL 数据库服务器、Arduino 聚合节点和无线传感器节点。

十、构建您的网络:Arduino 无线聚合器 + 无线传感器节点 + Raspberry Pi 服务器

根据目前为止你在书中学到的信息,尤其是第八章和第九章中的信息,是时候把它们放在一起,用 MySQL 数据库服务器构建你的第一个传感器网络了。

在本章中,您将所有组件放在一起,构建一个工作传感器网络,该网络以您的 Raspberry Pi MySQL 服务器作为数据存储库,一个数据聚合节点(您可以看到 Arduino 和 Raspberry Pi 的示例),以及许多通过 XBee 模块连接的传感器节点。这些是您在前面章节中构建的构建模块,现在结合起来演示如何构建低成本传感器网络。

数据聚合节点

回想一下,数据聚合器是一个特殊的节点,用于接收来自多个来源(传感器)的信息并存储结果。源数据可以来自节点本身上的多个传感器,但更常见的是,数据聚合节点从不直接连接到聚合节点的多个传感器节点接收信息(它们通过 XBee 模块连接)。

大多数情况下,这些传感器由其他节点托管并放置在其他位置,并且数据聚集节点通过有线或无线连接连接到传感器节点。例如,您可能在一个位置将一个传感器托管在低功耗 Arduino 上,而在另一个位置将另一个传感器托管在 Raspberry Pi 上,这两个传感器都使用 XBee 模块连接到您的数据聚合节点。除了所选网络介质的限制之外,您可以让几十个节点向一个数据聚合节点提供传感器数据。

使用数据聚集节点有几个优点。如果您使用无线技术,如带有 XBee 模块的 ZigBee,数据聚合节点可以让您通过将数据聚合节点放置在离传感器最近的位置来扩展网络范围。然后,数据聚集节点可以通过更可靠的介质将数据传输到另一个节点,例如数据库服务器。

例如,您可能希望在有电源和以太网连接的外屋放置一个数据聚合节点,以从位于其他各种建筑物中的远程传感器节点收集数据。一个要考虑的情况是监控一个或多个房间甚至外部仓库的温度。这些建筑可能有电,也可能没有电,但很可能没有以太网线路。因此,数据聚合节点可以放置在最近的有电源和以太网端口的建筑物中。

Note

在这种情况下,我指的是距离传感器节点最近的点,它仍然在无线传输介质(如 XBee)的范围内。

数据聚合节点还允许您将处理一组传感器的逻辑移动到一个更强大的节点。例如,如果您使用需要代码来处理值的传感器(如 TMP36),您可以使用数据聚合节点来接收来自这些传感器的原始数据,存储这些数据,并在以后计算这些值。这不仅确保了您只在一个位置拥有代码,而且还允许您为远程传感器使用不太复杂(功能不太强大)的主机。也就是说,您可以为传感器使用更便宜或更旧的 Arduino 板,为数据聚合节点使用更强大的 Arduino 板。这样做还有一个额外的好处,就是如果一个远程传感器损坏了,更换起来并不昂贵。

还记得,您必须决定要将传感器数据存储在哪里。数据聚合节点可以将数据本地存储在可移动介质或机载存储设备上(本地存储),也可以将数据传输到另一个节点进行存储(远程存储)。选择使用哪一个通常基于数据将如何被消费或查看。

例如,如果您只想存储从传感器读取的最后一个值,您可能需要考虑某种形式的可视化显示或远程访问机制。在这种情况下,使用仅存储最新值的本地存储可能更具成本效益且不太复杂。

另一方面,如果您需要记录一段时间内的数据值以供以后处理,您应该考虑将数据存储在另一个节点上,以便可以在不影响传感器网络的情况下访问数据。也就是说,您可以将数据存储在更强大的系统上(比如个人计算机、服务器或基于云的服务),并进一步降低聚合节点发生故障时丢失数据的风险。

以下部分基于前几章中的示例,探讨了每种形式的数据聚合器的示例。我将这些部分保持简短,以提供一个参考框架,并帮助您为本章后面讨论的项目积累知识。

本地存储数据聚合器

本地存储数据聚合器是一种节点,它被设计成从一个或多个传感器或传感器节点接收传感器数据,并将数据存储在内置于或连接到该节点的设备上。回想一下,对于基于 Arduino 的节点,这通常是通过 Arduino 以太网屏蔽或另一个 SD 卡屏蔽的 EEPROM(内存)或 SD 驱动器。回想一下,对于 Raspberry Pi,这可能是通过通用输入/输出(GPIO)引脚连接的 SD 引导驱动器、USB 驱动器或 EEPROM。

本地存储的性质是使用本地存储数据聚合节点的一个限制因素。也就是说,如果您想在以后处理数据,您可以选择一种介质,允许您检索数据并将其移动到另一台计算机上。如第七章所述,EEPROM 是一个不太可能的选择,因为它的易失性和连接到个人电脑的困难性。这使得 SD 卡或可移动驱动器成为唯一合理的替代方案。但是,如果传感器数据主要用于显示数据,您可以使用 EEPROM 来存储最新值或值的简短列表,以便按需显示。

这并不意味着本地存储数据聚合器是一个无用的概念。让我们考虑这样一种情况,您想要监控几个附属建筑的温度。您没有将数据用于任何分析,而只是希望能够在方便时(或需要时)读取这些值。

一个可能的解决方案是设计带有可视化显示的本地存储数据聚集节点。例如,您可以使用 LCD 来显示传感器数据。当然,这意味着数据聚集节点必须位于一个您可以轻松到达的位置。

但是,让我们考虑这样一种情况,您的数据聚合节点也位于远程位置。也许它也在另一个外屋,但是你大部分时间都在不同的地方度过。在这种情况下,远程访问解决方案是最好的。

幸运的是,您只需做很少的工作就可以提供这样的机制。考虑一下 Arduino 的以太网库。有一些示例草图向您展示了如何在 Arduino 上托管一个轻量级 web 服务器。如果您只想从远程位置访问传感器数据进行查看,web 服务器是理想的解决方案。您将浏览器指向数据聚合节点并查看数据。

这种数据聚合节点的设计需要在本地存储最新的值,比如说,在内存或 EEPROM 中,并且当客户端连接时,显示数据。对于本地存储数据聚合节点来说,这是一个简单而优雅的解决方案。下面的项目演示了这些技术。

项目:具有本地存储的数据聚合节点

如果您还没有从以前的项目中构建组件,或者在让一个或多个组件工作时遇到了问题,那么您可能想要返回并重温那些章节。我讨论了所需的每个组件,但没有达到前几章中的详细程度。如果您发现需要复习某些组件,请参考引用的章节。

综上所述,是时候用本地存储数据聚合节点来构建您的第一个传感器网络了。精明的读者会意识到,你已经在前几章中构建了所有传感器组件的示例。新的是本地存储的选择和显示数据的机制。

在这个项目中,您将构建一个数据聚合节点,它可以通过以太网访问,并支持一个轻量级 web 服务器来显示从多个传感器中的每一个读取的最新值。传感器节点通过 XBee 模块与数据聚合节点联网。除了 web 服务器部分和使用板载 EEPROM 存储数据的选择之外,数据聚合节点的代码与您在以前的项目中使用的代码相似。

五金器具

这个项目的硬件由几个基于 XBee 的温度传感器节点组成,这些节点与一个基于 Arduino 的节点通信,该节点将成为您的数据聚合节点。我在第 2 和第四章讨论了 XBee 模块,在第四章讨论了 XBee 温度传感器节点。

数据聚集节点

因为您想要使用 web 服务器,所以数据聚合节点需要 Arduino 以太网屏蔽和 XBee 屏蔽(或等效物)。如果您在 Arduino 上使用两个屏蔽,您可能需要使用一个可堆叠的接头套件( www.sparkfun.com/products/11417 ),以确保以太网屏蔽不会阻止 XBee 屏蔽的针脚正确就位。

图 10-1 显示了我在项目中使用的一种数据聚集节点。Arduino 屏蔽安装在 Arduino 板上,接头用于提高连接的高度,以便 XBee 屏蔽可以安全安装。尽管这使得电路板堆得相当高,但它仍然是一种紧凑的形式。

img/313992_2_En_10_Fig1_HTML.jpg

图 10-1

基于 Arduino 的数据聚合节点

如果您使用的 XBee 模块具有片上天线或其他形式的不从 XBee 模块顶部突出的天线,您可以先将 XBee 屏蔽放在 Arduino 上,然后将以太网屏蔽放在顶部。在这种情况下,您不需要额外的竖板。

可堆叠的接头套件是一个方便的附件,因为它允许您提高防护罩的高度,以便您可以访问或在某些情况下查看 Arduino 板上的组件(如 led 和一些防护罩通用的各种按钮或开关)。您可以在 SparkFun、Adafruit 和大多数供应商那里找到可堆叠的接头套件,这些供应商都有 Arduino 板和屏蔽板。

您还需要一种为数据聚合节点供电的方法。如果您计划将该项目作为实验来执行,并让节点通过 USB 电缆连接到您的笔记本电脑,那么您就可以了,不需要更多。但是如果您计划部署节点,您需要通过典型的壁式电源为 Arduino 供电。9V 电源应该足够了,或者您可以使用通过筒式连接器连接的 9V 电池。图 10-2 显示了 SparkFun 的壁式电源。图 10-3 显示了来自 SparkFun 的 9V 电池托架。

img/313992_2_En_10_Fig3_HTML.jpg

图 10-3

9V 电池托架(SparkFun 提供)

img/313992_2_En_10_Fig2_HTML.jpg

图 10-2

壁式电源(SparkFun 提供)

确保为数据聚合节点使用配置了协调器 API 固件的 XBee 模块。请参阅第 2 和 4 章,了解如何配置 XBee 模块的详细信息。

传感器节点

回想一下第四章,XBee 传感器节点的硬件包括一个试验板、一些跳线、一个试验板电源、一个电源(典型的 5–9V 壁式电源适配器即可)、一个 TMP36 温度传感器和一个 0.10uF 电容。您还需要一个 XBee 分线板,带有插头(试验板的间距为 0.1 英寸),就像 Adafruit 或 SparkFun 提供的那种。温度传感器节点可以通过 9V 电池供电,如果有电源,也可以通过 9V 壁式电源供电。

为了方便起见,我在图 10-4 中重复了第四章的接线图。您需要构建至少两个这样的温度传感器节点,但是三个会是更好的测试项目。

img/313992_2_En_10_Fig4_HTML.jpg

图 10-4

TMP 传感器节点接线

Note

有可能使用较低电压的电源。有关更多信息,请参考试验板电源的文档。

确保为每个传感器节点使用配置有终端设备路由器 API 固件的 XBee 模块。请参考第 2 和 4 章,了解如何配置 XBee 模块的详细信息。每个传感器节点的设置在表 10-1 中重复。

表 10-1

XBee 传感器节点选项和值

|

密码

|

设置名称

|

描述

|

价值

|
| --- | --- | --- | --- |
| D3 | a3/上帝 3 | 触发模拟或数字数据记录 | 2—ADC |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 红外的 | 输入输出采样率 | 等待发送数据的时间 | 3a 98—15000 毫秒 |
| 镍 | 节点标识符 | 节点的名称 | TMP36_* |
| V+ | 电源电压阈值 | 电源电压 | FFFF(总是发送) |

Note

您应该给每个 XBee 一个惟一的节点 id,比如TMP36_1TMP36_2TMP36_3等等。这对于本章后面的 Raspberry Pi 示例是必不可少的。

回想一下,连接到 Arduino 的 XBee 模块是使用表 10-2 中所示的以下设置的协调器角色。

表 10-2

XBee 协调器选项和值

|

代码

|

设定名称

|

描述

|

|
| --- | --- | --- | --- |
| 身份 | 平移 ID | 网络的 Id | Eight thousand and eighty-eight |
| 镍 | 节点标识符 | 节点的名称 | 协调者 |

继续构建您的温度传感器节点。将它们连接起来,并仔细检查试验板电源的电源设置。确保将其设置为 3.3V。一旦您连接好所有温度传感器节点,请不要给它们通电。在给传感器节点加电之前,您需要等待,直到您完成草图的编写并上传到数据聚合节点。我将在下一节讨论草图。

Tip

当您在处理这个项目时——实际上,最初几次启动网络时——您应该在同一位置测试所有内容。例如,在同一个表或工作台上设置传感器节点和数据聚集节点,并调试网络,直到一切正常工作。只有这样,您才能安全地将传感器节点部署到它们的远程位置。 1

软件

这个项目的软件不需要任何额外的特殊库或类似的下载,这些你在前面的章节中已经加载过了。我先给出草图的概述,然后更详细地讨论新的部分。为了简洁起见,我跳过了以前项目中使用的一些代码。请参考清单 10-4 中的完整代码了解更多细节,并确保您了解代码如何工作。

概观

您使用的草图结合了 Arduino IDE 中的 web 服务器示例以及以前项目中的代码。你可以稍微重新安排一下,因为向串行监视器写入数据的需求减少了。事实上,您不需要向串行监视器写入任何内容。但是出于调试的目的,您确实留下了一些语句。

如果你还没有在 Arduino IDE 中试验过 web 服务器示例(参见文件示例以太网文件示例WiFi ),如果你觉得这个草图的代码很有挑战性(或者如果你只是想在 Arduino 及其以太网或 WiFi 盾上找点乐子),你可能会想这么做。

Tip

如果草图不像您预期的那样工作,请考虑添加额外的打印语句,以便将调试信息打印到串行监视器。这是编写和调试大型或复杂草图的一种非常常见的做法。

这个草图的 web 服务器部分并不复杂,也不难理解。本质上,您使用EthernetServer库来监听连接;一旦建立了连接,您就可以通过一个EthernetClient实例将 HTML 代码写回客户端。

或者,如果你决定使用 WiFi 盾,你使用WiFiServer库监听连接;一旦建立了连接,您就可以通过一个WiFiClient实例将 HTML 代码写回客户端。在这一部分,我们将探索该草图的 WiFi 版本。

如前所述,您存储了每个传感器节点的最新值,以便在 web 服务器上显示。如第七章所述,使用 Arduino 时存储这些值会出现问题。您的选择仅限于使用 Arduino Ethernet shield 上的 SD 驱动器或板载 EEPROM。

虽然你在第七章中试验了外部 EEPROM,但为了简单起见,你使用了板载 EEPROM。板载 EEPROM 的大小因 Arduino 板的选择而异,但在大多数情况下,它足以为每个传感器节点存储十几个字节。

要使用板载 EEPROM,只需在草图中包含EEPROM.h文件。读取和写入板载 EEPROM 非常容易,一次只能读取一个字节。您调用EEPROM.read(),传入您想要读取的字节的地址。写入 EEPROM 类似。你调用EEPROM.write(),传入你想要存储字节的地址和你想要存储的数据(字节)的值。使用这个库的例子可以在 Arduino IDE 中找到。

现在让我们进入代码!以下部分介绍了草图的主要组件—web 服务器、本地存储和从传感器节点读取数据。我省略了处理传感器数据的代码,因为这是第四章中代码的直接复制,为了简洁,我跳过了一些普通的操作。在 Arduino IDE 中打开一个新的草图,并将其命名为Arduino_Web_Aggregate.ino

轻量级 Web 服务器

轻量级 web 服务器的代码取自 Arduino IDE 中的一个示例。您可以通过将向客户端发送数据的代码移动到一个单独的函数中来修改该示例。

要构建 web 服务器,首先必须包含正确的库并声明一些变量。以下摘录显示了所需的代码(为了清楚起见,省略了其他组件的代码):

#include <WiFi.h>
...
byte mac[] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
IPAddress ip(10, 0, 1, 111);

// Start Ethernet Server class with port 80 (default for HTTP)
WiFiServer server(80);
...

请注意,您包括了 WiFi(或以太网)和 SPI 头。您还声明了两个变量:一个 MAC 地址和一个 IP 地址。在以前的项目中,您允许以太网库使用 DHCP 来分配 IP 地址;但是在这种情况下,您需要知道 IP 地址,因此您必须使用静态 IP 地址。请确保选择一个对您的数据聚合节点将连接到的网段有效的地址。

最后,初始化WiFiServer类的一个实例,将它传递到端口 80(这是 HTTP 服务的默认端口)。您可以选择另一个端口号,但可能需要将其添加到您的 URL 中才能访问 web 服务器。例如,如果你选择3303,你将使用http://10.0.1.111:3303

现在真正有趣的部分来了。您构建的 web 服务器是一个简化的服务,它只向客户端返回少量的 HTTP 代码。除了包含文件和变量,您还需要在setup()方法中初始化以太网类。下面的代码显示了需要什么。本质上,首先初始化串行库,然后是 WiFi 库(为简洁起见省略),最后是服务器实例。请参考第 6 、 7 和 9 章中的 WiFi 示例,了解有关设置 WiFi 部分的更多详细信息。

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect

  // WiFi section
...
  server.begin();
...
}

为了让 web 服务器在客户端连接时做出响应,您向草图中添加了一个名为listener()的新方法。下面的代码显示了从loop()方法调用该方法的位置。在这种情况下,首先检查传感器的响应;如果没有可用的传感器数据,您将检查客户端是否已连接并响应呼叫:

void loop() {
  if [...]

  } else {
    // Listen for client and respond.
    listener();
  }
}

至于监听器部分,您需要做的是检查客户端是否已经通过server.available()方法连接。这个方法的返回是一个EthernetClient类的实例。如果变量不为空(一个客户端已经连接),那么通过client.available()方法检查客户端是否可用。如果是这样,就为每个请求的响应发送数据,直到检测到换行符。

首先通过client.print()client.println()方法发送 HTTP 头。您还可以发送欢迎用户的横幅。如果传感器数据存储在本地,则发送每个存储的传感器节点的数据(通过一个循环);否则,您会发送一个状态横幅,声明没有数据。

传感器数据的发送是通过名为send_sensor_data()的新方法进行的。该方法使用客户端实例的client.print()client.println()方法 2 以文本形式写入数据,并为每个数据块写入结束 HTTP 标记。在这种情况下,您发送传感器节点的地址、摄氏温度、华氏温度以及来自传感器节点的参考电压。

清单 10-1 显示了监听客户端并发送响应所需的代码。为了简洁起见,我展示了发送数据的客户端代码的摘录:setup()loop()代码。所需的完整代码请参考清单 10-4 。

...
void send_sensor_data(EthernetClient *client, int num_sample) {
  unsigned int address;
  float temp_c;
  float temp_f;
  float volts;

  // Read sensor data from memory and display it.
  read_sample(num_sample, &address, &temp_c, &temp_f, &volts);

  client->println("<br />");
  client->print("Node Address: ");
  client->print(address, HEX);
  client->print(".");
...
}

void listener() {
  // listen for incoming clients
  EthernetClient client = server.available();
  if (client) {
    Serial.println("Got a connection!");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println();

          // Print header
          client.println("Welcome to the Arduino Data Aggregate Node!");
          client.println("<br />");

          // read sensor data
          byte num_samples = EEPROM.read(0);
          for (int i = 0; i < num_samples; i++) {
            send_sensor_data(&client, i);
          }
          // if no data, say so!
          if (num_samples == 0) {
            client.print("No samples to display.");
            client.println("<br />");
          }
          break;
        }
        if (c == '\n') {
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(1);
    // close the connection:
    client.stop();
  }
}
...

Listing 10-1The Web Server Code

既然您已经理解了 web 服务器组件是如何工作的,那么让我们来研究一下本地存储组件。

使用板载 EEPROM 的本地存储

本地存储组件使用板载 EEPROM 来存储和检索传感器数据。在概述部分,我讨论了使用这个库有多容易。在这一节中,我将讨论如何存储和检索传感器数据的细节。

因为您只存储来自每个传感器节点的最新值(样本),并且您可能有多个传感器节点进行通信,所以您需要一种简单的机制来组织数据。你可以使用类似于第七章的外部 EEPROM 项目。

EEPROM 存储器的第一个字节(地址 0)用于存储当前样本数和每个样本的 10 字节块。不是存储每个传感器节点的整个 64 位地址(XBee 64 位网络地址),而是存储最后 2 个字节,当转换为十六进制并显示为文本时,将显示 4 个十六进制数字。 3 你也只存储原始传感器数据,它是一个浮点型(4 字节),和参考电压,它也是一个浮点型(4 字节)。因此,你需要 10 个字节来存储一个样本。

因为您只存储来自传感器的原始数据,所以您必须稍后像在第四章中一样执行温度计算。我把这个留给你在清单 10-4 中探索。

您还可以向setup()方法添加代码,以在初始启动时初始化 EEPROM。在这种情况下,向地址 0 写入 0 意味着没有存储样本。这确保您可以简单地通过重置(或关闭然后打开)数据聚合节点来从头重新启动。如果您发现您需要使值持久化,那么在setup()方法至少执行一次之后,从该方法中取出下面的代码:

void setup() {
 ...
  // Initialize the EEPROM
  EEPROM.write(0, 0);
}

为了使事情变得简单一点,我们创建了四种读写 EEPROM 的新方法。清单 10-2 显示了完整的方法。请注意,您有两组方法,一组用于整数(2 字节),一组用于浮点变量(4 字节)。

...
// Read an integer from EEPROM
int read_int(byte position) {
  int value = 0;
  byte* p = (byte*)(void*)&value;
  for (int i = 0; i < sizeof(value); i++)
      *p++ = EEPROM.read(position++);
  return value;
}

// Read a float from EEPROM
float read_float(byte position) {
  float value = 0;
  byte* p = (byte*)(void*)&value;
  for (int i = 0; i < sizeof(value); i++)
      *p++ = EEPROM.read(position++);
  return value;
}

// Write an integer to EEPROM
void write_int(byte position, int value) {
  const byte *p = (const byte *)(const void *)&value;
  for (int i = 0; i < sizeof(value); i++)
      EEPROM.write(position++, *p++);
}

// Write a float to EEPROM
void write_float(byte position, float value) {
  const byte *p = (const byte *)(const void *)&value;
  for (int i = 0; i < sizeof(value); i++)
      EEPROM.write(position++, *p++);
}
...

Listing 10-2EEPROM Helper Methods

请注意,在代码中,您使用了一些指针技巧来将整数转换为字节数组。这对于高级 C 和 C++程序员编写的代码来说并不少见。虽然可以使用其他方法(比如移位字节)将值分解成字节,但是我想用这种高级技术来让您思考指针是如何工作的。在更复杂的草图中,你很可能会遇到以类似方式操作指针的情况。

Tip

对于那些不能放任自流的人(你知道你是谁),毫无疑问你可以在这里看到一些优化的空间。请注意,这些方法非常相似。唯一真正改变的是类型。那么,如何进一步优化这段代码呢?提示:考虑一个模板 4 ( http://playground.arduino.cc/Code/EEPROMWriteAnything )。

现在,您已经了解了如何在 EEPROM 中存储和检索样本,接下来让我们看看这是如何融入从传感器节点读取数据的代码中的。

通过 XBee 从传感器节点读取数据

从多个 XBee 模块读取数据的代码与第四章中的项目相同。事实上,您为该草图编写的代码能够并且确实支持多个传感器节点的连接。回想一下,这是可能的,因为您的数据聚合节点使用配置为协调器的 XBee 模块,而您的传感器节点使用配置为路由器的 XBee 模块。

因为初始化代码与第四章中的项目没有变化,所以我在这里省略了那些细节。但是用于存储传感器数据的代码是不同的。在这种情况下,您需要将样本存储在存储器(EEPROM)中。因为您只想存储最新的值,所以必须首先通过地址在内存中查找样本。如果找到匹配项,将数据保存在相同的位置。如果在存储的样本中找不到匹配,则将其添加到末尾,并增加存储的样本数。你把这个方法和之前一样命名为record_sample()

但你不会就此结束。您还需要读取和写入示例数据的方法。您将这些方法分解成单独的方法,这样就可以使record_sample()方法更小,更容易阅读。因此,您创建了read_sample()write_sample()方法,它们使用前面描述的 EEPROM 辅助方法来存储和检索样本。

清单 10-3 显示了存储传感器数据的新代码的主要部分和loop()方法的完整代码。注意它是如何排除了listener()调用的。

// Read a sample from EEPROM
void read_sample(byte index, unsigned int *address, float *temp_f,
                 float *temp_c, float *volts) {
  float temp;
  byte position =  (index * bytes_per_sample) + 1;

  *address = read_int(position);

  temp = read_float(position + 2);

  *temp_c = ((temp * 1200.0 / 1024.0) - 500.0) / 10.0;
  *temp_f = ((*temp_c * 9.0)/5.0) + 32.0;

  *volts = read_float(position + 6);
}

// Write sample to EEPROM
void write_sample(byte index) {
  byte position =  (index * bytes_per_sample) + 1;

  write_int(position, address);
  write_float(position + 2, temperature);
  write_float(position + 6, voltage);
}

void record_sample(ZBRxIoSampleResponse *ioSample) {
  int saved_addr;

  // Get sample data from XBee
  get_sample_data(ioSample);

  // See if already in memory. If not, add it.
  byte num_samples = EEPROM.read(0);
  boolean found = false;
  for (byte i = 0; i < num_samples; i++) {
    byte position = (i * bytes_per_sample) + 1;

    // get address
    saved_addr = read_int(position);
    if (saved_addr == address) {
      write_sample(i);
      found = true;
    }
  }
  if (!found) {
    // Save sample
    write_sample(num_samples);

    // Update number of sensors
    num_samples++;
    EEPROM.write(0, num_samples);
  }
}

...

void loop() {
  //attempt to read a packet
  xbee.readPacket();

  if (xbee.getResponse().isAvailable()) {
    // got something

    if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {

      // Get the packet
      xbee.getResponse().getZBRxIoSampleResponse(ioSample);

      // Get and store the data locally (in memory)
      record_sample(&ioSample);
    }
    else {
      Serial.print("Expected I/O Sample, but got ");
      Serial.print(xbee.getResponse().getApiId(), HEX);
    }
  } else if (xbee.getResponse().isError()) {
    Serial.print("Error reading packet.  Error code: ");
    Serial.println(xbee.getResponse().getErrorCode());
  } else {
    // Listen for client and respond.
    listener();
  }
}

Listing 10-3Reading from XBee Sensor Nodes

对该草图的新组件的讨论到此结束。以下部分包括整个草图,所有这些组件都在适当的上下文中。一定要花时间通读代码。这是迄今为止你在这本书里用过的最大的草图(代码)。

把这一切放在一起

现在您已经了解了草图主要组件的工作原理,让我们更详细地检查完成的草图。清单 10-4 显示了草图的完整代码。回想一下,我们将该文件命名为Arduino_Web_Aggregate.ino

Tip

如果您使用的是 Leonardo,请查看软件序列号( www.arduino.cc/en/Reference/SoftwareSerial )代码上关于 XBee 保护罩引脚的注释。根据你的列奥纳多使用的盾,你可能需要改变这些。

/**
  Beginning Sensor Networks Second Edition
  Example Arduino Data Aggregate Node

  This project demonstrates how to receive sensor data from
  multiple XBee sensor nodes, save the samples in the onboard
  EEPROM and present them as a web page. It uses an Arduino
  with an XBee shield with an XBee coordinator installed.

  Note: This sketch was adapted from the examples in the XBee
  library created by Andrew Rapp.
*/

#include <XBee.h>
#include <SoftwareSerial.h>
#include <WiFi.h>
#include <EEPROM.h>

byte bytes_per_sample = 10; // address (2), temp (4), volts (4)

// Setup pin definitions for XBee shield
uint8_t recv = 8;
uint8_t trans = 9;
SoftwareSerial soft_serial(recv, trans);

// assign a MAC address and IP address for the Arduino
byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(192,168,42,8);  // IP of the MySQL *server* here
char user[] = "arduino_user";            // MySQL user login username
char password[] = "secret";              // MySQL user login password
char ssid[] = "SSID";
char pass[] = "PASSWORD";

// Start WiFi Server class with port 80
WiFiServer server(80);

// Instantiate an instance of the XBee library
XBee xbee = XBee();

// Instantiate an instance of the IO sample class
ZBRxIoSampleResponse ioSample = ZBRxIoSampleResponse();

// Sample data values
unsigned int address;   // Last 4 digits of XBee address
float temperature;      // Raw temperature value
float voltage;          // Reference voltage

// Get sample data
void get_sample_data(ZBRxIoSampleResponse *ioSample) {
  Serial.print("Received data from address: ");
  address = (ioSample->getRemoteAddress64().getMsb() << 8) +
             ioSample->getRemoteAddress64().getLsb();
  Serial.print(ioSample->getRemoteAddress64().getMsb(), HEX);
  Serial.println(ioSample->getRemoteAddress64().getLsb(), HEX);
  temperature = ioSample->getAnalog(3);
  int ref = xbee.getResponse().getFrameData()[17] << 8;
  ref += xbee.getResponse().getFrameData()[18];
  voltage = (float(ref) * float(1200.0 / 1024.0))/1000.0;
}

// Read an integer from EEPROM
int read_int(byte position) {
  int value = 0;
  byte* p = (byte*)(void*)&value;
  for (int i = 0; i < sizeof(value); i++)
      *p++ = EEPROM.read(position++);
  return value;
}

// Read a float from EEPROM
float read_float(byte position) {
  float value = 0;
  byte* p = (byte*)(void*)&value;
  for (int i = 0; i < sizeof(value); i++)
      *p++ = EEPROM.read(position++);
  return value;
}

// Write an integer to EEPROM
void write_int(byte position, int value) {
  const byte *p = (const byte *)(const void *)&value;
  for (int i = 0; i < sizeof(value); i++)
      EEPROM.write(position++, *p++);
}

// Write a float to EEPROM
void write_float(byte position, float value) {
  const byte *p = (const byte *)(const void *)&value;
  for (int i = 0; i < sizeof(value); i++)
      EEPROM.write(position++, *p++);
}

// Read a sample from EEPROM
void read_sample(byte index, unsigned int *address, float *temp_c,
                 float *temp_f, float *volts) {
  float temp;
  byte position =  (index * bytes_per_sample) + 1;

  *address = read_int(position);

  temp = read_float(position + 2);

  *temp_c = ((temp * 1200.0 / 1024.0) - 500.0) / 10.0;
  *temp_f = ((*temp_c * 9.0)/5.0) + 32.0;

  *volts = read_float(position + 6);
}

// Write sample to EEPROM
void write_sample(byte index) {
  byte position =  (index * bytes_per_sample) + 1;

  write_int(position, address);
  write_float(position + 2, temperature);
  write_float(position + 6, voltage);
}

// Record a sample
void record_sample(ZBRxIoSampleResponse *ioSample) {
  int saved_addr;

  // Get sample data from XBee
  get_sample_data(ioSample);

  // See if already in memory. If not, add it.
  byte num_samples = EEPROM.read(0);
  boolean found = false;
  for (byte i = 0; i < num_samples; i++) {
    byte position = (i * bytes_per_sample) + 1;

    // get address
    saved_addr = read_int(position);
    if (saved_addr == address) {
      write_sample(i);
      found = true;
    }
  }
  if (!found) {
    // Save sample
    write_sample(num_samples);

    // Update number of sensors
    num_samples++;
    EEPROM.write(0, num_samples);
  }
}

void send_sensor_data(WiFiClient *client, int num_sample) {
  unsigned int address;
  float temp_c;
  float temp_f;
  float volts;

  // Read sensor data from memory and display it.
  read_sample(num_sample, &address, &temp_c, &temp_f, &volts);

  client->print("<br />\nNode Address: ");
  client->print(address, HEX);
  client->print("<br />\nTemperature: ");
  client->print(temp_c);
  client->print("C<br />\nTemperature: ");
  client->print(temp_f);
  client->print("F<br />\nVoltage: ");
  client->print(volts);
  client->println("V<br />");
}

void listener() {
  // listen for incoming clients
  WiFiClient client = server.available();
  if (client) {
    Serial.println("Got a connection!");
    // an http request ends with a blank line
    boolean currentLineIsBlank = true;
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        // if you've gotten to the end of the line (received a newline
        // character) and the line is blank, the http request has ended,
        // so you can send a reply
        if (c == '\n' && currentLineIsBlank) {
          // send a standard http response header
          client.println("HTTP/1.1 200 OK");
          client.println("Content-Type: text/html");
          client.println();

          // Print header
          client.println("Welcome to the Arduino Data Aggregate Node!");
          client.println("<br />");

          // read sensor data
          byte num_samples = EEPROM.read(0);
          for (int i = 0; i < num_samples; i++) {
            send_sensor_data(&client, i);
          }
          // if no data, say so!
          if (num_samples == 0) {
            client.print("No samples to display.");
            client.println("<br />");
          }
          break;
        }
        if (c == '\n') {
          currentLineIsBlank = true;
        }
        else if (c != '\r') {
          currentLineIsBlank = false;
        }
      }
    }
    // give the web browser time to receive the data
    delay(100);
    // close the connection:
    client.stop();
  }
}

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect

  // WiFi section
  Serial.println("Starting WiFi.");
  int status = WiFi.begin(ssid, pass);
  // if you're not connected, stop here:
  if (status != WL_CONNECTED) {
    Serial.println("Couldn't get a WiFi connection!");
    while(true);
  }
  // if you are connected, print out info about the connection:
  else {
    Serial.println("Connected to network");
    IPAddress ip = WiFi.localIP();
    Serial.print("My IP address is: ");
    Serial.println(ip);
  }
  server.begin();

  soft_serial.begin(9600);
  xbee.setSerial(soft_serial);

  // Initialize the EEPROM
  EEPROM.write(0, 0);
}

void loop() {
  //attempt to read a packet
  xbee.readPacket();

  if (xbee.getResponse().isAvailable()) {
    // got something

    if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {

      // Get the packet
      xbee.getResponse().getZBRxIoSampleResponse(ioSample);

      // Get and store the data locally (in memory or on card?)
      record_sample(&ioSample);
    }
    else {
      Serial.print("Expected I/O Sample, but got ");
      Serial.print(xbee.getResponse().getApiId(), HEX);
    }
  } else if (xbee.getResponse().isError()) {
    Serial.print("Error reading packet.  Error code: ");
    Serial.println(xbee.getResponse().getErrorCode());
  } else {
    // Listen for client and respond.
    listener();
    delay(100);
  }
}

Listing 10-4Local-Storage Data-Aggregate Node

花些时间浏览草图,直到你完全理解了所有东西是如何协同工作的。一旦你熟悉并熟悉了代码,编译它并上传到你的 Arduino。

测试项目

一旦代码编译成功并上传到您的 Arduino 数据聚合节点,在任何传感器节点通电之前,通过 web 浏览器连接到您的 Arduino。请务必使用您在草图中填写的 IP 地址。图 10-5 显示了正确的响应。此时,您也可以打开串行监视器。

img/313992_2_En_10_Fig5_HTML.jpg

图 10-5

来自未连接传感器的数据聚合节点的示例响应

如果您看到这个响应,那么您已经成功地编写了一个运行在 Arduino 上的非常轻量级的 web 服务器!这有多酷?现在,打开一个温度传感器节点的电源,等待 5-10 分钟。如果您还没有打开串行监视器,现在就打开,然后等待传感器节点数据到达。您应该会在串行监视器中看到消息“Received data from address: NNNNN”。发生这种情况时,刷新浏览器并注意变化。

如果您在 web 浏览器中获得了一些数据,请打开所有传感器节点的电源,等待几个样本到达。如果有三个温度传感器节点在运行,下面显示了串行监视器应该打印的内容。请注意,地址会有所不同,应该与您的 XBee 模块相匹配。

Starting WiFi.
Connected to network
My IP address is: 192.168.42.12
Got a connection!
Got a connection!
Got a connection!

等到您看到几个迭代的样本到达,然后刷新您的浏览器。您应该只看到一个条目,其中包含每个连接的传感器的最新样本。下面显示了一个示例结果,其中有三个传感器节点提供数据。

Welcome to the Arduino Data Aggregate Node!

Node Address: DB79
Temperature: 21.60C
Temperature: 70.88F
Voltage: 3.82V

Node Address: D45C
Temperature: 18.20C
Temperature: 64.77F
Voltage: 3.24V

Node Address: 29DB
Temperature: 11.52C
Temperature: 52.74F
Voltage: 3.15V

等待几分钟,然后刷新浏览器。您应该会看到样本值发生了变化。如果没有(或者你只是想找点乐子),小心地影响传感器的值,把它们放在离热源或冷源更近的地方。

Note

如果您没有看到来自传感器节点的任何数据,并且串行监视器中没有任何指示接收到任何数据的内容,请使用第 2 和 4 章中的故障排除部分来诊断 XBee 模块的可能问题。请记住,它们都必须具有相同版本的 API 固件,传感器节点必须具有路由器角色,数据聚合节点必须具有协调器角色。此外,一定要留出至少 10 分钟的时间让 XBee 模块连接并形成网络。

好吧,那很有趣,不是吗?的确是这样:您现在已经展示了对传感器网络基本构建模块的掌握。虽然您只使用了 Arduino 节点来使这一点更容易理解,但下一个项目将 Raspberry Pi 引入其中,进一步完善了您的传感器网络工具缓存。

Note

如果您的输出与显示的有所不同,这是正常的。在某些情况下,您可能还会注意到传感器报告的数据与更精确的设备测量的数据之间存在微小差异。只要您看到特定传感器的值在公差范围内(查看供应商的数据表),您的传感器节点就工作正常。

为了更多乐趣

你可以用这个项目做很多很酷的事情。对我来说最明显的是用有意义的标签替换 XBee 地址。更具体地说,按位置标记传感器节点。例如,XBee 节点CD0F位于我的门廊,而节点29DB位于我的办公室。如果网页上的标签写明贝尔博士的办公室和贝尔夫人的门廊,那就更有意义了。提示:为这些数据创建一个查找表,以便在向客户端显示时可以替换这些值。

另一个探索领域是使用 SD 卡而不是板载 EEPROM 来存储数据。不是只存储每个传感器节点的最后一个值,而是在单独的文件中存储每个传感器的运行值列表。当客户端请求数据时,只显示最后写入每个文件的值。这将演示将本地存储数据聚合节点变成允许随时间存储值的节点所需的微小更改。注意不要超过 SD 卡的容量!

有一样东西是这个项目所没有的,它对于您想知道样本是何时采集的情况来说是必不可少的:样本的日期和时间!该项目存储每个传感器的最新值,但您不知道样本是何时采集的。例如,如果您只从一个传感器接收一次数据,并且发生了一些事情导致该传感器停止发送数据,该怎么办?没有日期和时间参考,你无法知道这一点。要解决这个问题,您可以修改项目以使用实时时钟模块,并存储每个样本的日期和时间,如第七章所示。提示:您需要通过添加实时时钟值来扩展存储样本数据的方法。

如果您正在寻找一个重大的挑战,修改代码以将数据发送到云,并将云用作您的数据存储机制。

远程存储数据聚合器

远程存储数据聚合器是一种节点,设计用于从一个或多个传感器或传感器节点接收传感器数据,并将数据存储在不同的节点上。最常见的情况是,另一个节点是一个具有更强大存储设备的系统。例如,它可能是一台可以存储大文件的计算机,也可能是一台允许您将数据存储在表中的数据库服务器。

远程存储数据聚合节点没有本地存储数据聚合节点复杂,因为不需要处理数据以在本地设备中显示或存储。相反,您只是将原始数据传递到远程节点(系统)进行存储。

如果您希望存储数据以供以后处理,远程存储也是首选。虽然丢失一些值是可以接受的,但更有可能的是,您希望收集所有产生的数据,以便您的分析更加准确。 5 因此,您希望从数据聚合节点到远程存储节点的连接是可靠的。

也可能有这样的情况,即您向多个节点发送数据。考虑这样一种情况,您正在使用不同的传感器或产生不同格式数据的传感器。在这种情况下,您可能希望将一些传感器的数据发送到一个远程节点,而将其他数据发送到其他节点。这样做的原因没有使用不同的本地存储数据聚合节点那么重要,但这仍然是一个问题。我将在下一章考虑这一问题以及类似的传感器网络规划问题。

在下面几节中,您将看到 Arduino 和 Raspberry Pi 的这种形式的数据聚集节点的工作示例。

项目:Arduino 数据-带数据库存储的聚合节点

该项目使用以前的项目作为其基础。您可以使用类似的硬件(电路板、屏蔽板)和软件,但草图稍有不同。如果您还没有构建上一个项目的组件,或者在让它工作时遇到了问题,那么您可能想先回去诊断并纠正问题。

由于我们将使用我们在第九章看到的连接器/Arduino,我们将需要使用一个内存更大的 Arduino 板。对于这个项目,我建议使用 Arduino Uno,或者更好的 Arduino Mega 2560 板。事实上,我将使用 Mega 2560 演示该项目。

Note

如果您使用 WiFi 屏蔽,请务必参考供应商的数据表或设置文档,以便在 Arduino Mega 2560 上正确使用主板。

您还可以使用您在第八章中创建的 MySQL 数据库服务器和第九章中的连接机制。如果你还没有建立数据库服务器或者在你的电脑上安装了数据库服务器,你需要回到第八章并建立它。

Note

如果你在用 MySQL 配置 Raspberry Pi 时遇到问题,或者想简化项目,你可以使用在另一台电脑上运行的 MySQL 服务器。但是,如果您计划安装或使用这个项目作为您自己的传感器网络的基础,您应该运行您的 Raspberry Pi 数据库服务器。

既然如此,是时候构建传感器网络,将样本存储在数据库中了。精明的读者会意识到您已经在前几章中构建了所有组件的示例。

五金器具

这个项目使用与上一个项目相同的传感器硬件作为传感器节点和数据聚集节点。不同之处在于,您使用一个更大的板作为数据聚合节点,而我们添加了一个新节点 MySQL 数据库服务器。

回想一下第八章,MySQL 数据库服务器是一个通过 USB 集线器连接外部硬盘的 Raspberry Pi(Raspberry Pi 无法通过 USB 总线为硬盘等设备供电)。如果您还没有构建 MySQL 数据库服务器,请参考第八章中的“构建 Raspberry Pi MySQL 服务器”部分。

继续启动数据库服务器,并确保它可以接受连接。在使用其他节点时,您可以让服务器保持开机和连接状态。在完成数据聚合节点的所有软件更改之前,最好关闭传感器节点的电源。

软件

与硬件一样,您使用与上一个项目中相同的软件,尽管为 MySQL 连接添加了额外的库,并对草图做了一些修改。因为您使用相同的 XBee 配置,所以从 XBee 模块读取数据的所有代码都与前一个项目中的相同。

我省略了创建 web 服务器并将数据值写入 EEPROM 的代码。这删除了许多代码,但基本结构是相同的。新的部分是对 MySQL 连接器 Arduino 库的调用,用于连接到数据库服务器并发出查询以保存数据,正如您在第九章中看到的。在以下段落中,您将学习草图的每个新部分。后面一节将介绍如何配置 MySQL 数据库。

将 MySQL 连接器代码添加到草图

如果你还没有安装 MySQL 连接器 Arduino 库,请参考第九章安装该库。一旦你安装了这个库,打开一个新的草图并命名为Arduino_MySQL_Aggregate.ino。下面显示了从 XBee 模块读取数据所需的库以及连接器库:

#include <XBee.h>
#include <SoftwareSerial.h>
#include <WiFi.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

您重用了与 XBee 通信的变量,但是从库中添加了一个MySQL_Connector类的实例、用户、密码、SQL INSERT和默认数据库字符串,如下所示:

char user[] = "arduino_user";      // MySQL user login username
char password[] = "secret";        // MySQL user login password
// Sample query
char INSERT_SQL[] = "INSERT INTO house.temperature (address, raw_temp, voltage) VALUES('%s','$s','%s')";
char DEFAULT_DATABASE[] = "house";
...
WiFiClient client;
MySQL_Connection conn((Client *)&client);

你还需要存储 MySQL 数据库服务器的 IP 地址 6 。通过下面的代码可以做到这一点。确保为您的 MySQL 服务器使用正确的 IP 地址——不使用正确的地址将导致草图首次启动时出现连接错误(因为您是通过setup()方法连接到服务器的):

IPAddress server_addr(192,168,42,8);  // IP of the MySQL *server* here

您保留了上一个项目中的get_sample_data()方法,但是删除了listener()send_sensor_data()和 EEPROM 读写方法。然而,record_sample()方法需要重写。既然这样,你还是叫get_sample_data();但是,您没有从原始数据中计算温度并将其显示到串行监视器上,而是构建了一个INSERT SQL 语句来将数据保存在一个表中(我将在下一节解释数据库的设置)。这需要构建字符串来保存字符串中十六进制地址的最后四位数字、原始温度和电压。一旦构建了字符串,您只需调用 MySQL 连接器类实例的cmd_query()方法(my_conn)。新方法如下所示:

void record_sample(ZBRxIoSampleResponse *ioSample) {
  int saved_addr;
  char temp_buff[20];
  char voltage_buff[20];
  char query[128];

  // Get sample data from XBee
  get_sample_data(ioSample);

  // Send data to MySQL
  String addr(address, HEX);
  dtostrf(temperature, 4, 4, temp_buff);
  dtostrf(voltage, 4, 4, voltage_buff);
  sprintf(query, addr.c_str(), temp_buff, voltage_buff);

  Serial.println(&query[0]);
  // Initiate the query class instance
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  // Execute the query
  int res = cur_mem->execute(query);
  if (!res) {
    Serial.println("Query failed.");
  } else {
    Serial.println("Ok.");
  }
  delete cur_mem;
}

setup()方法的更改需要添加连接到数据库服务器的代码。您删除了对server.begin()EEPROM.write(0, 0)的调用,因为您既没有启动 web 服务器,也没有使用 EEPROM 来存储样本。相反,将以下代码添加到setup()方法的末尾:

// Now connect to MySQL
Serial.println("Connecting to MySQL...");
if (conn.connect(server_addr, 3306, user, password, DEFAULT_DATABASE)) {
  delay(1000);
} else {
  Serial.println("Connection failed.");
}

修改loop()方法要容易得多。所有的调用都是为了读取 XBee 模块和调用record_sample()方法。剩下唯一要做的事情就是删除最后一个包含对listener()方法调用的 else 语句。

正如你所看到的,修改上一个项目的草图非常容易。事实上,如果您想节省一些编码时间,您可以从以前的项目中复制代码,并删除不需要的部分。清单 10-5 显示了完整的草图,包括上一个项目中重复使用的所有零件。

/**
  Beginning Sensor Networks Second Edition
  Sensor Networks Example Arduino Data Aggregate Node

  This project demonstrates how to receive sensor data from
  multiple XBee sensor nodes saving the samples in a MySQL
  database.

  It uses an Arduino with an XBee shield with an XBee
  coordinator installed.
*/

#include <XBee.h>
#include <SoftwareSerial.h>
#include <WiFi.h>
#include <MySQL_Connection.h>
#include <MySQL_Cursor.h>

// Setup pin definitions for XBee shield
uint8_t recv = 8;
uint8_t trans = 9;
SoftwareSerial soft_serial(recv, trans);

// assign a MAC address and IP address for the Arduino
byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(192,168,42,8);  // IP of the MySQL *server* here
char user[] = "arduino_user";      // MySQL user login username
char password[] = "secret";        // MySQL user login password
char ssid[] = "SSID";
char pass[] = "PASSWORD";
// Sample query
char INSERT_SQL[] = "INSERT INTO house.temperature (address, raw_temp, voltage) VALUES('%s','$s','%s')";
char DEFAULT_DATABASE[] = "house";

WiFiClient client;
MySQL_Connection conn((Client *)&client);

// Start WiFi Server class with port 80
WiFiServer server(80);

// Instantiate an instance of the XBee library
XBee xbee = XBee();

// Instantiate an instance of the IO sample class
ZBRxIoSampleResponse ioSample = ZBRxIoSampleResponse();

// Sample data values
unsigned int address;   // Last 4 digits of XBee address
float temperature;      // Raw temperature value
float voltage;          // Reference voltage

// Get sample data
void get_sample_data(ZBRxIoSampleResponse *ioSample) {
  Serial.print("Received data from address: ");
  address = (ioSample->getRemoteAddress64().getMsb() << 8) +
             ioSample->getRemoteAddress64().getLsb();
  Serial.print(ioSample->getRemoteAddress64().getMsb(), HEX);
  Serial.println(ioSample->getRemoteAddress64().getLsb(), HEX);
  temperature = ioSample->getAnalog(3);
  int ref = xbee.getResponse().getFrameData()[17] << 8;
  ref += xbee.getResponse().getFrameData()[18];
  voltage = (float(ref) * float(1200.0 / 1024.0))/1000.0;
}

// Record a sample
void record_sample(ZBRxIoSampleResponse *ioSample) {
  int saved_addr;
  char temp_buff[20];
  char voltage_buff[20];
  char query[128];

  // Get sample data from XBee
  get_sample_data(ioSample);

  // Send data to MySQL
  String addr(address, HEX);
  dtostrf(temperature, 4, 4, temp_buff);
  dtostrf(voltage, 4, 4, voltage_buff);
  sprintf(query, addr.c_str(), temp_buff, voltage_buff);

  Serial.println(&query[0]);
  // Initiate the query class instance
  MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);
  // Execute the query
  int res = cur_mem->execute(query);
  if (!res) {
    Serial.println("Query failed.");
  } else {
    Serial.println("Ok.");
  }
  delete cur_mem;
}

void setup() {
  Serial.begin(115200);
  while (!Serial); // wait for serial port to connect

  // WiFi section
  Serial.println("Starting WiFi.");
  int status = WiFi.begin(ssid, pass);
  // if you're not connected, stop here:
  if (status != WL_CONNECTED) {
    Serial.println("Couldn't get a WiFi connection!");
    while(true);
  }
  // if you are connected, print out info about the connection:
  else {
    Serial.println("Connected to network");
    IPAddress ip = WiFi.localIP();
    Serial.print("My IP address is: ");
    Serial.println(ip);
  }
  soft_serial.begin(9600);
  xbee.setSerial(soft_serial);

  // Now connect to MySQL
  Serial.println("Connecting to MySQL...");
  if (conn.connect(server_addr, 3306, user, password, DEFAULT_DATABASE)) {
    delay(1000);
  } else {
    Serial.println("Connection failed.");
  }
}

void loop() {
  //attempt to read a packet
  xbee.readPacket();

  if (xbee.getResponse().isAvailable()) {
    // got something

    if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {

      // Get the packet
      xbee.getResponse().getZBRxIoSampleResponse(ioSample);

      // Get and store the data locally (in memory or on card?)
      record_sample(&ioSample);
    }
    else {
      Serial.print("Expected I/O Sample, but got ");
      Serial.print(xbee.getResponse().getApiId(), HEX);
    }
  } else if (xbee.getResponse().isError()) {
    Serial.print("Error reading packet.  Error code: ");
    Serial.println(xbee.getResponse().getErrorCode());
  }
}

Listing 10-5Arduino Remote-Storage Data Aggregate

您可能想知道为什么删除了计算华氏温度和摄氏温度的代码。您这样做是因为您可以将此功能转移到数据库服务器。这不仅释放了一些处理能力(对于较小的微控制器来说是一个很大的帮助),而且对于像 Arduino 这样的平台来说,它还释放了少量的内存。这个项目节省的成本可能很小,但是考虑一下非常复杂的草图或者正在做其他事情的 Arduino 的情况。内存的任何节省都可以为存储数据或使用更多传感器提供更多空间。

例如,考虑构建一个节点的需求,该节点不仅通过 XBee 网络充当数据集合,而且还托管 Arduino 上通过 I2C 接口连接的许多传感器,并通过一些其他硬件专用接口显示数据,如 LCD 面板甚至硬拷贝打印机。 7 所有这些组件都需要包含库;根据 Arduino 的大小,内存可能会不足。我构建的草图迫使我使用 Arduino Mega,不是因为草图的大小,而是因为我需要使用的库所需要的内存总量。

现在您已经构建了草图,让我们转向数据库服务器,看看需要做些什么来支持在表中存储样本。

设置 MySQL 数据库

本节讨论了在 MySQL 服务器上创建数据库以保存和报告传感器数据所需的工作。您需要做的第一件事是创建想要使用的数据库,并用必要的对象填充它。在这种情况下,您需要两个表和一个触发器。我向您展示了所有需要的命令,但为了简洁起见,省略了与服务器的大部分交互。如果你还没有阅读这些章节,请参考第 6 和 7 章节,获得关于 MySQL 的快速入门教程。

连接到您的 MySQL 数据库服务器。回想一下,如果在 Raspberry Pi 上运行,您可以通过mysql -uroot -p<password>命令来完成,或者如果在 PC 上运行,您可以通过mysqlsh --uri root@localhost:3306 --sql命令来使用 MySQL Shell。继续创建数据库并将其命名为 house,如下所示:

CREATE DATABASE house;
USE house;

数据将存储在一个表中。如前所述,您希望存储 XBee 传感器节点的地址(64 位地址的最后四个十六进制数字)、原始温度样本和电压。

此时,您可以考虑向数据库服务器添加一些功能,否则需要在数据聚合节点上做更多的工作。例如,考虑这样一个事实:您想知道样本是什么时候采集的。也就是说,您希望存储样本的日期和时间。如果您还记得前面的章节,您必须使用连接到 Arduino 的实时时钟模块来显示样本的日期和时间。幸运的是,您可以通过使用 timestamp 数据类型创建一个列,简单地指示数据库服务器自动存储这些数据,从而避免所有的代码和硬件。当行被插入到表中时,此数据类型存储当前日期和时间。很酷,是吧?

但是你可能想知道这是怎么回事。让服务器为字段填充数据的技巧是不要在INSERT语句中传递值。这是一个特殊的 sentinel 值,服务器将其解释为您想要计算时间戳并保存它。请注意,如果需要存储特定的值,可以为该列提供特定的时间戳。

您还可以将计算华氏温度和摄氏温度的代码移到数据库中。这需要使用触发器(一个特殊的代码块,可以在插入、更新或删除行的特定时刻执行)。一会儿你看着扳机;现在,您可以简单地为每个温度值添加一列。

因此,您总共需要六列:样本的日期和时间、传感器节点的地址、原始温度样本、电压、华氏温度和摄氏温度。实现该表所需的CREATE TABLE语句如下。命名桌子温度:

CREATE TABLE `temperature` (
  `sample_date` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `address` char(8) DEFAULT NULL,
  `raw_temp` float DEFAULT NULL,
  `voltage` float DEFAULT NULL,
  `fahrenheit` float DEFAULT '0',
  `celsius` float DEFAULT '0'
) ENGINE=InnoDB DEFAULT CHARSET=latin18;

请注意,您没有使用主键。我把这个问题留给你来考虑,我将在下一章讨论数据库设计的注意事项。

What about Nodes Without Xbees? What Address Do I Use?

这里创建的表使用一个短字符串作为 XBee 传感器节点的地址。如果您添加一个不使用 XBees 的传感器节点(它直接连接到服务器以存储数据),或者如果有传感器连接到数据聚合节点,您会使用什么?无论哪种情况,您都可以为每个传感器创建自己的唯一值。您可以使用 XBee 64 位地址的最后四位数字的约定,并存储十六进制值。您可以轻松地用从 0000 到 FFFF 的值对传感器节点和传感器进行编号。这给你留下了大量的工作价值。但是一定不要使用与 XBees 相同的值。

回想一下,上一个项目的挑战之一是为每个传感器节点使用有意义的名称。您也可以将它以查找表的形式移动到数据库服务器。在这种情况下,您需要一个与存储在温度表中的值相匹配的列和另一个用于存储更有意义的名称的列。这允许您在保留数据的原始形式的同时添加更多对人友好的数据。稍后,您将看到在服务器上查询数据时如何检索这些信息。以下语句创建名为 sensor_names 的新表,并用数据填充它。您以一个示例SELECT语句结束,以检索输入的数据:

CREATE TABLE `sensor_names` (
  `address` char(8) DEFAULT NULL,
  `name` char(30) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

INSERT INTO sensor_names VALUES ('DB79', 'New Porch');
INSERT INTO sensor_names VALUES ('D45C', 'Living Room');
INSERT INTO sensor_names VALUES ('29DB', 'Office');

SELECT * FROM house.sensor_names;
+---------+-------------+
| address | name        |
+---------+-------------+
| 29a2    | New Porch   |
| 29db    | Living Room |
| cd0f    | Office      |
+---------+-------------+
3 rows in set (0.00 sec)

Tip

当您在项目中使用 XBee 节点时,请在下面的INSERT语句中使用这些地址。

现在让我们考虑一下触发器。这就是将计算华氏和摄氏温度的代码移植到数据库服务器的方法。我鼓励你检查一下在线 MySQL 参考手册中触发器的语法和用法( https://dev.mysql.com/doc/refman/8.0/en/create-trigger.html )。同时,我将向您展示添加您需要的触发器需要哪些语句。

您需要检测新行何时被添加到表中。当这种情况发生时,您希望执行计算并将结果存储在适当的列中。因此,您需要创建一个在插入新行之前运行的触发器。当该事件发生时,您可以执行计算。以下代码显示了您需要创建的触发器。这些计算看起来应该很熟悉,尽管语法不同。请注意 new 运算符的使用,它允许您引用传入(新)行中的值进行读取或写入:

DELIMITER //
CREATE TRIGGER calc_temp BEFORE INSERT ON temperature
FOR EACH ROW
BEGIN
  declare c float;
  set c = ((new.raw_temp * 1200.0 / 1024.0) - 500.0) / 10.0;
  set new.celsius = c;
  set new.fahrenheit = ((c * 9.0)/5.0) + 32.0;
END;
//
DELIMITER ;

您可能注意到的第一件事是使用了DELIMITER命令。这是一个特殊的命令,可用于替换;在mysql客户端中决定语句结束的字符。在这种情况下,您使用//而不是;

因为触发器主体包含以分号结尾的 SQL 语句,所以需要进行DELIMITER更改。如果您没有更改分隔符,mysql客户端将检测到语句结束,并尝试执行部分编码的触发器。如果在创建这个触发器时遇到语法错误,请检查以确保使用了如图所示的DELIMITER命令。请注意,您做的最后一件事是将分隔符改回分号。

还要注意,您设置了在插入之前执行的触发器,并且有一个循环来处理每个新行。虽然您发出的是单个INSERT语句,但是这个语法是必需的,因为可能会出现一次添加多个新行的情况。例如,如果涉及到事务,在处理完几行之前,可能不会提交(永久存储)更改。在这种情况下,触发器将触发一次,并且将为每个新行处理一次正文。

如果你还没有在第九章中授予用户访问权限,你也需要这样做。您可以使用CREATE USERGRANT语句来实现这一点:

CREATE USER 'arduino_user'@'%' IDENTIFIED WITH mysql_native_password BY 'secret';
GRANT ALL ON *.* to 'arduino_user'@'%';

现在,您已经设置好了 MySQL 数据库服务器并创建了必要的数据库对象,让我们把它们放在一起,看看它是如何运行的。此时,您可以将草图上传到 Arduino 数据聚合节点(确保它已接入您的网络),并打开传感器节点的电源。在给传感器节点通电之前,请等待 3-5 分钟。

测试项目

加载草图后,打开串行监视器,观察关于连接到 MySQL 数据库服务器的语句。如果一切正常,您应该会看到一条成功消息。如果没有,请检查您使用的 IP 地址,并确保您的 MySQL 服务器正在运行并接受连接。

当您看到连接成功消息时,您可以打开传感器节点的电源。您应该开始看到在每个传感器节点的串行监视器中打印出一条消息。回想一下清单 10-5 中,您打印了一份从 XBee 读取的数据的公告(并显示了地址)。您还可以显示样本数据的完整的INSERT语句。

如果让草图运行一段时间,并让几个传感器节点通电并通信,您将开始看到草图也记录了来自这些传感器节点的样本。清单 10-6 显示了为来自几个传感器节点的样本打印的语句示例。

Connected to network
My IP address is: 192.168.42.12
Connecting...
...trying...
Connected to server version 8.0.19
Received data from address: 13A20040A0D45C
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('d45c',573.0000,3.2062) ... Ok.
Received data from address: 13A200409029DB
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('29db',546.0000,3.1688) ... Ok.
Received data from address: 13A2004192DB79
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('db79',622.0000,3.8109) ... Ok.
Received data from address: 13A20040A0D45C
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('d45c',572.0000,3.2109) ... Ok.
Received data from address: 13A200409029DB
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('29db',547.0000,3.1734) ... Ok.
Received data from address: 13A2004192DB79
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('db79',622.0000,3.8109) ... Ok.
Received data from address: 13A20040A0D45C
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('d45c',572.0000,3.2109) ... Ok.
Received data from address: 13A200409029DB
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('29db',546.0000,3.1688) ... Ok.
Received data from address: 13A2004192DB79
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('db79',623.0000,3.8109) ... Ok.
Received data from address: 13A20040A0D45C
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('d45c',572.0000,3.2109) ... Ok.
Received data from address: 13A200409029DB
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('29db',546.0000,3.1734) ... Ok.
Received data from address: 13A2004192DB79
Query: INSERT INTO house.temperature (address, raw_temp, voltage) VALUES ('db79',622.0000,3.8098) ... Ok.

Listing 10-6Output from Arduino_MySQL_Aggregate Sketch

如果你看到类似这些例子的结果,你已经解决了这个项目!但是数据库里的数据呢?你怎么看?一旦草图运行了一段时间,连接到您的 MySQL 数据库服务器,发出以下命令,并观察结果:

> SELECT * FROM house.temperature;
+---------+--------+-------+---------+---------+--------------+
| sample_date | address | raw_temp | voltage | fahrenheit | celsius |
+---------+--------+-------+---------+---------+--------------+
| 2020-03-22 19:30:30 | d45c | 573 | 3.2062 | 62.8672 | 17.1484  |
| 2020-03-22 19:30:45 | 29db | 546 | 3.1688 | 57.1719 | 13.9844  |
| 2020-03-22 19:31:00 | db79 | 622 | 3.8109 | 73.2031 | 22.8906  |
| 2020-03-22 19:31:15 | d45c | 572 | 3.2109 | 62.6562 | 17.0312  |
| 2020-03-22 19:31:30 | 29db | 547 | 3.1734 | 57.3828 | 14.1016  |
| 2020-03-22 19:31:45 | db79 | 622 | 3.8109 | 73.2031 | 22.8906  |
| 2020-03-22 19:32:00 | d45c | 572 | 3.2109 | 62.6562 | 17.0312  |
| 2020-03-22 19:32:15 | 29db | 546 | 3.1688 | 57.1719 | 13.9844  |
| 2020-03-22 19:32:30 | db79 | 623 | 3.8109 | 73.4141 | 23.0078  |
| 2020-03-22 19:32:45 | d45c | 572 | 3.2109 | 62.6562 | 17.0312  |
| 2020-03-22 19:33:00 | 29db | 546 | 3.1734 | 57.1719 | 13.9844  |
| 2020-03-22 19:33:15 | db79 | 622 | 3.8098 | 73.2031 | 22.8906  |
+---------+--------+-------+---------+---------+--------------+
12 rows in set (0.0007 sec)

请注意,我有许多行要查看!这是因为我将 XBee 模块的睡眠时间设置为一个非常低的值。实际上,您可以将睡眠时间设置为几秒钟以上。对于这个项目,让它频繁地进行采样是很好的。

还要注意,您已经为样本日期、华氏温度和摄氏温度列填充了数据!这表明 timestamp 数据类型起作用了,您的触发器在INSERT触发,创建了计算出的值。这难道不比让你那可怜的超负荷工作的 Arduino 计算出那些值更容易吗?

现在让我们考虑数据库服务器的另一个特性。回想一下上一个项目,您可以很容易地看到每个传感器节点的最后已知样本。如果你从来没有把这些值存储在任何地方,你怎么能再现这个特性呢?您不太可能需要这个特性,但是如果您需要类似的特性,让我们来探索一下。

答案是你确实存储了那些值!你在一个样本中存储每一个值。问题是您不知道表中的哪一行是每个传感器的最新数据。但答案还在,不是吗?

这是精明的 SQL 程序员挣钱的地方。您确实可以通过使用一个叫做分组的 SQL 魔术和MAX()函数来获得这些数据。在这种情况下,您需要传感器的名称(不是地址)以及华氏和摄氏温度值,就像您在 web 服务器上拥有的一样。

要获得名称,您必须连接(组合两个表中在一组公共列上匹配的行)temperature 和 sensor_names 表,并在地址上匹配。回想一下,每个表中的值都将匹配,即 sensor_names 表中的一行将与 temperature 表中特定数量的行匹配。

但是最后的值呢?要获得这些数据,您可以在子查询(从另一个查询中执行的查询)上使用MAX()函数来返回每组地址的最新时间戳。注意子查询中的GROUP BY子句。您可以使用子查询中的结果来限制SELECT的输出,只输出那些与每个地址的最新值匹配的行。以下代码显示了完整的SELECT语句和示例结果:

SELECT name, fahrenheit, voltage
FROM temperature join sensor_names ON temperature.address = sensor_names.address
WHERE sample_date IN (
SELECT MAX(sample_date)
FROM temperature
GROUP BY address
);
+-------------+------------+---------+
| name        | fahrenheit | voltage |
+-------------+------------+---------+
| Living Room |    62.6562 |  3.2109 |
| Office      |    57.1719 |  3.1734 |
| New Porch   |    73.2031 |  3.8098 |
+-------------+------------+---------+
3 rows in set (0.0034 sec)

如果您认为这是一个非常复杂的查询,不要难过。当您开始使用数据库时,SQL 可能是一个相当大的挑战。如果您发现您需要使用这样的查询,那么购买一本关于学习 SQL 的书来更加熟悉 SQL 命令的能力和功能是值得的。

为了更多乐趣

在这个项目中,你可以做很多事情。事实上,前一个项目的所有挑战都适用。剩下唯一要做的事情就是用 Raspberry Pi 替换其中一个传感器节点,用 Raspberry Pi 替换数据聚集节点。你在下一个项目中做后者,但是让我们考虑如何做前者。

第七章探讨了如何创建一个由 Raspberry Pi 托管的传感器节点。考虑将这一挑战向前推进一步,并将其与您在第七章中学到的关于使用 TMP36 传感器的知识结合起来。将这样的节点添加到您的网络中。

更有趣的是,您可以将先前项目中的 web 服务器组件添加到草图中。保留这些元素还会引入一种在第一章中讨论的数据聚合节点形式——混合数据聚合节点。回想一下,这样做的好处是,如果节点失去了与服务器的连接(或者服务器出现故障),您至少可以从数据聚合节点获得最新的数据。

对于那些需要更多功能的人:如果你发现你需要一个更工业级的网关,你可以看看 Digi 工业网关;一个用于开发复杂应用程序的开源 Python 环境,支持 ZigBee,并有一个坚固的外壳。详见 www.digi.com/products/networking/gateways/xbee-industrial-gateway

既然您已经掌握了使用 Arduino 的数据聚合节点,那么让我们来探索使用 Raspberry Pi 构建数据聚合节点。

项目:Raspberry Pi 数据-带数据库存储的聚合节点

这个项目使用前一个项目中的传感器节点,但是不使用 Arduino 作为数据聚合节点的主机,而是使用 Raspberry Pi。您还将使用上一个项目中的同一个 MySQL 数据库服务器,通过 Raspberry Pi 数据聚合节点存储来自 XBee 传感器节点的传感器数据。

该项目的目标是重现上一个项目的功能。也就是说,您希望 Raspberry Pi 通过 XBee 模块(协调器)从多个传感器节点接收传感器样本,并将这些结果保存在 MySQL 数据库中。然而,由于我们使用的是 XBee Python 库,我们必须对如何根据原始数据计算温度做一些小的改动。正如您将看到的,变化很微妙,但并不困难。

该项目的基础是第五章的 Raspberry Pi XBee 项目。最好复习一下课文,以便熟悉这项任务。我展示了第五章的接线图作为复习。

五金器具

该项目需要以前项目中的 XBee 托管传感器节点、一个 Raspberry Pi、一个 GPIO 分线板和电缆、一个试验板、一个 XBee 适配器和一些试验板跳线。

布线与第五章“项目:为 XBee 传感器节点创建 Raspberry Pi 数据采集器”一节中的相同。图 10-6 显示了来自章节 5 的试验板和布线。如图所示连接 XBee 适配器,将 GPIO 电缆连接到您的 Raspberry Pi,然后通电!您不必安装 XBee,但这样做是个好主意。记住,你需要你的协调节点。

img/313992_2_En_10_Fig6_HTML.jpg

图 10-6

将 XBee 连接到树莓 Pi

软件

本项目的软件要求与第五章的项目相同。也就是说,您正在使用连接器/Python 库。参见第五章了解如何下载和安装库。但是,您必须安装另一个库 MySQL 连接器/Python 库。

您可以使用清单 10-7 中所示的命令在您的 Raspberry Pi 上安装 MySQL 连接器/Python 库,这也将安装任何可能需要的必备库。

$ pip3 install mysql-connector-python
Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple
Collecting mysql-connector-python
  Downloading https://files.pythonhosted.org/packages/5c/1e/3f372b31853b868153e453146d99ca787da3eb4bf0b654590b829b262afa/mysql_connector_python-8.0.19-py2.py3-none-any.whl (355kB)
    100% |████████████████████████████████| 358kB 50kB/s
Collecting protobuf==3.6.1 (from mysql-connector-python)
  Downloading https://files.pythonhosted.org/packages/77/78/a7f1ce761e2c738e209857175cd4f90a8562d1bde32868a8cd5290d58926/protobuf-3.6.1-py2.py3-none-any.whl (390kB)
    100% |████████████████████████████████| 399kB 103kB/s
Collecting dnspython==1.16.0 (from mysql-connector-python)
  Downloading https://files.pythonhosted.org/packages/ec/d3/3aa0e7213ef72b8585747aa0e271a9523e713813b9a20177ebe1e939deb0/dnspython-1.16.0-py2.py3-none-any.whl (188kB)
    100% |████████████████████████████████| 194kB 112kB/s
Requirement already satisfied: six>=1.9 in /usr/lib/python3/dist-packages (from protobuf==3.6.1->mysql-connector-python) (1.12.0)
Requirement already satisfied: setuptools in /usr/lib/python3/dist-packages (from protobuf==3.6.1->mysql-connector-python) (40.8.0)
Installing collected packages: protobuf, dnspython, mysql-connector-python
Successfully installed dnspython-1.16.0 mysql-connector-python-8.0.19 protobuf-3.6.1

Listing 10-7Installing MySQL Connector/Python

回想一下,我提到过我们需要改变摄氏和华氏的计算,因为我们使用 XBee Python 库来读取数据。如果你还记得第五章的话,我们必须使用一个稍微不同的公式来计算温度。因此,我们将使用下面的命令禁用触发器,并将计算放在代码中,而不是使用现有的触发器。这很好,因为 Raspberry Pi 有足够的能力来进行计算。

DROP TRIGGER house.calc_temp;

正如您将看到的,我们还将更改 INSERT SQL 命令来传递摄氏温度和华氏温度的值,而不是允许触发器填充这些值。

现在让我们开始编写您的 Python 脚本。如果你想复制第五章的剧本,你可以。只需将其复制到一个名为pi_xbee_mysql.py的文件中。或者你可以打开一个新文件,从头开始输入代码。

您需要输入的第一条语句是那些包含您需要的库的语句。回想一下第五章,其中包括serialxbee库。您还添加了mysql.connector库,如下所示:

import serial
from digi.xbee.devices import XBeeDevice
from digi.xbee.io import IOLine, IOMode
import mysql.connector

接下来,定义一些变量。你在第五章中使用了项目中相同的变量和定义,但是为你的 MySQL 代码添加了两个新的变量和定义。在这种情况下,您需要添加一个变量来存储数据库连接器类的实例。

您还需要扩展常量,以包含与 MySQL 服务器通信所需的常量。添加用户名、主机(或 IP)、端口和密码。这些值与您通过mysql客户端连接到 MySQL 服务器时使用的值相同。以下代码显示了所有常量和变量:

# MySQL constants
USER = 'arduino_user'       # MySQL user id
PASSWD = 'secret'           # MySQL password
HOST_OR_IP = '192.168.42.8' # MySQL server IP address
PORT = 3306                 # MySQL port
# Query string
INSERT_SQL = ("INSERT INTO house.temperature (address, raw_temp, voltage, celsius, fahrenheit) "
              "VALUES('{0}', {1}, {2}, {3}, {4})")

# Serial port on Raspberry Pi
SERIAL_PORT = "/dev/ttyS0"
# BAUD rate for the XBee module connected to the Raspberry Pi
BAUD_RATE = 9600
# Analog pin we want to monitor/request data
ANALOG_LINE = IOLine.DIO3_AD3
# Sampling rate
SAMPLING_RATE = 15

# Get an instance of the XBee device class
device = XBeeDevice(SERIAL_PORT, BAUD_RATE)

# Variables for MySQL Connector/Python code
db_conn = None

Caution

确保所有常量都与 XBee 配置和访问 MySQL 数据库服务器的细节相匹配。如果脚本运行不正确或者您无法连接到 MySQL,请仔细检查这些设置。

我们将搜索网络一次并获得所有节点的列表,而不是通过节点 id 搜索网络中的节点。虽然这个例子一开始就展示了如何做到这一点,但是您可以使用 XBee 库注册一个回调来检测何时添加新节点。详见 https://xbplib.readthedocs.io/en/latest/user_doc/discovering_the_xbee_network.html 的 API 文档。

以下代码显示了如何使用 XBee 库来发现网络上的节点:

def discover_nodes():
    """Get a list of the nodes (node ids) on the network
    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    xbee_network.start_discovery_process()
    print("Discovering network", end='')
    while xbee_network.is_discovery_running():
        print(".", end='')
        time.sleep(0.5)
    print("done.")
    devices = xbee_network.get_devices();
    node_ids= []
    for dev in devices:
        print("Found {0} at {1}.".format(dev.get_node_id(), dev.get_64bit_addr()))
        node_ids.append(dev.get_node_id())
    if not node_ids:
        print("WARNING: No nodes found.")
    return node_ids

回想一下,我们现在需要一种方法来获取网络中的 XBee 节点,这样我们就可以捕获它正在报告的数据。

def get_remote_device():
    """Get the remote node from the network
    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    remote_device = xbee_network.discover_device(REMOTE_NODE_ID)
    if remote_device is None:
        print("ERROR: Remove node id {0} not found.".format(REMOVE_NODE_ID))
        exit(1)
    remote_device.set_dest_address(device.get_64bit_addr())
    remote_device.set_io_configuration(ANALOG_LINE, IOMode.ADC)
    remote_device.set_io_sampling_rate(SAMPLING_RATE)

接下来,我们需要一个回调方法在数据准备好时执行。在这种情况下,我们将以相同的方法对数据形成INSERT查询,并调用save_sample()方法来执行它。

def io_sample_callback(sample, remote, time):
    address = str(remote.get_64bit_addr())
    # Get the raw temperature value
    raw_temp = sample.get_analog_value(ANALOG_LINE)
    # Calculate supply voltage
    volts = (sample.power_supply_value * (1200.0 / 1024.0)) / 1000.0
    # Save results in the table
    short_addr = address[-4:]
    print("Reading from {0}: {1}, {2}.".format(short_addr, raw_temp, volts))
    # Get the temperature in Celsius
    temp_c = (sample.get_analog_value(ANALOG_LINE) / 1023.0 * 1.25 - 0.5) * 100.0
    # Calculate temperature in Fahrenheit
    temp_f = ((temp_c * 9.0) / 5.0) + 32.0
    print("\tTemperature is {0:.2f}C. {1:.2f}F".format(temp_c, temp_f))
    query = (INSERT_SQL.format(short_addr, raw_temp, volts, temp_c, temp_f))
    save_sample(db_conn, query)

为了使用save_sample()方法,我们向它传递数据库连接实例和一个查询字符串。该方法创建一个游标,执行查询,然后检查结果。如果有结果,它会将结果返回给调用者。完整的方法如下:

def save_sample(conn, query_str):
    results = None
    cur = conn.cursor(
        cursor_class=mysql.connector.cursor.MySQLCursorBufferedRaw)
    try:
        res = cur.execute(query_str)
    except mysql.connector.Error as e:
        cur.close()
        raise Exception("Query failed. " + e.__str__())
    try:
        results = cur.fetchall()
    except mysql.connector.errors.InterfaceError as e:
        if e.msg.lower() == "no result set to fetch from.":
            pass # This error means there were no results.
        else:    # otherwise, re-raise error
            raise e
    conn.commit()
    cur.close()
    return results

现在你可以使用与第五章中的项目相同的形式来得到脚本的实质内容(一个无限循环 9 ,有一个break例外)。但是首先,您要连接到数据库服务器(记住,遇到函数声明时不会执行,只有被调用时才会执行)。您创建了一个值字典,将它们设置为前面显示的常量。这是一种可以在任何 Python 脚本中使用的技术,以避免传递大量参数。以下代码尝试连接到 MySQL 服务器,如果成功,将返回数据库连接器类的实例:

# Connect to database server
try:
    parameters = {
        'user': USER,
        'host': HOST_OR_IP,
        'port': PORT,
        'passwd': PASSWD,
        }
    print("Connecting to MySQL...", end='')
    db_conn = mysql.connector.connect(**parameters)
    print("done.")
except mysql.connector.Error as e:
    raise Exception("ERROR: Cannot connect to MySQL Server!")
    exit(1)

如果连接失败,就会抛出异常。如果发生这种情况,请确保检查您的常量值是否正确,并尝试使用相同的参数通过mysql客户端应用程序连接到 MySQL 服务器。一旦您可以通过mysql客户端成功连接,请再次尝试该脚本。

下一部分是 while infinity 循环,也取自第五章中的例子:

try:
    # Read and save temperature data
    print("Welcome to example of storing data from a set of remote TMP36 sensors in MySQL!")

    device.open()  # Open the device class
    # Get the nodes on the network
    remote_node_ids = discover_nodes()
    # Setup the remote device
    for remote_id in remote_node_ids:
        get_remote_device(remote_id)
    # Register a listener to handle the samples received by the local device
    device.add_io_sample_received_callback(io_sample_callback)
    while True:
        pass
except KeyboardInterrupt:
    if device is not None and device.is_open():
        device.close()

一旦 while infinity 循环终止,您必须断开与服务器的连接。下面的代码可以做到这一点。在这种情况下,您可以忽略任何错误—您正在断开连接,并且您不在乎是否失败,因为脚本将会停止:

# Disconnect from the server
try:
    db_conn.disconnect()
except:
    pass

如果您认为这不是很多代码,那么您是正确的。连接器/Python 库使得在 Python 中使用 MySQL 变得非常容易。清单 10-8 显示了该项目的完整代码。在您尝试运行脚本之前,请花一些时间来确保所有内容都输入正确。

#
# Beginning Sensor Networks Second Edition
# RasPi XBee Remote Storage Data Aggregator
#
# For this script, we read data from an XBee coordinator
# node whenever data is received from an XBee sensor node.
# We also need a connection to a database server for saving
# the results in a table.
#
# The data read is from one sample (temperature from a
# XBee sensor node and the supply voltage at the source) for
# each device on the network by node id.
#
import serial
import time
from digi.xbee.devices import XBeeDevice
from digi.xbee.io import IOLine, IOMode
import mysql.connector

# MySQL constants
USER = 'arduino_user'       # MySQL user id
PASSWD = 'secret'           # MySQL password
HOST_OR_IP = '192.168.42.8' # MySQL server IP address
PORT = 3306                 # MySQL port
# Query string
INSERT_SQL = ("INSERT INTO house.temperature (address, raw_temp, voltage, celsius, fahrenheit) "
              "VALUES('{0}', {1}, {2}, {3}, {4})")

# Serial port on Raspberry Pi
SERIAL_PORT = "/dev/ttyS0"
# BAUD rate for the XBee module connected to the Raspberry Pi
BAUD_RATE = 9600
# Analog pin we want to monitor/request data
ANALOG_LINE = IOLine.DIO3_AD3
# Sampling rate
SAMPLING_RATE = 15

# Get an instance of the XBee device class
device = XBeeDevice(SERIAL_PORT, BAUD_RATE)

# Variables for MySQL Connector/Python code
db_conn = None

# Save the sample in the database
def save_sample(conn, query_str):
    results = None
    cur = conn.cursor(
        cursor_class=mysql.connector.cursor.MySQLCursorBufferedRaw)
    try:
        res = cur.execute(query_str)
    except mysql.connector.Error as e:
        cur.close()
        raise Exception("Query failed. " + e.__str__())
    try:
        results = cur.fetchall()
    except mysql.connector.errors.InterfaceError as e:
        if e.msg.lower() == "no result set to fetch from.":
            pass # This error means there were no results.
        else:    # otherwise, re-raise error
            raise e
    conn.commit()
    cur.close()
    return results

# Method to connect to the network and discover the nodes
def discover_nodes():
    """Get a list of the nodes (node ids) on the network
    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    xbee_network.start_discovery_process()
    print("Discovering network", end='')
    while xbee_network.is_discovery_running():
        print(".", end='')
        time.sleep(0.5)
    print("done.")
    devices = xbee_network.get_devices();
    node_ids= []
    for dev in devices:
        print("Found {0} at {1}.".format(dev.get_node_id(), dev.get_64bit_addr()))
        node_ids.append(dev.get_node_id())
    if not node_ids:
        print("WARNING: No nodes found.")
    return node_ids

# Method to connect to the network and get the remote node by id
def get_remote_device(remote_id):
    """Get the remote node from the network
    Returns:
    """
    # Request the network class and search the network for the remote node
    xbee_network = device.get_network()
    remote_device = xbee_network.discover_device(remote_id)
    if remote_device is None:
        print("ERROR: Remote node id {0} not found.".format(remote_id))
        exit(1)
    remote_device.set_dest_address(device.get_64bit_addr())
    remote_device.set_io_configuration(ANALOG_LINE, IOMode.ADC)
    remote_device.set_io_sampling_rate(SAMPLING_RATE)

# Method to get the data when available from the remote node
def io_sample_callback(sample, remote, time):
    address = str(remote.get_64bit_addr())
    # Get the raw temperature value
    raw_temp = sample.get_analog_value(ANALOG_LINE)
    # Calculate supply voltage
    volts = (sample.power_supply_value * (1200.0 / 1024.0)) / 1000.0
    # Save results in the table
    short_addr = address[-4:]
    print("Reading from {0}: {1}, {2}.".format(short_addr, raw_temp, volts))
    # Get the temperature in Celsius
    temp_c = (sample.get_analog_value(ANALOG_LINE) / 1023.0 * 1.25 - 0.5) * 100.0
    # Calculate temperature in Fahrenheit
    temp_f = ((temp_c * 9.0) / 5.0) + 32.0
    print("\tTemperature is {0:.2f}C. {1:.2f}F".format(temp_c, temp_f))
    query = (INSERT_SQL.format(short_addr, raw_temp, volts, temp_c, temp_f))
    save_sample(db_conn, query)

# Connect to database server
try:
    parameters = {
        'user': USER,
        'host': HOST_OR_IP,
        'port': PORT,
        'passwd': PASSWD,
        }
    print("Connecting to MySQL...", end='')
    db_conn = mysql.connector.connect(**parameters)
    print("done.")
except mysql.connector.Error as e:
    raise Exception("ERROR: Cannot connect to MySQL Server!")
    exit(1)

try:
    # Read and save temperature data
    print("Welcome to example of storing data from a set of remote TMP36 sensors in MySQL!")

    device.open()  # Open the device class
    # Get the nodes on the network
    remote_node_ids = discover_nodes()
    # Setup the remote device
    for remote_id in remote_node_ids:
        get_remote_device(remote_id)
    # Register a listener to handle the samples received by the local device
    device.add_io_sample_received_callback(io_sample_callback)
    while True:
        pass
except KeyboardInterrupt:
    if device is not None and device.is_open():
        device.close()

# Disconnect from the server
try:
    db_conn.disconnect()
except:
    pass

Listing 10-8Raspberry Pi Remote-Storage Data Aggregator

测试项目

要测试项目,请确保 XBee 适配器中安装了 XBee 协调器节点。在打开 XBee 传感器节点之前,请稍等片刻。所有节点通电后,您就可以开始工作了。发出以下命令启动脚本:

$ python ./pi_xbee_mysql.py

如果您看到语法错误或异常,请确保修复它们并重新运行该命令。如果脚本启动并且没有发生任何事情,您知道它正在工作(或者至少没有任何错误)。回想一下,代码正在等待从 XBee 传感器节点接收数据包(样本)。当节点开始发送数据时,您会看到类似于清单 10-9 中所示的输出。记住,你可以随时通过按下 Ctrl+C 来停止你的脚本。

Connecting to MySQL...done.
Welcome to example of storing data from a set of remote TMP36 sensors in MySQL!
Discovering network.............done.
Found TMP36_2 at 0013A20040A0D45C.
Found TMP36_1 at 0013A2004192DB79.
Found TMP36_3 at 0013A200409029DB.
Reading from D45C: 543, 3.15.
    Temperature is 16.35C. 61.43F
Reading from DB79: 539, 3.82734375.
    Temperature is 15.86C. 60.55F
Reading from 29DB: 523, 3.0796875.
    Temperature is 13.91C. 57.03F
Reading from D45C: 544, 3.15.
    Temperature is 16.47C. 61.65F
Reading from DB79: 539, 3.82734375.
    Temperature is 15.86C. 60.55F
Reading from 29DB: 523, 3.0796875.
    Temperature is 13.91C. 57.03F
Reading from D45C: 544, 3.15.
    Temperature is 16.47C. 61.65F
Reading from DB79: 540, 3.82734375.
    Temperature is 15.98C. 60.77F
Reading from 29DB: 523, 3.084375.
    Temperature is 13.91C. 57.03F

Listing 10-9Example Output for pi_xbee_mysql.py

要查看您的样本是否保存在数据库中,请连接到服务器并执行以下查询:

> SELECT * FROM house.temperature;
+------------+-----+--------+--------+-----------+------------+
| sample_date | address | raw_temp | voltage | fahrenheit | celsius |
+------------+------+-------+--------+-----------+------------+
| 2020-03-23 14:41:24 | D45C | 543 | 3.15    | 61.4282 | 16.349  |
| 2020-03-23 14:41:31 | DB79 |539  | 3.82734 | 60.5484 | 15.8602 |
| 2020-03-23 14:41:34 | 29DB | 523 | 3.07969 | 57.0293 | 13.9052 |
| 2020-03-23 14:41:39 | D45C | 544 | 3.15    | 61.6481 | 16.4712 |
| 2020-03-23 14:41:46 | DB79 | 539 | 3.82734 | 60.5484 | 15.8602 |
| 2020-03-23 14:41:49 | 29DB | 523 | 3.07969 | 57.0293 | 13.9052 |
| 2020-03-23 14:41:54 | D45C | 544 | 3.15 | 61.6481 | 16.4712 |
| 2020-03-23 14:42:02 | DB79 | 540 | 3.82734 | 60.7683 | 15.9824 |
| 2020-03-23 14:42:04 | 29DB | 523 | 3.08437 | 57.0293 | 13.9052 |
| 2020-03-23 14:42:09 | D45C | 544 | 3.15 | 61.6481 | 16.4712 |
| 2020-03-23 14:42:17 | DB79 | 539 | 3.825 | 60.5484 | 15.8602 |
| 2020-03-23 14:42:18 | 29DB | 523 | 3.07969 | 57.0293 | 13.9052 |
+---------------+-------+------+------+-------+------+

您应该在结果集中看到许多行,并且能够将这些行与脚本的输出相匹配。一旦您验证了它正在工作,恭喜您自己:您现在已经掌握了使用 Arduino 和 Raspberry Pi 构建远程存储数据聚合节点!

此外,您已经通过在每个项目中使用相同的 XBee 传感器节点展示了 XBee 模块的多功能性。花些时间试验脚本和存储在数据库中的数据。

为了更多乐趣

那很有趣,不是吗?您可能想知道,对于如此坚实的 Python 代码,您还能做些什么。嗯,你可以做一些很酷的事情。

我能建议的最大挑战是使用这个脚本并稍微重写它,以便通过 ADC 模块从 TMP36 传感器获取数据。换句话说,将 Raspberry Pi 数据聚合节点更改为传感器节点,直接将其数据存储到数据库中。

除此之外,您可能想尝试将脚本更改为守护程序,这样您就可以在后台运行它,同时还可以使用您的 Raspberry Pi 做其他事情。

部件购物清单

本章中的项目没有必需的组件。表 10-3 显示了完成项目可能需要的可选组件列表。其余组件,如 XBee 模块和支持硬件,包含在其他章节的购物清单中;这些如表 10-4 所示。

表 10-3

所需组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| 可堆叠接头套件 | www.sparkfun.com/products/11417 | $1.50–1.95 | 1* |
| www.adafruit.com/products/85 |

*可选,可能不需要。

表 10-4

以前章节中重复使用的组件

|

项目

|

供应商

|

是吗?成本美元

|

所需数量

|
| --- | --- | --- | --- |
| Arduino(任何支持盾牌的) | 各种各样的 | 25 美元及以上 | 每个节点 1 个 |
| XBee 盾 | www.sparkfun.com/products/12847 | $24.95 | one |
| TMP36 传感器 | www.sparkfun.com/products/10988 | $1.50 | 每个传感器节点 1 个 |
| www.adafruit.com/products/165 |
| 0.10uF 电容 | www.sparkfun.com/products/8375 | $0.25 | 每个传感器节点 1 个 |
| 试验板(非迷你) | www.sparkfun.com/products/9567 | $5.95 | 每个传感器节点 1 个 Raspberry Pi 个 |
| 试验板跳线 | www.sparkfun.com/products/8431 | $3.95 | one |
| XBee-ZB (ZB)系列 2 或 2.5 | www.sparkfun.com | $25.00 | 2–4(每个节点 1 个) |
| www.adafruit.com |
| 树莓 Pi 型号 3B+或 4B 2GB 或 4GB | 大多数在线商店 | 35 美元及以上 | Two |
| HDMI 或 HDMI 转 DVI 电缆 | 大多数在线和零售商店 | 变化 | one |
| HDMI 或 DVI 监视器 | 大多数在线和零售商店 | 变化 | one |
| USB 键盘 | 大多数在线和零售商店 | 变化 | one |
| USB 电源 | 大多数在线和零售商店 | 变化 | one |
| USB A 型到微型 USB 插头 | 大多数在线和零售商店 | 变化 | one |
| SD 卡 2GB 或更大 | 大多数在线和零售商店 | 变化 | one |
| 剩余硬盘 | 任何 USB 硬盘(多余的或购买的) | 变化 | one |
| 树莓馅饼+ | www.adafruit.com/product/2029 | $7.95 | one |
| 墙壁适配器 9V(可选) | ?? | $5.95 | 每个节点 1 个** |
| 9V 电池盒(可选) | www.sparkfun.com/products/10512 | $2.95–3.95 | 每个节点 1 个** |
| www.adafruit.com/products/67 |
| XBee Explorer 用标题调节 10 | www.sparkfun.com/products/11373 | $10.95 | 每个传感器节点 1 个 Raspberry Pi 个 |

** 您可以混合搭配这些设备,前提是您有足够的电力为所有节点供电。

摘要

在本章中,您探索了如何构建数据聚合节点,以及如何将传感器节点连接到这些节点来构建无线传感器网络。您了解了如何使用本地存储来存储和显示来自通过 ZigBee (XBee)网络连接的传感器的传感器数据,还了解了如何使用 Raspberry Pi 作为数据库服务器来存储和检索传感器数据。您甚至探索了如何用 Arduino 和 Raspberry Pi 构建数据聚合节点。

在下一章中,我将介绍规划传感器网络的注意事项以及更高级的传感器网络主题。我将讨论如何处理来自多个传感器的传感器数据,您将了解更多关于如何使用 MySQL 数据库来生成报告和数据视图以供分析。

十一、把这一切放在一起

现在,您已经了解了使用 Arduino 和 Raspberry Pi 构建无线传感器网络的基本构建模块,您可以将注意力转向设计和实现传感器网络的一些更复杂的细节。本章探讨了规划传感器网络的注意事项,讨论了一些高级传感器网络主题,并提供了设计数据库的技巧。

传感器网络最佳实践

让我们从讨论一些最佳实践开始 1 你可以利用它们来使你的传感器网络项目更加成功。在本节中,我将讨论规划数据聚合节点、设计数据库的实践,以及构建传感器网络的一些技巧和技术。

数据聚合节点的注意事项

本节分析了规划数据聚合节点的一些重要注意事项。我讨论了网络中节点的放置以及数据存储的设计考虑。

网络类型和节点放置

一个重要的考虑因素是数据聚合节点可用的网络连接类型。这可能由节点的用途或物理位置决定。

如果您计划拥有一个希望通过您的计算机访问的数据聚合节点,您必须考虑将该节点放置在可以将其连接到以太网的位置。这可以通过无线以太网(WiFi)连接或电缆连接来实现。

另一方面,如果您的数据聚合节点通过 XBee 模块与传感器节点通信,模块的范围可能会决定您将数据聚合节点放在哪里。例如,如果您的传感器节点位于外屋或池塘中或池塘附近,离有网络连接的建筑物有一段距离,甚至离 WiFi 太远,您可能无法用计算机连接到该节点,因此必须定期实际访问该节点以检索数据。

这并不意味着你必须跳上你的 ATV 或高尔夫球车,每天晚上跑到老鸡舍去获取数据!事实上,你可以也应该考虑其他选择。首先,您可以使用串联放置的中间 XBee 路由器节点,直到您到达可以放置数据聚合节点的具有网络连接的位置。

这是如何工作的?这是 ZigBee 协议的优势之一——动态创建网络,并将信息从一个路由器转发到另一个路由器,以扩大最大范围。这是使用 API 模式的结果,但您也可以使用 AT 模式轻松控制这一点,并将您的数据发送到特定的路由器,然后该路由器将数据发送到另一个(和另一个)路由器,直到您到达数据聚合节点。

另一种可能性是使用定向 WiFi 连接,该连接使用视线、点对点连接来聚焦 WiFi 信号。你不必花一大笔钱去做这件事!事实上,如果你或你认识的人喜欢品客薯片,你可以使用品客薯片罐来创建一个定向 WiFi 天线( www.makeuseof.com/tag/how-to-make-a-wifi-antenna-out-of-a-pringles-can-nb/ )。

更极端的解决方案是在数据聚合节点上使用蜂窝调制解调器,通过互联网将数据发送到另一个节点。大多数手机运营商不赞成建立可从互联网访问的网络或数据库服务器(有些禁止)。因此,您只能将数据从家庭网络外的数据聚合节点发送到 web 或数据库服务器。此选项会产生连接的经常性费用(您需要一张 SIM 卡和您选择的运营商的数据计划)。

Note

虽然 Raspberry Pi 没有可插拔的蜂窝解决方案,但是您可以使用它的蜂窝模块。这样做需要更多的工作,也许还要构建更复杂的软件,但这应该是可能的。

例如,如果您选择将 Arduino 用于您的数据聚合节点,您可以使用 LTE CAT M1/n b-IoT Shield—spark fun(www.sparkfun.com/products/14997)的萨拉-R4 shield 以及 SIM 卡将您的节点连接到互联网。 2 图 11-1 显示的是盾牌。SparkFun 产品页面上有一个使用调制解调器的草图样本。如果您使用命令 3 拨号调制解调器,您将会认出在线文档中显示的许多命令。

img/313992_2_En_11_Fig1_HTML.jpg

图 11-1

LTE CAT M1/NB-IoT Shield(由 SparkFun 提供)

如果您发现这些解决方案都不起作用,因为您的传感器节点和数据聚合节点距离任何实用(且经济实惠)的网络替代方案都太远,您可能必须考虑将这些数据聚合节点作为本地存储节点,并定期收集数据以用于您的分析。

存储数据

设计数据聚合节点的一个主要考虑因素是它将存储的数据类型:也就是说,节点将支持哪些传感器。通常最好使用数据聚集节点来存储相同传感器的数据或生成相同类型数据的传感器节点的数据。

例如,如果您从几个位置收集温度数据,并从几个池塘收集水位数据,则这两个事件产生的数据是不同的。温度通常是浮点值;水位通常是一个布尔值(1 =水位正常,0 =水位低),它对应于测量水位的最常见形式:浮子和开关。 4

显然,将这两组传感器数据存储在一起需要更多的工作,因为您将混合不同的数据类型。这可能需要做出选择,例如将数据存储在不同的文件中,甚至存储在不同的数据库中。此外,消费数据和检测数据代表什么(传感器的类型)需要更多的逻辑,因为您需要某种方法来检测什么传感器节点具有什么数据类型。

虽然存储水位和温度的问题可能很容易解决,但请考虑存储来自两个传感器的样本,这两个传感器产生相同的数据类型,但被不同地解释。回忆一下读取大气压力的例子。它也表示为浮点数。例如,您如何知道哪个传感器产生了值65.71929——气压传感器还是温度传感器?也许可以编写特定于传感器本身的代码,但如果传感器数据被转发到另一个节点,该怎么办呢?那么你怎么知道如何解释数据呢?

这个问题的一个解决方案是为每组相似的传感器节点使用不同的数据聚集节点。在使用温度和水位传感器节点的示例中,一个数据聚合器用于温度传感器节点,另一个用于水位传感器。

数据聚合节点上本地存储的另一种可能性是存储一个特殊字段,该字段指示数据是从哪个传感器读取的。您可以像在一些示例项目中看到的那样,包括附加信息,如日期和时间以及表示您为传感器指定的名称的文本字符串。清单 11-1 展示了一个使用类似方案的文件格式的例子。第一行是为了文档的目的而提供的,通常不包含在文件中(但是精明的程序员通常会为了文档的目的而包含这样的内容)。

# sensor_number, datetime, value, comment
3, 2020-02-09 14:25:21, 1, Water level Ok pond 1
1, 2020-02-09 14:30:01, 65.90013, Water temp pond 1
3, 2020-02-09 14:37:04, 1, Water level Ok pond 2
2, 2020-02-09 14:38:31, 65.81723, Water temp pond 2
1, 2020-02-09 14:45:23, 66.00123, Water temp pond 1
3, 2020-02-09 14:45:36, 0, Water level LOW! pond 2
3, 2020-02-09 14:54:17, 1, Water level Ok pond 1
2, 2020-02-09 14:59:54, 66.00011, Water temp pond 2
3, 2020-02-09 15:08:41, 1, Water level Ok pond 1
1, 2020-02-09 15:10:22, 65.99913, Water temp pond 1

Listing 11-1Storing Additional Data with the Sample

注意清单中的数据被格式化为逗号分隔值(CSV)文件。这是一种实现选择(您可以选择使用制表符、分号等),使在计算机上阅读文件变得更容易。如果使用 Python,只需几次库调用就可以读取文件。

如果您检查数据,您会发现您必须了解一些关于传感器编号的信息,以便能够解释数据。如果传感器号是 1 或 2,你就知道是温度;但如果是 3 或 4,那就是水位。同样,这可能不是一个大问题,但如果您有一个数据聚合节点从几十个传感器(或者更糟,从为数据聚合节点编写代码后添加到网络中的传感器)接收样本,您可能会以传感器编号中的未知值结束,也就是说,您不知道如何解释这些值,因为您不知道哪种传感器生成了它们。您可以通过为每种类型的样本(传感器)设置单独的数据聚合节点来解决这个问题。

还要注意数据是如何排列的。你看到任何暗示一致性的东西了吗?如果您有数据库方面的知识和经验,毫无疑问您已经意识到了这一点,但是请考虑一下数据库中的表是由什么组成的:行和列,其中列是字段,行是数据。在这种情况下,您可以定义两个表,每个表有四列。水温数据可以在一个表中,因为它的值是一个整数(或者可能是布尔值),而水温数据是一个浮点数。

显然,将这些数据存储在数据库中是有意义的。这就是数据库的用途——为单个行存储逻辑上相关的数据组(在本例中,是一个事件或样本)。记住这一点,让我们看看使用数据库存储传感器数据的注意事项。

传感器网络数据库的注意事项

对数据库设计主题的深入、全面的讨论超出了本书的范围。事实上,关于数据库设计已经有了整本整本的书,甚至好几卷。与其深入研究所有的理论,然后将其与实践联系起来,不如让我们从一个稍微不同的角度来看这个主题:如何最好地设计数据库以便于存储和检索。

Note

我假设没有数据库设计的先验知识。如果您有数据库设计经验,您可能希望浏览这一部分。

正如您在第八章中看到的,您使用 MySQL 数据库系统作为数据库服务器。它不仅是开源的(它是免费的,就像免费啤酒一样),而且也是开发人员最受欢迎的选择,因为它以轻量级的形式提供了大型数据库系统的特性,可以在几乎任何消费类计算机硬件上运行。MySQL 也非常容易使用,它的流行已经产生了许多学习和使用该系统的在线和印刷资源。尽管如此,下面的示例和建议可以用于任何关系数据库服务器(但可能需要对一些服务器的语法稍作修改)。

数据是如何组织的

让我们从讨论数据如何在数据库服务器中分组开始。如您所知,服务器允许您创建任意数量的数据库来存储数据。通常,您希望为每个传感器网络创建一个单独的数据库。这使得处理数据成为一个逻辑整体,因此一个传感器网络的数据不会与另一个传感器网络的数据混杂在一起。

数据库本身是许多对象的容器。在第八章中,你已经看到了表格甚至触发器的例子。在这里,你把注意力放在桌子上。表是一个容器,专门用于保存按其布局(列数及其数据类型)描述(或分类)的数据实例。例如,对于清单 11-1 中显示的数据,您可以为温度传感器生成如下表格:

CREATE TABLE `pond_monitor`.`pond_water_temp` (
  `sensor_number` int DEFAULT NULL,
  `sample_collected_on` timestamp DEFAULT CURRENT_TIMESTAMP,
  `temperature` float DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

注意sample_collected_on field。您将它定义为 timestamp,当在表中插入一行时,MySQL 将用日期和时间填充它。除非您需要绝对的准确性,否则在采集样本后立即设置该值就足以记录采集样本的日期和时间。

如前所述,清单 11-1 中的例子有交错的数据。您希望分离这些数据,因此生成一个表来存储其他样本,如下所示:

CREATE TABLE `pond_monitor`.`pond_water_level` (
  `sensor_number` int DEFAULT NULL,
  `sample_collected_on` timestamp DEFAULT CURRENT_TIMESTAMP,
  `water_level` tinyint DEFAULT '1'
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

你可能想知道评论发生了什么。comments 字段(列)并不是真正需要的。回想一下在第八章中关于存储友好名称的讨论。这里,您创建一个查找表来存储数据。例如,查找表允许您将传感器编号 3 等同于水位池 1 的友好名称:

CREATE TABLE `pond_monitor`.`sensor_names` (
  `sensor_number` int(11) DEFAULT NULL,
  `sensor_name` char(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

你在这里做了什么?首先,您设计了两个表来存储来自两种不同类型的传感器的数据(根据收集的数据类型来定义),并添加了一个查找表来帮助消除重复数据(反复存储水温池 1 会浪费空间)。

但是这对数据聚合节点意味着什么呢?如果您再次考虑清单 11-1 ,您会看到节点必须写入传感器编号,计算并写入时间戳(可能来自板载 RTC 或 RTC 模块),写入来自传感器的值,并(基于查找代码)存储一个注释字符串(以便于人们阅读)。

但是,如果实现前面的表,数据聚合节点只需将传感器号和样本值发送到数据库服务器。听起来熟悉吗?这正是你在第十章的项目中所做的。

表格设计说明

让我们回到设计表格。当你设计你的桌子时,你应该记住一些事情。首先,考虑存储样本需要什么数据类型。您不仅要考虑每个样本包含多少个值,还要考虑它们的格式(数据类型)。可用的基本数据类型包括整数、浮点、双精度、字符和布尔。还有许多其他的,包括用于日期和时间的几个,以及用于存储大数据块(如图像)的二进制大对象(blob),大文本(与 blob 相同,但不解释为二进制)等等。有关所有数据类型( https://dev.mysql.com/doc/refman/8.0/en/data-types.html )的完整列表和讨论,请参见在线 MySQL 参考手册。

您还可以考虑添加额外的列,比如时间戳字段、传感器节点的地址、参考电压等等。写下所有这些,并考虑每个的数据类型。

一旦决定了表中的列,接下来要考虑的是表中是否允许重复,即包含相同数据的两行或更多行。为了避免这种情况,可以通过指定一个或多个列作为键来定义主键(特殊索引)。您希望选择一列(或多列),以确保该列中没有两行具有相同的数据。

例如,如果您选择前面示例中的sensor_number作为主键,那么您肯定有问题。事实上,当您试图为每个传感器保存第二个值时,数据库服务器会抱怨。为什么呢?因为要成为主键,sensor_number列必须包含表中每一行的唯一值!

但是表的布局不包含任何保证唯一的列。您可能认为时间戳字段可以是唯一的,但尽管这可能是真的,您通常不会对主键使用时间戳字段。那么,在这种情况下你会怎么做?

您可以使用自动生成的列作为主键。它在 MySQL 中被称为自动递增字段属性。您可以将它添加到任何表中,如下所示使用ALTER TABLE命令:

ALTER TABLE `pond_monitor`.`pond_water_temp`
    ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY FIRST;

这里添加一个名为id的新列,它是一个自动递增的字段,也是主键。添加第一个修饰符是因为主键列应该是表中的第一列(通常顺序并不重要,但在这里很重要)。

您可以对两个表执行相同的操作。一旦这样做了,当插入一个新行时,就像对时间戳字段那样指定NULL, MySQL 会为您填充数据。清单 11-2 显示了这一原则的作用。

> INSERT INTO `pond_monitor`.`pond_water_temp` (sensor_number, temperature) VALUES (3, 72.56);
Query OK, 1 row affected (0.00 sec)

> SELECT * FROM `pond_monitor`.`pond_water_temp`;
+----+---------------+---------------------+-------------+
| id | sensor_number | sample_collected_on | temperature |
+----+---------------+---------------------+-------------+
|  1 |             3 | 2020-02-10 11:39:51 |       72.56 |
+----+---------------+---------------------+-------------+
1 row in set (0.0004 sec)

> INSERT INTO `pond_monitor`.`pond_water_temp` (sensor_number, temperature) VALUES (3, 82.01);
Query OK, 1 row affected (0.00 sec)

> SELECT * FROM `pond_monitor`.`pond_water_temp`;
+----+---------------+---------------------+-------------+
| id | sensor_number | sample_collected_on | temperature |
+----+---------------+---------------------+-------------+
|  1 |             3 | 2020-02-10 11:39:51 |       72.56 |
|  2 |             3 | 2020-02-10 11:40:53 |       82.01 |
+----+---------------+---------------------+-------------+
2 rows in set (0.0005 sec)

Listing 11-2Auto-Increment Fields

Does that Really do Anything?

您可能会认为这个新字段向表中添加了一个人工主键,实际上并没有做任何事情。在很大程度上,你是正确的。

这个例子是为了说明的目的,因此教导了使用主键的概念,这是您在设计表时应该考虑的一个实践。实际上,自动递增键不用于引用另一个表,或者它与行本身相关,这一点在实践中被忽略了。

让我们回到查找表。虽然这个表不太可能有很多行(这取决于传感器的数量),但是这个表中的一行与一个且仅一个传感器匹配也是事实。因此,您可以在这里使用sensor_number列作为主键。我将ALTER TABLE语句留给您考虑。

Note

数据库设计者有时放弃在只有几行的表上使用主键,理由是维护索引需要额外的开销等等。事实上,这两种方式都无关紧要,因为查找表很少被修改(更改或添加数据),如果频繁使用,可能会导致整个表被缓存。也就是说,添加一个主键没有坏处。

添加索引以提高查询效率

主键是一种特殊类型的索引。它是一个索引,但是当与自动递增字段一起使用时,它是一种识别给定行并允许重复行(在其他列中)的好方法。然而,索引的另一个方面可以使您的数据访问更容易(也可能更快)。

现在考虑一个有几千行的表。 5 假设您想查看 2 号传感器的所有传感器样本。数据库服务器如何使用您之前定义的表找到所有这些行?您发出下面的查询,但是服务器内部发生了什么呢?

SELECT * FROM `pond_monitor`.`pond_water_temp` WHERE sensor_number = 2;

因为该表有一个索引(您添加的列id上的主键),所以它使用这个索引系统地按顺序读取每一行,选择(返回)那些匹配WHERE子句的行(在本例中是 sensor_ number = 2)。这很糟糕,因为服务器不知道这些行是否出现在前 N 行,甚至不知道sensor_number = 2是否在表的最后一行。因此,它必须读取每一行。这称为表扫描,最好在处理包含许多行的表时避免。

你是怎么做到的?通过添加另一个称为二级索引的索引!如果在sensor_number列上有一个索引,服务器可以使用该索引以不同的顺序检查每一行。它会从 r 1 开始浏览表格,然后是 2,依此类推。它知道在读到最后一行sensor_number为 2 时停止。事实上,MySQL 包含了一些额外的技巧,允许服务器(称为优化器的部分)进一步加速查询并跳到第一行sensor_number = 2。这是你怎么做的。您可以使用创建索引命令:

CREATE INDEX s_num ON `pond_monitor`.`pond_water_temp` (sensor_number);

CREATE INDEX命令允许您命名索引(s_num)并在括号中指定您想要索引的表(ON pond_water_temp)和列(sensor_number)。你可以在在线 MySQL 参考手册中看到这个命令和 MySQL 支持的所有其他命令的完整语法解释。

现在,当您发出前面的SELECT时,服务器使用新的索引以不同的顺序读取行。请注意,这些行在磁盘上没有被重新排序;相反,索引会创建一个备用的映射或访问方法,以特定的顺序查找行。

您可能会想,“但是等等:我不能一次完成所有这些表格设计步骤吗?”答案是肯定的,可以!让我们把pond_water_temp表看作一个单独的CREATE语句:

CREATE TABLE `pond_monitor`.`pond_water_temp` (
  `id` int NOT NULL AUTO_INCREMENT,
  `sensor_number` int DEFAULT NULL,
  `sample_collected_on` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `temperature` float DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `s_num` (`sensor_number`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1

;

请注意,首先定义的是自动递增列,然后是您的传感器编号、收集样本时的时间戳、值(温度)以及主键和辅助索引定义。这条语句取代了您刚才使用的三条语句——CREATE TABLE、ALTER TABLECREATE INDEX。酷吧。

正如您所看到的,一旦您理解了语法(并且知道您想要做什么),在 MySQL 中创建表是很容易的。你可以在在线 MySQL 参考手册( https://dev.mysql.com/doc/refman/8.0/en/sql-statements.html )中找到所有的语法和每个语法的许多例子。

同样,在设计表时要考虑的事情要多得多,但是这些至少是您需要知道的事情,以便充分利用您的数据库系统并有效地存储和检索您的数据。

其他考虑因素

本节探讨一些额外的最佳实践,这些实践有助于让您更愉快地使用传感器网络。

保持在 XBee 模块的范围内

XBee 模块具有令人印象深刻的范围,掩盖了其微小的尺寸。您在本书中使用的 XBee 系列 2 模块的规格如下

  • 室内/市区:最高 133 英尺

  • 室外视线:最远 400 英尺

然而,这些最大值在很大程度上受相似频率设备的干扰和您所在建筑的组成的影响。例如,假设你的房子很旧,有灰泥墙和铁皮屋顶。您家中的任何无线设备在低于指定范围的情况下都能正常工作。您需要在自己的位置测试您的 XBee 范围,以找到您的最大范围。如果这不切实际,我建议您在规划网络时将规格值减半,以确保不会将基于 XBee 的传感器节点置于范围之外。

在开发传感器网络之前,您可以测试 XBee 模块的最大范围。一种方法是创建 XBee 温度传感器节点项目,并在通过 XCTU 应用程序连接的笔记本电脑上使用 USB XBee 适配器。接下来,我将介绍一种不科学的方法来确定最大可靠范围。为此,您的传感器节点应该设置为每隔几秒钟(比如每 10 秒钟)发送一次数据,并运行 API 固件:

  1. 将协调器节点连接到笔记本电脑。

  2. 通过 XCTU 连接到协调员。

  3. 将传感器节点放在预定位置。

  4. 将您的笔记本电脑放在传感器节点附近(几英尺之内),并打开传感器节点的电源。

  5. 等到 XBee 网络形成,你开始接收数据。

  6. 慢慢走开,观察协调员收到的数据。

  7. 当协调器开始呈现错误包或停止接收数据时,您已经做得太过分了。

这种方法并不科学,但它可以给你一个粗略的标准,判断你的传感器节点是否离你的数据聚集节点足够近(在范围内)。

保存工程日志

许多开发人员、工程师和科学家使用 Evernote ( http://evernote.com/ )这样的应用程序在纸质笔记本或数字笔记本上记录他们的项目。当你没有时间或者使用纸笔太危险时,录音机也可以方便地捕捉那些即兴的想法。有些人比其他人更详细,但大多数人会在会议和电话交谈中做笔记,从而提供口头交流的书面记录。

最好的笔记者会在他们的想法出现时写下来。有时候,当你在执行琐碎的任务时,头脑工作得最好,想法会突然出现。当这种情况发生时,优秀的工程师知道记下这些想法——即使它们后来变得一文不值——因为最好的想法往往始于一个简单的概念。没有写下这些小细节往往会导致更多的实验,甚至浪费时间去寻找替代品。

如果你没有记录工程日志的习惯,你应该考虑这样做。我发现日志是我工作中的一个重要工具。是的,写东西确实需要更多的努力,如果你试图包含所有你认为重要的各种图纸和电子邮件,日志可能会变得混乱(我的笔记本经常塞满了从重要文件中剪下的剪报,就像某种工程师的剪贴簿)。然而,回报可能是巨大的。

在设计传感器网络以及相关的无数传感器和电子电路时尤其如此。你可能在一个贸易展(或制造商博览会)上看到一些真正激发灵感的东西。或者你在杂志上看到一个电路,或者发现一个非常酷的传感器,但需要设计一个电路来容纳它。写下这些想法可以让你实现目标。

它还可以帮助您记住概念和关键信息,如传感器的接线方式,以避免返工(或猜测),这可能会导致组件故障和挫折。我非常感谢我保留了一个日志,当我仔细检查我的布线时,只发现一个放错地方的跳线或布线。它节省了我的时间和金钱(不必更换油炸组件)。

当然,你可以使用任何类型的笔记本电脑;但是如果你想让你的笔记更高级一点,你可以买一个专门用来保存工程笔记的笔记本。这些通常有柔和的网格线,有时还有记录关键信息的文本区域,如项目名称和页码。我最喜欢的笔记本之一是来自创客小屋( www.makershed.com/products/makers-notebook-hard-bound )的创客笔记本。

这个笔记本有 150 页标有数字的图纸,每一页都有一个特殊的标题,用来标注项目名称、日期和页面参考指针。它还包括一些很好的附加功能,如一个目录空间,一个口袋,用于存放那些你写给自己但后来因为自己的笔迹而无法阅读的小纸条, 8 ,以及带有电子元件的贴纸,用于制作电路。这款笔记本比一般的线条或网格填充笔记本贵一点,但如果你想要一个好工具来帮助管理多个项目的笔记,它是值得一看的。

How to Manage Pages for Multiple Projects

为正在进行的多个项目保存一个笔记本的挑战之一是如何管理页面。也就是说,如果你正在进行项目 X,并在进行项目 Y 的过程中写下一些非常酷的想法,你如何跟踪每个项目的页面呢?

制作者的笔记本解决了这个问题,它允许你在每一页的底部留出空间来记录下一页的页码。当你的项目笔记开始交错时,这真的很有帮助。可以把它想象成一种手动链表。

另一个解决办法是在你的笔记本前面保留一个活索引,列出每个项目的页码。这没有 Maker Shed 解决方案好,但很有效。

综合考虑:测试和部署您的传感器网络

本书中的项目旨在教你如何通过将任务分解成可以组合的更小的组件来构建传感器网络。除了第十章中的项目(它们是完整的传感器网络示例),您可以在与其他项目相对隔离的情况下实施每个项目。有些是替代实现,比如展示相同项目首先使用 Arduino,然后使用 Raspberry Pi 的例子。

在某些情况下,尤其是在“为了更多的乐趣”一节中,我已经建议了一些修改和替代解决方案供您试验。实验是一种很好的学习方式,但在准备自己的传感器网络时,您应该考虑对解决方案进行更正式的评估。

换句话说,在部署网络之前先对其进行测试。我无法告诉你有多少次一个精心设计的硬件设计由于一些与设计无关的意外结果而失败。例如,您可能会发现在规划网络时不存在或没有考虑到的物理障碍;或者该区域可用的电缆或电源可能有故障;或者,您可能会发现您的无线电在目标环境中的实际范围比预期的要短。无论是哪种情况,通常在部署解决方案之前对其进行基准测试有助于消除节点本身的问题,让您能够专注于不同之处—物理环境。

我所说的基准测试是指在一个位置组装组件,并启动所有组件,就像在现场部署一样。这不仅允许您确保传感器网络正常工作,还允许您监控节点本身的异常情况。例如,错误地连接一个组件可能会损坏它,但有时您可以通过快速切断电源来挽救该组件。

让我们考虑第十章的最后一个项目——一个传感器网络,包括一个数据库节点、一个数据聚合节点和几个传感器节点。有几种很好的方法来测试这样的网络,但是下面的方法可以帮助您诊断在部署自己的传感器网络时可能遇到的问题。在这种情况下,我假设每个节点的软件都正确安装并运行(即 XBee 节点配置正确,草图和脚本运行正常):

  1. 从数据库节点开始,启动它并测试网络的连接性。确保您可以作为计划使用的用户帐户连接到 MySQL(并从需要访问它的机器-IP 地址-连接到 MySQL ),并且该用户具有更新您设计的数据库的权限。有关向用户授予权限的更多信息,请参见在线 MySQL 参考手册。

  2. 移动到数据聚合节点,并修改草图以将虚拟数据插入到数据库中。回到数据库,确保数据已经插入。

  3. 关闭数据聚合节点的电源,并将协调器 XBee 模块移至 USB XBee 适配器。将其连接到您的笔记本电脑,打开终端应用程序,并使用 XBee 模块连接到 USB 端口。

  4. 打开每个传感器节点的电源,一次一个,并观察终端窗口。确保每个传感器节点发送数据,并且数据被接收(在终端中回显)。关闭所有 XBee 节点的电源,并从笔记本电脑上移除协调器节点。

  5. 将数据聚合节点返回到其运行状态(包括运行最终草图或脚本),并给它加电。等待大约 5 分钟,然后打开传感器节点的电源。连接到数据库服务器,并确保数据被插入到表中。

一旦您的传感器网络组装完毕并正常运行,您就可以开始考虑部署了。我更喜欢先部署传感器节点,并且(如果可能的话)将我的数据聚合节点和数据库服务器移动到离传感器节点更近的地方。然后,我打开所有的电源,从数据库服务器开始,然后是数据聚集节点;然后,我等待 5 分钟,打开传感器节点的电源。

将数据聚集节点移动到靠近传感器节点的实际位置有助于最小化范围或障碍物的任何问题。当我看到网络运行正常时,我关闭所有设备,将我的数据聚合节点部署到正确的位置,然后重新开始这个过程。一旦这个阶段成功了,我就部署数据库服务器并再次测试网络。当一切正常工作时,我再次关闭所有电源,擦除数据(使用DELETE SQL语句)样本数据,并打开所有电源。至此,我的部署已经完成,我可以进入下一个阶段:使用积累的数据进行分析。

考虑到这些最佳实践和注意事项,让我们来看一个有时会导致即兴修补的主题。 9

Note

本章中的项目用于演示目的,因此不包括构建每个项目的所有步骤。然而,鉴于您目前的知识水平,您可以轻松地填补缺失的部分。

选择传感器节点

当您考虑如何安装传感器时,您需要做出一些选择。有时这个决定是基于传感器的类型和它产生的数据。例如,您可以使用 Arduino 托管几乎任何传感器。通过一些额外的硬件,你可以用 Raspberry Pi 做到这一点;您还可以使用 XBee 模块托管某些传感器(参见第二章)。

但是其他因素可以决定传感器节点和数据聚合节点的配置。其中包括要使用的网络类型,以及传感器节点是使用以太网还是 XBee (ZigBee)进行通信。到目前为止,您还没有研究过传感器节点的许多替代配置。在本节中,我将更详细地讨论这些方面。

有线还是无线?

我在第十章中提到,我认为有线以太网连接是数据聚合节点的一个要求。但那只是最典型的情况。可能是你有 WiFi 以太网代替。

主要原因是数据聚集节点通常比传感器节点被更频繁地访问。您可以包括拥有 web 服务器的数据聚合节点,正如您在第十章中看到的,或者您可以决定让数据聚合节点将数据发送到另一个节点(如数据库)进行存储。在这些情况下,拥有一个快速可靠的网络是必须的。

通常,您使用 XBee 模块和 ZigBee 协议将数据聚合节点连接到传感器节点。但是,您可以使用 ZigBee 中的 API 协议与您的数据聚合节点进行通信。挑战在于构建一组例程来匹配您打算如何与数据聚集节点进行交互。这不是不可能的(我已经看到了设计这种网络的人的证明),但是这需要更多的工作,并且消除了数据访问的许多可能性。

主要考虑的是将数据聚合节点放在最可靠的网络介质上。有线以太网是最强大的,其次是 WiFi 和 ZigBee。如果数据将存储在本地并手动检索,那么网络介质的选择可能并不重要。但是,如果您需要远程访问数据或者将数据存储在远程节点上(比如数据库服务器),那么有线以太网绝对是正确的选择。

Arduino 还是树莓 Pi?

选择 Arduino 还是 Raspberry Pi 应该基于很多因素。这些因素包括成本、安全性、功能性、可扩展性和连接性。您的选择可能基于其中的一个或多个,并可能决定(或限制)您的传感器或数据聚合节点的实施。

费用

如果您正在规划一个大型网络或预算有限,成本可能是首要考虑的问题。这可能会在每个节点的成本中看到。传感器是典型的商品,不同供应商的价格通常相差不大。但是主机本身的价格可能会有所不同。让我们从成本的角度来看每块板。

树莓派 3B(在美国)目前的平均价格约为 40 美元。这比这些主板的建议零售价高出了 5 美元,但是考虑到高需求和有限的供应,供应商要价更高也就不足为奇了。

Arduino 的价格有点难以确定。因为 Raspberry Pi 是闭源的,而 Arduino 是开源的,所以你可以发现很多不同的供应商在销售各种 Arduino 兼容的主板。虽然你可以从不同的供应商那里买到一个树莓派,但是没有树莓派的克隆版本。因此,你可以找到任何种类的 Arduino 兼容板,起价低至 15 美元。目前,一个 Uno 或 Leonardo 复制品的平均价格(在易贝和亚马逊上)约为 20 美元。

如果您计划部署 20 个传感器节点(没有一个是基于 XBee 的),那么通过选择 Arduino 而不是 Raspberry Pi,您可以节省大量成本。例如,如果您发现 Raspberry Pi 板的价格为每块 40.00 美元,Arduino 兼容板的价格为每块 20.00 美元,那么使用 Raspberry Pi 板将比 Arduino 板多花费 400.00 美元。

然而,如果你必须用屏蔽来增加你的 Arduino 板,屏蔽的成本可能会使你的总支出更接近 Raspberry Pi 的成本(除非你购买 Uno WiFi 版本)。在某些情况下,买一个 Arduino 和一个 shield 甚至比买一个 Raspberry Pi 还要贵。总的来说,如果成本是一个问题,Arduino 通常是更便宜的选择。

安全

在本书中,我没有过多地讨论安全或保护您的传感器和数据聚合节点。让我们花点时间来简单考虑一下这个话题。

我们通常都认为数据库节点应该是安全的,只有少量的密码安全和访问限制, 10 传感器节点本身呢?盗窃可能不太令人担心,但您至少应该考虑保护您的传感器节点免遭盗窃。寻找机会目标的普通窃贼不太可能窃取您的传感器节点。 11

但是,对节点的物理访问是一个问题。虽然如果有人可以直接访问 Arduino 节点,他就有可能利用 Arduino 节点,但是使用 Arduino 比使用 Raspberry Pi 要困难得多。主要原因是 Arduino 是电子加载的;有人可以对微控制器重新编程,但如果不知道草图是怎么写的,他们也无能为力。但是利用 Raspberry Pi 节点所需要的只是一张加载了新操作系统的 SD 卡。因此,你应该考虑让某人尽可能难以物理访问你的 Raspberry Pi 节点——尤其是当他们连接到你的本地网络或互联网时。

可悲的是,还有另一个问题——电子入侵。因为 Arduino 是一个微控制器,所以不太可能有人试图连接到它来进行邪恶的活动。有人试图利用 Raspberry Pi 节点的可能性要大得多。这意味着您在部署基于 Raspberry Pi 的节点时必须更加小心。基本的安全实践有很长的路要走,但是如果您不注意防范入侵,您的 Raspberry Pi 节点可能会很容易受到攻击。

如果你关心你的节点的安全,你应该考虑阅读更多关于传感器网络安全的知识。然而,这里的底线是 Raspberry Pi 节点比 Arduino 节点更容易被利用。

功能

主机提供的功能是您可能希望关注的另一个方面。如果您希望添加功能,例如 web 服务器、本地数据库服务器、通过 SSH 的远程访问,或者连接到硬盘、键盘、显示器等外围设备,那么您别无选择。Raspberry Pi 是一台功能齐全的个人电脑(和迷你服务器)。

另一方面,Arduino 非常容易编程,并且具有更广泛的硬件支持基础,因此可以支持更广泛的传感器选项,甚至电子电路。这是因为 Arduino 拥有比 Raspberry Pi 的 GPIO 更强大的硬件接口。

例如,假设 Raspberry Pi 需要一个 ADC 来与模拟传感器接口。因此,如果您计划仅使用模拟传感器,但仍需要 Raspberry Pi 的功能,传感器的成本会稍高(ADC 模块的价格)。

这取决于您是需要类似计算机的功能还是更好的硬件支持选项。如果您的节点需要个人计算机或服务器功能,您应该选择 Raspberry Pi。如果您需要支持更多样化的传感器和相关硬件,您应该选择 Arduino。

膨胀性

可扩展性(也可称为可伸缩性)与功能性密切相关。我把它作为一个单独的考虑因素,因为它与传感器网络有关。您应该考虑可扩展性的两个方面:可插拔模块的可用性和向节点添加更多功能的能力。

Arduino 是可插拔模块可用性的明显赢家。有几十种屏蔽支持各种硬件特性。从简单的传感器板到 XBee 主机,再到先进的电机控制和机器人技术,您可以为传感器网络做任何事情。

这并不意味着你应该把覆盆子酱排除在外。如果您需要在一个节点上存储大量数据,您不太可能选择 Arduino,因为将本地硬盘添加到 Raspberry Pi 非常容易。同样,如果您需要复杂的显示功能,Raspberry Pi 不需要额外的硬件(只需使用显示器)。

Note

您确实可以在 Arduino 上使用中小型液晶面板。Arduino IDE 中有很多例子,包括示例草图。然而,编写一个 Python 脚本来产生屏幕输出要比试图在一个小 LCD 上塞满大量信息容易得多。

因此,如果您从电子角度需要可扩展性,您应该选择 Arduino。如果您需要更多的可扩展性来连接存储设备或显示大量数据,您应该选择 Raspberry Pi。

连通性

最后要考虑的领域是连接性。还是那句话,这取决于你的视角。如果您想通过 XBee 模块将您的节点连接到其他节点,这些平台同样能够胜任。

如果您计划将您的节点连接到以太网,您必须考虑这样一个事实,即 Raspberry Pi 3B、3B+和 4B 带有 lan 端口(以太网)和 WiFi,而 Arduino(不包括 Yun 和 Arduino Uno 以太网变体)需要以太网或 WiFi 屏蔽;所以成本可能会近很多。例如,你可以花大约 30.00 美元购买一个基本的 Arduino 以太网克隆屏蔽。鉴于 Arduino 的旧克隆板价格约为 20 美元,您的成本已经超过了 Raspberry Pi。

然而,Arduino 目前在连接方面比 Raspberry Pi 有一个优势:它更容易连接专门的硬件。回想一下前面关于使用蜂窝调制解调器将节点连接到互联网以收集数据的讨论。因为 Raspberry Pi 没有可插拔的解决方案,所以在这种情况下 Arduino 是更好的选择。这也适用于通过使用专用屏蔽提供的其他形式的连接。

因此,考虑到以太网和蓝牙的连接性,Raspberry Pi 具有优势,而蜂窝调制解调器等专用通信则使 Arduino 具有优势。

Tip

有些情况下,您可能希望拥有 Raspberry Pi 的强大功能,但又希望拥有 Arduino 的灵活性和可扩展性。我将在下一节揭示一个这样的解决方案。

现在,您已经了解了选择要使用的主机的一些注意事项,让我们看看您可能想要考虑的几个备选解决方案——从专门构建的传感器节点开始。

替代主机

本节考虑了两种基于传感器和数据聚合节点的备选方案。您会看到一个专为传感器网络和户外操作设计的 Arduino 兼容板,以及一个子板,旨在创建一个结合了 Raspberry Pi 和 Arduino 的混合节点。

Seeed Studio 无线传感器套件

最好的 Arduino 兼容套件之一是 Seeed Studio 无线传感器套件——也称为 Stalker 防水太阳能套件( http://wiki.seeedstudio.com/Seeeduino_Stalker_V3-Waterproof_Solar_Kit/ )。该套件由 Seeed Studio Stalker 板、太阳能电池板、电池组、外壳(一个玻璃罐!)、XBee 适配器、硬件和附件。图 11-2 显示了套件中包含的内容。

img/313992_2_En_11_Fig2_HTML.jpg

图 11-2

Seeed Studio Stalker 无线传感器节点(Seeed Studio 提供)

Seeed Studio Stalker 板是一个 Seeeduino (Arduino 兼容)板,是该套件中真正的瑰宝。它不仅与 Arduino 完全兼容(因为它具有相同的处理器),而且还具有板载 RTC、XBee 头、microSD 卡驱动器、实时时钟(用于记录样本日期时间)、Grove 连接器等。完整规格可在 http://wiki.seeedstudio.com/Seeeduino-Stalker_v3/ 找到。图 11-3 更详细地展示了跟踪板的照片。

img/313992_2_En_11_Fig3_HTML.jpg

图 11-3

Seeeduino Stalker Arduino 兼容板(由 Seeed Studio 提供)

Stalker 基于其板载 XBee 支持作为无线传感器节点进行营销。你可能想知道为什么我把对这个板的讨论留到了书的结尾。简而言之,Stalker 是一个专门的电路板,需要用非常特定的硬件和软件来构建您的传感器节点。虽然它确实可以通过消除大量连接模块和与模块接口的困难工作来简化传感器网络的构建,但这种性质使得它对于了解传感器节点是如何构建的价值不大。

最好学习组装传感器节点的基本构建模块,这样当您开始使用更高级的传感器网络或将高级传感器集成到传感器节点中时,您就有了使用它们的适当经验和知识。此外,从头开始构建东西要有趣得多。 十三

然而,如果主板的特性是你所需要的,那么你应该考虑尽可能多地使用这些特性。成本有点高,可以想象。该套件的成本约为 59.50 美元,主板本身为 39.00 美元。如果你认为该板有一个 RTC 和 XBee 接头,39.00 美元的成本低于购买一个 Arduino,单独的 XBee 屏蔽和 RTC 模块相结合。

所有的机载功能都可以在你的草图中使用。例如,您可以只使用一个方法调用从板载 RTC(ds 3231 芯片有一个温度传感器)读取温度。要获得此功能,您必须从 https://jeelabs.org/pub/docs/rtclib/ 下载并安装 DS3231 库。

DS3231 库和 Stalker 使构建和部署温度传感器节点变得非常容易。你所需要做的就是添加你在以前的项目中探索过的 XBee 代码,你就可以快速构建一个太阳能供电的无线温度传感器节点。酷。

Tip

你可以在公司的维基上找到更多关于编程 Seeed Studio Stalker 的信息( http://wiki.seeedstudio.com/Seeeduino_Stalker_V3.1/ )。

回到套件的太阳能部分,Stalker 有一个锂聚合物(LiPo)电池充电电路,专门设计用于连接太阳能电池板和 LiPo 电池。太阳能电池板在白天为电池充电,为节点提供足够的电力运行一整夜(假设您的 XBee 正在使用睡眠模式,并且您没有太多消耗电力的电路)。这意味着您可以构建这一套件,并在户外使用它来将传感器数据传输到您的传感器网络,而不必担心供电或网络连接。如果你有一个没有电的附属建筑(或池塘),这个工具包有你需要安装一个远程传感器的功能。

我最喜欢 Seeed Studio Stalker 的一点是,它是一个完全兼容的 Arduino 克隆。如果您不使用防水外壳中的 Stalker,您可以使用它来代替您的一个 Arduino 节点(因为它是 Arduino)。借助板载 RTC、XBee 接头和 microSD 卡驱动器,您甚至可以将该板用于所有传感器节点,包括数据聚合器。

如果您正在规划一个家庭温度监控传感器网络,您至少应该考虑将此板用于您的远程传感器。然而,考虑到您在无线传感器套件中获得的所有好东西,这是一个很好的价值。

树莓派阿拉姆德

你可以考虑的另一种变体是树莓派阿拉莫代。该板是一个非常特殊的硬件,旨在弥合 Arduino 和 Raspberry Pi 之间的差距。虽然最初是为早期版本的 Raspberry Pi 开发的,但 Alamode 是 Raspberry Pi 的子板,可插入 GPIO 头,并具有完全兼容的 Arduino 克隆。

Seeed Studio 也提供这种板,具有许多相同的功能。详见 www.seeedstudio.com/Alamode-Arduino-Compatible-Raspberry-Pi-Plate-p-1285.html 。图 11-4 显示了板子的照片。

img/313992_2_En_11_Fig4_HTML.jpg

图 11-4

raspberry Pi Alamode(Seeed Studio 提供)

更具体地说,Alamode 是一个 Arduino 兼容板,可以连接到您的 Raspberry Pi 你可以写草图,通过树莓 Pi 上的另一个程序与之互动。像 Seeed Studio Stalker 一样,它也支持 Arduino shields,因此您可以利用这些 shields 编写草图,并通过 Alamode 上的草图将功能传递给 Raspberry Pi。您还可以在 Raspberry Pi 上运行 Arduino IDE,以便在 Alamode 上加载草图。Alamode 的一些最佳特性包括:

  • 兼容 Arduino

  • 通过 GPIO 头连接到 Raspberry Pi

  • 自动控制 GPIO 接头上的电压,在 GPIO 上提供 3.3V 安全电压,但为 Alamode 供电 5V

  • 有一个单独的微型 USB 端口为 Alamode 供电

  • 支持用于控制伺服系统的接头

  • 微型驱动器

  • 可由 Raspberry Pi 使用的板载 RTC

  • 支持 FTDI、ICSP 和 GPS 模块的附加标题

Alamode 代表了传感器节点的独特硬件解决方案。它允许您在单个节点上同时使用两个平台的优点。假设您需要使用仅适用于 Arduino 的特殊组件或功能,但您还需要计算机资源,如功能全面的 web 服务器和大量存储空间来存储数据聚合节点。要解决这个问题,你必须找到一种方法将 Arduino 连接到你的 Raspberry Pi。阿拉莫德就是那座桥。你可以为 Alamode 写草图(甚至直接从 Raspberry Pi!)提供来自任何屏蔽、传感器或其他连接到 Alamode Arduino 接头的硬件的数据。

例如,您可以从 Raspberry Pi 访问 Alamode 上的 RTC。为此,您必须在 Raspberry Pi 上安装 I2C 驱动程序。幸运的是,您在本书的前面部分实现了这一点。设置并不太复杂,只需要向 Raspberry Pi 添加一个新模块,这样它就可以通过 I2C 接口从 Alamode 上的 RTC 获取日期和时间。事实上,您可以像访问任何 I2C RTC 模块一样访问它。Adafruit ( http://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi )提供了通过 I2C 访问 RTC 的完整步骤。

由于其独特性和实现方式,存在一些限制。首先,虽然您确实可以从 Raspberry Pi 使用 Arduino IDE,但这样做需要为 IDE 安装一个特殊的补丁,该补丁会稍微更改 IDE 以识别 Alamode。您可以下载该修补程序,并在 Raspberry Pi 上使用以下命令应用它:

$ wget www.wyolum.com/downloads/alamode-setup.tar.gz
$ tar -xvzf alamode-setup.tar.gz
$ cd alamode-setup
$ sudo ./setup

完成这些步骤并重启 IDE 后,您会看到 Alamode 列在 Board 子菜单下。要设置 Alamode,请从该菜单中选择它,然后选择/dev/ttyS0 串行端口。

Tip

可以在 http://wyolum.com/projects/alamode/alamode-getting-started/ 找到 Alamode 的完整入门指南。

Alamode 和 Raspberry Pi 之间的通信可以使用内置于 Arduino IDE 中的 Firmata 库来完成。幸运的是,您可以在 Arduino IDE 中探索许多例子。Alamode wiki 上也有演练( http://wyolum.com/projects/alamode/ )。

Raspberry Pi Alamode 仍然是一个非常新的产品,还没有被充分利用(至少没有报道或记录)来实现其全部潜力。然而,我相信,如果你需要一个可用于 Arduino 的特殊硬件,但你需要直接在 Raspberry Pi 上使用它(如蜂窝盾),该产品可能会提供一个出色的解决方案。

项目:家庭温度监控网络

如果我没有一个项目要讨论,这一章看起来是不完整的。不过,到目前为止,您已经掌握了使用 Arduino 板、Raspberry Pi 计算机甚至专用 XBee 传感器节点构建传感器网络所需的所有知识。因此,本节不是提供另一个分步示例,而是介绍创建家庭温度监控网络的规划阶段。

这个项目看起来很像第十章的项目。那是故意的。构建这个网络所需的一切都在那个项目中得到了演示。我在这里讨论的是实际设计和部署这样一个传感器网络的考虑因素。该项目的目的是为如何开始规划和实施传感器网络提供一个可行的实例。

规划考虑

规划传感器网络时,你需要问的第一个问题是“为什么?”第二个问题是“我期望从数据中得到什么?”创建家庭温度网络的原因多种多样,但一般来说,您希望能够跟踪家庭的环境温度,以便您可以计划对加热和冷却系统进行更改,或者验证它们是否正常工作(恒温器设置与实际测量的温度相匹配)。

至于为什么要创建网络,请考虑这样的情况:房子很大,有几个供暖和空调系统(HVAC ),或者随着时间的推移进行了扩展,以包括由不同 HVAC 系统隔离或支撑较差的房间。或者你可能有更多的个人原因,比如家庭成员对冷热的不同看法,或者需要保护敏感的医疗设备。不管是什么原因,它们都应该被视为一个设计元素。

对于此示例,假设原因是您的家有多个 HVAC 系统,并且多年来一直在扩建,以至于一些房间在一年中的不同季节会明显变暖或变冷。在这种情况下,您希望能够预测室外气候(温度)对室内的影响。

规划节点

接下来,您应该评估传感器网络的可用资源。假设家里有两层楼,只有一楼和二楼的一个房间有线连接以太网,但有一个无线以太网路由器(无线接入端口),可以从家里的任何地方访问。有四个卧室,一个书房,一个厨房,一个正式的餐厅,三个浴室,和一个阳光房(封闭的门廊)。该住宅的结构将无线电信号限制在不超过 30-40 英尺的范围内。

这些标准意味着您必须按照特定的模型来设计传感器网络。也就是说,您需要随着时间的推移从多个传感器收集数据。您可以使用多达 12 个(11 个室内,1 个室外),但假设您确定了家中的 5 个区域,代表温度可能与家中其他区域不同的关键区域。

如果每 24 小时采集 6 次传感器样本,每天将存储 36 个样本(每个传感器 6 个),每周将存储 256 个以上,每年将存储 91,000 个以上。如果您正在测量温度,这可能会导致每年多达几兆字节的数据。虽然在 SD 卡上存储的数据并不多,但是如果您想要计算与外部变量(外部气候)相比的一段时间内的平均值,您必须读取数据并在某个时间点(可能一个月几次)计算比较结果。因此,您更适合使用数据库服务器来存储数据。此外,因为您想知道每个样本是何时采集的,所以需要设计数据库表来存储每个样本的时间戳。

如果您考虑到家庭的无线电限制以及它有多个楼层和多个房间的事实,您可以预期需要至少一个位于家庭中心的数据聚合节点。然而,你可能需要更多,这取决于传感器的位置和有限范围的影响。

对于这个项目,假设一个位于中心的数据聚集节点就足够了。此外,您决定数据聚合节点将通过以太网连接到数据库节点,但是传感器节点将使用 XBee 模块与数据聚合节点通信。

您将使用 XBee 模块(可以将 TMP36 直接连接到该模块)实现五个内部传感器节点,但对于外部节点,您将使用前面讨论过的 Seeed Studio 无线传感器套件。

至于为节点供电,您可以为所有传感器节点使用普通的 5V–9V 壁式电源。还假设 Raspberry Pi 数据库节点的外围设备是从现有的剩余组件中收集的。

成本考虑

最后,您希望尽可能地限制网络的成本。您还希望保持样本在比例上的相对性。最好的方法是对每个节点使用相同的温度传感器。最具成本效益的解决方案是使用 TMP36 之类的传感器。

总结一下您的节点需求,您需要六个传感器节点(包括一个可以安装在外面的节点)、一个数据库节点和一个数据聚合节点。考虑到所有这些因素,一个可能的硬件采购清单如表 11-1 所示。

表 11-1

家用温度传感器网络购物清单示例

|

描述

|

数量

|

成本美元

|

延伸文件系统成本美元

|
| --- | --- | --- | --- |
| Raspberry Pi(数据库服务器) | one | $35.00 | $35.00 |
| 带电阻的 TMP36 传感器 | six | $1.50 | $9.00 |
| Seeed Studio 无线传感器套件 | one | $59.50 | $59.50 |
| XBee-ZB (ZB)系列 2 或 2.5 | seven | $25.00 | $175.00 |
| Arduino 兼容板 | one | $21.00 | $21.00 |
| 电源适配器(每个节点一个) | eight | $6.95 | $55.60 |
| 以太网屏蔽 | one | $45.00 | $45.00 |
| Arduino XBee 神盾局 | one | $24.95 | $24.95 |
|   |   | 总计 | $425.10 |

为了简洁起见,我省略了一些更好的细节,但是列出了更昂贵的项目。也就是说,我省略了传感器节点的试验板、外壳等成本,因为这些只是实现电路的一种方式。你可以很容易地在预先打印好的电路原型板上构建一个电路,并将每个电路放置在一个小外壳中(像这样的小块被称为 3D 打印世界中的维生素——我认为这是一个恰当的描述)。

Adafruit ( www.adafruit.com/category/466 )的 Perma-Proto 试验电路板是最新的原型制作板之一。它们有各种尺寸和多种包装,可以节省一些费用,可以像试验板一样使用,但在这种情况下,你需要焊接组件,而不是插上电源。因此,Perma-Proto 试验板 PCB 使得将项目从试验板转移到可以安装的永久性电路板上变得更加容易。图 11-5 显示了 Adafruit ( www.adafruit.com/product/1609 )的半尺寸 Perma-Proto 试验板 PCB。

img/313992_2_En_11_Fig5_HTML.jpg

图 11-5

原型电路板(SparkFun 提供)

至于附件,有许多例子可供您使用。有些是防水的(大部分不是),有些是塑料的,有些是金属的。我甚至发现一些是由激光切割的胶合板和丙烯酸板制成的。如果你想要一个足够大的简单外壳来容纳一个 Arduino 板,甚至一个小 PCB,看看 SparkFun ( www.sparkfun.com/products/11366 )的红色大盒子外壳,如图 11-6 所示。

img/313992_2_En_11_Fig6_HTML.jpg

图 11-6

大红色盒子外壳(SparkFun 提供)

实施呢?

回想一下,在这个项目开始的时候,我说过你拥有实现这个项目所需的所有知识。如果您考虑了您需要的节点,您可以在前面的章节中找到如何构建每个节点的示例。数据库节点在第八章中,Arduino 数据聚合节点也在第十章中,XBee 传感器节点在第 3 和 4 章中,并且在本章中包括了一个室外传感器节点的示例(Seeed Studio 无线传感器套件)。

因此,我把实现的任务交给你了;您可以研究这些示例并实现它们。幸运的是,几乎不需要修改。除了用原型电路板代替试验板,用坚固的外壳代替所有节点之外,您的实现应该与示例相同。

结论

一旦您购买了所有组件并将传感器节点组装成最终形式,您就可以对整个网络进行基准测试,然后部署传感器节点,测试传感器节点和数据聚合节点之间的可靠连接。一旦所有这些都检查完毕,您就可以将传感器节点固定在它们的位置,将数据聚合节点安装在中央位置,并将数据库节点安装在安全的区域。

回到您期望从网络获得的信息,在网络运行一段时间(可能一周)后,如果没有错误或问题,您可以开始对数据库发出查询,以检查外部传感器值和室内传感器值之间观察到的差异。一旦有了几个月的数据,就可以开始考虑按季节对数据进行分组(通过选择时间戳列)。

我希望这个例子强化了书中的内容,以此来验证你在构建所有项目和试验它们时所做的努力。我完全相信,这样做将意味着,如果你以此为榜样,形成一个类似的网络,你会轻而易举地成功。

为了更多乐趣

这些组件的总成本约为 425.00 美元,不包括各种维生素和运费。这听起来可能很多,但可以考虑用其他组件来替代,例如使用更少的 XBee 传感器节点和更多的带 WiFi 屏蔽的 Arduino 传感器节点,或者使用 Raspberry Pi 作为数据聚合节点。

可选组件购物清单

本章中的示例不需要必需的组件。相反,本章列出的组件和附件是可选的,如果需要,您可以考虑购买。表 11-2 显示了本章中提到的组件列表。

表 11-2

可选组件

|

项目

|

供应商

|

是吗?成本美元

|
| --- | --- | --- |
| Seeed Studio Stalker 无线传感器套件 | http://wiki.seeedstudio.com/Seeeduino_Stalker_V3-Waterproof_Solar_Kit/ | $59.50 |
| seeed 工作室跟踪板 | http://wiki.seeedstudio.com/Seeeduino_Stalker_V3.1/ | $39.00 |
| 树莓派阿拉姆德 | www.seeedstudio.com/Alamode-Arduino-Compatible-Raspberry-Pi-Plate-p-1285.html | $35.00 |
| LTE 卡特彼勒 M1/NB-IoT 屏蔽 | www.sparkfun.com/products/14997 | $79.95 |
| 永久原板 | www.adafruit.com/category/466 | 2.50 美元及以上 |
| 附件 | www.sparkfun.com/products/11366 | $8.95 |

摘要

本章探讨了设计和实现无线传感器网络的一些细微差别。我讨论了一些比较流行的传感器网络最佳实践,规划数据库以存储传感器数据的注意事项,如何最好地检索和使用数据库中的数据,以及如何为每个传感器节点选择使用哪种类型的主机。您还探讨了整个家庭温度监控系统的设计,以及为传感器选择硬件的特殊考虑。

现在,您已经对无线传感器网络有了基本的(和一些更高级的)了解,您可以放下这本书,开始思考一些真正酷的方法来实现您所学到的东西。也许你想监控你的房子、车间或车库的温度。或者,您可能希望设计一个更复杂的网络来监控声音、运动和环境温度变化(比如家庭安全系统)。

一个更加雄心勃勃的项目是用离散组件构建自己的气象站,每个数据样本(风速、温度、气体读数、雨量计等)都有一个传感器节点。你在这本书中学到的一切甚至更多都是可能的。祝你好运,传感器网络快乐!

posted @ 2024-08-09 17:45  绝不原创的飞龙  阅读(206)  评论(0)    收藏  举报