MySQL-物联网教程-全-

MySQL 物联网教程(全)

原文:MySQL for the Internet of Things

协议:CC BY-NC-SA 4.0

一、物联网和数据

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​1) contains supplementary material, which is available to authorized users.

互联网使开发者能够创建解决方案,生成世界上任何地方的任何人都可以查看的数据。调整解决方案的原型或较小版本以融入互联网可能是一项挑战。这并不像在本地网络或类似的通信机制上采用一个可行的解决方案并增加互联网连接那么简单。例如,将您的传感器网络从几个传感器监控几个人查看的数据发展到一个包含数百个传感器的传感器网络,并且数据可能可供所有人查看,这可能需要重新设计您的通信方法、数据收集和数据存储。

你不仅要弄清楚如何扩展传感器、数据收集器和数据托管服务或数据库服务器之间的通信,还要处理数据的爆炸。也就是说,从十几个传感器中捕获数据相对容易,并且可能不需要太多的仔细规划来保存数据,但是从数百甚至数千个传感器中捕获数据要困难得多,因为数据以索引方式积累。

显然,将数据存储在可移动介质或文件中是不可能的——尤其是当您考虑数据将如何被使用时。此外,如何存储或更恰当地检索数据会使理解所有这些数据变得复杂。例如,如果你家里、汽车里、办公室里的每一个设备都产生数据,会发生什么?再加上对可穿戴传感器和类似设备日益增长的兴趣,你有可能产生比任何人都能管理甚至破译的数据更多的数据。

然而,不仅仅是传感器网络面临类似的数据危机。事实上,新兴和发展中物联网世界(IOT)的创新者最大的担忧是随着越来越多的设备生成、交流和呈现数据,数据爆炸的可能性。我们需要的是一种探索和利用这些数据的方法,一个起点是如何在较小的规模上收集和存储数据,如传感器网络。

What Is The Internet Of Things?

IOT 的本质是简单的互联设备,它们从观察、事实和其他数据中生成和交换数据,使其可供任何人使用。虽然似乎有一些营销努力试图将连接到互联网的任何东西都变成 IOT 的解决方案或设备(就像无耻地将一切都贴上“云”的标签一样),但 IOT 的解决方案旨在使我们对周围世界的知识更加及时和相关,使我们可以随时随地获得任何东西的数据。无论如何,很明显 IOT 设备的数量有可能超过地球上的人口数量。

本书将指导您成功规划和实施传感器网络的数据存储组件以及类似的 IOT 解决方案。让我们先简短讨论一下 IOT 解决方案,然后看看开发以数据为中心的 IOT 解决方案的主要挑战。

IOT 解决方案

IOT 解决方案就是一组设计用于产生、消费或呈现关于某个事件或一系列事件或观察结果的数据的设备。这可以包括生成数据的设备(如传感器)、组合数据以推断某些东西的设备、设计用于制表和存储数据的设备或服务,以及设计用于呈现数据的设备或系统。任何或所有这些都可以连接到互联网。

IOT 解决方案可能包括一个或所有这些品质,无论它们是结合在一个单一的设备,如网络摄像头,使用传感器包和监测单元,如气象站,或使用专用传感器,聚合器,数据存储和演示的复杂系统,如完整的家庭自动化系统。图 1-1 展示了通过数据库、数据收集器或集成器、显示服务甚至其他设备连接到互联网的所有设备的未来图景。

A978-1-4842-1293-6_1_Fig1_HTML.jpg

图 1-1。

The future of IOT—all devices, everywhere2

IOT 不仅仅是连接到互联网

那么,如果一个设备连接到互联网,这是一个 IOT 解决方案吗?那取决于你问谁。一些人认为答案是肯定的。然而,其他人(比如我自己)认为答案是否定的,除非这样做有好处。

例如,如果你把你的烤面包机连接到互联网,这样做有什么好处?如果你的烤面包机给你的手机发短信说你的烤面包准备好了,这是毫无意义的(或者至少是极端古怪的)。因此,在这种情况下,答案是否定的。然而,如果你想监控不负责任的青少年或老年人,那么查看他们使用烤面包机的频率和时间可能会有所帮助。也就是说,您可以使用这些数据来帮助您对他们的护理和安全做出决策。

请允许我用另一个例子来说明。我有幸参加了 20 世纪 90 年代末在微软校园举行的设计研讨会。在我们参观校园期间,我们看到了世界上第一台可以上网的冰箱(也称为智能冰箱)。 3 货架上有传感器来检测食物的重量。有人建议,只要有点独创性,有人可以在牛奶供应不足时使用传感器通知杂货店,这将使人们不仅可以在线购物,而且可以自动购物。如果你住在杂货店送货上门的地方,这就太好了,但对我们这些生活在农村地区的人来说没什么帮助。虽然它没有被吹捧为 IOT 设备(该术语是后来创造的),但许多人认为该设备展示了如果设备连接到互联网可能会发生什么。

因此,连接到互联网不是 IOT 的事情,也不是一个新概念。相反,IOT 解决方案必须是那些提供一些意义的东西——无论对某人或其他设备或服务的好处有多小。图 1-2 比图 1-1 描述得更清楚一些。

A978-1-4842-1293-6_1_Fig2_HTML.jpg

图 1-2。

“Connect the world” by Wilgengebroed on Flickr,5 via Wikimedia Commons

请注意,在这里您可以看到连接到互联网的逻辑分组。观察相连的植物。该图描绘了几个传感器,但每个传感器不一定直接连接到互联网。将来自一个或多个工厂的传感器连接到中间节点更有可能且更实际,该中间节点将数据发送到互联网上的服务或网络中的另一个节点以供以后处理。图 1-3 显示了这在逻辑形式下的样子。

A978-1-4842-1293-6_1_Fig3_HTML.jpg

图 1-3。

How IOT devices connect to the Internet

1-3 的左侧是 IOT 装置。这些可能是简单的传感器、整个传感器网络、具有一个或多个传感器的设备、具有传感器的嵌入式微控制器解决方案、更复杂的基于微处理器的解决方案,甚至是微波炉、闹钟或电视等设备。一些设备将直接连接到互联网,如图 1-3 顶部所示。这些设备是具有内置网络功能的更复杂的设备。底部是连接到中间节点的设备,如数据聚合器、计算机等,它们在通过互联网连接呈现数据或将数据发送到云服务之前过滤、增强和存储数据。最右边是通过互联网或云服务访问数据的用户。当然,这可以是任何类型的设备,如笔记本电脑、台式机、平板电脑、手机、手表或其他智能设备或电器(包括另一个 IOT 设备)。

IOT 服务

可悲的是,有些公司吹嘘拥有 IOT 的产品和服务,这只不过是营销炒作——就像一些公司通过在名称前加上“云”或附加“为了云”所做的那样。幸运的是,有一些真正好的产品和服务是专为 IOT 打造的。这些范围从数据存储和托管到专门的硬件。

事实上,企业在他们的产品中加入 IOT 服务的速度比任何人都快。它不是通常的嫌疑人,如互联网巨头。我看到思科、美国电话电报公司、惠普以及无数初创公司和小型企业提供 IOT 解决方案和服务。我使用术语 IOT 供应商来描述那些为 IOT 解决方案提供服务的企业。

你可能想知道这些服务和产品是什么,为什么有人会考虑使用它们。也就是说,什么是 IOT 服务,你为什么会决定购买它?您决定购买服务的最大原因是成本和上市时间。

如果您的开发者没有资源或专业知识,并且获得这些资源或专业知识所需的费用超过了服务的费用,那么购买服务可能更经济。但是,您也应该考虑在决策中任何必要的重组。我曾经遇到过一个善意的、有良好文档记录的合同服务,它允许一个产品比预期的更快地进入市场,从而节省了大量成本。可悲的是,虽然该合同的冠军赢得了技术成就奖,但他们没有考虑到系统必须重新调整才能使用新服务。更具体地说,采用新服务比从头开始编写新服务需要更长的时间。因此,该组织没有节省资金,而是花费了近三倍的资金,并且上市时间较晚。显然,你必须考虑所有的因素。

同样,如果您的时间不多,或者您很难在截止日期前准备好解决方案,那么购买 IOT 服务可能比创建或修改您自己的服务更快。这可能需要多花一点钱,但在这种情况下,动机是时间,而不是(必然)成本。当然,实际上这是成本和时间的混合。

那么,IOT 有哪些服务呢?以下是过去几年出现的几个例子。随着 IOT 解决方案和服务的成熟,可能会提供更多的服务。

How To Raise Capital For Development: Kickstarter

在开发新的解决方案时,有时可能需要资金来赚钱。无论听起来多么滑稽,大多数开发者并不富裕,也没有资金投入到他们设备的大规模生产工具和生产中。幸运的是,互联网为开发商提供了一个筹集资金的机制,以将他们的想法推向市场。

虽然有几个网站提供类似的服务,但 Kickstarter ( http://kickstarter.com )提供了一种革命性的融资方式。你可以在网站上发布你的想法,并提供你的产品或服务,收取少量的捐款。事实上,大多数成功的 Kickstarter 活动都会为捐款者提供回报和同情。例如,小额捐赠可以让你得到一件 t 恤或者打折购买产品的机会。对于一个更大的捐赠,你可能会得到第一个生产单位,免费升级到新的模式,甚至在利润的股份。

Kickstarter 不仅允许开发者筹集资金,还允许个人以比典型的风险投资家低得多的货币承诺参与项目融资。如果你感兴趣,请访问 Kickstarter 主页,浏览数百个可用的活动。你永远不知道,你可能会找到你想投资的东西。

为了让你知道什么是可能的,看看 Kossel Pro 的 Kickstarter 活动( http://kickstarter.com/projects/ttstam/openbeam-kossel-pro-a-new-type-of-3d-printer )。开发商的资金目标增加了一倍多,他们的产品已经成为现实,订单源源不断。我应该知道;我拥有首批生产单位之一!

IOT 解决方案示例

让我们来看一些 IOT 解决方案的例子。本节中描述的 IOT 解决方案是多种解决方案的组合,您应该对 IOT 解决方案的规模和复杂性有所了解。我还指出了其中一些解决方案如何利用 IOT 供应商的服务。

传感器网络

传感器网络是 IOT 解决方案最常见的形式之一。简单地说,传感器网络让你可以观察你周围的世界并理解它。传感器网络可以采取池塘监测系统的形式,提醒你水位、水的纯度(污染)和水温;探测捕食者;甚至可以自动开启照明或喂鱼等功能。如果你或你认识的人在医疗机构呆过一段时间,就有可能使用传感器网络来监控身体功能,如体温、心脏和呼吸,甚至运动。现代汽车还包含传感器网络,专用于监控发动机、气候,甚至一些汽车的路况。例如,车道警告功能使用传感器(通常是摄像头、微处理器和软件)来检测您何时偏离车道或道路分界线太远。

因此,传感器网络采用一个或多个传感器,这些传感器对事件或状态进行测量(观察),并将该数据传送给网络中的另一个组件或节点,然后以某种形式呈现该数据以供分析。让我们来看一个有趣的替代天气服务的例子。

Weather Underground ( http://wunderground.com )是一个社区驱动的网站,它允许世界各地的业余和专业气象爱好者将他们的气象站连接到互联网,并共享他们的传感器提供的数据。这意味着您只需点击地图并放大即可获得您所在地区、城市甚至地方的最新天气信息。您将看到代表每个当地气象站(显示当地温度)的图标,单击这些图标可以查看更多信息。图 1-4 显示了 Weather Underground 使用谷歌地图显示气象站的神奇地图。如你所见,你可以点击任何一个气象站来查看更多信息。

A978-1-4842-1293-6_1_Fig4_HTML.jpg

图 1-4。

Weather Station Network (courtesy of http://wunderground.com/wundermap )

你可以用神奇地图做更多的事情。正如你所看到的,地图还显示了雷达数据,你可以使用它来查看哪里可能会发生降水。您还可以看到地图在动态回放以前从雷达和气象站更新的数据。对于那些对天气感兴趣的人来说,Weather Underground 是一个非常棒的网站——无论你是否有自己的气象站,你都可以从这个网站上获得大量的信息。

这可能是我发现的最好的例子之一,来说明 IOT 的力量,特别是传感器网络。您不仅可以看到邻居气象站的传感器生成的数据,还可以看到来自该地区、该州甚至整个国家的几十个气象站的数据!这就是 IOT 物化的真正力量。

车队管理

IOT 解决方案的另一个例子是车队管理系统( https://en.wikipedia.org/wiki/Fleet_management )。虽然远在 IOT 一词出现之前就已开发和部署,但车队管理系统允许企业监控他们的汽车、卡车、船只——几乎是任何移动设备——不仅可以跟踪他们的当前位置,还可以使用位置数据(一段时间内获得的 GPS 坐标)来规划更有效的路线,从而降低运输成本。

车队管理系统不仅仅是为了路由。事实上,车队管理系统允许企业监控每个单位进行诊断。例如,可以知道每辆卡车中有多少燃料,上次维护是什么时候进行的(或者更重要的是,下次维护是什么时候),等等。车辆地理跟踪和诊断的结合被称为远程信息处理。图 1-5 说明了车队管理系统。

A978-1-4842-1293-6_1_Fig5_HTML.jpg

图 1-5。

Fleet management example6

在该图中,您将看到 GPS 系统如何跟踪位置,以及如何通过卫星通信传输更多数据,如诊断信息、有效负载状态等。所有这些最终都会通过互联网,业务分析师可以访问这些数据。

你可能认为船队管理系统只适用于大型航运公司,但随着 GPS 模块甚至微控制器市场的激增,任何人都可以创建船队管理系统。也就是说,它们不需要花费数百万美元来开发。例如,如果您拥有一家自行车送货公司,您可以轻松地将 GPS 模块与每个送货人的蜂窝或无线连接相结合,以跟踪他们的位置、平均行程时间等。更具体地说,您可以用一个 Arduino 和一组小型支持电子设备构建一个 GPS 跟踪解决方案。事实上,我建议这样一个解决方案可以用来最小化递送时间,它允许包裹从一个递送人交给另一个递送人,而不是让他们在每次完成一组递送后都返回仓库。

家庭自动化

另一个越来越流行的 IOT 解决方案是家庭自动化 8 (也称为智能家庭)。虽然从任何意义上来说都不新鲜(家庭自动化已经存在很长时间了),但家庭自动化解决方案已经变得越来越有趣,因为许多供应商正在让它们为互联网做好准备。事实上,大多数解决方案要么提供从互联网直接访问,要么通过云服务访问。

家庭自动化解决方案通常是一组传感器、致动器和设备,允许您控制家中的事物。您可以找到传感器来检测运动、门窗状态(开/关)、温度、湿度等。你还可以找到像锁这样的致动器,让你远程解锁或锁门,打开和关闭车库门,甚至开灯和关灯。最后,你可以找到更复杂的设备,如可以远程编程的智能恒温器、可以观看和记录图像的摄像机、机器人吸尘器,甚至可以让你像在家里一样拨号的电话。虽然这些都不是新的,但新的是将这些设备打包到 IOT 解决方案中。

例如,许多家装商店,如 Lowes 和 Home Depot 都有自己的家庭自动化解决方案系列。您可以找到现成的安装设备,如门锁、摄像头、恒温器等,您可以快速轻松地安装这些设备,并借助随附的软件进行远程访问。甚至有一种设备可以让你开关厨房的水龙头!

Lowes 解决方案名为 Iris ( http://irisbylowes.com ),是一种订阅服务,结合了一个特殊的网络集线器(称为 Iris smart hub),所有设备都连接到这个网络集线器。要访问生成的数据和家庭安全、宠物监控等功能,您可以使用适用于 Android 或 IOS 设备的应用(app)。该订阅有几个级别,从免费的基本服务(允许您连接到设备以获得基本服务,如锁定和解锁门、检测移动、查看相机的短视频等)到。付费服务允许您更多地访问设备并远程控制它们,例如安排设备电源(当您离开时随机开灯,让它看起来像您在家)等等。Iris 是作为入门套件出售的,带有一些设备,您可以根据预算或需求进行扩展。有关更多信息,请参见 Iris 页面。

家得宝解决方案( http://homedepot.com/c/Home_Automation_Basics )也使用了一个集线器(称为 Wink),但与 Lowe 的解决方案不同,您可以添加的设备不需要来自单一供应商。事实上,您可以添加通过 Wi-Fi、蓝牙 LE、Z-Wave、ZigBee、Lutron、ClearConnect 和 Kidde 进行通信的设备。因此,您可以更自由地混合搭配您的设备,根据您的需求扩展您的解决方案。像 Lowe 的解决方案一样,你可以通过一个名为 Wink app 的应用监控各种事情并查看数据。我还应该补充一点,家得宝提供的一些家庭自动化设备可以在没有集线器的情况下运行,但远程访问需要集线器。像劳氏一样,你可以购买易于安装和使用的初学者工具包。请参见家得宝家庭自动化页面,了解更多详细信息和可用套件的链接。

劳氏公司和家得宝公司的解决方案只是众多可用方案中的两个。在谷歌上快速搜索会有几十个结果供你选择——有些是像 Lowe's solution 这样的专利产品,有些则更加开放,或者可以被多家厂商的设备扩展。

您还可以使用 Arduino、Raspberry Pi 和 BeagleBone 等微控制器构建自己的家庭自动化解决方案。有很多关于这个主题的书,从简单的解决方案到复杂的解决方案。以下只是越来越多的 DIY 家庭自动化书籍中的几本。

  • Steven Goodwin,使用 Linux 和 Raspberry Pi 的智能家庭自动化(Apress,2013 年)
  • Marco Schwartz,Arduino 家庭自动化项目(Packt 出版社,2014 年)
  • Onur Dundar,《采用英特尔 Galileo 的家庭自动化》( Packt 出版社,2015 年)

您还可以在互联网上找到创建家庭自动化解决方案的说明。复杂解决方案的一个例子是 Eric Tsai 在 http://instructables.com/id/Uber-Home-Automation-w-Arduino-Pi/ 发表的文章(称为 instructable)。在他的文章中,Eric 描述了一个家庭自动化解决方案的基础,您可以使用易于获得和易于使用的组件来构建自己的解决方案。图 1-6 展示了 Eric 文章中的一个例子,描述了一点想象力就能实现的事情。

A978-1-4842-1293-6_1_Fig6_HTML.jpg

图 1-6。

DIY home automation (courtesy of Eric Tsai, http://etsai.net )

如你所见,有了 DIY 家庭自动化解决方案,你可以随心所欲地建造它。您可以添加通过 ZigBee 模块通信的 Arduino 控制的传感器、监控摄像机的 Raspberry Pi 板、控制车库门的定制电子设备等等!事实上,如果你渴望成为一名创造者或者是一名有成就的创造者,你可以修改现有的设备来增加远程功能。例如,车库门开启器很容易添加无线模块,以便通过本地 WiFi(甚至互联网)发送信号。即使你不是一个经验丰富的软件开发者,你也可以使用 XIV ly(http://xively.com/)等服务来发送你的家庭自动化数据进行监控。当然,如果你花一点时间开发一个简单的网页,你可以通过本地网络连接你的家庭自动化解决方案。虽然它可能不像商业单位那样花哨,但你可以让它做你想做的任何事情。

现在,您已经知道了什么是 IOT 解决方案,并且已经看到了一些例子,让我们来讨论一下 IOT 解决方案中最重要的组成部分:数据。

什么是 IOT 数据?

无论您的 IOT 解决方案是在您洗完澡后看着您的玉米煎饼早餐,还是在暴风雨中依靠仪器将您的船引导到安全的地方,产生和处理的数据都是最重要的工件,实际上是 IOT 解决方案的生命线。

为什么呢?因为没有数据,解决方案是没有意义的。例如,如果你有一个 IOT 解决方案,监测你的身体功能,但从来没有存储数据,你最多可以实现的是一个瞬时读数。如果不存储数据,您将无法对过去的事件进行任何诊断。显然,数据很重要。

或许同样重要的是如何存储这些数据并防止被利用或滥用。这是 IOT 解决方案的一个特别重要的方面,我将在后面详细讨论。现在,让我们从一个简单的工厂监控系统开始考虑 IOT 解决方案的数据。图 1-7 显示了单个工厂的简单解决方案。虽然一个好的解决方案还包括一个光传感器来监控植物接收到多少光,但这个已经被简化了,所以你可以看到产生的数据。

A978-1-4842-1293-6_1_Fig7_HTML.jpg

图 1-7。

Plant monitoring with Arduino

在本例中,您会看到一台连接了土壤湿度传感器的 Arduino。Arduino 从放置在植物土壤中的土壤湿度传感器生成一个读数列表。这样做的代码并不复杂,如清单 1-1 所示。

Listing 1-1.Plant Soil Monitoring

/*

Simple Plant Monitor Example

Display the value of the soil moisture sensor and threshold status.

*/

// Thresholds for sensor to detect wet/dry conditions. Adjust these

// to match your soil characteristics.

int upper = 400;

int lower = 250;

// Pin for reading the sensor

int sensorPin = A0;

void setup() {

// Open serial communications and wait for port to open:

Serial.begin(115200);

while (!Serial);

Serial.println("Simple plant monitor");

Serial.println("raw value, moisture state");

}

void loop() {

int value;

// Read the sensor

value = analogRead(sensorPin);

Serial.print(value);

Serial.print(",");

// If value is less than lower threshold, soil is dry else if it

// is greater than upper threshold, it is wet, else all is well.

if (value <= lower) {

Serial.print("dry");

} else if (value >= upper) {

Serial.print("wet");

} else {

Serial.print("ok");

}

Serial.println();

delay(1000);

}

我使用串行监控器作为输出,这样做很简单,以便您可以看到生成的数据。清单 1-2 显示了一个示例运行的输出,其中我在运行期间给植物浇水。请注意,在输出列表中,当我给植物浇水时,传感器正确地测量到了一个变化(数值没有显著变化)。如果您正在构建一个更复杂(更有用)的工厂监控器,您可能会使用某种形式的输出,如 LCD 面板或 web 服务器,或者您会将数据存储在数据库中。

Listing 1-2.Output of Soil-Monitoring Example

Simple plant monitor

raw value, moisture state

159,dry

217,dry

225,dry

224,dry

225,dry

225,dry

226,dry

248,dry

249,dry

256,ok

261,ok

279,ok

276,ok

254,ok

266,ok

295,ok

291,ok

302,ok

394,ok

467,wet

506,wet

419, wet

Note

您可以从 Apress 网站下载本书的源代码示例。

对于这个示例,文本输出就足够了。这是因为我想提醒大家注意数据行。第一列是从传感器读取的原始值。显然,这些数据几乎没有人类可读的信息。毕竟对于模拟信号来说只是一个数值。这就是我使用阈值来确定或限定数据的原因。随着数值的变化和水分含量的上升,您可以看到这一点。

如您所见,对我们最有意义的数据是“干”、“湿”和“正常”值。原始值并不那么有趣。这是一个很好的,虽然过于简单的例子,说明了来自传感器的原始数据如何需要额外的增强才能变得有用。但是,我还必须指出,如果您只存储了派生值,如果您需要调整阈值,您就不能重新评估派生值。例如,如果您确定阈值上限需要更改,并且您想要对该更改如何影响过去的读数进行一些分析,因为您只有“干”、“湿”和“正常”值,所以您不能执行此分析。因此,保存原始数据总是一个好的做法。

Tip

始终保存原始数据以及计算值或导出值。你永远不知道你什么时候会需要它。

如果您有兴趣构建这个示例,或者用 LCD 或 LED 来修饰它,以便在植物需要水时发出警告,或者更好地用伺服或步进电机和水源来自动给植物浇水,我鼓励您这样做。这是一个有趣的项目。您甚至可以修改代码来支持一个 web 服务器,您可以使用它来远程检查您的植物的状态。 9 这些零件很容易找到,可以从大多数在线电子商店买到,如 SparkFun ( http://sparkfun.com )、Maker Shed ( http://makershed.com )、AdaFruit ( http://adafruit.com )。我包含了一些文章的链接,这些文章从简单到复杂,解释了如何构建这个项目和类似的项目。图 1-8 显示了传感器如何连接到 Arduino。

A978-1-4842-1293-6_1_Fig8_HTML.jpg

图 1-8。

Wiring the simple plant monitor

我想向你们展示这些数据还有一个原因。IOT 解决方案通常采用网络中的中间节点。更具体地说,通常情况下,传感器被安装或连接到更小的一组电子设备,例如微控制器或甚至简单的集成电路。然后,该节点将获取传感器值,并将它们发送到网络上其他地方的另一个节点。值得注意的是,该级别可以使用除以太网或 WiFi 之外的网络协议来简化和减少每个设备拥有唯一地址的需求。这些设备通常没有足够的资源来支持更复杂的网络协议,因此需要可以用有限资源实现的更轻量级的东西。

当像这样在节点之间交换数据时,它被称为机器对机器数据交换,并且通常以原始形式传输。这有几个原因。最值得注意的是节省内存,有助于加快通信速度,特别是对于小型微控制器和类似的嵌入式处理器。也就是说,发送单个整数甚至浮点值比发送格式化的文本字符串要快得多。如果传感器级别的节点使用在较低资源(内存)下运行的通信协议,如 XBee 模块,这可能是至关重要的。

谈到 IOT 解决方案中的所有数据,人们一定想知道这些数据都去了哪里,这些数据将如何积累。

物联网预测:数据霸主?

你不用在网上找太多或太久就能发现一些关于 IOT 的可怕预测。 10 这提出了两个问题:需要更多的地址空间(IP 地址)和可寻址性,以及需要更好的方法来管理极其大量的数据(大数据)。不太受欢迎但同样重要的是 IOT 数据、设备和服务的安全性。我将在接下来的章节中逐一讨论这些问题。

寻址 IOT 设备

仅仅因为你可以在地球上的任意一个地方找到一个地址,就不一定容易找到。也就是说,如果你在沙漠中的某个地方用一个鲜红色的“X”标记一块石头,或者简单地在上面写一个数字,没有人会找到它。即使人们花时间去搜索沙漠,他们也需要一个大概区域的提示来找到它。当然,您可以添加元数据,如纬度和经度,甚至是 GPS 导航信标,但您放在岩石上的“X”显然是不够的信息。这说明了寻址 IOT 设备的两个问题:拥有唯一标识设备的方法和查找或寻址设备。

有足够的地址吗?

人们有理由担心 IOT 设备的数量将很快超过可用 IP 地址的最大容量。目前,IPv6 协议 11 允许大约 3.4×10 38 个地址。那是 340 个十亿分之一的数字!虽然这是一个巨大的数字,但公共地址的可用范围可能会少得多,但仍处于十一亿分之一的水平。这很好,因为有人预测未来 IOT 设备的数量将达到数十亿,甚至数千亿、数千亿或数百万亿。

即使公共可寻址 IPv6 地址的数量是可用 IPv6 地址的一半甚至四分之一,我们也不会很快用完。事实上,IPv6 有可能在未来为所有可能的 IOT 设备提供足够的寻址能力,但是所有这些设备都被寻址并不意味着它们会很容易被发现。

你如何找到一个 IOT 设备?

给 IOT 设备分配一个 IP 地址并不一定容易找到。事实上,如果每台 IOT 设备都有自己的 IP 地址,那么所有设备都可以连接到互联网,甚至可以互相连接,但是你怎么找到你要找的设备呢?如果他们是您的设备或您认识的愿意分享其地址的人,您可以通过了解正确的信息找到他们。但是如果你想知道你的邻居中是否有人有户外摄像机呢?假设您需要访问他们的图像来帮助破案或识别流浪动物的行踪?除了敲邻居的门,没有简单的方法找到这些 IOT 相机。

A Simple Search Is Not Enough

仅仅搜索可用的 IPv6 地址来寻找 IOT 设备是不够的。一些人估计,即使使用速度适中的搜索引擎,搜索和识别所有连接到互联网的 IPv6 设备也可能需要许多年甚至数千年的时间。这还不包括每天添加或删除的内容。显然,我们不能简单地将 IPv6 功能添加到我们生活中的每一个移动(或不移动)的电子设备或事物中,并期望能够在不了解它们是什么、它们在哪里以及它们做什么的情况下访问它们。

就像沙漠中的岩石一样,你需要更多的信息。你需要知道那个设备是如何连接的,它是做什么的,它是如何使用的,它产生什么数据。因此,您需要某种形式的代理或服务来跟踪设备。也许一个更尖锐的问题是,为什么你首先需要知道或使用这个设备?你不是更有可能把邻居的家当作可以请求数据的服务吗?例如,如果您可以使用谷歌地图找到您的邻居,并点击您周围的家庭,以查看哪些 IOT 数据是公开可用的,这难道不比试图找到一个特定的 IOT 设备(相机)更有用吗?这听起来不耳熟吗?应该的。这正是 WunderMap 在 Weather Underground 网站中的工作方式。

在这种情况下,每个家庭都是一个生成数据的 IOT 服务提供商。一些数据可以公开,如面向外部的摄像头,而其他数据可能是私人的,需要安全登录才能访问家中的设备。回想一下家庭自动化用例。想象一下,如果能够远程检查你的家,或者允许保姆看电视或使用厨房里的设备,会有多方便。

不管 IOT 服务是什么,事实是你不太可能需要直接访问 IOT 设备。该服务可以提供所有需要(或允许)的功能。这不仅大大减少了搜索问题,还有助于限制可公开寻址的 IOT 设备的数量。也就是说,IOT 服务“背后”或“内部”的设备不需要公开。更重要的是,这使您能够以更强大的方式保护您的 IOT 设备。想象一下,如果你发现你的 IOT 相机可以被任何地方的任何人通过简单的黑客攻击访问,那将意味着什么。

通过将 IOT 设备置于 IOT 服务(或代理、应用等)之后,您还允许使用资源较少的通信协议。也就是说,IOT 设备可以由更便宜和更有限的硬件构建。例如,您不需要笔记本电脑来监控植物传感器。此外,如果您有一个家庭自动化解决方案,允许您连接一个使用 XBee 协议的工厂监控器,那么您可以使用更少的硬件来构建工厂传感器,从而降低成本。

更小或更低资源的硬件和通信协议解决了特别是 IOT 设备和传感器的另一个问题。传感器没有按照预设的时间间隔生成数据。虽然有些传感器包含计时器电路或只能定期生成值,但传感器通常会由另一个设备(如 Arduino、Raspberry Pi 等)以特定间隔轮询。

此外,诸如 XBee 之类的通信协议不是无损机制。事实上,尽管很少,数据丢失是可能的,并且协议支持数据包的完全丢失。如果您的传感器每分钟生成 30 个值,那么您只收到 29 个而不是 30 个值真的有关系吗?也许有些解决方案可能需要更高的精度,但对于监控事件的传感器来说,这是可以接受的,并且非常适合传感器模型。它考虑了值未准备好的可能性;传感器没有改变它的状态,等等,允许您支持更广泛的传感器行为。

我在这里描述的解决方案已经以多种方式被标记,但是最正确的和实际上最深刻的 IOT 架构被称为 Chirp。弗朗西斯·达科斯塔(Francis daCosta)在他的著作《重新思考物联网》(Apress,2013)中描述了这种架构。

啁啾只是一个小数据包的名称,它只包含将数据传送到目的地所需的最少信息。这符合轻型协议的小开销要求,也允许偶尔丢失数据包。在 IOT 解决方案的最低端,设备使用支持将啁啾发送到节点的协议,这些节点可以监控数据并采取适当的行动。例如,可以构建节点来获取一分钟内生成的 30 个样本,并在一段时间内(比如每五分钟)对它们进行平均,以生成更平滑的数据样本。我见过这种技术被用于使用不太精确的传感器的解决方案,其中不使用超过阈值的尖峰(高或低),从而允许以较低的速率从传感器获得更平滑的读数。这是更少的数据,但可能更准确。

因此,IOT 解决方案将由几个层构成。在最底层,IOT 设备通过网络连接到收集或处理数据的节点,这些节点再连接到提供或支持特定服务的设备。例如,IOT 摄像机可以连接到一个中间节点,该节点从更高层的节点接收命令。类似地,可以使用几个数据聚合和数据处理节点来使用和监控传感器网络,这些节点将数据发送到数据库,然后由更高层中的节点访问该数据库。图 1-9 显示了这个概念的一个例子。

A978-1-4842-1293-6_1_Fig9_HTML.jpg

图 1-9。

Concept of layered IOT solution

请注意,在顶部,我已将 IOT 服务背后的图层标记为私有。这表明这些层和其中的设备不能从互联网直接访问。这并没有停止让它们成为 IOT 设备;相反,它有助于定义解决方案分层的架构,以便通过仅使用必要的协议(以及每层所需的硬件)来实现优化。例如,设备、安全实践和编程的复杂程度在图中从左向右增加。

例如,图 1-9 的左侧是一组使用 ZigBee 网络连接的传感器或轻型 IOT 设备。这可以采用小型设备的形式,使用 XBee 模块将数据发送到包含数据聚合和数据库节点的下一层。在这一层,您可以使用私有的 IPv4 以太网。为什么选择 IPv4?因为像 Arduino 和 Raspberry Pi 这样的小设备大多是原生默认支持的。因此,您可以开始将这些节点与一个应用节点连接起来,该应用节点提供了到基于云的服务的网关。此时,您可以继续使用 IPv4 或切换到 IPv6,因为这可能是一个更复杂的服务器。在图 1-9 的右侧是您将用来授权访问您的 IOT 解决方案的 IOT 服务的描述。这可以是任何东西,从前端 web 服务器到由商业 IOT 供应商提供的完整的 IOT 解决方案。

显然,像这样的架构允许较低层(左边的那些)与资源较少的协议通信,甚至处理偶尔的数据丢失,而较高层(右边的那些)与更复杂的协议通信。这还允许您将解决方案中更复杂的部分放在最适合该任务的层或设备上。例如,虽然你可以将 Arduino 连接到 MySQL 数据库服务器, 12 但你不太可能将它托管在与其他服务相同的平台上(但你可以在 Raspberry Pi 上托管)。最后,它允许您为每一层设计适当的安全级别。

现在您已经了解了 IOT 设备的寻址问题,让我们来讨论一下数据。随着数十亿台 IOT 设备生成数据,这些数据将流向哪里?您将如何访问这些数据?

IOT 和大数据

IOT 专家担心的另一个问题是,IOT 解决方案生成的数据量会以多快的速度增长。也就是说,随着越来越多的 IOT 设备的添加和数据的归档,数据的大小将呈索引级增长。当数据变得足够大时,使用传统的数据库访问机制来访问它就不再可行了。例如,访问位于美国的所有恒温器的传感器数据最终可能会变成一个大得离谱的数字(行数、字节数等等)。即使你有理由看到这些数据,数据量也是巨大的。如果按州来缩小范围,数据可能仍然比可能搜索或检索的要多。

What Is Big Data?

大数据 13 是指将被处理、分析、查看或以其他方式操纵以得出结论的数据的相对大小(例如,数据分析、数据仓库等)。相对大小是指大数据超出了大多数计算平台在合理时间内容纳或处理数据的能力这一特征。也就是说,它超出了单个系统甚至复杂系统的处理能力。 14

有许多处理大数据的方法,但大多数解决方案使用几十到几千个计算平台来分而治之。两个显著的例子是 Oracle 的大数据产品( http://oracle.com/big-data/index.html )和 MySQL + Hadoop ( http://mysql.com/why-mysql/white-papers/mysql-and-hadoop-guide-to-big-data-integration/ )。虽然这些解决方案基于客户或用例而有所不同,但它们都解决了利用分布式数据库和执行来处理数据的问题(在较高层次上)。

因此,一些专家认为 IOT 将产生大量数据,并且只能用于大数据解决方案。例如,如果您想要分析某个地理区域(州、国家)内某类 IOT 设备的使用模式,即使每台 IOT 设备可能单独托管其数据,也需要汇总数据以进行编译和处理。因此,即使每台设备只有少量数据,将数百万甚至数十亿台 IOT 设备上的数据聚合起来也会产生大数据危机。

此外,虽然我和其他人提出了 IOT 解决方案的分层方法,并且大多数解决方案很可能构建为托管自己的数据,但在某个时候,我们仍可能希望在类似的 IOT 解决方案中搜索和挖掘数据。虽然恒温器的例子有点虚构,但更有可能的是,您需要研究这些解决方案的数据,以开发温度波动、高峰天气月份的燃料使用量,甚至池塘和水库的蒸发率等模式,从而帮助预测耗水量和节水率。在这种情况下,您可能需要从多个存储库中挖掘数据。

因此,即使对数据进行了划分,没有一个数据库可以容纳所有的数据,您挖掘的数据也可能需要临时存储以供分析,这使得数据分析成为处理大数据的一种情况。然而,从某种程度上来说,从 IOT 获取、聚合和处理数据确实需要专业的大数据解决方案。

Not Everyone Agrees

您可能会觉得有趣,关于大数据有几个思想流派。一些人批评它的存在,另一些人批评我们用来处理大数据的机制,还有一些人坚持认为真正的解决方案尚未实现。无论如何,大数据的前景仍在发展。

幸运的是,有许多供应商正在解决这个问题,云服务提供商的激增确保我们不必自己创建大数据解决方案。然而,我们仍然需要处理存储和访问我们的 IOT 解决方案数据——这也是本书的全部内容。

Tip

如果您想了解更多关于大数据及其与 IOT 的关系,请参阅 Stackowiak、Licht、Mantha 和 Nagode 的书《大数据和物联网》(Apress,2015)。

既然您已经了解 IOT 数据不是小事,而且确实有可能成为一个大规模的档案库,可以从我们周围的世界中获取更多的知识,那么让我们讨论一下除了如何存储数据之外的首要问题,即如何保护数据和 IOT 解决方案免受利用。

IOT 安全

IOT 开发者还需要考虑保护他们的设备、数据和服务。事实上,所有使用互联网的解决方案都必须开发更好的安全实践。由于存在多个漏洞,IOT 解决方案的独特之处使得计划和实施严格的安全措施变得尤为困难。更具体地说,每个组件可能有不同类型的漏洞,从对传感器和 IOT 设备的物理访问到针对 IOT 服务的远程攻击。

最近一连串的大规模数据泄露事件证明,安全性根本不够好。我们已经看到了从直接盗窃到利用从知名企业(如 Target)和政府机构(如美国人事管理办公室)窃取的数据(超过 4000 万个信用卡号码可能已被泄露)的一切。有趣的是,违规的源头可以追溯到第三方承包商和服务。显然,没有人是安全的。我们需要一个革命性的步骤,而不是改进行之有效的机制。

可悲的是,在保护我们的解决方案方面,我们能走的路是有限的。任何信息技术(IT)专业人士都会告诉您,应用最好的、严格的密码策略和严密的安全措施会迫使用户危及旨在保护他们及其数据的策略。例如,考虑要求密码为 16 个或更多字符的密码策略,其中至少有四个大写字母、六个数字、三个特殊字符,并且没有英语词典单词;每 60 天过期一次;并且与以前的密码相同的字符不超过七个。在这种情况下,一些用户将被迫写下他们的密码,因为他们记不住字母、大写字母、数字和特殊字符的随机组合。

然而,让密码更难猜测或破解只是一种策略。事实上,关于如何正确保护系统有各种各样的理念。虽然对所有技术的深入讨论超出了本书的范围,但考虑一种我们可以用于 IOT 解决方案的更流行的理念是很重要的。这是一种使用多个组件来识别个人的理念。

例如,用户可能需要知道一个关键短语(密码),拥有一个授权的有形护身符(RFID 徽章),并拥有一个生物特征签名(指纹)。要进入其中一个系统,用户必须知道密码,出示有效的 RFID 徽章,并读取和验证他们的指纹。

这听起来可能有点像科幻小说,甚至是超级机密的间谍工作,但它确保访问权限将只授予满足所有三个组成部分的人。也就是说,有可能猜测、破解或简单地窃取用户的密码,并且有可能获取甚至欺骗 RFID 徽章;甚至有可能(无论多么牵强)复制某人的指纹。在不泄露用户身份的情况下获取所有三个组件要困难得多。然而,缺点是用户无法获得访问权限,除非她拥有所有这三个组件。虽然用户不太可能丢失他们的指纹(但受伤和皮肤状况可能会使读取器失败),但用户可能会丢失或放错他们的徽章或忘记他们的密码。因此,安全实践再次削弱了用户的体验,使用户更难访问系统。

那我们该怎么办?我们是否实施了良好的实践来确保系统不被轻易破坏,或者我们是否为了易用性而冒降低安全性的风险?底线是,您必须选择最能满足保护数据和服务需求的安全解决方案,而不会迫使用户忍受繁重的工作,也不会给他们的生活带来困难。

您可能想知道这与您基于 Arduino 的传感器平台有什么关系。毕竟,没有什么人可以利用 Arduino,不是吗?这取决于 Arduino 及其连接方式。例如,可以利用支持通用轻量级操作系统或连接到您的网络的较新的 Arduino。我不会详述这一点,但可以说这是可能的。IOT 设备越复杂,被利用的风险就越大。例如,一般来说,Raspberry Pi 可能比裸机传感器和 XBee 模块具有更大的风险。这是因为 Raspberry Pi 能够运行 Linux,因此支持各种黑客工具和实用程序。

Caution

无论您对用户可访问的设备采用哪种安全理念或策略,您仍然必须考虑保护网络中其余节点的安全。

但是不仅仅是软件可以被利用。例如,将 IOT 设备放置在通过以太网连接的室外机箱中,很容易受到黑客的攻击,黑客可以访问以太网电缆。诚然,有人必须知道 IOT 设备的存在,但被利用的风险是真实的。为了解决这个问题,你可以使用重量更轻的硬件和更简单的通信协议 17 ,它们不容易被黑客攻击。

但是,对于设计良好的 IOT 解决方案来说,安全性真的是一个问题吗?让我们看一个最近的例子。美国(和世界)最大的汽车品牌之一吉普最近因其信息娱乐 18 解决方案的漏洞而受到抨击。一群技术高超的黑客能够远程访问该系统,侵入车内的其他电子设备。该小组能够鸣喇叭,打开雨刷,甚至影响操纵和刹车。更糟糕的是,这一切都发生在《黑客在高速公路上远程杀死一辆吉普车:我在里面》一文的作者安迪·格林伯格驾驶?? 19T7 的时候!不,这不是神话。这种情况确实发生了,Jeep 已经针对其系统的安全补丁发布了两次召回,而不是一次。那么,这对于 IOT 驱动汽车的未来意味着什么呢?你最好确定安全性不仅是内置的,而且做得非常好。显然,吉普有更多的工作要做。

Why Security?

您可能想知道为什么我们在一本专门讨论 IOT 解决方案中的数据库的书中讨论安全性。你可能听说过“慈善始于家庭”,这意味着我们必须教导我们的孩子通过慷慨照顾他人的道德和伦理。对于 IOT 解决方案,有一个适用于安全的类比。我们必须从一开始就将安全纳入我们的 IOT 解决方案。也就是说,我们必须以保护数据和对数据的访问不被利用或窃取为首要目标进行设计。每个组件都必须有安全设计目标,从连接到无害、分立通信电子设备的简单传感器,到可以完全访问互联网的复杂嵌入式微处理器。出于本书的目的,我们将重点关注从数据收集点(例如,传感器和设备)到数据库以及其间所有节点的安全性。正如您将看到的那样,一点点安全预防措施对保护您的数据大有帮助。

对于对人类来说风险更高的解决方案,如核电站或医疗机构,您可能还会考虑安全性需要更强。虽然这些确实是我们期望非常好的安全性的解决方案,但是考虑一下家庭自动化的情况。如果有人能够侵入你的智能家居,并且能够锁门和开门,会发生什么?事实上,最近流行的婴儿监控器被发现很容易被黑客攻击,黑客可以查看图像,监听对话,甚至操纵摄像头。

你可能想知道有人如何利用普通数据进行邪恶的活动。考虑这样一种情况,一个拥有智能住宅的家庭决定去度假。让我们也考虑一下这家人的安全意识,他们没有通过社交媒体公布他们的假期计划。 20 让我们也假设黑客能找到的唯一漏洞是家庭智能电表的数据转储。那又怎样?嗯,考虑到当一家人去度假时,他们用的电更少。空调可能会被设置为较高(较低)的温度以节省电力,没有电视将被打开,没有热水将被使用,没有烹饪正在进行,等等。因此,用电量的突然下降可以告诉小偷,这家人不在家。因此,即使无害的数据也可能被利用。

Tip

安全没有金科玉律或灵丹妙药。安全实践必须不断调整,需要发明新的机制,并且您必须积极主动地走在那些规避安全措施的人的前面。也就是说,您必须认真对待安全性,并围绕可靠的最佳实践开发您的解决方案。

现在我已经吓到你了,让我们谈一谈 IOT 解决方案的安全性,首先概述最常见的安全威胁以及如何应对它们。同样,我们正在研究这些问题,以便能够在我们的 IOT 解决方案中从头开始构建安全性。

常见的安全威胁

IOT 解决方案的几乎每个方面都存在安全风险。您已经看到了有人利用 IOT 设备是多么容易。即使是内置安全功能的 IOT 设备也可能不够。例如,惠普 21 最近的一项研究显示,10 台设备中有 8 台未能实施强密码访问要求。事实上,大多数人用的是简单的“1234”正如我们已经讨论过的,密码安全性只是安全性需要改进的一个方面。

该报告还得出结论,在测试的设备中,60%的具有某种形式的用户界面的设备容易受到攻击,70%的设备使用未加密的网络服务,80%的设备根本不需要密码(即使是他们的云和移动组件),90%的设备收集了某种形式的个人信息或数据。考虑到这一点,以下部分讨论了我们 IOT 开发者在规划我们的 IOT 解决方案时需要考虑的几个关键领域。

通信协议

使用的网络或通信协议可能会被拦截,特别是如果数据是使用定义明确、格式化的明文数据块(在某些协议中称为数据包)传输的。检测网络电缆上的电流或截取 WiFi 信号来确定交换的数据并不困难。解决这个问题的一个方法是使用加密。

数据加密虽然有点普遍,但却是保护数据的好方法。如果您使用的加密使用难以猜测的 128 位算法和密钥,情况尤其如此。幸运的是,加密技术已经被嵌入到几种形式的集成电路中,这使得将它添加到小型电子设备中成为可能。的确,你可以为一个有加密功能( http://sparkfun.com/products/13183 )的 Arduino 买一个盾。

无论您是否使用加密,保护您的通信协议不被直接访问是一些解决方案失败的另一个方面。也就是说,不要把你的以太网电缆放在你家外面容易够到的地方。如果您必须运行以太网或类似电缆,请确保将它们埋在导管中,这样就不会有人意外发现它们。如果您无法隐藏或保护电缆,请将任何暴露的电缆涂上与周围区域相同的颜色,使其更难被发现。白色围栏柱上的白色电缆,如果你不知道它在那里,你很难看到它。

隐私政策

一个经常被忽视的安全方面是收集和保留数据的隐私政策(有时称为数据保留政策)。如果您正在为自己开发一个 IOT 解决方案,并将数据存储在您自己的数据库服务器上,可能不会有问题。但是,如果您正在使用 IOT 云服务,您可能需要考虑该服务的隐私政策。例如,如果您使用一项服务来存储数据以供以后访问或分析,并决定取消该服务,公司会如何处理这些数据?他们会把它放在任何人都可以偶然发现的地方,还是在你的帐户到期后公司会删除它?

对于价值很小或没有价值且不能用来对付您的数据,隐私政策可能不是问题。但是对于诸如您的地址、姓名、病史等数据,隐私政策可能是一个问题。因此,您应该经常检查您计划使用的所有服务的隐私和数据保留政策。

远程维护

提供具有嵌入式软件的 IOT 设备和解决方案的公司通常会提供定期更新固件或软件的方法。事实上,考虑提供这种功能的解决方案非常重要,这样您就可以获得最新的修复和改进。您不仅可以获得新功能,更重要的是,您可以获得最新的安全更新。例如,Jeep 已经修补了其信息娱乐系统,并提供补丁(需要经销商安装)来提高安全性。

但是,如何传输和应用补丁或修复程序的机制应该是安全的。例如,如果修补程序需要特殊的管理帐户,请确保该帐户使用您设置的密码进行保护。换句话说,永远不要使用默认因子。此外,补丁如何到达您的系统是另一个问题。如果你不得不将你的解决方案暴露给互联网上的机器或人,你可能要重新考虑。仅使用安全的补丁传输机制。也就是说,将补丁下载到 USB 驱动器,然后将其传输到 IOT 系统,并故意应用它,这比允许 IOT 供应商自动更新您的系统更安全。

密码策略

我之前讨论过密码策略。在回顾中,确保尽可能在所有账户上使用密码,并选择足够复杂的密码(不是 1234 或你的狗、街道或配偶的名字),但也不至于让你记不住。不要使用默认密码、没有密码的用户帐户或多个帐户使用同一个密码。

人身安全

我之前也提到过这个话题。对于 IOT 解决方案,这适用于系统中的所有设备。对于物理上必须位于安全区域之外的设备,请确保通过最大限度地减少暴露的硬件并锁定机箱来尽可能确保它们的安全。例如,向 IOT 服务器发送视频的摄像机可以被分割,使得只有摄像机部分在建筑物外部(例如),而通信电子设备在建筑物内部。虽然有人有可能黑进相机(或破坏它),但除了拦截信号,他们不太可能做任何事情。

同样,将 IOT 设备锁在 Shell 中可以降低风险,但风险要根据锁和 Shell 的坚固程度来权衡。也就是说,如果围栏是由可以切割的材料制成的,或者锁可以很容易地被移除,那么安全措施只会减慢犯罪者的速度。意志坚定的人仍将获胜。 22

软件和固件

另一个值得关注的方面是 IOT 解决方案中使用的固件和软件(操作系统)。我们需要确保基本操作系统和其他软件的安全。更具体地说,该软件使用安全的帐户,不能被远程破坏,使用加密,并可以加强攻击。例如,安全的 Linux 操作系统优于开放的访问系统。这也适用于防火墙外的任何 IOT 服务(公众可访问),包括 web 服务器、IOT 云服务等。

现在,您已经看到了一些更常见、更严重的安全风险,让我们从 IOT 解决方案的角度来讨论安全性。

保护 IOT 解决方案

让我们将注意力转向如何为 IOT 解决方案采用安全实践。虽然本节并未涵盖所有可能的安全实践,但它旨在让您思考如何保护自己的 IOT 解决方案。但在此之前,让我们回顾一下 IOT 解决方案的一般架构和术语。下面从最底层(IOT 设备)到最高层(IOT 云服务)介绍了几种可用于构建 IOT 解决方案的节点。请记住,就功能而言,这些层也是从简单到复杂排序的。这恰好也大致对应于每个级别的安全性。也就是说,较低层比较高层更容易保护(除了一些例外,如外部传感器的物理访问)。图 1-10 显示了如何在 IOT 架构中安排节点。

A978-1-4842-1293-6_1_Fig10_HTML.jpg

图 1-10。

IOT device architecture Tip

我在整本书中使用这些术语。

  • 数据收集器:传感器、IOT 设备等,从某些事件或观察中产生数据。
  • 数据聚集器:从一个或多个数据收集器接收信息的节点(嵌入式控制器、微控制器、小型计算机等)。其目的是聚合和扩充数据,以便在下一层进行存储。
  • 可操作设备:一种 IOT 设备,提供一些用户可控制的功能,如移动传感器、操作锁等。
  • 数据库服务器:一个节点,通常是存储收集的数据以供以后检索和分析的服务器。
  • IOT 服务:为数据库服务器和可操作设备提供访问层的计算机系统。它可能是位于解决方案防火墙内部或外部的系统。系统通常是互联网服务器或云服务,允许用户查看数据和操作可操作的设备。

现在让我们讨论如何保护每一层。

保护数据收集器

数据收集器(或 IOT 设备)是指那些具有一个或多个产生数据的传感器的设备。这些设备通常由低成本的电子设备构成,仅提供最低限度的数据传输能力。我已经建议使用 XBee 模块通过简单的协议传输数据。我还提到了从物理上保护设备不被篡改。除了这些原则之外,你应该考虑用低成本的电子设备制造数据收集器,避免使用更复杂的小型计算机,例如微控制器或 XBee 模块。如果计划得当,这个级别的设备不需要帐户或其他登录功能。

保护数据聚合器

数据聚合器代表了成熟的下一步。这里我们需要更强大的电子设备来处理数据。例如,我们可能希望像前面工厂监控示例中那样对数据进行限定。此外,数据聚合器是开始使用更复杂的通信协议(如以太网或 WiFi)的第一层。因此,您需要考虑帐户访问安全性(密码)以及远程访问能力。例如,您的数据聚合器可能支持通过远程登录进行网络访问。这些需要以与网络上任何其他计算系统相同的方式进行保护。

保护可操作的设备

保护可操作的设备可能更具挑战性。这是因为,除非你自己制作,否则这种设备很可能是一种商用设备,其功能比你需要的更多。您应该考虑禁用任何不需要的功能,尽可能确保远程访问的安全。此外,我建议将设备放在防火墙或 IOT 服务之后,如具有更安全访问机制的计算机应用或系统。例如,使用可通过加密和高度安全的远程访问来保护的应用向设备发送命令。如果这样做,不仅会使它更加安全,还可以限制外部(互联网)可用的功能或操作。

保护数据库服务器

数据库服务器(如果使用的话)应该像任何其他计算机系统一样受到保护,不被访问。有许多文本(书籍、博客、维基等等)非常详细地涵盖了这个主题。我将在后面的章节中介绍一些最佳实践。在此期间,考虑使您的数据库服务器成为解决方案中的一个节点,防止防火墙外的访问,所有帐户都使用密码保护,数据也防止本地文件访问。换句话说,强化您的数据库服务器。

Tip

您可以在 http://mysql.com/why-mysql/presentations/mysql-security-best-practices/ 找到 MySQL 安全最佳实践。

保护 IOT 服务

这一层是最难保护的。如果您从第三方购买(租赁)IOT 服务,安全性是为您构建的。您有责任确保使用所有可能的合理做法来保护您在服务中的数据。例如,使用良好的密码策略。另一方面,如果您构建自己的面向互联网的服务,您应该尽可能以最安全的方式对待系统。也就是说,如果没有加密、安全登录等等,您的服务器不应该被远程访问。如果您计划这样做,您应该考虑精通如何保护 web 服务。

摘要

物联网对每个人来说都是一个激动人心的新世界。我们这些内心年轻但年龄足够大的人还记得杰特森的电视连续剧,回想起在虚幻的土地上看到的可能。尽管会说话的烤面包机、从公文包里蹦出来的会飞的汽车和有态度的机器人,几十年前的电视幻想正在成为现实。我们有手表,可以兼作电话和视频播放器,我们可以从世界各地解锁我们的汽车,我们可以知道我们的狗是否出去了,我们甚至可以在城市的另一边开门。所有这些都是可能的,并且随着 IOT 的出现而发挥作用。

在本章中,您了解了什么是 IOT 以及 IOT 解决方案是如何构建的,了解了一些描述 IOT 解决方案架构的术语,并看到了一些著名的 IOT 解决方案的示例。我们还讨论了 IOT 解决方案的两个最关键的问题:大数据和安全性,通过实际例子和对问题要点的讨论。在这个过程中,您甚至看到了一些源代码!

在下一章中,您将看到许多可用于构建 IOT 解决方案的硬件。你将会看到用于装载或读取传感器的设备,以及用于收集、扩充、存储和展示数据的设备。

Footnotes 1

https://en.wikipedia.org/wiki/Internet_of_Things

2

https://pixabay.com/en/network-iot-internet-of-things-782707/

3

https://en.wikipedia.org/wiki/Internet_refrigerator

4

许多企业在软件中内置了自动再订购功能。大多数是由软件或数据库事件(如数量不足)触发的,而存储单元中的传感器会触发其他事件。

5

CC 乘 2.0 ( http://creativecommons.org/licenses/by/2.0 )。

6

CC 乘 2.0 ( http://creativecommons.org/licenses/by/2.0 )。

7

http://makezine.com/projects/make-37/gps-cat-tracker-2/ 关于如何为宠物建造一个小型 GPS 追踪器的信息。技术将是相同或相似的。

8

https://en.wikipedia.org/wiki/Home_automation

9

提示:请参见 Arduino IDE 中以太网类别下的 web 服务器示例。

10

我曾经看到过一个保险杠贴纸,上面写着,“互联网已经满了。去外面玩吧。”虽然这个口号异想天开,但它包含了一点事实和对年轻一代的一些建议。

11

https://en.wikipedia.org/wiki/IPv6

12

https://github.com/ChuckBell/MySQL_Connector_Arduino

13

https://en.wikipedia.org/wiki/Big_data

14

比如,不应该用 750 万年才能得到 42 的答案。

15

这太荒谬了。

16

不,真的,这是真的。这是一个很好的例子,说明良好的安全实践可能会出问题,不管意图多么好。也就是说,如果该策略使用户的生活如此艰难,以至于他们必须违反最佳安全实践来应对,那么该策略就走得太远了。

17

这不是一个真正的解决方案,但它确实降低了风险。

18

我完全厌恶组合词( https://en.wikipedia.org/wiki/Portmanteau )。为什么不能只说“资讯娱乐”?。

19

http://wired.com/2015/07/hackers-remotely-kill-jeep-highway/

20

你不会这么做的,对吧?如果你这样做,停止它!在你回来后贴这些照片,而不是当你在 3000 英里外的沙子里。

21

www8.hp.com/h20195/V2/GetPDF.aspx/4AA5-4759ENW.pdf

22

有点像窗户。当然,我们都锁窗户,但是一块砖头或大小合适的石头会很快打破玻璃。

二、IOT 解决方案的硬件

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​2) contains supplementary material, which is available to authorized users.

大多数 IOT 解决方案,无论是手工制造的还是用于商业销售的大规模生产的,都是从使用分立元件的基本设计原型化而来。大多数业余爱好者和发烧友的解决方案都是基于他们可以从零售商(或者批发商,如果他们生产很多产品的话)那里买到的组件。因此,许多商用硬件都是现成的。

本章介绍了几个在构建 IOT 解决方案时可以使用的较为流行的商用硬件示例。因为每样东西都有如此多的种类,我不会试图或声称这一章包罗万象。例如,我简要描述了几种常见的低成本计算板,但每天都有更多的板被添加进来。要把它们全部列出来,需要几倍于这本书的大部头。然而,我在第 8 章中展示了如何使用本章提到的一些硬件的例子。

本章讨论了流行的 Arduino 微控制器板,简要介绍了 Arduino 软件的使用,讨论了流行的低成本计算板,调查了通信硬件,甚至简短讨论了可用的传感器类型。

虽然您不需要成为列出的任何硬件的专家,通读本章将使您获得选择要购买的硬件和开始使用硬件所需的知识。毕竟,这是一本关于使用 MySQL 实现 IOT 解决方案的书,而不是关于如何构建 IOT 解决方案的深入指导——但我在本章中有一些这方面的信息,更多信息在第 8 章中。

让我们先来看看 IOT 解决方案中最常见、也可能是唯一最通用的硬件元件——微控制器。

Hobbyist Or Enthusiast: What’S The Difference?

爱好者建造东西是因为他们可以,而且通常是因为他们从建造中获得了很多乐趣,而不一定是因为他们需要建造。爱好者也倾向于囤积或收集。业余爱好者为一个单一的目的建造东西,通常不像爱好者那样沉迷。因此,一个爱好者就是一个在业余爱好和痴迷之间转了弯的爱好者。

微控制器

微控制器微控制器的可用内存量有限,命令功能有限(例如与硬件交互的操作),连接很少(通常是编程和电源连接器),并且通常使用专门的编程语言。

有许多可用的微控制器,从安装了 AVR 固件的裸 IC2到支持可加载程序和各种硬件扩展的基于微控制器的平台。幸运的是,有很多这样的平台。

Arduino 是最受欢迎和最广泛使用的微控制器平台之一。以下部分提供了大量关于 Arduino 的信息,包括您可以购买的主板示例,甚至还有如何对 Arduino 进行编程的教程。

Note

我经常用术语微控制器或微控制器平台来讨论产品类别,用主板来指代特定版本的平台。

什么是 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

您可以通过编写封装了某些功能(如联网、使用存储卡、连接数据库、做数学等)的库来扩展草图的功能并提供重用。IDE 中包含了许多这样的库。也有一些由他人编写并通过开源协议贡献给 Arduino.cc 的库——其中一些已经与 IDE 捆绑在一起。

Arduino 支持许多模拟和数字引脚,您可以使用这些引脚来连接各种设备和组件并与之交互。主流主板有特定的引脚布局或接头,允许使用称为屏蔽的扩展板。Shields 允许您为 Arduino 添加额外的硬件功能,如以太网、蓝牙和 XBee 支持。Arduino 和盾牌的物理布局允许你堆叠盾牌。因此,您可以拥有以太网屏蔽和 XBee 屏蔽,因为两者使用不同的 I/O 引脚。在探索如何将 Arduino 应用于 IOT 网络的过程中,您将了解插针和屏蔽的用法。

在开发电路或制作电路原型时,Arduino 最适合与试验板一起使用。试验板旨在允许您插入电气元件,并提供各列之间的互连,以便您可以将两个元件的引线插入同一列,从而实现连接。电路板分为两行,便于在电路板中央使用 IC。电线(称为跳线)可用于将试验板上的电路连接到 Arduino。在本章的后面你会看到一个这样的例子。

接下来的几节将研究各种 Arduino 板——Arduino 品牌板和第三方板。有更多的电路板和变体可供使用,在本书出版时可能会有一些新的电路板问世,但这些是我在 IOT 项目和实验中使用的电路板。这些都可以成为你自己项目的良好基础。

Arduino 型号

越来越多的 Arduino 板可供使用。一些是为特殊应用而配置的,而另一些是为不同的处理器和内存配置而设计的。一些主板被认为是官方的 Arduino 主板,因为它们是由 Arduino.cc 品牌化和认可的。因为 Arduino 是开源的,更具体地说,是使用知识共享归属共享许可授权的,任何人都可以构建 Arduino 兼容的主板(通常称为 Arduino 克隆)。但是,您必须遵守 Arduino.cc. 3 制定的规则和指南。本节将介绍一些更受欢迎的 Arduino 品牌主板。

Arduino 板的基本布局由一个 USB 连接、一个电源连接器、一个复位开关、用于电源和串行通信的 led 以及一组用于连接屏蔽的标准间距接头组成。官方电路板上有一个独特的蓝色印刷电路板,上面印有白色字体。除了一个型号之外,所有官方主板都可以安装在一个机箱中(它们在 PCB 上有孔,用于安装螺钉)。例外情况是 Arduino 设计用于安装在试验板上。

Arduino 零号

Arduino Zero 是 Arduino.cc 提供的最新小尺寸 Arduino 板。它与 Arduino Uno 系列( http://arduino.cc/en/Main/ArduinoBoardUno )中的板具有相同的物理布局,但具有更快的 32 位处理器。该板类似于 Leonardo 板(参见“Arduino Leonardo”部分),有 20 个数字 I/O 引脚,其中 18 个可用作模拟引脚。它有更多的内存,256KB 的闪存和 32KB 的 SRAM。时钟速度也更快,为 48MHz。图 2-1 显示了 Arduino Zero 板的早期版本。

A978-1-4842-1293-6_2_Fig1_HTML.jpg

图 2-1。

Arduino Zero

该板配有 USB 主机端口,因此您可以通过 USB 编写草图来访问设备,如键盘或鼠标。

Arduino Zero 是大多数 Arduino 项目的绝佳选择,可以解决许多使用旧主板的内存和性能问题。虽然 Arduino Zero 不能取代糟糕或低效的编程,但它可以改善需要更高处理能力,更重要的是需要更多内存的解决方案。

Tip

Arduino Zero 在 3.3V 下运行。使用屏蔽和连接组件时要格外小心,确保不会损坏主板。

虽然这是我的微控制器产品组合中最新的 Arduino 板,但我已使用该板测试需要更多内存的草图。事实上,在我的 IOT 解决方案中,它已经被证明是一个很好的节点选择,我可以在这里聚合或扩充数据。增加的内存意味着我还可以在数据库服务器上发出查询来查找计算值。

您可以在 http://arduino.cc/en/Main/ArduinoBoardZero 找到 Arduino Zero 的具体文档。

arduino yún

Arduino Yún 是非常不同的 Arduino 板。虽然它是并支持作为普通 Arduino 板使用(您可以运行相同的草图),但 Yún 有两个处理器:Atmel ATmega32U4 是一个与 Leonardo 兼容的微控制器,Atheros AR9331 运行一个缩小版本的 Linux 和 OpenWrt 无线堆栈。

Yún 有一个 USB 主机控制器以及 WiFi 和以太网网络。事实上,您可以使用 WiFi 功能来形成无线接入端口,使 IOT 解决方案能够托管自己的 WiFi 连接设备。与 Zero 一样,该板采用 3.3V 电源供电,要求您仔细选择组件和屏蔽,以确保不会损坏该板。图 2-2 描绘了 Arduino Yún。

A978-1-4842-1293-6_2_Fig2_HTML.jpg

图 2-2。

Arduino Yún

Yún 并不适合所有人。但是,如果您需要能够将 Arduino shields 的使用与用 Python 编写的更强大的脚本结合起来,那么对于将 Python 的强大功能与 Arduino 兼容组件和 shields 的多功能性结合起来,Yún 可能是一个不错的选择。有其他一些主板在这方面做得更好,但大多数都不包括 WiFi 接入端口功能。如果您需要或希望在您的解决方案中提供强大的无线接入端口功能,可以运行您的任何 Arduino 草图,Yún 是一个不错的选择。

到目前为止,我已经使用我的 Yún 创建了离散的 WiFi 网络,用于网络问题的取证和诊断。使用 Yún 作为接入端口,我可以做我在自己的 WiFi 网络上不会做的事情。毕竟,如果事情变糟了,我可以重置我的 Yún,再试一次。用你自己的无线接入路由尝试这种方法要危险得多。

您可以在 http://arduino.cc/en/Guide/ArduinoYun 找到 Arduino Yún 的具体文档。

阿尔狄诺·莱昂纳多

莱昂纳多董事会是一个基于 Uno 的老董事会的演变。虽然它支持标准的头部布局,但它不支持一些旧的盾牌。但是,它增加了一个更快的处理器和一个 USB 控制器,使主板可以作为主机的 USB 设备。较新的 ATmega32u4 处理器有 20 个数字 I/O 引脚,其中 12 个可用作模拟引脚,7 个可用作脉宽调制(PWM)输出。它有 32KB 的闪存和 2.5KB 的 SRAM。

莱昂纳多比它的前辈有更多的数字 pin,但继续支持大多数盾牌。USB 连接使用较小的 USB 连接器。该板有带接头和不带接头两种。图 2-3 描绘了一个官方的莱昂纳多董事会。

A978-1-4842-1293-6_2_Fig3_HTML.jpg

图 2-3。

Arduino Leonardo

我已经用 Leonardo 升级了我的许多基于 Arduino 的老项目。虽然该板并没有提供比一些旧板更多的功能(但有一些,特别是内存),我可以使用新的盾牌,这开始需要新的头部布局。

你可以在 http://arduino.cc/en/Main/ArduinoBoardLeonardo 找到 Arduino Leonardo 的具体文档。

arduino 号

Arduino Due 是一款基于 Atmel SAM3X8E ARM Cortex-M3 处理器的新型、更大、更快的主板。处理器为 32 位处理器,板卡支持海量的 54 个数字 I/O 端口,其中 14 个可用于 PWM 输出;12 路模拟输入;以及 4 个 UART 芯片(串口);以及两个数模(DAC)和两个双线接口(TWI)引脚。新处理器有几个优点。

  • 32 位寄存器
  • DMA 控制器(允许独立于 CPU 的内存任务)
  • 512KB 闪存
  • 96KB SRAM
  • 84 兆赫时钟

Due 具有更大的外形尺寸(称为 mega footprint ),但仍然支持使用标准屏蔽,但它也支持 mega 格式屏蔽。这种新板有一个明显的限制:不像其他板在 I/O 引脚上可以接受高达 5V 的电压,在 I/O 引脚上的 Due 限制为 3.3V。图 2-4 显示了一个 Arduino Due 板。

A978-1-4842-1293-6_2_Fig4_HTML.jpg

图 2-4。

Arduino Due

Arduino Due 可用于需要更多处理能力、更多内存和更多 I/O 引脚的项目。我在极少数需要更多 I/O 引脚或更多内存的情况下使用 Due。然而,对于新的 Zero,除非我需要增加 I/O 引脚,否则我可能会在物理大小是一个问题的情况下使用 Zero。如果你没有空间问题,看看你的项目需要最大的硬件性能。

您可以在 http://arduino.cc/en/Main/ArduinoBoardDue 找到 Arduino 的具体文档。

Arduino Mega 2560

Arduino Mega 2560 是 Due 的旧版本。它基于 ATmega2560 处理器(因此得名)。与 Due 一样,该板支持大量的 54 个数字 I/O 端口,其中 14 个可用作 PWM 输出;16 路模拟输入;和 4 个 UARTs(硬件串行端口)。它使用 16MHz 时钟和 256KB 闪存。图 2-5 显示了 Arduino Mega 2560 板。

A978-1-4842-1293-6_2_Fig5_HTML.jpg

图 2-5。

Arduino Mega 2560

Mega 2560 本质上是标准 Arduino (Uno、Duemilanove 等)的更大形式,并支持标准盾牌。有趣的是,Arduino Mega 256 是 Prusa Mendel 和类似 3D 打印机的首选主板,这些打印机需要使用名为 rep rap Arduino Mega polo Lu Shield(RAMPS)的控制板。事实上,这就是我的 Mega 2560 主板现在所在的位置——在我的 3D 打印机中!也就是说,像 Due 一样,如果您需要额外的 I/O 引脚或更多内存,并且大小不是问题(大小与 Due 相同),您可以使用此板。

您可以在 http://arduino.cc/en/Main/ArduinoBoardDue 找到 Arduino 的具体文档。

arduino 翻制

第三方 Arduino 板卡(也叫克隆体)越来越多。它们的外形、组件甚至功能都各不相同。幸运的是,大多数都 100%兼容 Arduino,这使得在基于 Arduino 的项目中使用它们变得很容易。即使该板与特定的 Arduino 板(例如 Leonardo)不是 100%兼容,第三方板也已经添加到 Arduino 集成开发环境中,只需要您在编译时选择特定的板。你将在本章后面看到这一点。

以下部分描述了我在项目中使用的一些第三方 Arduino 板。你可能会遇到一些这样的人,甚至更像他们。正如你将看到的,有些有非常不同的布局,在实现你的项目时给你更多的选择。由于这些主板保持了与 Arduino 品牌主板的兼容性,因此我将通过关注其独特的功能来简要描述它们。

Sparkfun 红板

Sparkfun Redboard ( http://sparkfun.com/products/12757 )是 Arduino Uno 的高级版本。它使用相同的引导加载程序,增加了一个 FTDI 接口(用于旧的 Duemilanove 主板),以及最新 Arduino UNO R3 的 R3 shield 兼容性。事实上,Sparkfun 通过带回旧主板的最佳品质,彻底改造了 Arduino Uno。是的,它是红色的。图 2-6 显示了 Sparkfun 红板。

A978-1-4842-1293-6_2_Fig6_HTML.jpg

图 2-6。

Sparkfun Redboard

我最喜欢 Redboard 的一点是,它比 Arduino 品牌的电路板便宜一点,而且很容易从 Sparkfun 买到。更好的是,Sparkfun 库存了许多补充 Redboard 的盾牌和配件。事实上,Sparkfun 有一个非常棒的工具包,叫做 spark fun Inventor ' s Kit(http://sparkfun.com/products/12060),可以帮助你学习 Arduino 编程和构建电路。

Tip

如果你的朋友和家人有兴趣了解更多关于 Arduino 的知识,Sparkfun Inventor 的工具包将是一个很好的礼物。

你可以在 https://learn.sparkfun.com/tutorials/redboard-hookup-guide 找到 Sparkfun Redboard 的具体文档。

TinyCircuits TinyDuino

TinyDuino 是对标准 Uno 物理布局的有趣改变。顾名思义,就是一个小板子。事实上,它只比美国的四分之一稍大一点! 4 如图 2-7 所示,主板(中底)约 21×21mm。那很小。

A978-1-4842-1293-6_2_Fig7_HTML.jpg

图 2-7。

TinyDuino

2-7 显示了四个模块(也称为板卡或核心模块);最上面一排从左到右是一个用于编程 TinyDuino 的 USB 盾、一个 MicroSD 读卡器和一个原型板。中间底部的模块是主处理器模块。

TinyDuino 与 Arduino Uno 兼容,具有相同的处理器、内存、引脚等。有一些小差异,但没什么大不了的。事实上,TinyDuino 将运行你所有的草图,并且可以像 Arduino Uno 一样进行编程。你不能用 Arduino 护盾来对付 TinyDuino。

除了尺寸小之外,TinyDuino 处理器板还有一个电池座,使用 CR1632 纽扣电池为板供电。这使得你的 Arduino 解决方案非常小,甚至可以用电池供电!更好的是,还有其他版本的主处理器板具有锂电池连接器,甚至没有电池支持,您可以使用它来硬连线电源。

这些板通过一个微型连接器连接起来,这样你就可以把它们堆叠起来。除了一些例外,它们可以按任何顺序堆叠。处理器板底部没有连接器,原型板顶部也没有连接器。不管怎样,即使你把四块板子都叠起来,TinyDuino 叠起来也只有 20 毫米高。

Tip

为了对 TinyDuino 进行编程,您必须有 USB 板,但是一旦编程完成,您就可以移除该板。

如果您需要使用多个引脚,我认为还有一种板是必备的。原型板有一些引脚断裂,但不是全部。图 2-8 中所示的端子模块允许访问所有 Arduino 引脚,并为每个引脚配备螺丝端子!那很酷。虽然它比核心处理器模块大一点,但仍然非常小。

A978-1-4842-1293-6_2_Fig8_HTML.jpg

图 2-8。

TinyDuino terminal board

更好的是,TinyDuino 的制造商 TinyCircuits ( http://tiny-circuits.com/ )提供了越来越多的模块选择,可以添加到你的小 Arduino 套件中。有用于 LED、蓝牙、WiFi、实时时钟(RTC)、加速度计、音频、双七段显示器、GPS 甚至微型有机发光二极管屏幕的模块。有了这么多可用的模块,您可以在普通 Arduino 板的一小部分大小上构建一个真正强大的 Arduino 解决方案。

因此,对于需要尽可能小的解决方案来说,TinyDuino 是一个极好的选择。我在可穿戴设备中见过这些板, 5 在小玩具中,甚至在手表大小的 Arduino 设备中也见过。体积小,功耗低,非常适合藏在小地方。

您可以在 http://tiny-circuits.com/tinyduino_overview 找到 TinyDuino 的具体文档。

spikenzielabs 啜饮

SpikenzieLabs ( www.spikenzielabs.com )的 Sippino 可以用在无焊料的试验板上。它的成本更低,因为它的元件更少,占地面积更小。幸运的是,SpikenzieLabs 还提供了一种称为 shield dock 的特殊适配器,允许您将 Sippino 与标准 Arduino shields 一起使用。

它基于 ATmega328 处理器,具有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,6 个模拟输入引脚。Sippino 板有 32KB 的闪存和 2KB 的 SRAM。图 2-9 显示了带有试验板接头的 Sippino。

A978-1-4842-1293-6_2_Fig9_HTML.jpg

图 2-9。

Sippino

Sippino 没有 USB 连接,所以你必须使用 FTDI 电缆对其进行编程。好消息是,无论您的项目中有多少个 Sippinos,您都只需要一根电缆。我有许多 Sippinos,并在我的许多 Arduino 项目中使用它们,这些项目的空间非常宝贵。

shield dock 是一个令人惊叹的附件,它可以让您像使用标准的 Uno 或 Duemilanove 一样使用 Sippino。图 2-10 显示了安装在屏蔽坞上的 Sippino。

A978-1-4842-1293-6_2_Fig10_HTML.jpg

图 2-10。

Sippino shield dock

我已经在许多项目中使用了 Sippino,从小型极客闹钟(想想伺服系统、模拟仪表和老式发光灯泡)到传感器网络中的传感器节点。小尺寸和卡布局使得 Sippino 很容易隐藏在一个小 Shell 中。如果您需要或想要在一个小封装中兼容较旧的 Arduino 板(或偶尔需要使用 Arduino Uno 兼容屏蔽), Sippino 是一个不错的选择。

我喜欢这些板的另一个原因是它们作为套件出售,你可以自己组装板。组装很容易,除了读取电阻值(这是每个电子爱好者都必须掌握的技能),元件很容易对准电路板上的正确位置。

你可以在 http://spikenzielabs.com 找到西皮诺和神盾局船坞的具体文档。

斯皮肯齐实验室普罗托蒂诺

Prototino 是 SpikenzieLabs 的另一个产品。它与 Sippino 具有相同的元件,但它不是一个友好的试验板布局,而是安装在一个包含完整原型制作区域的 PCB 上。与 Sippino 一样,它基于 ATmega328 处理器,具有 14 个数字 I/O 引脚,其中 6 个可用作 PWM 输出,6 个模拟输入引脚。Prototino 板有 32KB 的闪存和 2KB 的 SRAM。

Prototino 是构建具有支持组件和电路的解决方案的理想选择。在某些方面,它类似于 Nano、Mini 和类似的板,因为您可以将其用于永久安装。但与这些电路板(甚至 Arduino Pro)不同的是,Prototino 为您提供了一个直接将组件添加到电路板的空间。我在项目中使用了许多 Prototino 板,在这些项目中,我将组件添加到 Prototino 中,并将其安装在机箱中。这让我可以使用一块板创建一个解决方案,甚至可以快速轻松地构建几个副本。

像 Sippino 一样,Prototino 没有 USB 连接,所以你必须使用 FTDI 电缆来编程。图 2-11 显示了一个 Prototino 板。

A978-1-4842-1293-6_2_Fig11_HTML.jpg

图 2-11。

Prototino

我在许多项目中使用过 Prototino,在这些项目中,我在试验板上制作了一个电路原型,并希望将其快速转移到一个永久电路板上。事实上,我开始在我的许多早期项目中使用 Prototino,因为它非常容易处理焊盘的试验板布局。

与 Sippino 一样,Prototino 仅提供自组装套件。您可以在 http://spikenzielabs.com 找到 Prototino 的具体文档。

Sparkfun ESP8266 WiFi 模组

在我收集的第三方 Arduino 板中,还有最后一块板。ESP8266 WiFi 模块是该类别的最新产品之一。由于其独特的特征,它有时被列入 IOT 类别。Sparkfun 的 ESP8266 有许多很酷的功能,如 LiPo 电池连接器,开/关开关(非常方便),外部天线连接器(也方便增加范围),USB 编程端口,当然还有所有可用的引脚。板子也很小。

您可以通过添加一个特殊的插件,使用 Arduino 集成开发环境对 ESP8266 进行编程。有关如何配置板管理器的更多详细信息,请参见 https://github.com/esp8266/Arduino 。图 2-12 显示了 Sparkfun ESP8266 板。

A978-1-4842-1293-6_2_Fig12_HTML.jpg

图 2-12。

ESP8266 Thing

ESP8266 是另一种小型物理布局板,可用于为 IOT 网络(或单一解决方案)创建节点。我还没有在任何真正的项目中使用过我的模块,但是这个模块的功能非常吸引人。

Sparkfun 还出售一种 Arduino 大小的电路板,称为 ESP8266 WiFi shield,你可以与你的 Arduino 电路板一起使用( http://sparkfun.com/products/13287 )。它可以用来为您的 Arduino 项目提供 WiFi 功能。我也有一个,我用它来代替普通的 Arduino WiFi shield(参见“推荐附件”侧栏),主要是因为我想尝试 ESP8266 WiFi 功能,而不需要额外的工作来编程 ESP8266。

你可以在 Sparkfun 的优秀资源库 https://learn.sparkfun.com/tutorials/esp8266-thing-hookup-guide 找到更多关于 ESP8266 的信息。

Recommended Accessory

如果你打算用你的 Arduino 或其他微控制器板做任何原型,我建议你买一个或做一个你自己的实验底座(有时叫做试验板支架)。你可以很容易地用丙烯酸做一个,就像我在这里展示的例子中所做的那样。在这种情况下,我有一个旧的 Duemilanove 板安装在一个半尺寸的试验板旁边。我甚至有空间放置一个 9V 的电池架来为实验供电。

A978-1-4842-1293-6_2_Figa_HTML.jpg

您也可以购买预钻孔的产品,用于大多数 Arduino Uno 尺寸的主板。既有 Sparkfun ( http://sparkfun.com/products/11235 )又有 Adafruit ( http://adafruit.com/products/275 )的股票优秀例子。你也可以找到这些其他板,如树莓派。

将微控制器和试验板放在一起,可以将实验从一个地方移到另一个地方,或者停止实验,以后再继续。

Arduino 教程

本节是一个简短的教程,介绍如何开始使用 Arduino。它涵盖了获取和安装 IDE 以及编写示例草图。我没有重复本书之前的优秀作品,而是涵盖了重点,并向不太熟悉 Arduino 的读者推荐在线资源和其他提供更深入介绍的书籍。此外,Arduino IDE 有许多示例草图,您可以使用它们自己探索 Arduino。大多数在 http://arduino.cc 网站上都有相应的教程。

学习资源

有很多关于 Arduino 平台的信息。如果您刚刚开始使用 Arduino,Apress 提供了一系列令人印象深刻的书籍,涵盖了与 Arduino 相关的各种主题,从微控制器入门到学习其设计和实现的细节。以下是一些比较受欢迎的书籍:

  • 迈克尔·麦克罗伯茨的《Arduino 入门》
  • 实用的 Arduino:开源硬件的酷项目(技术在行动中)
  • 戴尔·威特的 Arduino 内部(2011 年出版)

还有一些优秀的在线资源,可以帮助您了解更多关于 Arduino、Arduino 库和示例项目的信息。以下是一些最好的:

Arduino IDE

Arduino IDE 可供 Mac、Linux (32 位和 64 位版本)和 Windows 平台下载。你可以从 http://arduino.cc/en/Main/Software 下载 IDE。每个平台都有链接,如果需要为不同的平台编译 IDE,还可以链接到源代码。

安装 IDE 非常简单。为了简洁起见,我省略了安装 IDE 的实际步骤,但是如果您需要安装 IDE 的指导,您可以点击下载页面上的入门链接,或者阅读 Michael McRoberts 的《Arduino 入门》( Apress,2010)中的更多内容。

Tip

如果您需要在 Windows 上安装驱动程序的帮助,请参见 http://arduino.cc/en/Guide/Howto

IDE 启动后,您会看到一个简单的界面,其中有一个文本编辑器区域(默认情况下为白色背景),编辑器下方有一个消息区域(默认情况下为黑色背景),顶部有一个简单的按钮栏。按钮(从左至右)是编译、编译和上传、新建、打开和保存。右边还有一个打开串行监控器的按钮。您可以使用串行监控器查看通过串行库发送(或打印)的 Arduino 消息。您将在您的第一个项目中看到这一点。图 2-13 显示了 Arduino IDE。

A978-1-4842-1293-6_2_Fig13_HTML.jpg

图 2-13。

The Arduino IDE

注意,在图 2-13 中,你可以看到一个样本草图(称为闪现)和一个成功编译操作的结果。注意在底部,它告诉你你正在一个特定的串行端口上编程一个 Arduino Uno 板。

由于处理器和支持架构的不同,编译器构建程序的方式(以及 IDE 上传程序的方式)也有所不同。因此,当您启动 IDE 时,首先要做的事情之一就是从“工具”“➤板”菜单中选择您的板。图 2-14 显示了在 Mac 上选择板卡的示例。

A978-1-4842-1293-6_2_Fig14_HTML.jpg

图 2-14。

Choosing the Arduino board

请注意可用的电路板数量。请务必选择与您的主板相匹配的产品。如果您使用的是克隆板,请查看制造商的网站,了解推荐使用的设置。如果你选择了错误的板,你通常会在上传时得到一个错误,但是你选择了错误的板可能并不明显。因为我有很多不同的主板,所以我养成了每次启动 IDE 时都选择主板的习惯。

接下来您需要做的是选择 Arduino 板连接的串行端口。要连接到板,使用工具➤端口菜单选项。图 2-15 显示了一个 Mac 上的例子。在这种情况下,没有列出串行端口。如果您没有将 Arduino 插入电脑的 USB 端口(或集线器),如果您插入了它但在某个时候断开了它,或者如果您没有加载 Arduino 的驱动程序(Windows ),就会发生这种情况。通常,这可以通过简单地拔出 Arduino 并将其插回,然后等待直到计算机识别该端口来解决。

A978-1-4842-1293-6_2_Fig15_HTML.jpg

图 2-15。

Choosing the serial port Note

如果你使用 Mac,你选择哪个端口并不重要——无论是以tty开头的端口还是以cu开头的端口都可以。

现在您已经安装了 Arduino IDE,您可以连接 Arduino 并设置板和串行端口。您会看到 Arduino 上的 led 亮起。这是因为 Arduino 从 USB 获得电源。因此,当 Arduino 连接到电脑时,您不需要提供外部电源。接下来,让我们深入一个简单的项目,这样您就可以看到 Arduino IDE,并了解如何构建、编译和上传基本草图。

项目:硬件“你好,世界!”

无处不在的“你好,世界!”Arduino 的项目是闪烁的灯。该项目使用一个 LED,一个试验板,和一些跳线。Arduino 在loop()迭代过程中打开和关闭。这是一个很好的开始项目,但它与如何使用传感器无关。

因此,在本节中,您将通过添加传感器来扩展闪光灯项目。在这种情况下,您仍然可以通过使用可以说是最基本的传感器来简化事情:按钮。目标是每当按钮被按下时,LED 就会亮起。

给电路布线

让我们从组装 Arduino 开始。确保首先断开(关闭)Arduino。您可以使用任何带有 I/O 引脚的 Arduino 变体。在试验板上放置一个 LED 和一个按钮。将 5V 引脚连接到试验板电源轨,将接地引脚连接到接地轨,并将按钮放在试验板的中心。将 LED 放在试验板的一侧,如图 2-16 所示。

A978-1-4842-1293-6_2_Fig16_HTML.jpg

图 2-16。

Diagram of an LED with a pushbutton Tip

如果您打开闪亮的新 Arduino,您可能会看到板上的 LED 闪烁。这是因为一些 Arduino 板预装了闪烁草图。

你就快到了。现在,将一根跳线从电源轨连接到按钮的一侧,并将按钮的另一侧连接到 Arduino 上的(数字)引脚 2(位于带有 USB 连接器的一侧)。接下来,将 LED 连接到试验板上的地和一个 150 欧姆的电阻(颜色:棕色、绿色、棕色、金色)。电阻的另一端应连接到 Arduino 上的第 13 号针脚。你还需要一个电阻在按钮没被按下时把按钮拉低。将一个 10K 欧姆电阻器(颜色:棕色、黑色、橙色、金色)放在按钮的侧面,导线连接到引脚 2 和接地。

LED 最长的一边是正极。正极应该是连接到电阻的一侧。电阻接哪个方向都没关系;它用来限制 LED 的电流。再次检查图纸,以确保您有一个类似的设置。

Note

大多数 Arduino 板都有一个连接到引脚 13 的 LED。您将重复使用该引脚来演示如何使用模拟输出。因此,您可能会看到引脚 13 附近的一个小 LED 与试验板上的 LED 同时亮起。

写素描

这个项目需要的草图在 Arduino 上使用了两个 I/O 引脚:一个输出和一个输入。输出引脚将点亮 LED,输入引脚将检测按钮接合。将正电压连接到按钮的一侧,另一侧连接到输入引脚。当您检测到输入引脚上的电压时,您告诉 Arduino 处理器向输出引脚发送正电压。这种情况下,LED 的正极连接到输出引脚。

如图 2-17 中的图所示

A978-1-4842-1293-6_2_Fig17_HTML.jpg

图 2-17。

Arduino Ethernet shield

,输入引脚为引脚 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);

}

}

现在让我们来看看完整的草图,以及相应的文档。清单 2-1 显示了完成的草图。

Listing 2-1.Simple Sensor Sketch

/*

Simple Sensor - MySQL for the IOT

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);

}

}

当你已经输入了草图,你就可以编译和运行它了。

编译和上传

写好草图后,使用 IDE 左上角的 Compile 按钮测试编译。修复消息窗口中出现的任何编译错误。典型的错误包括变量或方法的拼写错误或大小写改变(编译器区分大小写)。

修复任何编译错误后,单击上传按钮。IDE 编译草图,并将编译后的草图上传到 Arduino 板上。您可以通过消息窗口上方右下角的进度条来跟踪进度。当编译好的草图上传后,进度条会消失。

测试项目

上传完成后,你会在 Arduino 上看到什么?如果你做对了每一件事,答案是什么都没有。它只是用一个黑暗的 LED 回望着你——几乎是嘲弄地。现在,按下按钮。LED 是否点亮?如果是这样,那么恭喜你:你是一名 Arduino 程序员!

如果指示灯不亮,按住按钮一两秒钟。如果这不起作用,请检查您的所有连接,以确保您插入了试验板上的正确走线,并且您的 LED 正确就位,长边连接到电阻器,电阻器连接到引脚 13。

另一方面,如果 LED 灯一直亮着,尝试将按钮重新调整 90 度。您可能将按钮设置在了错误的方向。

尝试这个项目几次,直到兴奋过去。如果你是 Arduino 的老手,那可能是一段很短的时间。如果这一切对你来说都是新的,那就按下那个按钮,享受建造第一个传感器节点的荣耀吧!

What About Other Microcontrollers?

是的,除了 Arduino,还有其他微控制器可供选择。事实上,有些在 Arduino 之前就存在了。如果你有使用这些设备的经验,特别是如果你已经拥有一些这样的主板和它们的附件,你应该考虑使用它们,因为它们提供了许多与 Arduino 相同的好处。

一些更受欢迎的微控制器替代品包括。我提供了一个链接,可以获得关于每一个的更多信息。

  • Esquillo:这是一个相对较新的 IOT 平台,具有基于 web 的板载开发环境,用于在 Squirrel 中进行开发,具有交互式调试器、云支持等等。它还支持一些 Arduino 盾牌。(见 http://esquilo.io/ )。)
  • mbed:这是一个新的 IOT 平台解决方案(目前处于测试阶段),使用 ARM 处理器。(见 http://mbed.com/en/ )。)
  • Photon(以前的 Spark):这是一个 IOT 开发平台,支持 WiFi,甚至还有一个板载 web IDE。哦,它是 Arduino 兼容的。(见 http://particle.io )。)
  • Propeller:这是一个功能强大的处理器,有多种附件可供选择。许多较老的项目使用这种处理器,它是用类似 C 的编程语言编程的(见 http://parallax.com/catalog/microcontrollers/propeller )。)
  • Teensy:这是一个小型的微控制器,用一个特殊的基于 USB 的加载程序编程。由于它使用与 Arduino 相同的处理器系列,因此可以使用 Arduino IDE 进行编程(参见 http://pjrc.com/teensy/ )。)

你可以在 Sparkfun ( http://sparkfun.com )和 Adafruit ( http://adafruit.com )等受欢迎的在线电子商店找到许多这样的东西。

现在你已经看到了一些 Arduino 板,让我们来讨论一些常见和流行的 Arduino 硬件附件。

附加 Arduino 硬件

回想一下,Arduino 通过安装在 Arduino 接头上的子板(称为屏蔽板)支持附加硬件。有许多这样的屏蔽,从简单的原型屏蔽到复杂的电机控制器。在本节中,我将从以太网屏蔽开始,讨论一些您可能在 IOT 解决方案中使用的屏蔽。还有更多可用的 Arduino 屏蔽,但我将讨论集中在这些方面,以突出我在 IOT 解决方案中最常用的屏蔽。

Arduino 以太网盾

Arduino Ethernet shield 使您的 Arduino 能够通过以太网连接连接到网络,并最终连接到互联网。Arduino Ethernet shield 由 Arduino.cc 制造,与大多数 Arduino 板兼容,包括大型 Mega。

该板还配备了一个 MicroSD 读卡器,使其成为需要在本地存储或读取存储数据的项目的绝佳选择。板上有一个重置按钮,与 Arduino 上的重置按钮相同。图 2-17 显示了 Arduino 以太网屏蔽。

该板使用内置的以太网库与网络资源进行交互。有一些可用的示例草图,你可以用来学习如何使用盾牌以及优秀的文档( http://arduino.cc/en/Reference/Ethernet )。

Arduino 以太网屏蔽是复制最多的屏蔽之一。我发现许多第三方防护罩的价格很低。事实证明,有些不是 100%兼容,有些没有配备 MicroSD 读卡器。有趣的是,该板有一个变体,允许您使用以太网供电(POE),允许您使用以太网电缆和交换机或路由上的特殊电源为 Arduino 供电。

如果您需要让您的 Arduino 解决方案具备网络感知能力,并且可以使用以太网连接,Arduino 以太网屏蔽是一个绝佳的选择。如果想省点钱,可以选择其中一个第三方版本;在你购买它之前,确保它是 100%兼容的。

A Warning About Ethernet Shield Variants

你可能认为所有的以太网屏蔽都是平等的,就像台式电脑中的网卡一样工作。那就是,任何一个都行;把它扔进去就走。但事实远非如此。Arduino 兼容的以太网屏蔽不仅仅是因为它可以与您的 Arduino 兼容;它还必须使用内置的以太网库。

例如,有许多 Arduino 以太网屏蔽,甚至一些以太网模块(如分线板)使用不同的芯片组,如 CC3000 或其他几种芯片组中的任何一种。其中许多与以太网库不兼容。你能辨别的方法是盾牌是自带的还是需要你下载一个库才能使用。

对于单个项目来说,这可能不是什么大事,但对于 IOT 开发商来说,至少还有另外两个问题。首先,如果您的解决方案中有许多节点,您可能需要为使用的每种类型的以太网屏蔽写一个不同的草图。第二,如果你计划使用 Arduino 与 MySQL 服务器对话,你必须有一个使用以太网库的屏蔽。这是因为设计用来与 MySQL 对话的特殊库(称为 MySQL 连接器/Arduino)是为以太网库编写的。使用不支持以太网的不同库。客户端类可能使其与连接器不兼容。您将在第 6 章的中了解更多关于连接器的信息。

Arduino WiFi 盾牌 101

Arduino WiFi shield 101 是 Arduino.cc 的最新产品,用于通过 WiFi 信号将 Arduino 连接到互联网。有更老的 WiFi 屏蔽可用,你将在下面的章节中看到其中的一些。这种盾的有趣和新颖之处在于它具有板载加密认证,使用支持 WEP 和 WPA2 安全企业协议的更安全的认证,使其更容易在无线网络上使用。由于这些特点,它是为 IOT 解决方案营销的。然而,与旧的 Arduino WiFi shield 不同,它没有配备 MicroSD 读卡器。图 2-18 显示了 Arduino WiFi shield 101。

A978-1-4842-1293-6_2_Fig18_HTML.jpg

图 2-18。

Arduino WiFi shield 101

有一个缺点,这取决于你将如何使用盾牌。像一些 Arduino Ethernet shield 克隆一样(参见“关于 Ethernet shield 变体的警告”侧栏),它不使用以太网库。您必须下载一个新的库来使用它( http://arduino.cc/en/Reference/WiFi101 )。如果您只有几个节点,这可能不是问题,但是如果您想要将 shield 与现有草图或为以太网库编写的草图一起使用,您可能需要进行一些修改以使其工作。

幸运的是,新的库有许多例子可以用来写你的草图。我还没有在我的项目中使用 shield,但是初步的实验表明新的库类似于以太网库,因此代替 Arduino 以太网 shield 应该不难使用。

Arduino WiFi 盾

Arduino WiFi shield 是最新的 101 shield 的旧版本。它只支持简单的身份验证(没有加密功能),但配有一个 MicroSD 读卡器。我最喜欢 shield 的一点是,它与以太网库 100%兼容,这使得 Arduino 以太网和 WiFi shields 可以轻松地在草图中使用相同的核心代码。启动代码略有不同,但是对以太网库及其类的所有调用保持不变。图 2-19 显示了 Arduino 以太网屏蔽。

A978-1-4842-1293-6_2_Fig19_HTML.jpg

图 2-19。

Arduino

星火娱乐无线屏蔽:ESP8266

Sparkfun WiFi 盾 ESP8266 是来自 Sparkfun ( http://sparkfun.com/products/13287 )的一块有趣的板子。它具有 ESP8266 芯片上 WiFi 系统(SoC)处理器,具有分裂的个性。回想一下前面关于 Arduino 板的部分,ESP8266 WiFi SoC 是一个可编程模块,但通过将该芯片放置在 Arduino shield 布局上,它允许您使用 Arduino 板的 AT 命令集将 shield 用作简单的 WiFi 连接。图 2-20 展示了 Sparkfun WiFi 盾 ESP8266。

A978-1-4842-1293-6_2_Fig20_HTML.jpg

图 2-20。

Sparkfun WiFi shield, ESP8266

虽然您可以将这个盾牌用作 Arduino 的 WiFi 网关,但它的编程方式不同。它有自己的库,您必须下载并使用,而不是使用以太网库。这个库( https://github.com/sparkfun/SparkFun_ESP8266_AT_Arduino_Library )允许你使用方法访问 ESP8266 的 AT 命令集来连接网络。幸运的是,这些方法类似于以太网库,但是不兼容。因此,您可能需要重写现有草图的网络部分来使用盾牌。

ESP8266 WiFi SoC 被编程为与 Arduino 一起作为 WiFi 屏蔽使用。然而,它可以被重新编程。注意电路板顶部的 ESP8266 连接。您可以使用这些对 ESP8266 进行编程。你只需要在板上焊接一个引脚接头,并通过 FTDI 电缆连接。这允许您通过修改 ESP8266 芯片以添加额外的命令或功能来试验它,使这个屏蔽成为 Arduino 和 ESP8266 分线板之间的中间地带。

要了解更多关于这种特殊板的信息,请参见 https://learn.sparkfun.com/tutorials/esp8266-wifi-shield-hookup-guide

Adafruit WiFi 盾

Adafruit WiFi shield 是我发现的为数不多的 Arduino 以太网克隆板之一,是一个很好的替代品。Adafruit WiFi shield 使用 CC3000 芯片组,需要新的库。幸运的是,Adafruit 有一个使用该库的优秀教程( https://learn.adafruit.com/adafruit-shield-compatibility/cc3000-wifi-shield )。

shield 还有一个 MicroSD 读卡器,对我来说是一个很好的选择,因为我的许多 Arduino 节点都使用 SD 卡来存储日志数据和作为数据库存储的备份。图 2-21 为 Adafruit WiFi 盾。

A978-1-4842-1293-6_2_Fig21_HTML.jpg

图 2-21。

Arduino

不幸的是,这个屏蔽与以太网库不兼容,这意味着要重写现有草图的网络部分。这使得我在开发 IOT 节点时不太感兴趣,因为我喜欢在开发草图时使用以太网连接,以消除较慢的 WiFi 连接固有的延迟。

Sparkfun 密码盾

这个盾是个有趣的盾。这是我发现的第一款包含高级加密功能的产品,例如用于保持准确时间的实时时钟(RTC)模块、用于 RSA 加密/解密的可信平台模块(TPM)、用于存储少量加密数据(如服务器的用户 id 和密码)的 AES-128 加密 EEPROM、对 SHA-256 和 HMAC-256 的支持,以及执行椭圆曲线数字签名算法(ECDSA)的 ATECC108。哇哦!那是很多很酷的东西。图 2-22 显示了 Sparkfun 密码盾。

A978-1-4842-1293-6_2_Fig22_HTML.jpg

图 2-22。

Sparkfun CryptoShield for Arduino

你可以在发送数据前用这个盾加密数据,在收到数据后再解密。这听起来可能有点过了,但是请考虑一下您的解决方案中的节点必须通过无线或不安全的网络连接发送数据的情况。在这种情况下,添加硬件级加密意味着即使网络不安全,您也可以保护您的数据。

Note

Sparkfun 还为 BeagleBone Black 制作了这种板的版本。

我已经非常成功地使用了这个盾来保护 Arduino 节点之间的数据安全。我计划在我的 IOT 解决方案中使用这个保护罩来帮助他们免受监控或盗窃。

Sparkfun MicroSD 盾

我列举的最后一个 Arduino 盾牌是 Sparkfun MicroSD 盾牌。它只是一个带有 MicroSD 读卡器的屏蔽。这种屏蔽还有其他版本,但我喜欢 Sparkfun 版本,因为它还带有原型制作区,可以在屏蔽上构建自己的电路,并为您的解决方案提供一个方便的平台和一个 MicroSD 读卡器。图 2-23 显示了 Sparkfun MicroSD 盾。

A978-1-4842-1293-6_2_Fig23_HTML.jpg

图 2-23。

Arduino microSD shield from Sparkfun

XBee 模块

我将讨论的下一个硬件选项不是专门的 Arduino 屏蔽,甚至不是专门用于 Arduino 的。正如您将看到的,XBee 是一个用于低成本、低功耗(如参考资料中所示)通信的通用模块。

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

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

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

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

Tip

我在我的书《用 Arduino 和 Raspberry Pi 开始传感器网络》(Apress,2013)中对 XBee 模块进行了更完整的解释。如果你想知道更多关于 XBee 硬件和规格的信息,请看那本书的第二章。

有几个 XBee 模块可用。有普通型号和 Pro 型号,功率更大,功能更强。还有各种各样的天线选项,从板载芯片天线到有线鞭状天线,再到远程天线连接。图 2-24 显示了我经常使用的几个 XBee 模块。

A978-1-4842-1293-6_2_Fig24_HTML.jpg

图 2-24。

XBee modules and various host boards

请注意,每个 XBee 模块(总共五个)都安装在不同的小板上。这些是可以用来编程和运行 XBee 模块的主机类型的示例。中间是 Sparkfun ( https://www.sparkfun.com/products/12847 )的 XBee 盾,可以让你直接把 XBee 连接到你的 Arduino 上。其余的板是你可以用来编程 XBee 的。从左上角开始,您可以看到以下板:

你可以在大多数在线电子商店,如 Sparkfun 和 Adafruit,找到 XBee 收音机出售。除了我的传感器网络书,Adafruit ( https://learn.adafruit.com/xbee-radios )和 Sparkfun ( https://learn.sparkfun.com/tutorials/xbee-shield-hookup-guide )都有关于使用 XBee 的优秀教程。

现在,您已经了解了流行的 Arduino 微控制器、其开发环境和额外的硬件(屏蔽),现在让我们了解更多关于低功耗(也称为低成本)计算平台的信息。

低功耗计算平台

低功率计算平台,有时被称为低成本计算机板或迷你计算机,由廉价组件构建而成,旨在运行低资源密集型操作系统。大多数主板都具备低成本计算机的所有正常功能,包括视频、USB 和网络功能。然而,并不是所有的主板都具备这些特性。

它们有时被称为低功耗的原因不是因为它们的 CPU 或内存容量较小;相反,这是因为它们的电源要求,通常在 5V 和 24V 之间。由于它们不需要大规模的类似 PC 的电源,这些板可以用于需要具有真实操作系统的计算机的能力但没有空间容纳全尺寸计算机、不能投入计算机成本或必须在较低电压下运行的项目。

Tip

由于含糊不清,似乎缺乏一个标准的术语,当在这一类别中讨论这些主板时,我将把它们称为低成本计算主板或简称为主板。

有许多种类的低成本计算板。一些支持典型计算机的全部功能(并且可以作为一个相当不错的笔记本电脑替代品),而另一些只具备作为嵌入式计算机的基本功能。例如,一些主板允许您连接网络电缆、键盘、鼠标和显示器,用作普通的笔记本电脑或台式机,而其他主板只有网络和 USB 接口,需要您远程访问它们。

这些主板最强大的一个方面是,由于它们运行 Linux 操作系统的变体,许多可以用 C、C++、Perl、Python 和类似的编程语言进行编程。这使得您可以使用在任何桌面上都可以找到的工具来开发您的软件,并将其部署在小型嵌入式计算设备上的解决方案中。不仅如此,考虑到所使用的处理器速度更快,有些甚至具有多重处理能力,你的软件将比在微控制器上运行得更快。多酷啊。

我有几块这样的板 8 ,并且已经以各种方式使用过。我发现我所使用的主板最有趣的地方是有些支持 Arduino 屏蔽(类似于 Arduino Yún),允许您安装和访问 Arduino 屏蔽以及直接在主板上运行的连接电路。我称这些板为 Arduino 混合板。我将剩余的主板归入计算机主板类别,因为它们基本上都是全功能计算机。在接下来的部分中,我将讨论每个类别的几个例子。

Arduino 混合动力车

Arduino 混合板是一种包含 Arduino 接头的微控制器或低成本计算平台,可用作传感器或 IOT 网络中的网关,您可以远程登录到该板并访问 Arduino 屏蔽,就像该板是 Arduino 一样。这些板可以有更多的用途,但它们在这个角色上表现出色。您可以在网络上设置该板,使您能够远程访问(通过另一台计算机或终端)计算机端,同时连接并使用 Arduino shields 的资源。

一些 Arduino 混合板提供板载 Arduino 兼容处理器,以便您可以使用 Arduino 草图与电脑上的程序进行交互。这些板有点难以使用,因为它们通常不支持所有的 Arduino 屏蔽,但当使用 Python 等语言编程时,它们可能非常强大。

Note

一些电路板,如 Raspberry Pi,可以通过特殊的附加组件进行扩展(如 Raspberry Pi AlaMode9),提供与 Arduino hybrids 类似的功能。

我展示了两个 Arduino 混合板:流行的 pcDuino3B 和 Intel Galileo。

多氯联苯

pcDuino3B 是我用过的功能更强大的主板之一。它实际上是一台功能齐全的小型台式计算机。事实上,我已经将我的 pcDuino3B 作为笔记本电脑的替代品,只需插入鼠标、键盘和显示器。你不仅可以通过 WiFi 连接到互联网,而且主板运行的是一个更老的专门版本的 Ubuntu 12.04,它提供了一个功能齐全的桌面和许多与你在笔记本电脑或台式机上使用的相同的应用。除了桌子上有一块裸板,处理器的速度让体验更像笔记本电脑。

该主板拥有强大的 A20 处理器(比其他一些主板快得多),1Gb 内存,以及 HDMI 接口,通过板载视频处理器支持惊人的 1080p 分辨率。它还具有 WiFi、以太网、音频、USB、MicroSD 驱动器,甚至还有 SATA 驱动器控制器,允许添加主轴或固态硬盘进行存储。事实上,该板的功能列表令人印象深刻。

根据给定的类别,该板与流行的 Arduino 生态系统(如 Arduino shields)兼容。它还有自己的 I/O 引脚接口,包括 14 个通用 I/O 引脚(简称 GPIO)、2 个 PWM、6 个 ADC 以及 UART、SPI 和 I2C 引脚各 1 个。图 2-25 显示了最新的 pcDuino3B 板卡。

A978-1-4842-1293-6_2_Fig25_HTML.jpg

图 2-25。

pcDuino3B Note

pcDuino3B 仅支持 Arduino Uno R3 3.3v 屏蔽。

我经常使用 pcDuino3B 作为巡回 Arduino 实验室。鉴于其强大的计算功能,操作系统是从板载内存存储运行的,我可以从桌面运行 Arduino IDE,这使得我可以轻松地与 Arduino 屏蔽和硬件进行交互,而不必携带单独的计算机、电缆、Arduino 等。最好的是,它可以通过 USB 供电。

我还将 pcDuino3B 用作数据库服务器(见第 5 章)和网络服务器。由于它运行 Ubuntu,这个小主板几乎没有什么不能做的。也许唯一的缺点是它的价格是其他主板的两倍左右(但远不及英特尔主板)。因此,如果您必须在此板和更便宜的板之间做出选择,可能会归结为您是否需要 pcDuino3B 的强大功能,而不是更低的成本(和更少的功能)。

欲了解更多信息和 pcDuino3B 的完整规格,请参见 http://linksprite.com/wiki/index.php5?title=PcDuino3B

英特尔伽利略第二代

英特尔 Galileo Gen 2 基于英特尔 Quark SoC X1000 应用处理器,这是一款 32 位 CPU,基于封装为 SoC 的久负盛名的英特尔奔腾 CPU。它还具有 Arduino 接头,用于 Arduino Uno R3 屏蔽(3.3V 和一些 5V 屏蔽)。

也许它最有趣的特点是,由于 CPU 是英特尔芯片,主板可以运行几个操作系统(但不是同时),如新的 Windows 10 IOT 核心 10 和大多数 Linux 变种。它配有 Yocto Linux ( http://yoctoproject.org/ ),可从板载内存启动,但可以轻松升级。

Note

Yocto Linux 目前不支持像 pcDuino 上的 Ubuntu 那样广泛的软件工具。然而,越来越多的包被添加到 Yocto,所以如果你在 Yocto 上找不到你要找的包,它可能很快就会被添加。

该板具有许多功能,如 USB、以太网、RTC、MicroSD(用于运行操作系统,但与 pcDuino3B 不同的是,它具有有限的板载可启动操作系统)、8MB 闪存和 mini-PCI Express 插槽,这与任何其他板不同(但尚未成为主流)。图 2-26 显示了英特尔 Galileo Gen 2 主板。

A978-1-4842-1293-6_2_Fig26_HTML.jpg

图 2-26。

Intel Galileo Gen 2

该板需要外部电源,因此不能通过 USB 供电,但支持以太网供电。虽然它有许多有趣的功能,但它是市场上相对较新的产品,似乎不像其他一些主板那样受欢迎。也就是说,有几个 IOT 扩展或实验套件可用,如 SeeedStudio ( http://seeedstudio.com/depot/Grove-starter-kit-plus-Intel-IoT-Edition-for-Intel-Galileo-Gen-2-and-Edison-p-1978.html )的套件。

鉴于操作系统不如完整的 Linux 实现强大,您很可能想要创建一个引导映像并从 SD 驱动器运行操作系统的新版本。您可以在英特尔的 IOT 网站( https://software.intel.com/en-us/programming-blank-sd-card-with-yocto-linux-image-linux )上找到这方面的完整文档。

迄今为止,我一直将我的英特尔 Galileo Gen 2 作为一些基于 Arduino shield 的电路的网关。这是因为主板允许我对它进行编程,就像我使用英特尔的特殊版本 Arduino IDE(http://intel.com/support/galileo/sb/CS-035101.htm)对普通 Arduino 进行编程一样。此外,当我需要更多计算能力或希望从远程计算机(我可以远程登录的计算机)轻松访问 Arduino 硬件时,我使用 Galileo 代替 Arduino。

有关英特尔 Galileo Gen 2 的更多信息,请参见 https://software.intel.com/en-us/iot/hardware/galileo

计算机主板

第二类低成本计算板旨在成为嵌入式计算系统,不包括 Arduino 硬件兼容性(板上无屏蔽接头或 Arduino 兼容微控制器)。然而,我在这一类别中使用的所有电路板都支持各种硬件引脚,您可以使用这些引脚来连接传感器和其他组件和电路。因此,它们仍然是良好的 IOT 发展平台。他们不是 Arduino 混血儿。

这并不意味着它们的功能不比 Arduino 混合板差(pcDuino3B 可能是个例外);相反,它们通常比 Arduino 混合板更强大、更通用。凭借他们使用电脑时的感觉,你可能会对他们的能力感到惊讶。

这些主板的物理尺寸是另一个需要适应的特征。虽然大多数非常小,但它们的尺寸要求使用 PCB 的两面来安装元件。因此,大多数电路板在顶部和底部都有连接器和元件。这没什么大不了的,但是如果你打算将这些板安装在机壳中,可能需要一些仔细的安排,或者如果你裸着使用它们,可能需要一些小心的处理(只是裸板 12 )。幸运的是,你可以找到大多数这些电路板的定制 Shell,包括几个来自 http://thingiverse.com 的 3D 打印 Shell。

但是它们不仅仅是电脑。大多数都提供 GPIO 引脚,用于连接传感器、LCD、电路等。在这方面,它们类似于 Arduino,可以在许多相同的情况下使用。使它们不同的是计算机方面提供的更大的处理能力和连接性。也就是说,您可以使用 Raspberry Pi 代替 Arduino,并获得远程访问该节点并与之交互的能力,这在 Arduino 上是很难做到的。

此类主板通常是具有专用扩展硬件支持、更强大的操作系统支持(pcDuino 除外)和内置网络功能的计算机。这一类别中最受欢迎的板是 Raspberry Pi,这将在本书的后面部分重点介绍。然而,正如您将看到的,还有其他同样强大的替代方案。让我们从树莓酱的绝佳替代品——猎兔犬黑开始。

比格犬骨黑

BeagleBone Black 是一款小型主板,专为开发者和业余爱好者设计,使用基于 Linux 的体验来试验嵌入式解决方案中的硬件。事实上,BeagleBone Black 的设计可以在短短十秒内启动其 Linux 操作系统。越来越多的支持者每天都在增加知识库和论坛。或许 BeagleBone Black 最棒的一点是位于 http://beagleboard.org/getting-started 的启动指南。它旨在让你在短短五分钟内使用你的新“骨骼”开始运行。因此,对于熟悉 Linux 并希望快速入门的人来说,这种板很有吸引力。图 2-27 所示为黑色的猎兔犬骨。

A978-1-4842-1293-6_2_Fig27_HTML.jpg

图 2-27。

BeagleBone Black

该板也是一台功能强大的低成本计算机,配有 A8 ARM 处理器、512MB RAM、4Gb 闪存、USB 主机和客户端(用于电源和终端访问)、带 HDMI 输出的图形芯片和以太网。还有一个持续运行的电源连接器。

比格犬黑色的足迹是相当独特的。它可以放在一个更大的金属罐里,就像一些薄荷糖的包装一样。你需要用一对锡剪来切断 USB、电源和以太网的通路,但它会合适的。换句话说,它比大多数主板小一点。

它还有两排 46 针 GPIO 接头,用于连接传感器、LCD 面板等硬件。事实上,您可以像在 Arduino 或 Raspberry Pi 上一样使用这些头。您还可以连接称为 capes 的子板,以增加功能。Sparkfun 等供应商提供了几种披风,它们提供了以下披风:

我发现 BeagleBone Black 是一款功能强大的主板,几乎可以在任何需要更强大的处理器或需要进行超出基于微控制器的解决方案能力的额外处理的地方使用。我在一些硬件实验中使用了 BeagleBone Black,发现它是更受欢迎的 Raspberry Pi 的可行替代品。然而,在某些方面,Linux 的感觉和 BeagleBone Black 的适应性更吸引我,但这是个人的选择。也就是说,我的树莓板比骨头还多。

您可以在 http://elinux.org/Beagleboard:BeagleBoneBlack 了解有关 BeagleBone Black 硬件的更多信息,包括兼容的硬件附件、创建可启动的 Linux 映像等。

树莓 Pi 2 型号 B

树莓 Pi 2 Model B 是树莓 Pi 的最新迭代。它拥有原始 Raspberry Pi 的所有功能,但拥有更快的处理器和更多 USB 端口。Raspberry Pi 是一个受开发者欢迎的板,主要是因为它的低成本和易用性。鉴于 Raspberry Pi 的流行,我将在第 6 章中更详细地介绍它,包括如何开始使用它的简短教程。因此,我将在这里简要地介绍一下重点,并在第六章中对使用该板进行更详细的讨论。

Raspberry Pi 2B 硬件包括 900MHz A7 ARM CPU、1GB RAM、带 HDMI 输出的视频图形、4 个 USB 端口(旧款主板上只有 2 个)、以太网、摄像头接口(CSI)、显示器接口(DSI)、MicroSD 卡和 40 个 GPIO 引脚。图 2-28 显示了树莓派 2B 板。

A978-1-4842-1293-6_2_Fig28_HTML.jpg

图 2-28。

Raspberry Pi Model 2B

相机界面真的很有意思。您可以购买类似 Adafruit ( http://adafruit.com/categories/177 )的摄像头模块,并将其连接到板上,用作远程视频监控组件。我广泛使用了这一功能,将我的几块 Raspberry Pi 板变成了 3D 打印中心,我可以通过网络发送打印任务,远程打印和检查打印进度。使这成为可能的软件叫做 OctoPrint ( http://octoprint.org/ ),我在我的书《3D 打印机的维护和故障排除》(Apress,2014)中详细介绍了它。详见 http://apress.com/9781430268093?gtmf=s

LCD 界面也很有趣,因为现在有一个 7 英寸的 LCD 触摸板连接到 DSI 端口( http://element14.com/community/docs/DOC-78156/l/raspberry-pi-7-touchscreen-display )。我还没有得到其中的一个(它们供应短缺),但我有一些非常好的想法,一个壁挂式控制台,用于监控我的 IOT 解决方案。我还看到了许多有趣的 Raspberry Pi 平板电脑,它们使用了新的 LCD 触摸板。你可以在 http://thingiverse.com/thing:1082431 了解一个很有前途的例子(Adafruit 做的,所以我期待它很优秀)。 13

除此之外,Raspberry Pi 一直是我满足各种需求的首选,从更强大的传感器节点到数据聚合节点,再到托管数据库和 web 服务器。社区中也有很多关于如何在项目中使用 Raspberry Pi 的例子。有关树莓派的更多信息,请参见第 6 章。

树莓派 B

Raspberry Pi B 模型是前面讨论的 Pi 2B 的旧版本,具有较少的功能。我把它包括在这个列表中,因为这种板是丰富的,有时可以找到与较新的树莓 Pi 板相比的折扣。即便如此,它也有 512MB 的内存、两个 USB 端口和一个以太网端口。另外,像箱子这样的配件也很丰富,而且比新款便宜。

我发现只有少数情况下,我需要更强大的 Pi 2B。因此,如果您能找到一些旧的主板,您可以节省一些钱(也就是说,如果您不需要额外的 USB 端口或 P2i 2B 的其他功能)。图 2-29 显示了早期版本的 Raspberry Pi B 的示例。

A978-1-4842-1293-6_2_Fig29_HTML.jpg

图 2-29。

Raspberry Pi Model B

每当我需要 Raspberry Pi 作为嵌入式计算节点的能力,但不需要额外的 USB 设备的额外功能时,我经常使用我的旧 Raspberry Pi B 板(我似乎有太多的板),特别是在我可以使用以太网而不是 WiFi 的情况下。

我也用这种旧主板来做项目原型,因为损坏旧主板的风险(成本)没有新主板大。如果我炸了旧的板,我只是拉另一个出来或订购另一对夫妇使用的。如果我损坏了我最新的 2B 树莓酱,我会损失更多的钱,并且可能不得不等待一个新的(即使在今天,它们有时也很难找到)。

关于树莓派 B 的更多信息,请参见 https://www.raspberrypi.org/products/model-b/

英特尔爱迪生(带火花块)

英特尔 Edison 是另一种低成本计算板,但它不是一台微型计算机,而是一种比其他板更嵌入式的解决方案。我把它放在这里,是为了那些需要在尽可能小和多功能的封装中获得低成本计算机的强大功能的人。这块板子只有 35×25×4 毫米大小。Edison 拥有一个英特尔凌动 500MHz 双核双线程 CPU 和一个英特尔 Quark 100MHz 微控制器。一个大的 RFID 屏蔽隐藏了它的所有组件。Edison 还运行一个名为 Yocto 的 Linux 操作系统版本,该版本存储在固件中,启动速度非常快。

与类似的主板英特尔 Galileo 不同,英特尔 Edison 使用一个带有微型板对板连接器的模块,旨在允许您在 Edison 下方堆叠额外的主板,类似于 TinyDuino。与 Galileo 不同,您可以使用最新版本的 Arduino IDE 直接对 Edison 进行编程。您也可以用 C、C++或 Python 编写程序,在本地运行并访问硬件 GPIO 引脚。

由于英特尔 Edison 是一个嵌入式平台,它没有 USB、视频和网络连接,但它有 WiFi (802.11a/b/g/n)和蓝牙(4.0 和 2.1 EDR)。您可以通过使用附加板获得额外的端口。毕竟,它是一个嵌入式 IOT 解决方案,而不是一个计算平台。它也小得多,因此更容易装入远程节点或更小的设备中。

2-30 显示了英特尔 Edison 及其下方堆叠的许多附加板。如你所见,这是一个整洁的包裹。请注意带有英特尔爱迪生品牌的凸起主板。那是爱迪生!照片中的其他东西都是附加组件。

A978-1-4842-1293-6_2_Fig30_HTML.jpg

图 2-30。

Intel Edison (courtesy of http://​sparkfun.​com)

由于该板是作为 SoC 模块提供的,您需要购买一个主板来使用它,或者将其连接到您的计算机上进行编程或控制台访问。英特尔出售一种这样的主板,但 Sparkfun 还有其他几种选择,如下所述:

  • 英特尔 Edison Mini Breakout:比 Edison 略大,迷你 Breakout 提供 USB 端口,用于通过 UART 控制台(想想终端)和电源进行通信。英特尔通常出售这种带有爱迪生模块的迷你分线板。 http://sparkfun.com/products/13025
  • 英特尔 Arduino 扩展板:这是一个更大的主板,带有 Arduino 接头,用于安装 Arduino 屏蔽。它提供了与以前的主板相同的 USB 端口,并且像以前的主板一样,作为套件提供。如果你想用 Arduino IDE 给 Edison 编程,这个主板是个不错的选择。 http://sparkfun.com/products/13097
  • Sparkfun Base Block:这是一个较小的主板,具有相同的 USB 端口,但也带有一个直通微型板对板连接器,以便您可以使用额外的板。您也可以使用此板像 Arduino 一样对 Edison 进行编程。该板没有 Sparkfun 的爱迪生。 http://sparkfun.com/products/13045
  • Sparkfun 控制台模块:这类似于基础模块,但没有电源端口,因此只提供控制台访问。 http://sparkfun.com/products/13039

Edison 的模块化特性使您可以基于强大的主内核,仅使用所需的硬件来构建您的解决方案。Sparkfun 的爱迪生积木的加入更是如此。Sparkfun 已经创建了可堆叠模块(称为块),您可以使用它们来构建硬件堆栈,以满足您的嵌入式计算需求。有相当多的块可用于你想做的任何事情。

事实上,在 Raspberry Pi 或试验板布局中,有一些模块可以断开 GPIO 引脚,提供电池电源,并提供一个基于有机发光二极管的微型显示器;甚至还有一个 Arduino 标题块。使用积木真正酷的一面是它们的堆叠方式,这不仅可以让你根据需要定制硬件,还可以让硬件尽可能小,以匹配爱迪生的小尺寸。Sparkfun 的爱迪生积木样本列表如下:

有关英特尔 Edison 和 Sparkfun 的 Edison 模块的更多信息,请参见 Sparkfun 在 https://learn.sparkfun.com/tutorials/edison-getting-started-guide 的优秀指南。

Just How Low-Cost Are These Boards?

你可能想知道这些板的价格。以下是每种主板的平均价格,按提及的顺序排列。我不包括 Sparkfun Edison 积木,因为正如你将看到的,价格取决于你购买的积木,但一般来说,积木的价格在 15 美元到 35 美元之间,有些套件有折扣。我也省略了旧的树莓 Pi 模型 B,但使用的价格略低于这些板的 30 美元。

  • pcDuino3B : $60.00
  • 英特尔伽利略第二代:75.00 美元
  • 2B 覆盆子酱:42.00 美元
  • 英特尔爱迪生迷你突破:75.00 美元
  • 英特尔 Edison 和 Arduino 主机:100.00 美元

请注意,更贵的主板是英特尔选项。鉴于它主要是专有硬件,这并不令人惊讶。真正的便宜货是树莓派,这也是它受欢迎的部分原因。然而,就价格和功能而言,pcDuino3B 是我最喜欢的另一款主板。

现在,您已经了解了可以用来安装传感器的各种电路板,包括微控制器和低成本计算机电路板,接下来让我们看看另一个关键的硬件组件——传感器。

传感器

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

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

传感器有多种形式。它们通常是为单一用途设计的低成本设备,处理能力有限。大多数简单的传感器都是分立元件;甚至那些具有更复杂部件的设备也可以被视为单独的组件。传感器可以是模拟的,也可以是数字的,通常只用于测量一种东西。但是越来越多的传感器模块被设计来测量一组相关的现象。

模拟传感器

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

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

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

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

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

数字传感器

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

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

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

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

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

存储传感器数据

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

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

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

有许多简单的存储设备和技术可以用来构建自己的传感器网络,从 Arduino 的本地设备到 Raspberry Pi 上的现代硬盘。这里列出了这些存储机制,并在第 3 章中进行了更详细的讨论。

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

传感器的例子

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

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

  • 加速度计:这些传感器测量传感器或它所连接的任何东西的运动或移动。它们被设计用来感应几个轴上的运动(速度、倾斜度、振动等等)。一些包括回转仪特征。大多数是数字传感器。Wii 双截棍(或 WiiChuck)包含一个复杂的加速度计,用于跟踪运动。啊哈:现在你知道 Wii 附带的那些有趣的小东西的秘密了。
  • 音频传感器:也许这是显而易见的,但麦克风是用来测量声音的。大多数是模拟的,但一些更好的安全和监控传感器具有数字版本,用于传输数据的更高压缩。
  • 条形码阅读器:这些传感器设计用于读取条形码。最常见的是,条形码阅读器生成代表条形码的数字等价物的数字数据。这种传感器通常用在库存跟踪系统中,以在工厂或运输过程中跟踪设备。它们数量众多,而且许多价格经济实惠,使您能够将它们整合到自己的项目中。
  • RFID 传感器:射频识别使用无源设备(有时称为 RFID 标签)通过电磁感应使用射频传输数据。例如,RFID 标签可以是信用卡大小的塑料卡、标签或类似的包含特殊天线的东西,通常以线圈、细线或箔层的形式调谐到特定频率。当标签靠近阅读器放置时,阅读器发射无线电信号;标签可以使用电磁能量以无线电信号的形式传输嵌入在天线中的非易失性消息,该无线电信号然后被转换成字母数字串。 16
  • 生物传感器:读取指纹、虹膜或掌纹的传感器包含一个用于识别模式的特殊传感器。鉴于指纹和掌纹等图案的独特性,它们是安全访问系统的优秀组件。大多数生物传感器产生一组代表指纹或掌纹的数字数据。
  • 电容传感器:电容传感器的一种特殊应用,脉搏传感器被设计用来测量你的脉搏率,通常使用指尖作为传感部位。称为脉搏血氧仪(一些医疗专业人员称为 pulse-ox)的特殊设备通过电容传感器测量脉搏率,并通过光传感器确定血液中的含氧量。如果你拥有现代电子设备,你可能会遇到触敏按钮,它们使用特殊的电容传感器来检测触摸和压力。
  • 硬币传感器:这是一种最不寻常的传感器。这些设备就像典型的自动售货机上的投币口。像它们的商业等价物一样,它们可以被校准以感应何时插入一定大小的硬币。虽然不像商业单位那样复杂,可以区分真假硬币,硬币传感器可以用来为您的项目添加一个新的维度。想象一个投币 WiFi 站。现在,这应该可以防止孩子们在互联网上花太多时间!
  • 电流传感器:这些是用来测量电压和电流的。有些是为测量变化而设计的,而有些是为了测量负载。
  • 挠曲/力传感器:电阻传感器测量一块材料的挠曲或压力对传感器的影响。弯曲传感器可能有助于测量扭转效应或作为测量手指运动的一种手段(就像任天堂的电动手套)。当传感器弯曲时,传感器电阻增加。
  • 气体传感器:有很多种气体传感器。有些测量潜在有害气体,如液化石油气和甲烷以及其他气体,如氢气、氧气等。其他气体传感器与光传感器相结合来感测空气中的烟雾或污染物。下次当你从烟雾探测器中听到那种警告性的、通常令人讨厌的低电量警告声 18 时,想想那个设备包含了什么。为什么,这是一个传感器节点!
  • 光传感器:测量光线强度或缺乏的传感器是特殊类型的电阻器:光敏电阻器(ldr),有时被称为光敏电阻器或光电池。因此,它们本质上是相似的。如果你有一台 Mac 笔记本电脑,当你的发光键盘在弱光下自动打开时,你很可能已经看到了光敏电阻的作用。特殊形式的光传感器可以检测其他光谱,如红外线(如旧的电视遥控器)。
  • 液体流量传感器:这些传感器类似于阀门,安装在管道系统中。他们测量液体通过时的流量。基本的流量传感器使用旋转轮和磁铁来产生霍尔效应(快速开/关序列,其频率等于通过的水量)。
  • 液位传感器:一种特殊的电阻固态装置可以用来测量水体的相对高度。一个例子是当水位高时产生低电阻,当水位低时产生高电阻。
  • 位置传感器:现代智能手机有 GPS 传感器来感应位置,当然 GPS 设备使用 GPS 技术来帮助你导航。幸运的是,GPS 传感器以低成本的形式提供,使您能够将位置检测添加到您的传感器网络中。GPS 传感器以经度和纬度的形式生成数字数据,但有些传感器也可以感知海拔高度。
  • 磁条读取器:这些传感器从磁条(像信用卡上的磁条)读取数据,并返回数字形式的字母数字数据(实际的字符串)。
  • 磁力计:这些传感器通过磁场强度测量方向。指南针是一种用来寻找磁北的传感器。一些磁力计提供多个轴,以便更好地检测磁场。
  • 接近传感器:通常被认为是距离传感器,接近传感器使用红外线或声波来检测距离或范围到/从一个对象。受低成本机器人套件的欢迎,视差超声波传感器通过感应发送脉冲和接收脉冲之间的时间量(回声),使用声波来测量距离。对于近似的距离测量, 19 把时间转换成距离是一个简单的数学问题。这有多酷?
  • 辐射传感器:更严重的传感器是那些检测辐射的传感器。这也可能是电磁辐射(也有传感器),但盖革计数器使用辐射传感器来检测有害的电离。事实上,可以使用一个传感器和一个 Arduino(以及一些电子元件)来构建自己的盖革计数器。
  • 速度传感器:类似于流量传感器,许多自行车上的简单速度传感器使用磁铁和簧片开关来产生霍尔效应。频率结合车轮的周长可以用来计算速度,以及随着时间的推移,行驶的距离。是的,自行车计算机是简单传感器网络的另一个例子:车轮和前叉上的速度传感器为车把上的监控器提供数据。
  • 开关和按钮:这些是最基本的数字传感器,用于检测某个东西是设置(开)还是复位(关)。
  • 倾斜开关:这些传感器可以检测设备何时向某个方向倾斜。虽然简单,但它们对于低成本运动检测传感器非常有用。它们是数字的,本质上是开关。
  • 触摸传感器:形成小键盘、键盘、定点设备等的触摸敏感膜是一种有趣的传感器形式。你可以将像这样的触摸感应设备用于需要从人类身上收集数据的传感器网络。
  • 视频传感器:如前所述,有可能获得小型视频传感器,这些传感器使用摄像机和电路来捕捉图像并将其作为数字数据传输。
  • 天气传感器:温度、气压、降雨量、湿度、风速等传感器都属于天气传感器。大多数生成数字数据,可以组合起来创建全面的环境传感器网络。是的,通过十几个廉价的传感器、一个 Arduino(或一个 Raspberry Pi)和一些解释和组合数据的程序,你可以建立自己的气象站。

计算机系统

如果不提及计算机系统,对 IOT 硬件(至少是网络中节点的硬件)的完整讨论将是不完整的。台式机或服务器计算机可以以多种方式用于您的 IOT 解决方案,从为数据库服务器、web 服务器、防火墙到互联网或云网关(或所有这些)提供可靠的平台。

我通常尽量避免在我的 IOT 解决方案中使用计算机系统,主要是为了降低成本,但也因为低成本的计算机板和微控制器板对于我已经构建(或梦想构建)的解决方案来说绰绰有余。

但是,如果您确实想在您的 IOT 解决方案中使用计算机系统,您应该这样做。只要考虑一下额外的成本(即使是小鞋盒电脑也是树莓派的好几倍)、物理尺寸和安装或位置,以及额外的电力需求(它们不容易用电池或太阳能供电)。

最后,确保您采取必要的预防措施来保护您的计算机系统,使它们不容易受到入侵或邀请攻击。黑客对计算机系统的破坏通常比 Arduino 更大。但是,不要认为低价电脑主板是安全的;他们不是。鉴于它们通常运行强大的 Linux 操作系统,大多数也是黑客的好目标。

摘要

IOT 解决方案提供的硬件包括用于为传感器、电源和许多其他需求创建电路的任何分立电子元件。在这个庞大的电子产品类别中,有用于支持 IOT 解决方案功能的组件。更具体地说,我们可以选择建立一个节点网络来观察、记录和显示我们周围世界的信息。

在本章中,您研究了一长串微控制器,重点是 Arduino 系列微控制器板、Raspberry Pi 等低成本(低功耗)计算板,以及可用于连接节点的各种通信硬件,从使用连接到互联网的以太网或 WiFi 网络到用于连接传感器节点的低成本、低功耗无线通信模块(XBee 无线电)。

您还看到了关于如何使用 Arduino 编程环境的简短教程,甚至看到了一个简单的基于传感器的 Arduino 项目的示例(回想一下,开关就是一个简单的传感器)。

最后,您简要了解了传感器,包括您可以在 IOT 解决方案中使用的传感器类型和种类的概述。

在下一章中,您将研究 IOT 解决方案中的数据如何存储(在本地设备上,如基于内存附加文件的解决方案),如何发送到网络中的其他节点,或者如何保存到数据库服务器。

Footnotes 1

https://en.wikipedia.org/wiki/Microcontroller

2

http://atmel.cimg/doc8161.pdf

3

有关 Arduino.cc 许可政策的完整描述以及有关构建和销售您自己的 Arduino 兼容主板的更多信息,请参见 http://arduino.cc/en/Main/FAQ

4

或者像我祖母常说的,“一个 25 美分的硬币。”

5

TinyCircuits 还制造了一种兼容 LilyPad 的模块线,大小相当于美国一角硬币(10 分硬币),称为 TinyLily ( http://tiny-circuits.com/products/tiny-lily.html )。

6

好吧,永久的,直到你卖掉它!

7

Mini-computers 不是一个很好的名字,因为其中一些主板不包括视频控制器或支持普通的笔记本电脑或台式机外围设备。

8

如你所见,我有一颗狂热的心。总会有地方多放一块板的!

9

http://makershed.com/products/alamode-for-raspberry-pi

10

目前,Windows 10 IOT 套件尚未正式发布,但测试版已被证明是有趣和有前途的。

11

与 Arduino 分线主板一起使用时,Intel Edison 是一个小例外。

12

把那东西盖上,附近可能有小孩!

13

这个案例是 3D 打印的,所以如果你没有 3D 打印机,你需要找人帮你打印。一个完整的教程就如何建立它包括在内。

14

令人震惊,不是吗?

15

这取决于并行缓冲区的宽度。8 位缓冲区一次可以传输 1 个字节,16 位缓冲区一次可以传输 2 个字节,依此类推。

16

http://en.wikipedia.org/wiki/Radio-frequency_identification

17

www.sparkfun.com/products/11719

18

我从来不知道哪个探测器在响,所以我更换了所有探测器的电池。

19

精度可能取决于环境变量,如海拔、温度等。

三、IOT 数据是如何存储的

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​3) contains supplementary material, which is available to authorized users.

询问任何有成就的专业软件工程师、开发者、架构师或项目负责人,他们都会告诉您,一个性能良好的解决方案的关键是从一个设计和测试良好的计划开始,该计划用于设计和实现项目的数据存储元素。虽然有许多其他方面对质量和成功同样重要,但数据存储元素无疑是成功的主要竞争者之一。

IOT 解决方案也是如此。这超出了最初的数据存储位置问题。为了取得成功,开发解决方案的计划还必须考虑存储什么,更重要的是,如何存储。

我见过许多业余爱好者用各种方式存储数据的解决方案。一些(如果不是大多数)解决方案运行良好,并且几乎没有与数据相关的问题,也就是说,直到出现问题,例如需要修改数据、恢复数据或添加功能来存储其他数据。在这些情况下,很快就会发现数据存储组件无法胜任任务。

有时结果是需要改变数据的存储方式,使其与软件的早期版本不兼容。其他时候,它需要重新设计,比应该需要的甚至预期的要多得多,导致项目延期完成。这些弊病的根本原因通常是数据存储组件设计不良或设计不足。

例如,如果一个解决方案将数据作为文本存储在一个文件中(比如日志),会有什么影响。当然,数据就在那里,您可以很容易地编写一个程序来读取它,但是该解决方案不能扩展到数据变得非常大(数千甚至数百万行)的情况。此外,将数据存储在文件中意味着每一段数据都必须转换为字符串,这需要在执行任何数学运算之前转换回其原始类型。也许更糟的是,没有简单的方法来对数据执行任何类型的特别查询。也就是说,要执行查询,您必须增加用于读取数据的程序代码,编写专门的代码来检查数据。

在这种情况下,需要更多地考虑如何使用数据以及如何存储数据。在本章中,您将更详细地研究这些主题。

Note

这些例子非常简单,目的是为了让 2 更容易理解。Arduino IDE 和程序的简要概述在前一章中已经介绍过。

让我们首先回顾一下 IOT 解决方案,特别是分布式 IOT 解决方案,是如何由网络架构中的几种类型的节点形成的。

分布式 IOT

IOT 解决方案有多种设计和组装方式。一些解决方案使用单个组件设计,将所有硬件装在一个盒子里。虽然这种设计理念似乎在早期的 IOT 解决方案中很常见,但还有另一种设计理念可以提供比单一硬件解决方案更多的通用性、可扩展性和功能。

这种理念(在某种程度上)是从传感器网络借鉴来的,在传感器网络中,解决方案由多个分布式组件组成。在分布式解决方案中,组件使用一个或多个网络协议相互通信。在前一章中,我们讨论了如何收集数据并在网络中的节点间传递,您已经看到了这一点。

在本节中,我们将讨论分布式 IOT 解决方案。这些解决方案由一个或多个数据收集器和一个或多个传感器组成,使用通信方法或协议来传输数据。如上所述,通信方法可以使用微控制器(例如 Arduino)、嵌入式系统等设备,甚至是 Raspberry Pi 等小尺寸计算机。

通常,数据收集器(在传感器网络中称为传感器节点)是为无人值守操作而设计的;它们有时安装在移动物体上或有线通信不切实际的地方。在这些情况下,数据采集器可以被设计成在不受电源(使用电池或太阳能)或通信源(使用无线机制)限制的情况下工作。数据收集器的接收器可以是处理数据和存储数据的节点(数据聚合器节点)或单个数据库服务器。

虽然您已经在第 1 章中看到了每种类型节点的概述,但本节将详细介绍如何使用每个节点组成网络来收集、传输、增加和存储数据。正如您将看到的,我划分了一些类别来进一步定义节点的类型。

3-1 显示了每种类型的节点将如何在一个虚构的 IOT 解决方案中使用。我们将在下面的小节中更详细地讨论每一个。

A978-1-4842-1293-6_3_Fig1_HTML.jpg

图 3-1。

IOT distributed network nodes

在本例中,顶部的几个数据收集器节点将数据无线发送到中间的数据聚合器节点(有时称为数据节点)。数据节点收集数据并将其保存到安全数字卡中,然后通过有线计算机网络与数据库服务器通信以存储数据。将数据存储在中间数据聚合器节点上可以确保在数据库服务器出现故障或网络中断时不会丢失任何数据。

数据收集器

网络的最低层(或叶层)是数据收集器。它具有至少一个传感器和通信机制,通常是无线协议。这些节点不以任何方式存储或处理捕获的数据,它们只是将数据传递给网络中的另一个节点。

带存储器的数据收集器

下一类节点是存储数据的数据收集器节点。虽然这些节点可以将数据发送到另一个节点,但通常它们是将数据保存到数据卡等存储机制的设备,通过与台式机或服务器计算机的链接保存到数据库,或者直接保存到 LCD 屏幕、面板仪表或 LED 指示灯等视觉输出设备。

数据节点需要的设备不仅仅是将数据传递给另一个节点。他们需要能够记录或展示数据。这是微控制器的一个很好的用途,你会在后面的章节中看到。数据节点可用于形成自主或无人值守的传感器网络,记录数据供以后存档。

例如,考虑一个鱼或花园池塘。有许多商业池塘监控系统采用具有多个传感器的独立传感器设备,这些传感器向数据节点发送数据;用户可以访问数据节点并在计算机上读取用于分析的数据。

可操作装置

可动作设备是类似于数据收集器的另一个节点,并且实际上可以具有数据收集特征。然而,与仅观察和发送数据的数据收集器不同,可操作设备可以直接控制或被给予命令来执行。例如,具有平移和倾斜功能的摄像机可以产生大范围的视频流或静态照片,并接收平移和倾斜功能的命令。

通常,可动作设备需要网络中的另一个节点来接收和发送命令。这可能是一个面向前方(如在互联网中)的计算机或微控制器或远程安装的控制面板,如平板电脑。

数据聚合器

另一种类型的节点是聚合节点。这些节点通常使用通信设备和记录设备(或网关)而没有传感器。它们用于从一个或多个数据收集器或其他数据聚合器节点收集数据。在到目前为止讨论的例子中,监控系统将具有一个或多个聚集器节点来从传感器读取数据。

可以使用数据聚合器来扩充数据,方法是添加对数据进行分类的逻辑、添加日期和时间信息,甚至在保存数据之前对数据执行转换。

数据聚合器还可以用于在数据发送到数据库服务器之前存储数据的临时副本。如果网络在一个或多个节点甚至所采用的通信协议中遇到故障,这允许一些小的可恢复性或连续的数据收集。

数据库服务器

数据库服务器节点顾名思义就是一台托管数据库服务器的计算机,该服务器可用于保存和检索网络中其余节点的数据。虽然有些解决方案使用专用的数据库服务器(我设计的网络使用这种服务器),但有些支持可访问互联网的功能(如网站或控制面板)的解决方案会将数据库服务器与应用放在同一台机器上(例如 web 服务器)。

但是,这通常被认为是一个潜在的漏洞。也就是说,如果 web 服务器受损,数据库服务器受损的可能性相当大。如果可能,最好将数据库服务器放在单独的节点上。如果解决方案与其他网络或互联网隔离,这种脆弱性就会减少。

现在您已经知道了分布式 IOT 解决方案由哪些节点组成,让我们看看如何在整个网络中存储数据。我最后重点介绍数据库服务器选项,但首先介绍几个备选方案,因为根据解决方案的需要,可能需要一些本地存储。例如,在本地存储对数据采取的操作的副本可能有助于诊断问题。

Note

由于收集的大多数数据是传感器数据,以下存储选项使用传感器数据来说明概念。

本地设备上存储

根据数据代表的内容,在本地设备(如 SD 卡、硬盘、电子存储器等)上存储数据可能会很复杂。例如,传感器数据可以有几种形式。传感器可以产生由浮点数或整数组成的数字数据。一些传感器产生更复杂的信息,这些信息被组合在一起,可能包含多种形式的数据。知道如何解释读取的值通常是使用传感器最困难的部分。事实上,您可以在许多传感器节点示例中看到这一点。例如,温度传感器产生的值必须转换成刻度才有意义。

虽然可以将所有数据存储为文本,但如果您希望在另一个应用中使用数据,或者在电子表格或统计应用中使用数据,您可能需要考虑以二进制形式或易于转换的文本形式存储数据。例如,大多数电子表格应用可以很容易地将文本字符串如123.45转换成浮点数,但是它们可能无法将12E236转换成浮点数。另一方面,如果您计划为 Arduino 草图或 Raspberry Pi Python 脚本编写额外的代码来处理数据,您可能希望以二进制形式存储数据,以避免必须编写成本高昂(并且可能很慢)的转换例程。

但这只是问题的一部分。你在哪里存储数据是一个更大的问题。您希望以您需要的形式存储数据,但也希望将数据存储在一个位置(在设备上),您可以从该位置检索数据,并且在主机重新启动时不会被擦除。例如,将数据存储在 Arduino 的主内存中并不是一个好主意。它不仅消耗宝贵的程序空间,而且是易失性的,当 Arduino 断电时会被擦除。

以下部分研究了本地存储数据的几种选择。我从 Raspberry Pi 和 Arduino 的例子开始。然而,相似的平台提供相同的(如果不是相似的话)选项。

树莓码头上的本地存储

Raspberry Pi 为本地存储提供了许多选项。您可以轻松地创建一个文件,并将数据存储在根分区或 SD 卡上的主目录中。这是非易失性的,不会影响 Raspberry Pi 操作系统的运行。唯一的缺点是,如果数据显著增长,它可能会导致磁盘空间过少。但是数据必须增长到接近 2GB(对于 2GB 的 SD 卡)才会威胁到操作系统的稳定性(尽管这种情况可能发生)。

也有可能你有一个可移动的驱动器,如 USB 拇指驱动器,甚至 USB 硬盘驱动器连接。一旦安装了设备和驱动器分区,您就可以从 Raspberry Pi 读取和写入文件。当您发现如何使用 Raspberry Pi 构建数据库服务器时,您将在第 5 章中看到这一点。

因为 Raspberry Pi 实际上是一台个人电脑,它具有创建、读取和写入文件的能力。虽然可以使用通过 GPIO 头连接的 EEPROM,但考虑到编程简单和使用文件方便,几乎不需要其他形式的存储。

Raspberry Pi 可以用于许多编程语言。Python 是最流行的语言之一。在 Python 中处理文件很容易,并且是默认库所固有的。这意味着您不需要添加任何东西来使用文件。

以下示例展示了使用 Python 处理文件的简易性。你会注意到,文件放在哪里并不重要——是放在 SD 卡上还是 USB 驱动器上。您只需要知道您想要存储数据的位置(文件夹)的路径,并将其传递给open()方法。

Tip

在线 Python 文档详细解释了读写文件( http://docs.python.org/2/tutorial/inputoutput.html#reading-and-writing-files )。

将数据写入文件

这个例子展示了用 Python 在 Raspberry Pi 上使用文件是多么容易。我将演示如何从登录用户的主目录中读取和写入文件。这不需要任何额外的硬件或软件库。因此,您可以在任何 Raspberry Pi(或任何兼容 Linux 的系统)上执行这个示例。在本例中,您将访问的文件就像一个日志。也就是说,您总是将新数据写到文件的末尾。

首先创建一个包含 Python 命令的文件。如果您不了解 Python,不要担心,因为这些命令很容易理解,并且在大多数情况下使用起来很直观。如果你曾经写过一个程序来读取文件(或者一个脚本或者一个命令/批处理文件),这段代码看起来会很熟悉。

如果您没有使用过 Raspberry Pi,但是有一台运行 Mac、Linux 或 Windows 的计算机,您也可以在那里执行这个示例。只需记住将您想要读取和写入的文件的路径改为适合您的系统的路径。

首先打开电源并登录到您的树莓 Pi。然后用下面的命令(或类似的命令)在您的主目录或您有读写权限的地方打开一个新文件。你可以使用任何你想要的编辑器。清单 3-1 显示了这个例子的代码。

nano log_file_example.py

Tip

用扩展名.py命名该文件,以表明它是一个 Python 脚本。在文件中输入清单 3-1 中的代码。

Listing 3-1.Log File Example (Raspberry Pi)

from __future__ import print_function

import datetime   # date and time library

# We begin by creating the file and writing some data.

log_file = open("log.txt", "a+")

for i in range(0,10):

log_file.write("%d,%s\n" % (i, datetime.datetime.now()))

log_file.close()

# Now, we open the file and read the contents printing out

# those rows that have values in the first column > 5

log_file = open("log.txt", "r")

rows = log_file.readlines();

for row in rows:

columns = row.split(",")

if (int(columns[0]) > 5):

print(">", row, end="")

log_file.close()

在本例中,您首先导入datetime。您使用datetime来获取当前的日期和时间。接下来,打开文件(注意,因为没有指定路径,所以使用的是当前目录),向文件中写入十行,然后关闭文件。

注意open()方法。它需要两个参数——文件路径和名称以及打开文件的模式。您使用"a+"添加到文件中(a),如果文件不存在(+)则创建该文件。其他值包括用于读取的r和用于写入的w。其中一些可以合并。例如,"rw+"创建一个不存在的文件,并允许读写数据。

Note

使用写入模式会截断文件。对于大多数需要存储传感器样本的情况,可以使用追加模式。

接下来,您关闭文件,然后重新打开它进行阅读。这演示了如何读取一个文件并在其中搜索数据。在这种情况下,您读取所有的行,然后对每一行划分(使用split()函数)成列。请注意,在写入数据的行中,各列用逗号分隔。

在这种情况下,您将查找第一列大于 5 的任何行。由于该文件是一个文本文件,因此读取的每一行都是一个字符串,所以使用int()函数将第一列转换为一个整数。一旦你找到这一行,你就把它打印出来。

在这一点上,我应该提到,阅读文件需要对它的布局(组成)有深入的了解,包括列数、数据类型等等。没有这些知识,你就不能编写一个可靠地读取数据的程序。

如果您正在跟进,请继续运行该脚本。要执行该文件,请使用以下命令:

python ./log_file_example.py

如果出现错误,请检查代码并更正任何语法错误。如果打开文件时遇到问题(运行脚本时出现 I/O 错误),请尝试检查您正在使用的文件夹的权限。尝试多次运行该脚本,然后显示文件的内容。清单 3-2 显示了连续运行三次的完整命令序列。

Listing 3-2.Log File Example Output (Raspberry Pi)

$ python ./log_file_example.py

> 6,2015-10-14 20:42:33.063794

> 7,2015-10-14 20:42:33.063799

> 8,2015-10-14 20:42:33.063804

> 9,2015-10-14 20:42:33.063808

$ python ./log_file_example.py

> 6,2015-10-14 20:42:33.063794

> 7,2015-10-14 20:42:33.063799

> 8,2015-10-14 20:42:33.063804

> 9,2015-10-14 20:42:33.063808

> 6,2015-10-14 20:42:38.128724

> 7,2015-10-14 20:42:38.128729

> 8,2015-10-14 20:42:38.128734

> 9,2015-10-14 20:42:38.128739

$ python ./log_file_example.py

> 6,2015-10-14 20:42:33.063794

> 7,2015-10-14 20:42:33.063799

> 8,2015-10-14 20:42:33.063804

> 9,2015-10-14 20:42:33.063808

> 6,2015-10-14 20:42:38.128724

> 7,2015-10-14 20:42:38.128729

> 8,2015-10-14 20:42:38.128734

> 9,2015-10-14 20:42:38.128739

> 6,2015-10-14 20:42:39.262215

> 7,2015-10-14 20:42:39.262220

> 8,2015-10-14 20:42:39.262225

> 9,2015-10-14 20:42:39.262230

有没有得到类似的结果?如果没有,请更正任何错误,然后重试,直到成功为止。请注意,我第一次运行该脚本时得到了三行,接下来是六行,然后是九行。这是因为脚本的第一部分将数据附加到文件的末尾。如果您想重新开始,只需删除创建的文件。

从这个简单的例子中可以看出,使用 Python 读写日志文件很容易。

Arduino 上的本地存储

虽然 Arduino 确实没有板载存储设备,但有两种方法可以在 Arduino 上本地存储数据。您可以将数据存储在一种特殊形式的非易失性存储器中,或者通过特殊的 SD 卡盾或以太网盾(大多数以太网盾都有内置的 SD 卡驱动器)托管在 SD 卡上。

Note

如果你真的很有创造力(或者无法抗拒挑战),你可以使用一些通信协议向其他设备发送数据。例如,您可以使用串行接口将数据写入串行设备。

以下部分将更详细地讨论每个选项。后面的小节介绍了一些小项目,您可以用它们来学习如何使用这些设备来存储数据。

非易失存储器

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 的内容转储到另一个节点(远程存储)。

如何在 Arduino 上使用 EEPROM 的详细示例超出了本书的范围。但是我想讨论这个选项,因为它是 Arduino 的一个可行的替代方案,并且少量的数据是很方便的。然而,我在我的书《使用 Arduino 和 Raspberry Pi 开始传感器网络》的第 5 章中详细描述了这个过程(Apress,2014)。

sd 卡

您还可以在 SD 卡上存储(和检索)数据。Arduino IDE 有一个用于与 SD 驱动器交互的库。在这种情况下,您可以使用库通过 SD shield 或以太网 shield 访问 SD 驱动器。

在 SD 卡上存储数据是通过文件完成的,在概念上类似于前面的例子。您打开一个文件,以最适合下一阶段数据分析的格式将数据写入其中。Arduino IDE 和其他地方的示例演示了如何为 Arduino 创建 web 服务器界面,以显示 SD 卡上可用文件的列表。

在数据节点被设计为不与其他节点连接的远程单元的情况下,您可以选择将数据存储到 SD 卡,或者在数据节点断开连接或数据聚合器节点关闭时,您可以将它用作备份日志记录设备。由于该卡可在其他设备上移动和读取,当您想要使用数据时,可以在另一个设备上读取它。

使用 SD 卡意味着您可以将数据从传感器节点移动到计算机,只需从 Arduino 上拔下 SD 卡,然后将其插入计算机中的 SD 读卡器即可。让我们看看如何在 Arduino 上读写 SD 卡的数据。清单 3-3 展示了日志文件概念的完整示例。你需要一个格式化为 FAT 分区的 SD 卡。

Storing Date And Time With Samples

Arduino 没有机载实时时钟(RTC)。如果您想在本地存储数据,您必须存储带有大致日期和时间戳的数据,或者使用 RTC 模块读取准确的日期/时间值。幸运的是,有 RTC 模块可以和 Arduino 一起使用。

Listing 3-3.Log File Example (Arduino)

/**

Example Arduino SD card log file.

This project demonstrates how to save data to a

microSD card as a log file and read it.

*/

#include <SPI.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

File log_file;

void setup() {

char c = ' '

char number[4];

int i = 0;

int value = 0;

String text_string;

Serial.begin(115200);

while (!Serial); // wait for serial to load

Serial.print("Initializing SD card...");

if (!SD.begin(SD_PIN)) {

Serial.println("ERROR!");

return;

}

Serial.println("done.");

// Begin writing rows to the file

log_file = SD.open("log.txt", FILE_WRITE);

if (log_file) {

for (int i=0; i < 10; i++) {

text_string = String(i);

text_string += ", Example row: ";

text_string += String(i+1);

log_file.println(text_string);

}

log_file.close();

} else {

Serial.println("Cannot open file for writing.");

}

// Begin reading rows from the file

log_file = SD.open("log.txt");

if (log_file) {

// Read one row at a time.

while (log_file.available()) {

text_string = String("");

// Read first column

i = 0;

while ((c != ',') && (i < 4)) {

c = log_file.read();

text_string += c;

if (c != ',') {

number[i] = c;

}

i++;

}

number[i] = '\0'

value = atoi(number);

// Read second column

c = ' '

while (c != '\n') {

c = log_file.read();

text_string += c;

}

// If value > 5, print the row

if (value > 5) {

Serial.print("> ");

Serial.print(text_string);

}

}

// close the file:

log_file.close();

} else {

// if the file didn't open, print an error:

Serial.println("Cannot open file for reading");

}

}

void loop() {

// do nothing

}

Note

我省略了日期和时间的书写。有一种方法可以做到这一点,但它需要更多的代码,我想把精力集中在文件操作上。要了解如何在 Arduino 上使用DateTime库,请参见 http://playground.arduino.cc/Code/DateTime

请注意,这里的代码比 Raspberry Pi 示例多得多。这是因为 Arduino 代码库没有 Python 那样的高级原语。于是,我们不得不自己做很多底层的操作。

为了简单起见,我将代码放在setup()方法中,这样它只运行一次。loop()方法中的回调代码重复运行,直到 Arduino 断电(或者您启动低级代码来暂停执行或重启)。如果您在 IOT 解决方案中使用这个例子,启动 SD 卡的代码将保留在setup()方法中,甚至可能是打开文件的代码,但是写文件的代码将被移到loop()方法中,以记录从传感器或其他数据收集器读取的数据。

代码示例从一些变量开始,这些变量用于选择与 SD 卡通信的 pin。我包括了几个受欢迎的选项。查看硬件文档,确保指定了正确的 pin。

接下来,您将看到打开文件并写入几行的代码。在本例中,我简单地使用计数器的值编写了一个简单的文本字符串,后跟一些简短的文本。正如我提到的,我在这个例子中没有记录日期和时间信息,因为 Arduino 不包括 RTC(尽管较新的电路板可能包括 RTC)。请注意,我打开了文件,写入了行,然后关闭了文件。

接下来是从文件中读取行的代码块。与 Raspberry Pi 示例一样,我们必须自己读取行并拆分列。但是,在这种情况下,我们必须一次读一个字符。因此,我使用一个循环来完成这项工作,并在定位到逗号时终止。循环的第二部分从该行中读取剩余的文本,直到找到换行符。

然后,我检查第一列的值(在将其转换为整数之后),如果大于 5,我将该行输出到串行监控器。让我们看看这段代码的运行情况。清单 3-4 显示了在串行监控器中看到的代码输出。

Listing 3-4.Log File Example Output (Arduino)

Initializing SD card...done.

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

Initializing SD card...done.

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

Initializing SD card...done.

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

> 6, Example row: 7

> 7, Example row: 8

> 8, Example row: 9

> 9, Example row: 10

如您所见,输出类似于 Raspberry Pi 示例。显然,两者都可以用来在本地文件中存储数据,但是 Arduino 需要做更多的工作。幸运的是,Arduino IDE 包括一个设计良好的 SD 卡库。

事实上,SD 卡库中还有许多其他功能可以用来处理文件。例如,您可以列出卡上的文件,创建文件夹,甚至截断或删除文件的内容。您可能会发现这段代码在处理前面的代码时很有用。下面显示了截断一个文件是多么容易。如果您想使用它,请在文件第一次打开之前将其放在代码中。

if (SD.remove("log.txt")) {

Serial.println("file removed");

}

Tip

有关在 Arduino 上使用 SD 卡的更多信息,请参阅 Arduino 在线参考指南( http://arduino.cc/en/Reference/SDCardNotes ))。

现在,您已经看到了在数据收集器节点上本地存储数据的一些选项,下一节将讨论如何将数据发送到数据聚合器进行本地或远程存储。在这种情况下,远程存储通常是数据库服务器。

将责任推卸给聚合者

回想一下,数据聚合器是一个特殊的节点,用于从多个来源(数据收集器或传感器)接收信息,并在本地或远程存储结果。源数据可以来源于节点本身上的多个传感器,但更常见的是,数据聚集器从多个数据收集器节点接收信息,这些数据收集器节点不直接连接到聚集节点(它们可以通过诸如 XBee 模块提供的低功率、低开销的通信协议连接)。

What’S An Xbee?

XBee 是 Digi International 的品牌名称,代表小型、独立、模块化、高性价比的组件,使用射频(RF)在 XBee 模块之间交换数据。XBee 模块在 2.4GHz 或远程 900MHz 上传输,并有自己的网络协议(ZigBee)。

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

在一些 IOT 解决方案中,传感器由其他节点托管并放置在远程位置。数据聚集器节点通过有线或无线连接连接到数据收集器节点。例如,您可能在一个位置将一个传感器托管在低功耗 Arduino 上,而在另一个位置将另一个传感器托管在 Raspberry Pi 上,这两个传感器都使用 XBee 模块连接到您的数据聚合器。除了所选网络介质的限制之外,您可以让几十个节点向一个或多个数据聚合器节点提供传感器数据。

使用数据聚合器节点有几个优点。如果您使用无线技术,如带有 XBee 模块的 ZigBee,数据聚合器节点可以允许您通过将数据聚合器节点放置在离传感器最近的位置来扩展网络的范围。然后,数据聚合器节点可以通过更可靠的介质将数据传输到另一个节点,例如数据库服务器。

例如,您可能希望将数据聚集器节点放置在有电源和以太网连接的外屋中,以从位于各种其他建筑物中的远程数据收集器节点收集数据。

Note

在这种情况下,我指的是距离传感器节点最近的点,该点仍在无线传输介质(如 XBee 模块上使用的 ZigBee)的范围内。

数据聚合器节点还允许您将处理一组传感器的逻辑移动到一个更强大的节点。例如,如果您使用需要代码来处理值的传感器,则可以使用数据聚合器节点来接收来自这些传感器的原始数据,存储这些数据,并在以后计算这些值。

这不仅确保了您只在一个位置拥有代码,而且还允许您为远程传感器使用不太复杂(功能不太强大)的主机。也就是说,您可以为传感器使用较便宜或较旧的 Arduino 板,为数据聚合器节点使用功能更强大的 Arduino 板。这样做还有一个额外的好处,就是如果一个远程传感器损坏了,更换起来并不昂贵。

还记得,您必须决定要将传感器数据存储在哪里。数据聚合器节点可以将数据本地存储在可移动介质或机载存储设备上(本地存储),也可以将数据传输到另一个节点进行存储(远程存储)。选择使用哪一个通常基于数据将如何被消费或查看。

例如,如果您只想存储从传感器读取的最后一个值,您可能需要考虑某种形式的可视化显示或远程访问机制。在这种情况下,使用本地存储,只存储最新的值,可能更经济,也更简单。

另一方面,如果您需要记录一段时间内的数据值以供以后处理,您应该考虑将数据存储在另一个节点上,以便可以在不影响传感器网络的情况下访问数据。也就是说,您可以将数据存储在更强大的系统上(比如个人计算机、服务器或基于云的服务),并进一步降低聚合节点发生故障时丢失数据的风险。

本地存储的性质是使用本地存储数据聚合器节点的一个限制因素。也就是说,如果您想在以后处理数据,您可以选择一种介质,允许您检索数据并将其移动到另一台计算机上。

这并不意味着本地存储数据聚合器是一个无用的概念。让我们考虑这样一种情况,您想要监控几个附属建筑的温度。您没有将数据用于任何分析,而只是希望能够在方便时(或需要时)读取这些值。

一个可能的解决方案是设计带有可视化显示的本地存储数据聚合器节点。例如,您可以使用 LCD 来显示传感器数据。当然,这意味着数据聚合器节点必须在一个您可以轻松到达的位置。

但是,让我们考虑这样一种情况,您的数据聚合器节点也位于远程位置。也许它也在另一个外屋,但是你大部分时间都在不同的地方度过。在这种情况下,远程访问解决方案是最好的。

这种数据聚合器节点的设计将需要在本地存储最新的值,比如在存储器或 EEPROM 中,并且当客户端连接时,显示数据。对于本地存储数据聚合器节点来说,这是一个简单而优雅的解决方案。

但是,将数据从数据收集器传递到数据库服务器更可靠。正如您将在第 5 章中看到的,您可以使用 Raspberry Pi 或类似的低成本板来构建数据库服务器,并将其部署在您的 IOT 解决方案中。

然后,数据聚合器节点需要一个名为连接器的库,以允许您编写程序(脚本、草图)来连接到数据库服务器并发送数据。在本书中,我重点介绍了 MySQL 数据库服务器。在 Raspberry Pi 的例子中,我将演示的数据库连接器称为 Connector/Python,可以从 Oracle ( http://dev.mysql.com/downloads/connector/python/ )获得。我还将讨论 Arduino 的一个数据库连接器,名为 Connector/Arduino,它是我创建的 4 ,可以在 GitHub ( https://github.com/ChuckBell/MySQL_Connector_Arduino )上获得,也可以通过 Arduino IDE 中的库管理器获得。因此,连接器形成了到 MySQL 服务器的通信路径。

即使使用了连接器,您也可以在解决方案中使用数据库服务器做很多事情。数据库服务器不仅提供了健壮的存储机制,还可以用来从数据聚合器中卸载一些数据处理步骤。我讨论了在您的 IOT 解决方案中使用数据库服务器节点时需要考虑的一些问题。

数据库存储

数据库选项代表一种更稳定、更易于扩展、更易于查询的存储选项。事实上,这就是本书的全部内容——发现如何在您的 IOT 解决方案中最好地使用数据库服务器!

在本节中,我们将探讨在您的 IOT 解决方案中使用数据库服务器的一些好处、技术和注意事项。正如您将看到的,当您使用数据库服务器时,有许多功能可供您使用。我将在第 5 章中给出这些主题的高层次概述和更深入的实际操作解释。让我们从讨论为什么在您的 IOT 解决方案中使用数据库服务器开始。

Tip

虽然对数据库设计的深入、全面的讨论超出了本书的范围,但是下面的文本从一个稍微不同的角度来看待这个主题:如何最好地设计数据库以便于存储和检索。因此,我假设没有数据库设计的先验知识。如果您有数据库设计经验,您可能希望浏览这一部分。

数据库服务器提供了存储和检索数据的结构化方式。与数据库服务器的交互需要使用一组特殊的命令,或者更准确地说,需要使用一种表示存储和检索的特殊语言。这种语言被称为结构化查询语言(SQL)。大多数数据库服务器的大多数命令都使用 SQL 的形式。虽然不同的数据库服务器之间存在一些差异,但是语法和核心 SQL 命令是相似的。

在本书中,我们使用 MySQL 数据库系统作为数据库服务器。MySQL 6 是开发者最受欢迎的选择,因为它以轻量级的形式提供了大型数据库系统的特性,可以在几乎任何消费类计算机硬件上运行。MySQL 也很容易使用,它的流行已经产生了许多学习和使用该系统的在线和印刷资源。

利益

如上所述,在您的 IOT 解决方案中使用数据库服务器有很多优点。数据库服务器不仅允许结构化的、健壮的数据存储,而且还提供了强大的数据检索机制。

考虑一个将数据存储在文件中的解决方案。正如您在前面几节中看到的,读取文件和查找信息需要解析(分离数据元素)数据,然后比较数据以符合所需的标准。问题是每次你想进行搜索时,你都需要修改你的程序或脚本。

这听起来不算太糟糕,但是考虑到你可能需要随时执行搜索(查询)的可能性。此外,请考虑您可能希望无需重写代码就能做到这一点,或者您可能希望允许您的用户执行查询。显然,使用文件或类似的存储机制不容易允许这种行为。

这是使用数据库服务器最有益的方面之一。您可以在任何时候(特别地)执行查询,并且您不需要特殊的程序或者需要重写任何东西来使用它。我应该注意到,有些解决方案将查询硬编码到代码中,有些人认为这与访问文件是一回事。但事实并非如此。

在基于文件的解决方案中,即使您使用编程原语,您仍然必须编写代码来执行查询,而在数据库解决方案中,您只需要替换查询语句本身。例如,考虑清单 3-1 中的 Raspberry Pi 文件示例。执行查询所需的代码(只选择第一列的值大于 5 的行)有几行长 7 ,不仅需要选择匹配的行,还需要一次读取一个字符的列。

现在考虑下面的 SQL 语句。不要担心命令的细节。仅考虑我们可以将查询表示为如下的单个语句:

SELECT * FROM db1.table1 WHERE col1 > 5;

请注意,标准已被移动到数据库服务器。也就是说,代码不需要读取、解释和测试列的值。相反,该逻辑是在数据库服务器上执行的。事实上,数据库服务器已经过优化,能够以最有效的方式执行这样的查询——这在基于文件的解决方案中需要做大量的工作(但也不是闻所未闻)。出于好奇,执行和检索行的代码如下:

cur.execute("SELECT * FROM db1.table1 WHERE col1 > 5")

rows = cur.fetchall()

for row in rows:

print ">", row

虽然这是一个微不足道的例子,但这一点仍然有效。具体来说,数据库服务器是一个强大的搜索工具。这不仅允许您简化代码,还允许在构造查询时有更大的表达能力。

例如,您可以构造执行复杂数学比较、文本比较(甚至通配符匹配)和日期比较的查询。例如,您可以使用日期操作来选择从执行时间起超过 N 天的行或在特定年、月、日或小时记录的行。显然,这比您自己编写的代码强大得多!

使用数据库服务器的另一个好处是,您可以按逻辑方式对数据进行分组。数据库服务器允许您创建任意数量的数据库来存储数据。通常,您希望为每个 IOT 解决方案创建一个单独的数据库。这使得在逻辑层次上处理数据更加容易,这样一个解决方案的数据就不会与另一个解决方案的数据混杂在一起。因此,一台数据库服务器可以支持许多 IOT 解决方案。

技术

在您的解决方案中使用数据库服务器需要使用一些与其他存储解决方案不同的技术。您已经看到,基于文件的系统容易受到文件布局变化的影响(如果文件布局发生变化,那么代码也必须发生变化);对于数据库来说,情况并非如此。

例如,如果您需要向表中添加一列,您不必重写代码来读取数据。事实上,可以在不影响代码的情况下对数据库进行许多更改。虽然某些更改可能会影响 SQL 语句,但与基于文件的解决方案相比,您在如何存储数据方面有很大的自由度。

因此,开发中最大的变化是如何使用数据库服务器,而不是代码本身。也就是说,代码开发可以独立于数据库开发进行。您可以独立于代码开发数据库、其组件和 SQL 语句。事实上,在开发任何其他节点之前,您可以创建一个工作的、经过测试的数据库组件!

这看起来比基于文件的解决方案的代码要多,但实际上并不是这样,原因有二。首先,您可以用测试数据单独测试您的 SQL 语句。这意味着您不需要启动并运行整个 IOT 解决方案,这使得开发更加容易。其次,相关的是,您可以重复执行 SQL 语句以确保获得正确的数据,这使得从代码中分离数据更加容易,从而使解决方案更易于维护。也就是说,如果有什么变化,您可以简单地更改 SQL 语句,而不是重写代码。

但这并不意味着您不需要花时间来设计和测试数据库设计。相反,为了获得这些好处,您的数据库应该设计得很好。如果数据本身或您对数据的使用很复杂,数据库设计可能会很复杂。幸运的是,对于大多数 IOT 解决方案来说,这不是问题。

最后,使用数据库服务器允许您快速设置测试数据,操作它,并刷新它。同样,这在其他存储解决方案中也是可能的,但在数据库服务器中要容易得多。因此,设置测试数据的技术允许您确保您的查询准确地返回您期望的结果。这是因为您知道输入(样本数据),并且可以轻松地手动确定结果应该是什么。

毕竟,在基于文件或基于内存的存储解决方案上使用数据库服务器改变了您开发解决方案的方式,通过在应用之外测试 SQL 语句或部署和执行网络节点,使处理数据变得更加容易。

考虑

由于您正在阅读这本书,您很可能确信或者几乎确信您想要在您的 IOT 解决方案中使用数据库服务器。但是,您可能想知道在您的解决方案中使用数据库服务器会有什么后果或限制。

也许最重要的考虑是您需要一个平台来托管数据库服务器。如果您的解决方案使用计算机,您就拥有了所需的一切。MySQL 运行在商用硬件上,对于像 IOT 这样的小型解决方案,即使是最基本的计算机也绰绰有余。

然而,如果你没有电脑呢?在这种情况下,您需要添加硬件来托管数据库服务器。幸运的是,MySQL 可以在大多数低成本的计算机主板上运行,如 Raspberry Pi、Beaglebone Black、pcDuino,甚至最新的英特尔 IOT 主板。虽然这意味着网络中的另一个节点,但是您已经看到这个节点非常适合这个计划(一个数据库节点)。我在第五章中给出了一个关于构建 MySQL 数据库节点的完整教程。

除了添加一个新节点之外,还需要考虑在代码中使用 SQL。为此,我们使用一个连接器通过以太网连接到 MySQL 服务器,向服务器发送查询以供执行,然后检索并处理结果。如果你不知道 SQL,你将不得不学习如何形成查询。然而,正如你将在第 5 章第 6 章中看到的,SQL 语句并不难学习和使用。

另一个需要考虑的问题是,在数据库服务器中存储数据需要一个好的设计,仅限于数据库服务器的术语和特性。更具体地说,您必须形成表的布局,包括从一长串可用类型中选择正确的数据类型。

这个过程的一部分(称为数据库设计)涉及到查询本身的设计,或者更具体地说,涉及到设计查询以使它们准确地返回您想要的结果。因此,应该在数据库服务器上测试查询(或者通过客户端连接,正如您将在第 5 章中看到的那样)以确保您的 SQL 语句是正确的,并且在出现异常数据时不会出现意外。正如您在上一节中看到的,这样做的好处是更容易开发处理数据的解决方案层。

当你设计你的桌子时,你应该记住一些事情。首先,考虑存储样本需要什么数据类型。您不仅要考虑每个样本包含多少个值,还要考虑它们的格式(数据类型)。可用的基本数据类型包括整数、浮点、双精度、字符和布尔。还有许多其他的,包括用于日期和时间的几个,以及用于存储大数据块(如图像)的二进制大对象(blob),大文本(与 blob 相同,但不解释为二进制)等等。

您还可以考虑添加一些列,比如时间戳字段、数据收集器节点的地址、参考电压等等。写下所有这些,并考虑每个的数据类型。

Tip

有关所有数据类型( http://dev.mysql.com/doc/refman/5.7/en/ )的完整列表和讨论,请参见在线 MySQL 参考手册。

数据库设计可能要考虑的是数据库所需的物理存储。如果您的 IOT 解决方案使用没有任何物理存储的节点,您应该添加一个。物理存储不仅意味着数据不易受到节点故障的影响(比如内存被擦除),而且它还允许您使用具有大存储容量的介质。虽然您可以使用安全的数字存储卡,但最好使用固态或更旧的磁盘。

数据的维护是另一个考虑因素,但可能没有提到的其他因素那么重要。更具体地说,即使是基于文件的解决方案也需要维护,或者至少需要执行维护(如备份和恢复)的工具。对于基于文件的解决方案,这可能只是复制文件的问题。对于数据库解决方案,备份和恢复稍微复杂一些。

幸运的是,您可以使用 mysqlpump 8 (或更老的 mysqldump 9 )或 MySQL 实用程序 10 来对数据进行逻辑备份。也就是说,这些实用程序生成一个 SQL 命令文件,您可以重放这些命令来创建和存储数据。对于物理备份(在字节级别),您必须使用商业应用(如 MySQL Enterprise Backup)来备份和恢复数据。

最后,如果从网络外部可以看到数据库服务器节点,安全性也是一个需要考虑的问题。在这种情况下,您必须小心确保不仅平台访问(例如,Raspberry Pi)是安全的,而且所有的数据库安全性都经过适当的设计,以便访问数据库服务器的用户仅具有足够的权限来执行对解决方案的查询,仅此而已。换句话说,您需要在设计解决方案时考虑安全性。

既然您已经研究了数据库节点,那么让我们看看设计和实现分布式 IOT 网络的一些最佳实践。

分布式 IOT 网络最佳实践

如果您的 IOT 解决方案需要部署在数据收集器物理上分离的分散区域,或者您希望使用低成本的商用硬件来开发您的解决方案,您可能需要使用分布式节点网络来设计您的 IOT 解决方案。

本节分析了规划网络时的一些重要注意事项。我讨论了网络中节点的放置以及数据存储的设计考虑。这些最佳实践中的大多数都是以数据为中心的,这是有充分理由的——如果没有可访问的数据,解决方案就毫无用处。

节点布局

规划解决方案时,您应该考虑要收集哪些数据。此外,您应该考虑在哪里以及如何收集数据。这包括传感器需要放置的位置以及产生的数据。

放置带有传感器(数据收集器)的节点可能是一个简单的选择——它们需要靠近它们正在观察的东西。如果数据采集器在室外,它们可能需要防风雨的 Shell。即使在内部,你也可能需要保护硬件,以避免意外篡改(如小手指或爱管闲事、好奇的朋友 11 )。事实上,我建议为每个节点使用合适的机箱。

然而,将数据聚合器放在哪里可能更成问题。这通常由所选择的通信机制决定。回想一下,如果您使用 ZigBee 或蓝牙等低开销机制,您可能会被限制在一定的范围内。因此,数据聚合器需要足够近才能与数据收集器通信。

此外,数据聚合器节点最好放置在可以通过 WiFi 或以太网与数据库服务器或可视化应用(可能是托管 web 服务器或云网关的另一个节点)通信的位置。因此,根据您拥有的数据收集器的数量以及它们与可以放置数据聚集器的位置的物理距离,您可能需要使用多个数据聚集器。如果所有的数据收集器都位于您的低成本通信机制的范围内,您也许能够使用单个数据聚合器。

也许规模较小的是数据聚集器硬件的能力。如果您选择一个可以支持有限数量连接的平台,那么您就会受到数据聚合器可以支持的数据采集器数量的限制。同样,您可能需要不止一个数据聚合器来支持所有的数据收集器。

另一种要考虑节点放置的节点类型是可操作设备。如果设备产生数据,您可能需要将其连接到数据聚合器。但是,如果该设备具有被编程或运行脚本的能力,您可以对其进行编程,以将数据直接写入数据库服务器。

最后,放置数据库节点不太重要,因为它将使用 WiFi 或以太网。因此,它只需要在同一个网络上(或者可以从数据聚合器使用的网络上访问)。

数据存储

在考虑您的数据存储时,您应该考虑数据看起来像什么,也就是说,传感器产生什么数据来包括数据类型以及如何使用数据。

例如,如果传感器数据是一个数值范围内的数字,比如-5.0 到+5.0,该数值可能没有太大意义。例如,当值更接近-5.0 与+5.0 时,这意味着什么?您应该总是存储原始值,但是您可能希望存储一个代表值。在这种情况下,可能有几个确定定性值的阈值。考虑以下虚拟传感器范围的阈值。在这种情况下,该值是电压读数。

  • -5.0:错误,无信号
  • -4.9-2.0:低
  • -2.00.0:好,递减
  • 0.0到+ 2.0:好,增加
  • +2.0到+ 4.9:高
  • +5.0:错误,无信号

注意有两种错误情况。我以前在其他传感器中见过这种情况,这取决于传感器如何供电或发送信号。虽然看到这样的值并不罕见,但您不太可能遇到有多个错误读数的传感器(但我见过一些)。

还要注意,有四个不同的阈值告诉我们这些值的含义。例如,如果读取的值是+3.3,我们知道该数据可以解释为“高”因此,我们可以在一列中存储原始值,在另一列中存储类别。例如,我们可以有一个浮点字段(列)和另一个值为(低、减少、增加、高和错误)的文本列。我们将使用数据聚合器或数据库服务器来分配这些值。我在第五章中给你展示了一个这样的例子。

这些信息对于规划您的数据聚合器代码至关重要。你需要理解数据的含义,以及如何最好地解读它。事实上,我建议将它记录在你的代码和笔记本中。如果您更换传感器或发现需要调整阈值,这些信息将非常重要。

例如,如果需要将增加或减少的阈值调整到更高或更低的值,则可能需要修改接近原始阈值的读取值。如果您没有存储原始值,您将无法调整您已经存储的数据。

除了如何解释数据,您还应该考虑数据如何在您的网络中流动。我喜欢画一张图,显示每种类型的数据来自哪里,以及它们是如何在网络中传输的。图 3-2 显示了一个数据流程图的例子。您可以使用任何想要的形式——从写在日志中的简单列表到用结构化设计语言(如统一建模语言(UML ))编写的图形图片。 12

A978-1-4842-1293-6_3_Fig2_HTML.jpg

图 3-2。

Data flow chart

这个例子是一个池塘监控系统。我展示了以原始形式收集的数据,以及应该如何解释这些数据。这有助于我设计一个数据库来存储数据,并决定在哪里放置任何可能有助于解释数据的代码或功能。

例如,水位检测使用传感器所在的 Arduino 上产生的声音。这允许我在水位下降时听到警告(传感器被触发)。因为我每天在上下班的路上都会经过池塘,所以这是在用户界面中使用多种提示的一个很好的例子。在这种情况下,用户界面是一个网页,信号发生器在 Arduino 上。

我还包括沟通机制。正如您所看到的,两个数据收集器被硬连线到数据聚合器,因此通过有线连接(在这种情况下是 I2C)传递收集的数据。另一个数据收集器使用 XBee (ZigBee 协议),因为水温传感器距离数据聚合器太远(数据聚合器必须靠近房屋才能通过 WiFi 将数据传输到数据库服务器)。最后,请注意,我使用数据库服务器节点来托管 web 服务器,以便通过 web 页面呈现数据。

我建议使用这样的图来帮助您规划您的 IOT 网络,甚至是您的解决方案的功能。你真的不能有太多的设计文档。

陈述

当您规划包含允许用户查看数据或控制可操作设备的功能的 IOT 解决方案时,您还有另一个层面的放置要考虑。

如果设备是平板电脑或电脑,这可能相当简单。在这种情况下,您只需将其连接到与数据库服务器和可操作设备相同的网络。但是,如果演示特性支持云,您可能需要一个中间节点来将您的内部节点与云隔离开来。这可能是放置在内部防火墙之外的设备,它与数据库服务器通信并将数据传输到云服务。

简而言之,一定要考虑如何将数据库中的数据呈现给用户,这样您就不会以一个设计良好的数据收集机制而告终,因为它缺少可视化功能。虽然这听起来显而易见,但有时专注于节点放置、数据收集和数据库设计可能会影响数据的显示方式。

摘要

为您的 IOT 解决方案选择数据存储方式有多种选择。您可以选择将数据存储在云中,不在任何节点上本地存储任何数据。您可以选择将数据存储在本地文件或内存中,构建自己的存储和检索机制。或者,您可以选择使用专用于高效存储和检索 IOT 数据的数据库服务器。

当然,对于本书来说,假设您已经选择或将选择使用数据库服务器来存储您的数据。虽然您仍然可以选择在某些节点上缓存甚至本地保存一份副本,但最终数据库服务器会成为数据的焦点。

在本章中,您研究了一些可用于存储数据的方法。您已经看到了如何在 Arduino 和 Raspberry Pi 上读写文件数据的示例。我还讨论了在您的 IOT 解决方案中部署数据库服务器的好处、注意事项和建议。最后,我讨论了为您的 IOT 解决方案设计节点网络的一些最佳实践。

在下一章中,您将探索转换数据的细节,从处理数据类型到规范化数据,甚至讨论如何处理寻址和聚合。这个讨论将帮助您了解使用 MySQL 数据库系统的更详细的细节。

Footnotes 1

听起来熟悉吗?我在太多的应用中遇到过这种情况。虽然这并不总是可能的,但是一个好的数据存储设计应该是可扩展的。

2

有时为了更容易阅读的代码而牺牲效率或首选技术。

3

除了新的 Arduino Yún,它有一个 SD 驱动器和用于连接外部设备的 USB 端口。Yún 和更新的主板肯定会改变 Arduino 世界的游戏规则。

4

正式归甲骨文所有,但由我独家支持。

5

https://en.wikipedia.org/wiki/SQL

6

http://dev.mysql.com/

7

Arduino 示例的代码行数是它的三倍!

8

mysqlpump 在服务器版本 5.7.8 及更新版本中可用( http://dev.mysql.com/doc/refman/5.7/en/mysqlpump.html )。

9

mysqldump 在服务器 5.6 及更早版本中可用( http://dev.mysql.com/doc/refman/5.7/en/mysqldump.html )。

10

使用 mysqldbexport 和 mysqldbimport ( http://dev.mysql.com/doc/mysql-utilities/1.6/en/mysqldbexport.html )。

11

我有更多的实验被一句不经意的“嘿,这是什么?”询问通常涉及设备的误操作或重新定位。

12

http://uml.org

四、数据转换

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​4) contains supplementary material, which is available to authorized users.

无论您有一个现有的 IOT 解决方案,您正在开发一个新的 IOT 解决方案的想法,或者您正在完善一个现有的 IOT 解决方案,数据在某些时候将是一个主要焦点。拥有一个拥有近乎完美的硬件、通信甚至显示能力的优秀 IOT 解决方案,并不能保证数据不会存储不当或处理不当。获得正确的数据可能是最困难的任务。事实上,我见过 IOT 的解决方案很好,但却悲惨地失败了,因为数据要么不容易呈现、理解,要么不完整。

当您考虑正在生成的数据类型以及如何解释这些数据时,问题就来了。由于大多数 IOT 解决方案都是为了观察我们周围的世界而设计的,因此很自然地,生成的大多数数据都是某种形式的传感器数据。当然,可能还有其他数据,如来自相机的图像或视频、用户输入,甚至来自社交媒体应用的数据。幸运的是,这些数据很容易理解。存储可能仍然是一个挑战,但通常很容易解决。

然而,传感器数据可能更难得到正确的。这是因为,正如你在第 2 章中看到的,传感器可以产生模拟(例如电压)或数字值,这些值代表观察值。例如,您可能会发现想要使用的传感器产生-5.0 到+5.0 之间的值。但传感器可能测量的是通过管道(软管)的流量,您可能希望看到的是升或加仑每分钟。诀窍是将传感器的值转换成预期的速率。

您可能需要对数据进行两类转换。您可能需要添加额外的信息,如测量单位、事件注释、日期或时间等。这些附加数据是一种注释形式。您可能还需要对数据执行更大规模的操作,将来自多个来源(传感器)甚至多个节点的数据进行组合。这些操作是数据的聚合,可能需要更多的工作来实现。

在本章中,您将发现许多处理这些和类似转换的技术。这些示例从两个角度展示:用于低成本计算机节点的 Arduino 或 Python 脚本草图的代码摘录,以及如何在数据库中实现该技术的建议。提供内部和外部使用的两种技术 1 数据库服务器为您提供更广泛的知识,从而为您开发 IOT 解决方案提供更多工具。

数据库示例将提供建议,而不是具体的示例。重点是展示它是如何在代码中完成的,然后是数据库实现的建议,这样当您阅读接下来的两章时,您将看到如何实现其中的一些建议。我将在第 8 章中更详细地介绍这些建议。接下来,您将看到使用数据库服务器可以获得多大的能力。

Note

一些示例可能没有数据库替代方案。在这些情况下,我提到了存储数据的注意事项。此外,一些例子使用了我们已经讨论过的技术。对于这些例子,重点应该放在所说明的概念上。

我们首先简要讨论和回顾什么是 IOT 数据以及理解数据的策略,即从数据中获取知识。

理解 IOT 数据

在我们进入如何实现数据的注释和聚集的例子之前,让我们讨论和回顾一些理解数据的策略。关注这个主题非常重要,这样我们才能了解如何成功设计我们的数据存储。在接下来的几节中,我会以数据库设计过程中应该问的问题的形式介绍一些重要的概念。

观察到了什么?

首先要考虑的是你在观察什么。不要专注于传感器产生的数据(这也很重要),但是知道你在观察什么可以让你计划如何使用数据。我喜欢从问题的角度或在考虑传感器本身时,列出我想观察的事情。也就是说,如果我使用气体探测器,我会列出它可以测量的气体,并考虑如何使用这些观察结果。虽然数据的形式(来自传感器的数据类型)可能需要转换,但更重要的是专注于您正在观察的内容。有时候这并不明显。

例如,如果您使用传感器监控敏感设备 Shell 内的温度,温度会告诉您什么?假设您决定使用传感器来捕捉温度何时达到某个阈值,因为您知道设备无法承受超过该阈值的温度。因此,通过保存测量的温度以及事件的时间和日期来存储这些事件似乎是很自然的。

但是,如果您想知道其他温度范围是否会影响器件,该怎么办呢?在这种情况下,如果设备在某个温度下运行一段时间,该温度可能对设备有害。那么,仅仅存储温度并在温度达到初始阈值时发出信号或发出警报是否足够?可能不是。一段时间后,您可能会发现某些温度会对设备的功能或精度产生负面影响。如果这是真的,记录超过阈值的事件是不够的,因为数据(随时间变化的温度)已经丢失,有利于存储事件。

因此,你不仅要考虑你正在观察的东西,还要考虑观察结果可以被解读的任何其他方式,并计划好你需要为这些用途储存什么。例如,您可能会发现记录某个事件对于一些使用观察来说已经足够了,但是您可能会从一段时间内收集的数据中学到比单个事件更多的东西。

有没有另一种观察方法?

与你期望观察的密切相关的是如何进行观察。有时用某些传感器进行直接观察是不可能的(或者成本太高)。例如,假设您有一个花园池塘,并想确定过滤器何时需要清洗。

在一些花园池塘中,过滤器用于去除水中漂浮的碎片(如树叶、废物等)。随着时间的推移,这些过滤器可能会充满碎屑,从而失去水流过过滤器的能力。此外,让我们假设没有简单的机制来测量通过过滤器的水流量。也就是说,过滤器在容纳泵的 Shell 内,因此过滤器在水流入泵井之前净化水。此外,没有设计传感器来测量 Shell 中过滤器的流速。你如何观察到过滤器需要清洗?

如果你拥有并维护了一个使用这种过滤系统的花园池塘,你会知道有一个因果关系,你可以观察到,以确定过滤器是否需要清洗。更具体地说,随着过滤器充满碎屑,泵井中的水降低(流量降低,蓄水池中的水减少)。因此,您可以确定当泵井中的水位降低到一定水平时,过滤器需要清洗。

然而,这种观察结果并不理想,因为如果池塘出现渗漏或出现大量蒸发,抽水井中的水位也可能会变低。因此,我们必须把这些事件加入到我们从观察中可能学到的东西的列表中。在这种情况下,我们有一个主要的和两个次要的事情可以从抽水井水位的观察中得知。

Know Your Domain

这提出了一个很好的观点。如果你被要求为一个花园池塘制作一个 IOT 解决方案,但你不知道或没有任何相关的经验,你可能无法理解如何观察过滤器的状态。因此,了解或研究你工作的领域是很重要的,这样你就可以探索观察的替代方法。

Tip

你应该考虑观察的替代方法,包括观察因果事件,而不是实际或物理现象。

你需要多久记录一次观察结果?

您还应该考虑您想要记录观察值的频率,也就是说,您想要记录多少次来自传感器的值。一些传感器可能具有计时器电路或最小阈值,用于何时可以进行测量。开关等简单传感器可以是瞬时的,而气体、水的盐度或氧气等传感器可能具有显著的阈值,从而减少了在给定时间框架内可以进行的观察次数。

也有可能观察的频率与你期望获得的知识没有什么关系。例如,如果我们想测量建筑物或房间的温度,如果每三秒钟记录一次温度值,我们能学到什么吗?一分钟两次怎么样?一小时一次或者六小时一次怎么样?

答案取决于我们想学什么。是的,我们又回到那个话题了。更具体地说,我们是否希望能够跟踪温度何时变化到一天中特定的分钟、小时或时间?此外,我们是否想对变化率进行分析?

例如,如果我们在一个没有受控气候的建筑或房间(例如,谷仓或廊桥)中测量温度,我们是否有兴趣了解温度如何随时间变化?如果是,我们希望数据有多精确?也就是说,我们希望能够检测到温度变化有多快吗?如果我们这样做了,存储更频繁地进行的测量可以允许更准确地检测这些变化以及更准确的变化率。另一方面,如果我们只想能够确定一天中不同时段(例如,早晨、中午、晚上)的平均温度,每隔几个小时进行一次测量可能就足够了。

因此,我们必须考虑我们想要观察什么,以及随着时间的推移变化是否有益。也就是说,使用短时间间隔进行的测量可以比以更长时间间隔进行的测量更准确地检测变化和趋势。事实上,如果间隔足够大,测量中的波动可能会被忽略。

Tip

根据您可能从一段时间的变化中学到的东西,考虑您希望记录观察结果的频率。

传感器产生什么类型的数据?

接下来要考虑的是传感器产生的数据类型。抵制跳到如何解释数据的诱惑——我们接下来会这么做。现在,记下数据类型,以便在实现解决方案时可以引用它。

来自传感器的数据的数据类型可能与我们观察到的完全不同。对于以电压形式产生模拟数据的传感器,我们已经看到了这一点。电压的正负可能并不清楚它所代表的意义。记下每个传感器产生的数据将有助于您编写代码来读取、分析和存储观察结果。

Tip

列出传感器产生的数据类型。设计解决方案时,请将此列表放在手边。

观察数据需要解释吗?

一旦我们知道了我们在观察什么,我们能从观察中学到什么,我们需要多频繁地进行观察,以及数据类型是什么,我们现在必须考虑是否有任何需要进行的解释。

回到模拟传感器的讨论,大多数模拟传感器都有特定的解释,而且这些解释确实是有文档记录的。也就是说,传感器的文档将告诉您如何解释产生的值。这是通过一种称为数据手册的通用机制呈现的。

Read The Data Sheet

关于如何解读传感器数据手册中的数据,您可以找到更多信息。大多数制造商都会对其设备进行简短描述,包括工作参数(电压、电流等)以及传感器产生的数据类型和如何解释这些数据。如果您不熟悉传感器或其分立元件,请参考数据手册。

一旦理解了数据类型是如何解释的,就要记下如何执行解释。我喜欢把这些数据和伪代码一起记在笔记本上,以便进行观察。它不仅可以帮助您编写代码来解释数据,还可以帮助您考虑将解释包含在数据库中。

此外,您还应该考虑如何呈现数据,也就是说,您希望如何向用户显示数据。例如,读取-4.12 伏的值对用户没有帮助,但是如果该值被表示为诸如“正常”、“潮湿”或“干燥”的类别,则对用户有帮助

Tip

为如何解释和展示数据制定一个计划。把它记录在笔记本上以供参考。

你需要什么样的精确度?

这可能有些令人惊讶,但传感器并不总是 100%准确。事实上,大多数面向业余爱好者和发烧友的传感器只有 90%到 95%的精确度。也就是说,100 次中有 5 到 10 次,传感器不会产生准确的值。

如果你的传感器只有 95%的准确性,你只能期望你的数据是相同的。因此,在选择或分析传感器数据时,必须确定解决方案所需的精度。幸运的是,如果您需要更高的精度,通常可以找到符合您期望的传感器。然而,根据我的经验,传感器越精确,它们就越贵。

Tip

平衡传感器的精确度和数据的预期精确度。

数据的生命周期有多长?

最后,考虑您希望或需要保留数据多长时间。一些 IOT 解决方案,尤其是那些使用云的解决方案, 2 似乎只保留数据几个小时、几天或几周。这不仅显得武断,而且还确保您可能永远无法对历史数据执行任何数据分析。也就是说,你可能会因为样本集太小而丢失知识。

在考虑数据的生命周期时,您应该考虑前面的所有问题,并为您希望保留数据的时间制定计划,更具体地说,对更长时间内的数据进行数据分析是否有益。

要考虑的一个因素是大量数据是否会影响解决方案的效率。在这种情况下,更多的数据会使搜索甚至某些代码功能变慢,因为它必须花更多的时间来读取和比较数据。也有可能是您的存储解决方案对可以存储的数据量有限制。因此,在确定数据生命周期时,您应该考虑硬件和软件的限制。

一旦确定了数据的生命周期,就应该确定是否需要数据可访问。如果您不需要从您的解决方案或其应用中访问它,您可以考虑删除旧数据以便安全保存,如文件存档或备份。如果它仍然必须是可访问的,您可能要考虑使用替代或补充的存储机制。例如,如果您将数据存储在一个文件中,您可以简单地打开一个新文件。

Archive, Don’T Delete

如果您确定可以清除某个日期之前的数据,请不要删除该数据。取而代之的是,将它存档,以便在需要访问旧数据时可以使用。对于数据库,数据库备份可能就足够了,或者您可以使用相同模式的表来存储较旧的数据。对于基于文件的存储,请在可移动媒体或其他设备上制作文件的副本。

一旦您对数据的生命周期以及如何处理旧数据有了计划,请将此计划记录在笔记本上,以便您可以编写代码或实施正确的流程来定期清除数据。

Tip

为您希望存储数据的时间制定一个计划。将任何清除事件实施为备份或存档,而不是删除。

现在我们已经看到了理解数据需要考虑的一些事情,让我们从注释开始,看看实现这些策略的几种技术。

以下部分中的一些代码示例是为基于文件的存储而编写的。其他介质形式类似,但实际写入方法可能不同。我还模拟从传感器收集数据。您通常会使用以类似方式编写的方法来检索数据。

我还介绍了使用数据库添加注释或实现聚合的注意事项。我用这几节来介绍数据库概念,为您在本书的其余部分学习更深入的 MySQL 教程做准备。

让我们先来看看您可能想要做的一些更常见的注释。

注释

为您的 IOT 解决方案注记数据只是您添加、组合或计算数据的任何附加数据。例如,您可能希望存储一个字符串来描述事件、传感器、节点等。或者,您可能希望对数据执行一些转换,并将其与原始数据一起保存。

Tip

保存原始值始终是一个好的做法。

我在本章中演示了其中的一些转换,提供了一个代码示例,您可以在您的数据聚合器或数据节点上使用它,例如 Arduino、Raspberry Pi 或类似的低成本计算机节点上托管的数据聚合器或数据节点。因此,在适当的地方,我会给出一个 Arduino 草图摘录和一个使用 Python 的例子。虽然您可能不熟悉这两种语言,但是您可以使用这些示例来帮助指导您使用您选择的语言进行写作。

Can You Have Too Much Annotation?

注释应该是为了用户的利益,或者使数据更具信息性。因此,您必须考虑注释是否会添加任何内容,但更重要的是,注释不应该使数据复杂化或模糊化。本章中的一些例子接近于利益与损害的界限,但这是为了说明的目的。使用您自己的判断,多少注释足够满足您的需求。太多会有模糊知识的风险,太少会使数据更难使用。

现在您知道了什么是注释,让我们看几个例子,从最简单的转换形式开始——添加文本和记录注释。

Note

下面的代码示例是为了简洁而摘录的。完整的草图或脚本可以从书籍源代码下载网站获得。

记录传感器名称或添加注释

最简单的注释形式是向存储的数据添加一个短字符串。您可能希望这样做,以确保所存储的值能够被理解,例如正在观察什么事件,或者哪个数据收集器(传感器)生成了数据。您可能还想为存储的数据添加任何注释,比如用户输入或者主观观察,比如“猫睡在花盆里”添加的数据在以后读取数据时会很有帮助,特别是当您将数据作为日志或数据存储写入文件时。

但是,您必须小心使用这项技术。添加过多的文本可能会使数据更难解释,或者在将来需要时更难解析。因为添加字符串很容易,所以我将给出一些例子和简短的解释。

代码实现

向存储在文件中的数据添加文本并不困难。清单 4-1 展示了如何在 Arduino 草图中将文本添加到一行数据中。在这种情况下,我添加了来自两个(模拟的)传感器读数的数据以及对每个数据的解释。我还添加了用于调试的语句(Serial.print语句)。

Listing 4-1.Simple Annotation (Arduino)

/**

Example of simple annotation.

This project demonstrates how to save data to a

microSD card as a log file with sting annotation.

*/

#include <SPI.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

File log_file;   // file handle

String strData;  // storage for string

// Simulated sensor reading method

float read_sensor(int sensor_num) {

if (sensor_num == 1) {

return 90.125 + random(20)/10.00;       // sensor #1

} else if (sensor_num == 2) {

return 14.512313 + random(100)/100.00;  // sensor #2

} else {

if (random(25) >= 5) {

return (float)random(14)/10.00;

} else {

return -(float)random(14)/10.00;

}

}

}

void setup() {

Serial.begin(115200);

while (!Serial);

Serial.print("Initializing SD card...");

if (!SD.begin(SD_PIN)) {

Serial.println("ERROR!");

return;

}

Serial.println("ready.");

if (SD.remove("data_log.txt")) {

Serial.println("file removed");

}

// initiate random seed

randomSeed(analogRead(0));

}

void loop() {

delay(2000);

log_file = SD.open("data_log.txt", FILE_WRITE);

if (log_file) {

Serial.println("Log file open.");

strData = String("Temperature sensor: ");

strData += read_sensor(1);

strData += " ";

delay(1000);

strData += ", Barometric sensor: ";

strData += read_sensor(2);

log_file.println(strData);

Serial.print("Data written: ");

Serial.println(strData);

log_file.close();

Serial.println("Log file closed.");

} else {

Serial.println("ERROR: Cannot open file for reading.");

}

}

在这个例子中,我使用String类将字符串与传感器读数连接起来。虽然还有其他方法,但这种方法似乎更清楚地展示了注释。请注意,我在第二个传感器之前的字符串中放置了一个逗号。这个逗号可以用来帮助解析数据,如果您将来需要这样做的话。例如,您可以拆分逗号上的数据,然后将数据值放在冒号后的字符串末尾。以下是输出示例,或者说是日志文件的摘录:

Temperature sensor: 90.82, Barometric sensor: 15.00

Temperature sensor: 91.43, Barometric sensor: 15.09

Temperature sensor: 91.13, Barometric sensor: 15.23

虽然本例显示了如何将传感器名称与数据一起保存,但是通过简单地附加字符串,在输出行的末尾添加更多的文本也同样容易。例如,您可以在字符串末尾添加一个注释,如下所示。这里我添加了一个方法,它从提示(或 web 表单)返回用户的输入。

...

strData += ", Barometric sensor: ";

strData += read_sensor(2);

strData += " Notes: ";

strData += read_user_input();

log_file.println(strData);

要在 Python 中实现类似的注释,可以使用清单 4-2 中所示的代码。在这里,我重现了 Arduino 代码生成的结果。也就是说,我写的是同样的数据,但是我用的是完全不同的方法。为了帮助理解一些格式,我包含了整个代码。

Listing 4-2.Simple Annotation (Python)

from random import random, randint

import string

def read_sensor(sensor_num):

if (sensor_num == 1):

return 90.125 + random()*10  # sensor #1

elif (sensor_num == 2):

return 14.512313 + random()  # sensor #2

else:

if (randint(0,25) >= 5):

return randint(0,14)/10.00

else:

return -randint(0,14)/10.00

strData = "Temperature sensor: {0:.2f}, Barometric sensor: {1:.2f}\n"

file_log = open("data_log_python.txt", 'a')

file_log.write(strData.format(read_sensor(1), read_sensor(2)))

file_log.close()

这里你可以看到一个使用format()方法格式化字符串的例子。注意我是如何使用名为strData的字符串中的格式化代码将输出限制为两个小数点,比如{0:.2f}{1:.2f}。这些代码告诉format()方法替换第一个和第二个参数(read_sensor(1)read_sensor(2)),将数据格式化为带两位小数的浮点数。

虽然这个例子是基本的,只是一个例子,但是大多数开发者可能不会保存每行的事件或传感器的名称。更有可能的是,您将传感器的名称作为标题行写在文件的开头。这样,您总是知道数据列的含义。事实上,这正是您在数据库解决方案中使用这样的注释的方式。

数据库注意事项

在数据库中用文本进行注释很容易。如果我们想要包括传感器的名称或类型,我们可以简单地相应地命名表格的列。这样,列名描述了数据。例如,如果我们有一个表来存储本节示例中的行,该表可能类似于清单 4-3 。不要担心知道命令的所有部分是什么;只关注例子的可读性。

简而言之,表是我们用来存储数据的数据库结构。它形成了数据的布局。我们将在下一章学习更多关于创建表格的知识。现在,请注意这些列。这里我们看到有两列的名称与我们正在收集的数据相对应。事实上,如果我们输入前面例子中的数据,它看起来会像清单末尾的结果。我使用一个SELECT语句(从数据库中检索行的方法)来显示示例条目。

Listing 4-3.Sample Database Table and Sample Results

CREATE TABLE simple_annotation_test (

``id int(11) NOT NULL AUTO_INCREMENT,

``temperature float NOT NULL,

``barometric float NOT NULL,

``notes char(128) DEFAULT NULL,

PRIMARY KEY (id)

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

mysql> SELECT * FROM simple_annotation_test;

+----+-------------+------------+-------+

| id | temperature | barometric | notes |

+----+-------------+------------+-------+

|  1 |       90.82 |         15 | NULL  |

|  2 |       91.43 |      15.09 | NULL  |

|  3 |       91.13 |      15.23 | NULL  |

+----+-------------+------------+-------+

3 rows in set (0.01 sec)

请注意,还有一个注释栏。这表明我们还可以处理保存任何额外的注释或用户输入以及采集的样本。为此,我们在保存数据时将其包括在内。

Descriptive Column Names

对于糟糕的数据库设计习惯,这是我最讨厌的事情之一。也就是说,在命名对象(表)、列、索引等时,应该始终使用描述性名称。这并不意味着您需要为每个列名使用 200 个字符(那将是荒谬的),但是您应该避免使用单个字符名称的诱惑,例如abc等等。

如您所见,数据库中数据的注释是通过命名列、创建存储文本的列,甚至创建特殊的数据类型来帮助注释数据来完成的。例如,您可以定义一个包含一组值(称为枚举)的列,这样我们就可以在保存数据时指定枚举的数值。事实上,我们有很多方法可以在数据库中进行注释。我们将在后面的章节中看到更多的例子。

记录日期和时间

除了以列名、注释等形式向数据添加文本之外,我们经常需要存储观察到一个事件或一系列事件的日期和时间。也就是说,我们希望保存传感器被读取的时间。然而,大多数微控制器板甚至一些低成本计算机板都没有实时时钟(RTC)电路。例如,Arduino 没有 RTC,也没有 Raspberry Pi。

需要 RTC 来确保精确的时间保持,因为用于在处理器(微控制器)中推进指令的时钟(产生脉冲的特殊晶体或类似机制)不会以可用于精确计算时间的频率脉动或循环。此外,RTC 电路具有为少量存储器供电的电池,以存储时间值(有时是从特定时期开始的秒)。因此,没有 RTC 的电路板必须被编程为具有开始日期和时间,或者被指示从互联网上的时间服务器获取日期和时间。

幸运的是,有几款出色的产品性能良好,包括一个板载电池,即使在电路板掉电时也能为时钟供电。Adafruit 的 DS1307 实时时钟分线板套件( www.adafruit.com/products/264 )是添加到您的项目中的优秀模块。Sparkfun 还有一个名为实时时钟模块( www.sparkfun.com/products/99 )的产品,它使用与 Adafruit 产品相同的 DS1307 芯片和接口。您可以配合 Arduino、Raspberry Pi 或任何具有 IC2 接口的主板使用。

使用实时时钟模块

RTC 模块使用易于连接到 Arduino 的 I2C 接口。只需将 5V 电源连接到 5V 引脚,将 GND 引脚接地,将 Arduino 上的 SDA 引脚连接到引脚 4,将 Arduino 上的 SCL 引脚连接到引脚 5。图 4-1 显示了将 RTC 模块连接到 Arduino 的接线图。

A978-1-4842-1293-6_4_Fig1_HTML.jpg

图 4-1。

Arduino with an Ethernet shield and RTC module Note

如果您使用的是莱昂纳多板,您必须使用新的 SCL/SCA 引脚旁边的 AREF,GND,和引脚 13 最接近 USB 连接器。

初始化 RTC 需要一点代码,但并不困难。首先,我们必须下载 RTC 库。你可以在 https://github.com/adafruit/RTClib 找到一个优秀的库使用。通过单击 Download Zip 按钮下载库,解压缩归档文件,然后将文件夹复制到您的Arduino/Library文件夹中。您可能需要重命名该文件夹。我推荐使用RTCLib作为文件夹名。

Tip

如果您在 Arduino IDE 打开时复制了该库,您可能需要重新启动 IDE 以识别该库。

一旦库准备好了,我们就可以添加代码来使用库了。我们需要包含库头并定义 RTC 类的一个实例。以下是所需的代码行:

#include "RTClib.h"

...

RTC_DS1307 rtc;  // real time clock

接下来,我们需要用begin()方法初始化 RTC 类,如果模块之前没有用过或者你换了电池,我们还需要用adjust()方法设置时间。以下是可用于完成这两项任务的代码:

...

rtc.begin();

if (!rtc.begin()) {

Serial.println("Couldn't find the RTC module.");

while (1);

}

if (!rtc.isrunning()) {

Serial.println("ERROR: The RTC module is not working.");

} else {

// Comment out this line after the first run, it is only needed for setting

// the date and time for the first time.

rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

}

注意传递给方法rtc.adjust()的参数。这将当前日期和时间的值转换为一个DateTime类,该方法需要用它来为模块设置日期和时间。__DATE____TIME__是编译草图时从系统中检索日期和时间的宏。因此,只有在第一次开始使用 RTC 时,才需要调用这个方法。您不需要每次运行草图时都调用它。如果这样做,您将每次都将 RTC 设置为相同的值。这不是你想要的。因此,我编写了代码,以便您可以在第一次编译后注释掉该方法。

现在让我们看看如何在草图中使用 RTC 来捕捉 Arduino 上的日期和时间。

代码实现

将日期和时间添加到写入文件的行中的代码所需的工作可能比您预期的要多一些。RTC 模块提供了获取日期和时间的独立元素(如月、年、小时等)的原语。因此,我们需要获取这些值,并将它们组合成我们期望看到的格式。在这种情况下,我们希望看到月/日/年小时:分钟:秒的格式。清单 4-4 显示了获取和格式化日期和时间的代码。

Listing 4-4.Date and Time Annotation (Arduino)

String get_datetime() {

DateTime now = rtc.now();

String dateStr = String(now.day());

dateStr += "/";

dateStr += now.month();

dateStr += "/";

dateStr += now.year();

dateStr += " ";

dateStr += String(now.hour());

dateStr += ":";

dateStr += String(now.minute());

dateStr += ":";

dateStr += String(now.second());

return dateStr;

}

void loop() {

delay(2000);

log_file = SD.open("data_log.txt", FILE_WRITE);

if (log_file) {

Serial.println("Log file open.");

strData = String("Temperature sensor: ");

strData += read_sensor(1);

strData += " ";

delay(1000);

strData += ", Barometric sensor: ";

strData += read_sensor(2);

strData += " ";

strData += get_datetime();

log_file.println(strData);

Serial.print("Data written: ");

Serial.println(strData);

log_file.close();

Serial.println("Log file closed.");

delay(2000);

} else {

Serial.println("ERROR: Cannot open file for reading.");

}

}

如您所见,代码与前面的示例几乎相同。唯一的区别是添加了名为get_datetime()的方法来获取并格式化日期和时间,返回一个字符串,然后我们将该字符串追加到正在写入文件的行中。此代码中的示例行如下所示:

Temperature sensor: 92.33, Barometric sensor: 15.23 Datetime: 11/07/2015 22:36:32

Temperature sensor: 90.72, Barometric sensor: 15.32 Datetime: 11/07/2015 22:36:48

Temperature sensor: 94.38, Barometric sensor: 15.13 Datetime: 11/07/2015 22:36:50

Temperature sensor: 96.74, Barometric sensor: 14.95 Datetime: 11/07/2015 22:36:50

如果您想用 Python 做类似的事情,并且您的平台上有一个 RTC,那么代码相当容易。您只需添加日期和时间,如清单 4-5 所示。我在这里包含了完整的代码,这样您就可以看到读取日期和时间的支持代码。

Listing 4-5.Date and Time Annotation (Python)

from datetime import datetime

from random import random, randint

import string

def read_sensor(sensor_num):

if (sensor_num == 1):

return 90.125 + random()*10  # sensor #1

elif (sensor_num == 2):

return 14.512313 + random()  # sensor #2

else:

if (randint(0,25) >= 5):

return randint(0,14)/10.00

else:

return -randint(0,14)/10.00

def get_datetime():

return datetime.strftime(datetime.now(), "%m/%d/%Y %H:%M:%S")

strData = "Temperature sensor: {0:.2f}, Barometric sensor: {1:.2f} Datetime: {2}\n"

file_log = open("data_log_python.txt", 'a')

file_log.write(strData.format(read_sensor(1), read_sensor(2), get_datetime()))

file_log.close()

请注意,我们使用了一种方法来获取和格式化日期和时间,其方式与我们在 Arduino 代码中所做的类似。然而,Python 有更高级的方法(当然是从 C 语言借鉴来的),我们可以使用包含格式代码的字符串来格式化日期和时间。注意get_datetime()方法。我们使用来自名为strftime()datetime类的方法,通过datetime.now()方法从当前日期和时间创建一个字符串。然后格式字符串被strftime()方法使用。这段代码的输出类似于 Arduino 代码。

Temperature sensor: 92.33, Barometric sensor: 15.23 Datetime: 11/07/2015 22:36:32

Temperature sensor: 90.72, Barometric sensor: 15.32 Datetime: 11/07/2015 22:36:48

Temperature sensor: 94.38, Barometric sensor: 15.13 Datetime: 11/07/2015 22:36:50

Temperature sensor: 96.74, Barometric sensor: 14.95 Datetime: 11/07/2015 22:36:50

如果您的低成本计算机主板没有 RTC,您可以添加一个,如果主板有 I2C 接口,并且操作系统对 RTC 有足够的支持。例如,您可以向 Raspberry Pi 添加 RTC。Adafruit 在 https://learn.adafruit.com/adding-a-real-time-clock-to-raspberry-pi/overview 有一个很好的教程。一旦添加了时钟,Python(或其他语言)脚本将从操作系统原语中获得正确的日期和时间。

数据库注意事项

有两种方法可以保存数据库行中的日期和时间:您可以向类型为datetime的表中添加一列,并在与数据库服务器通信的代码中提供日期和时间(您发出一个INSERT语句向表中添加数据),或者您可以使用一个timestamp列,这是一个特殊的列,当行被插入时,数据库服务器会为您填充该列。让我们来看看这些选项中的每一个。

您可以通过指定datetime数据类型向表中添加日期和时间列。像添加或更新任何其他列一样,添加或更新该值。清单 4-6 展示了一个包含一个日期和时间列的示例表模式。为了便于阅读,我突出显示了该列。

Listing 4-6.Database Table with a datetime Column

CREATE TABLE date_annotation_test (

``id int(11) NOT NULL AUTO_INCREMENT,

``temperature float NOT NULL,

``barometric float DEFAULT NULL,

``date_saved datetime DEFAULT NULL,

``notes char(128) DEFAULT NULL,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=latin1

使用datetime列要求您以与 MySQL 兼容的格式提供值。幸运的是,我们到目前为止使用的相同格式工作得很好(其他人也一样——参见 MySQL 在线参考手册以获得更多示例)。下面显示了如何将一行保存到表中,为新列提供日期和时间值。

INSERT INTO date_annotation_test VALUES (null, 91.34,15.04,``'11/7/15 23:16:30'

现在让我们看一个添加了示例数据的表的输出示例。在本例中,我添加了几行,为每一行指定日期和时间。

mysql> SELECT * FROM  date_annotation_test;

+----+-------------+------------+---------------------+----------------+

| id | temperature | barometric | date_saved          | notes          |

+----+-------------+------------+---------------------+----------------+

|  1 |       90.82 |         15 | 2011-07-15 23:15:10 | NULL           |

|  2 |       91.34 |      15.04 | 2011-07-15 23:16:30 | Test datetime. |

+----+-------------+------------+---------------------+----------------+

2 rows in set (0.00 sec)

添加日期和时间注释的另一种形式是使用一个timestamp列。这是一种特殊的数据类型,由数据库服务器自动更新。每个表我们只能使用一个timestamp列。下面显示的是摘自一张CREATE声明的表格。我用下面的ALTER语句将这个列添加到前面的例子中。

ALTER TABLE date_annotation_test ADD COLUMN data_changed TIMESTAMP AFTER notes;

注意DEFAULT选项后的附加选项。这些是默认添加的,它显示每当行被创建或更新时,该值都被更改。所以,是的,一旦设置了时间戳列,它就不是固定的。让我们看一个用新列添加行的例子。

INSERT INTO date_annotation_test VALUES (null, 91.34,15.04, null, 'Test timestamp.',null);

这里我传入了NULL值,它告诉数据库服务器使用默认值,正如我们在前面的列定义中看到的,这是对列的更新。还要注意,我将日期和时间列(date_saved)留空,这意味着不要更新该列。因此,该行不应该显示日期和时间列的值,而应该显示为timestamp列(data_changed)插入该行的日期和时间。下表显示了累积数据:

mysql> SELECT * FROM  date_annotation_test;

+----+-------------+------------+---------------------+-----------------+---------------------+

| id | temperature | barometric | date_saved          | notes           | data_changed        |

+----+-------------+------------+---------------------+-----------------+---------------------+

|  1 |       90.82 |         15 | 2011-07-15 23:15:10 | NULL            | 2015-11-08 15:55:50 |

|  2 |       91.34 |      15.04 | 2011-07-15 23:16:30 | Test datetime.  | 2015-11-08 15:55:50 |

|  3 |       91.34 |      15.04 | NULL                | Test timestamp. | 2015-11-08 15:57:27 |

+----+-------------+------------+---------------------+-----------------+---------------------+

3 rows in set (0.00 sec)

注意最后两行有相同的数据。我故意这样做,这样您就可以看出添加带有datetime列的行和添加带有timestamp列的行之间的区别。

因此,使用timestamp列意味着数据库为您完成了工作,因此您可以跟踪数据是何时添加或更改的。但是,使用 timestamp 数据类型有一些限制。首先,每张桌子只能有一个。其次,对行的任何更改都会覆盖这些值。因此,如果您想要保存一个特定的日期和时间,但是稍后想要更新该行(比如添加注释),您可能想要重新考虑使用timestamp或者添加一个或多个datetime列来存储静态日期和时间值。

但是等等,为什么前两行的data_changed值是一样的?您可能已经注意到前两行的data_changed列具有相同的值。这是因为我将新列添加到了一个现有的表中。在内部,数据库服务器必须更新所有行以适应这种变化,因此表中已经存在的每一行都会发生变化;由于该列使用了ON UPDATE CURRENT TIMESTAMP(默认情况下),新列被更新。在更改现有表的模式时,请记住这一点。

Tip

您应该考虑向现有表中添加时间戳列的影响,以及如何更新时间戳列,以确保保存您想要的数据。

数据类型转换

更改数据元素的类型是您应该花些时间仔细考虑和计划的事情。这是因为如果没有正确执行转换,很容易丢失数据。这不仅仅是将一个浮点数转换成一个字符串,然后再转换回来(尽管如果去掉太多的小数,也会有问题)。您必须考虑所需的存储量以及值的范围。例如,整数和无符号整数可能大小相同【3】(2 字节),但是值的范围不同,因为整数允许将值的符号限制(+/-)到范围-32768 到 32767,而无符号整数存储范围 0 到 65535 的值。

您还需要考虑数据的精度。浮点数是有若干小数的实数。大多数平台都非常明确地声明浮点数据类型是估计的,并不是精确的——同样是因为小数部分。因此,在转换数据类型时,您必须考虑这一点。例如,如果将浮点数更改为整数,可能会出现舍入问题。当然,将整数改为浮点数也可能会引入舍入问题。类似地,将类型从较大的存储空间(字节数)更改为较小的存储空间(字节数),会有使目标内存溢出并生成无效值的风险。

另一个考虑是执行算术。也就是说,结果值或中间值必须“适合”该数据类型的值范围。例如,如果将两个无符号整数相加,并将结果赋给另一个无符号整数,则必须确保该值在 0 到 65535 的范围内。否则,该值可能溢出并导致许多问题,最明显的是不正确的值。

Note

一些编程语言有严格的类型检查,并且可以在可能溢出时检测或警告,但是这不能代替谨慎的编程(使用正确的数据类型)。

你可能想知道溢出是什么。观察以下简短的 Arduino 草图。这里有两个变量,一个长整型和一个以 65535 开始的无符号整数。看看会发生什么。

void setup() {

unsigned int uint_val = 65535;

long long_val = 65535;

Serial.begin(9600);

while (!Serial); // Wait until Serial is ready - Leonardo

Serial.print("1> ");

Serial.print(uint_val);

Serial.print(" ");

Serial.println(long_val);

Serial.print("2> ");

uint_val++;

long_val++;

Serial.print(uint_val);

Serial.print(" ");

Serial.println(long_val);

Serial.print("3> ");

uint_val++;

long_val++;

Serial.print(uint_val);

Serial.print(" ");

Serial.println(long_val);

}

void loop() {

}

以下是输出(来自串行监控器):

1> 65535 65535

2> 0 65536

3> 1 65537

我将输出的值打印在第一行,然后给每一行加一,再打印在第二行。注意当我给变量加 1 并打印它们时会发生什么。您可能希望无符号整数是 65536,但它是 0,因为表示该数字的位数超过了变量的最大大小(2 个字节),所以出现了溢出。65536 的值需要 17 位(10000000000000000),两个字节中只有 16 位,前 16 位从右边开始取(00000000000000)。似乎令人担忧的是,没有任何溢出的警告。第三行显示了当我再次添加 1 时发生的情况,但是我们看到无符号整数变量已经重置。

因此,您应该努力使用尽可能最小的数据类型,尤其是在使用微控制器或其他内存有限的设备时。要练习这种做法,您需要了解每种数据类型可以安全存储的值的范围。为了了解它对 Arduino 平台的意义,表 4-1 列出了一些常见的数据类型、内存大小和值的范围。注意浮点变量比整数大多少。

表 4-1。

Arduino Data Types

| 数据类型 | 以字节为单位 | 值的范围 | | --- | --- | --- | | `boolean` | one | 逻辑真/假 | | `byte` | one | 从 0 到 255 的无符号数 | | `char` | one | 从-128 到 127 的有符号数字,通常是 ASCII 字符 | | `unsigned char` | one | 字符值 0 到 255 | | `word` | Two | 从 0 到 65535 的无符号数 | | `unsigned int` | Two | 从 0 到 65535 的无符号数 | | `int` | Two | 从-32768 到 32767 的签名号码 | | `unsigned long` | four | 0-4,294,967,295 之间的无符号数 | | `long` | four | 签名号码从-2,147,483,648 到 2,147,483,647 | | `float` | four | 从-3.4028235E+38 到 3.4028235E+38 的有符号数字 |

尽可能使用最小的数据类型还有一个好处。某些数据类型会导致草图运行速度变慢。例如,在算术运算中使用浮点数可能比使用整数慢 16 倍。如果精度不是问题,而性能是问题,您可能需要考虑将浮点值舍入到整数(提示:乘以 100 以在转换前保留 2 个小数点)。请确保使用尽可能大的整数。

下一节不是给出一组代码示例,而是讨论转换和使用数据类型的一些细节。

代码实现

虽然大多数有经验的程序员会告诫不要转换数据类型,除非绝对必要,但是在一些情况下,如果小心行事,是有可能证明这一点的。例如,如果您正在使用一种具有松散类型检查的语言,并且希望进行一些算术运算来产生浮点值,但是无意中在公式中使用了整数,则您可能会得到整数结果。为了克服这一点,您可能需要使用一种称为强制转换(也称为造型)的特殊操作将整数转换为浮点数。Arduino C 中的强制转换是通过将新的数据类型放在括号中实现的。这里显示了一个示例:

int shift_value = 100;

val_shifted = (float)shift_value * 95.0675;

使用某些数据类型转换的另一个理由是节省内存。这在处理微控制器和其他对变量的内存有限的处理器时最为常见。那些刚接触内存有限的设备的人可能不认为在这里或那里保存一个字节会有什么不同,但它确实会产生巨大的影响。

例如,如果您的草图使用大量变量进行计算,您可能需要考虑减少变量的类型和大小。例如,考虑一下,如果每个单元都是双精度的,那么一个相对较小的 20×20 值的矩阵将消耗多少字节。对,就是 20×20×4 = 1600 字节。如果你的 Arduino 只有 2Kb 的内存,你可能会遇到问题。

如果您遇到类似这样的问题,并且您正在处理的值的范围小于您选择的数据类型,您可以转换它们。例如,如果矩阵中的值从未超过+/-320,并且精度只允许使用两位小数,则可以将这些值转换为整数,如下所示:

int new_val;

new_val = (int)(orig_val * 100.00);

这段代码产生一个值,比如 222.66 到 22266,这个值足够小,适合一个整数数据类型,因此可以在内存中节省 800 个字节。对于旧的 Arduino 主板来说,这是相当大的节省!但是,我应该注意,这些值将被截断,而不是四舍五入。因此,虽然您可以通过除以 100 将数据恢复为浮点型,但您不会恢复原始值的任何额外精度。

您可能遇到的另一种可能的数据类型转换是将字节转换为字符,或者从一系列字节中检索更大的数值。这很可能在设备之间通信时发生。也就是说,大多数通信机制以字节流的形式发送数据。在这种情况下,我们可能需要从字节流中提取整数、浮点数甚至字符串(文本)等数据。

将字节转换成字符很简单,因为它们在代码中被视为相同的(每个都是一个字节)。然而,它们可以有不同的解释。例如,一个 ASCII 字符只使用前七位,而一个字节被视为八位。

如果您必须从字节流中提取数字,您将需要知道这些值是如何编码的。例如,一个适合一个字节的小值将需要一个字节。较大的值可能是一个整数,需要两个字节,长整数可能需要四个字节。除了字节数,您还必须知道各个字节的存储顺序。大多数微处理器和微控制器平台首先用最小的字节存储它们。 5

因此,要从字节流中检索一个值,您必须一次读取一个字节,并将每个字节移位。也就是说,先读取第一个字节,然后读取第二个字节,将其向左移位,并添加第一个字节。可以这样想:一个整数有两个字节,你要把它们分开。要重建整数,必须取第一个字节(最左边的位)并将其左移 8 位。这使得最右边的字节为空。下面是一个例子。我使用十六进制值来使这个例子易于阅读。

Integer: 0x5AFE

Left-most byte: 0x5A

Right-most byte: 0xFE

Left-most byte shifted: 0x5A << 8 = 0x5A00

Adding in the right most byte: 0x5A00 + 0x00FE = 0x5AFE

类似地,存储整数需要将最左边的字节向右移动以保留值。我把这个留给你作为练习来探索。清单 4-7 显示了在字节缓冲区中读取和存储整数值的两种方法。如果您需要在字节流中读取或存储整数,请花些时间通读这些内容。

Listing 4-7.Reading and Storing Integers in a Byte Array

/**

* get_lcb_len - Retrieves the length of a length coded binary value

*

* This reads the first byte from the offset into the buffer and returns

* the number of bytes (size) that the integer consumes. It is used in

* conjunction with read_int() to read``length

* from the buffer.

*

* Returns integer - number of bytes integer consumes

*/

int get_lcb_len(byte buffer[], int offset) {

int read_len = buffer[offset];

if (read_len > 250) {

// read type:

byte type = buffer[offset+1];

if (type == 0xfc)

read_len = 2;

else if (type == 0xfd)

read_len = 3;

else if (type == 0xfe)

read_len = 8;

}

return 1;

}

/**

* read_int - Retrieve an integer from the buffer in size bytes.

*

* This reads an integer from the buffer at offset position indicated for

* the number of bytes specified (size).

*

* buffer[in]      byte stream in memory

* offset[in]      offset from start of buffer

* size[in]        number of bytes to use to store the integer

*

* Returns integer - integer from the buffer

*/

int read_int(byte buffer[], int offset, int size) {

int value = 0;

int new_size = 0;

if (size == 0)

new_size = get_lcb_len(offset);

if (size == 1)

return buffer[offset];

new_size = size;

int shifter = (new_size - 1) * 8;

for (int i = new_size; i > 0; i--) {

value += (byte)(buffer[i-1] << shifter);

shifter -= 8;

}

return value;

}

/**

* store_int - Store an integer value into a byte array of size bytes.

*

* This writes an integer into the``buffer

* buffer. It will transform an integer of size to a length coded binary

* form where 1-3 bytes are used to store the value (set by size).

*

* buff[in]        pointer to location in internal buffer where the

*                 integer will be stored

* value[in]       integer value to be stored

* size[in]        number of bytes to use to store the integer

*/

void store_int(byte *buff, long value, int size) {

memset(buff, 0, size);

if (value < 0xff)

buff[0] = (byte)value;

else if (value < 0xffff) {

buff[0] = (byte)value;

buff[1] = (byte)(value >> 8);

} else if (value < 0xffffff) {

buff[0] = (byte)value;

buff[1] = (byte)(value >> 8);

buff[2] = (byte)(value >> 16);

} else if (value < 0xffffff) {

buff[0] = (byte)value;

buff[1] = (byte)(value >> 8);

buff[2] = (byte)(value >> 16);

buff[3] = (byte)(value >> 24);

}

}

Note

这段代码是如何编码和解码的一个例子。如果您想在自己的解决方案中使用它,您可能需要修改它以适合您的代码。

注意,一种方法使用循环来遍历字节,而另一种方法使用更常见的方法,即使用条件语句。我两者都包括,但是你可以使用最容易理解的。请注意,这两种方法都需要整数的大小。这是必要的,因为整数的长度可以是 1、2 或 4 个字节。

数据类型用法的另一个概念涉及从不改变的值。在这种情况下,您可以将变量声明为常量,这将告诉编译器用常量值替换变量,从而节省一点内存。对于较大的数据类型,这样做可以节省大量内存。考虑下面的代码片段。第一行代码使用 2 个字节,而第二行代码强制编译器在变量出现的任何地方替换为 10。

int multiplier = 10;

const int multiplier = 10;

处理数据类型有相当多的技巧和技术——如此之多,以至于已经写了整本多卷的书来探索每种可能的技术的细微差别和优缺点。如果您在您的 IOT 解决方案中使用 Arduino,并希望确保尽可能小心高效地对其进行编程,请参阅 Simon Monk 博士的著作《Arduino 后续步骤编程》(McGraw Hill,2014)。

如果您正在用 Python 编写低成本的计算机主板,这些技术中有许多是相同的或者有类似的应用。Wolfram Donat 的书《用 Python 学习 Raspberry Pi 编程》(Apress,2014)是一本关于在 Raspberry Pi 上进行高效 Python 编程的优秀书籍。如果你想超越 Python 编程的基础,可以看看 J. Burton Browning 和 Marty Alchin 的书 Pro Python (Apress,2014)。

数据库注意事项

幸运的是,在数据库中存储数据时,您不太可能遇到与微控制器或类似的小内存设备相同的内存限制。这是因为数据库擅长以特定格式存储数据,并且在某些情况下可以优化所使用的存储(尤其是文本)。也许更重要的是,数据库服务器比微控制器有更多的存储空间。

因此,对于一个数据库来说,以正确的类型存储数据确实没有问题,只是有一些小的例外。也就是说,在一些可能的边界情况下,复杂的数据类型可能在数据库服务器上不可用,但是肯定有足够的原语,您可以保存您需要的任何类型。

Note

MySQL 支持许多数据类型。有关更多详细信息,请参见 MySQL 在线参考手册中标题为“数据类型”的部分( http://dev.mysql.com/doc/refman/5.7/en/ )。

此外,处理规模的问题不是存储数据的问题。这是因为,再一次,数据库真的很擅长这个。因此,浮点数的舍入或限制小数更多的是表示,而不是其他。但是,如果你需要做这样的事情,你可以在数据库服务器。

例如,MySQL 为浮点数据类型提供了额外的控件。也就是说,您可以设置整数位数和小数位数。您可以使用float(m,d)语法来实现这一点,其中m是最大位数,d是小数位数。当您需要限制 MySQL 中浮点数的大小或显示时,请使用此语法。

添加导出或计算的数据

另一个常见的注释是向行中添加额外的数据,这些数据是原始值的派生或计算。这包括转换以更改度量单位(华氏温度到摄氏温度)的值、基于范围的枚举或以某种方式(小数位数、精度)转置的枚举,或者结果有意义的计算列(算术运算)。

例如,可能存在为一组事件或传感器生成的传感器数据,这些数据与其他传感器一起使用以提供结果,或者可能存在您想要缩放、分割、将数据转换为新的测量单位或修改值的情况。在这些情况下,我们将新的派生或计算数据添加到行中。假设您有一个以英寸为单位测量距离的传感器,但您需要以毫米为单位的数据。 6 你可以用下面的公式轻松地将数据转换成英寸: 7

value_millimeters = value_inches * 25.4;

回想一下,我们从不想丢弃原始值,所以我们会保存以英寸为单位的值以及派生值。因此,我们将在现在熟悉的 Python 脚本摘录中编写如下数据:

strData = "Distance milimeters: {0:.2f}, Distance inches: {1:.2f} Datetime: {2}\n"

file_log = open("data_log_python.txt", 'a')

dist_inches = read_sensor(1);

dist_mm = dist_inches * 25.4;

file_log.write(strData.format(dist_mm, dist_inches))

file_log.close()

另一个常见的派生列是使用一个列来存储自上次读取以来值的变化量。虽然可以从保存的数据中计算出这个值,但是添加一个额外的列来存储该值可以使读取和理解数据更加容易。为此,我们必须保存旧值,并通过从新值中减去旧值来计算变化。一个积极的结果是价值增加的数量;负值是值减少的量。

让我们仔细看看一个常见的派生列和计算列的示例。

代码实现

派生和计算的数据(列)将以特定的方式实现。也就是会有一个精确的公式或者翻译需要进行。在下面的示例中,我们将看到我在自己和他人的 IOT 解决方案中遇到的三个派生和计算列的示例。

第一个是校准的推导。有时,传感器需要校准,校准结果将决定一个值(如果差值不是线性的,有时是一个公式),必须加上或减去该值才能获得更精确的值。

第二个例子是一个简单的计算,我们有一个传感器,可以测量盘子或浅盘上许多物体的重量。在本例中,对象的数量是固定的,但在大多数情况下,这也是一个变量。精明的 IOT 爱好者和爱好者会注意到计算不规则物体的平均重量可能并不有趣,所以我们假设这个例子中的物体在大小和组成上都是相似的。

第三个是派生值的另一个例子,我们希望存储从上一次读取的值中读取的值的差异。这代表了大量的注释,包括平均值、运行总数等等,以及数字数据。

虽然所有这些都是技术上的变化或添加新数据,但我们保留原始值,以确保我们可以从推导或计算中的任何变化中恢复。此外,我们存储这些值以使读取和处理数据更容易。

清单 4-8 显示了一个 Arduino 草图的摘录,该草图模拟并实现了三个传感器的读数:一个需要校准的血氧传感器、8一个测量几个物体重量的重量传感器,以及一个我们用来保存自上次读数以来的差值的电压传感器。

Listing 4-8.Derived and Calculated Annotations (Arduino)

/**

Example of derived or calculated columns annotation.

This project demonstrates how to save data to a

microSD card as a log file with additional column annotation.

*/

#include <SPI.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

File log_file;   // file handle

String strData;  // storage for string

float blood_oxygen;

// Simulated sensor reading method

float read_sensor(int sensor_num) {

if (sensor_num == 1) {

return 90.125 + random(20)/10.00;       // sensor #1

} else if (sensor_num == 2) {

return 94.512313 + random(100)/100.00;  // sensor #2

} else if (sensor_num == 3) {

return 45.6675 + random(100)/100.00;    // sensor #3

} else {

if (random(25) >= 5) {

return (float)random(14)/10.00;

} else {

return -(float)random(14)/10.00;

}

}

}

void setup() {

Serial.begin(115200);

while (!Serial);

Serial.print("Initializing SD card...");

if (!SD.begin(SD_PIN)) {

Serial.println("ERROR!");

return;

}

Serial.println("ready.");

if (SD.remove("data_log.txt")) {

Serial.println("file removed");

}

// initiate random seed

randomSeed(analogRead(0));

}

float oldVal = 67.123;

float newVal = 0.00;

float weight = 0.00;

void loop() {

delay(2000);

log_file = SD.open("data_log.txt", FILE_WRITE);

if (log_file) {

strData = String("Blood oxygen: ");

blood_oxygen = read_sensor(2);

strData += blood_oxygen;

strData += ", Calibrated: ";

// calculated column adjusting for calibration

strData += String(blood_oxygen + 0.785, 2);

strData += ", Weight: ";

weight = read_sensor(3);

strData += weight;

// calculated column for number of objects

strData += ", Avg weight: ";

strData += String((weight/4.0), 4);

// Calculating change since last read

strData += ", Volts: ";

newVal = oldVal+read_sensor(4);

strData += String(newVal, 2);

strData += ", Delta: ";

strData += String(newVal-oldVal,3);

oldVal = newVal;

log_file.println(strData);

log_file.close();

} else {

Serial.println("Cannot open file for reading.");

}

}

Note

以下代码不代表实际的解决方案;相反,它旨在展示生成派生列和计算列的概念。

请注意,我们再次添加新数据作为列,用逗号分隔它们。下面显示了此代码中的一组示例行:

Blood oxygen: 94.88, Calibrated: 95.67, Weight: 46.07, Avg weight: 11.5169, Volts: 68.42, Delta: 1.300

Blood oxygen: 95.23, Calibrated: 96.02, Weight: 46.56, Avg weight: 11.6394, Volts: 68.52, Delta: 0.100

Blood oxygen: 94.97, Calibrated: 95.76, Weight: 46.42, Avg weight: 11.6044, Volts: 68.62, Delta: 0.100

Blood oxygen: 95.46, Calibrated: 96.25, Weight: 46.49, Avg weight: 11.6219, Volts: 69.62, Delta: 1.000

Blood oxygen: 94.96, Calibrated: 95.75, Weight: 46.18, Avg weight: 11.5444, Volts: 70.12, Delta: 0.500

Blood oxygen: 94.62, Calibrated: 95.41, Weight: 46.11, Avg weight: 11.5269, Volts: 70.52, Delta: 0.400

请注意,这里我们看到的是血氧传感器,其校准值与四个物体的重量和平均重量一起存储,然后存储电压和自上次读取值以来的变化。现在让我们看看数据库服务器如何使这些注释变得更容易。

数据库注意事项

到目前为止,您应该认为数据库服务器是一个强大的工具,事实也的确如此。事实上,它非常擅长像前面展示的那样进行推导和计算。此外,根据所需的计算,对于如何实现推导或计算,您有几种选择。本节讨论三种常见的替代方法。还有其他的选择, 9 但这些都是最常见的方法。

您可以添加一个触发器,这是一个在添加数据时执行(触发)的特殊过程;要在一个特殊的列中添加新数据,您可以将计算放在SELECT语句(检索行的方法)中,这样新数据就可以动态生成;或者您可以将计算放在INSERT语句中(保存数据的方法)。让我们从为什么你会选择一个而不是另一个开始来看看每一个。

您应该考虑的第一件事是,是动态地生成新数据,还是以列的形式存储在表中。动态生成新值将意味着使用更少的存储,但在检索行时可能会带来额外的好处。也就是说,检索大量的行可能需要更长的时间,因为计算被推迟并一次完成。因此,在表中保存新数据意味着检索行花费的时间更少,但是需要更多的存储空间。幸运的是,存储空间通常不是问题。请注意,SELECT语句中指定的计算仍然在数据库服务器上执行,尽管在检索每一行时只执行一次。

至于是使用触发器还是作为INSERT语句的一部分在数据库中生成数据,我们应该考虑希望在哪里执行计算。如果放在触发器中,计算将在数据库服务器上执行。但是如果放在INSERT语句中,计算是在客户机(发送数据的那个)上执行的。因此,如果您有复杂的计算或有限的处理能力,您可能希望选择触发器选项。

在我们查看数据库示例之前,下面显示了执行示例所需的表。我在这里包括它,以便您可以自己测试这些示例,并且您可以看到唯一添加的新列是 calibrated 列,它由触发器示例使用。清单 4-9 显示了测试表。

Listing 4-9.Table Schema for Derived and Calculated Columns Example

CREATE TABLE derived_annotation_test (

``id int(11) NOT NULL AUTO_INCREMENT,

``blood_oxygen float NOT NULL,

``blood_oxygen_corrected float DEFAULT NULL,

``weight float DEFAULT NULL,

``volts float DEFAULT NULL,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1

请注意,我们添加了一列用于血氧示例的校准,但没有添加其他两个值的校准。那些我们将在运行中生成的。现在让我们看看如何用代码实现这三个选项。

使用触发器的派生列和计算列

触发器是对数据库进行编程以填充列的好方法。在这种情况下,我们希望编写一个触发器,在每次向表中添加一行时执行,并在添加时使用一个简单的公式计算校准值。触发器可以以多种方式执行(或触发),例如在插入或删除之前或之后。一个触发器与一个特定的表相关联,但是一个表可以有多个触发器。我们将在第 5 章中更详细地讨论触发器,但现在观察以下内容:

CREATE TRIGGER calibrate_blood_oyxgen BEFORE INSERT ON derived_annotation_test

FOR EACH ROW SET NEW.blood_oxygen_corrected = NEW.blood_oxygen + 0.785;

我们在这里看到的是一个触发器,设置为在名为derived_annotation_tes t 的样本表的插入之前执行。注意,第二行非常清楚地显示,将用blood_oxygen值加上0.785的值为blood_oxygen_corrected列设置一个新值。

现在让我们看看当我们插入一行时会发生什么。以下代码在表格中插入一行。注意,我指定了一个列列表,后跟一个值列表。如果不指定列列表,则必须为表中的所有列提供值。

INSERT INTO derived_annotation_test (blood_oxygen, weight, volts) VALUES (94.88, 46.07, 68.42);

现在让我们看看数据是什么样的。回想一下,为了从表中获取数据,我们使用如下的SELECT命令:

mysql> SELECT * FROM  derived_annotation_test;

+----+--------------+------------------------+--------+-------+

| id | blood_oxygen | blood_oxygen_corrected | weight | volts |

+----+--------------+------------------------+--------+-------+

|  5 |        94.88 |                 95.665 |  46.07 | 68.42 |

+----+--------------+------------------------+--------+-------+

1 row in set (0.00 sec)

在这里,我们看到修正值已保存在blood_oxygen_corrected栏中。最棒的是,我们添加数据时什么都不用做。事实上,触发器的美妙之处在于,你设置一次,它就能对所有插入的数据起作用。

Note

一些 Python 代码示例使用一个称为数据库连接器的特殊库来连接 MySQL。在本例中,它是来自 Oracle 的连接器/Python 连接器库( http://dev.mysql.com/downloads/connector/python/ )。你会在第 6 章和第 8 章中看到这个连接器的更多细节。

使用 SELECT 语句的派生列和计算列

回想一下,我们还可以在读取数据时动态生成派生列或计算列。更具体地说,我们将该操作作为SELECT语句的一部分。让我们插入一些数据来看看这是如何做到的。在这个例子中,我们将展示如何计算从最后一次读取volts列的值以来的差值。回想一下示例表,我们没有存储增量的列。下面显示了一个插入多行的示例INSERT。这也称为大容量插入。

INSERT INTO derived_annotation_test (blood_oxygen, weight, volts)

VALUES (94.88, 46.07, 68.42), (95.23, 46.56, 68.52), (94.97, 46.42, 68.62);

注意,我们只是提供了一个逗号分隔的值列表,每个值代表一个新的数据行。在这种情况下,我们使用与之前 Arduino 草图中所示相同的数据。插入数据后,我们可以按如下方式查看它:

mysql> SELECT * FROM derived_annotation_test;

+----+--------------+------------------------+--------+-------+

| id | blood_oxygen | blood_oxygen_corrected | weight | volts |

+----+--------------+------------------------+--------+-------+

|  5 |        94.88 |                 95.665 |  46.07 | 68.42 |

|  7 |        94.88 |                 95.665 |  46.07 | 68.42 |

|  8 |        95.23 |                 96.015 |  46.56 | 68.52 |

|  9 |        94.97 |                 95.755 |  46.42 | 68.62 |

+----+--------------+------------------------+--------+-------+

4 rows in set (0.00 sec)

为了计算增量,我们编写一个 Python 脚本来连接数据库并从表中检索行。我们首先保存读取的第一个值的值,然后在后续行中进行比较,计算自读取最后一个值以来的变化。清单 4-10 显示了一个使用 MySQL 连接器/Python 库从服务器读取数据的 Python 脚本。

Listing 4-10.Calculated Columns Using SELECT (Python)

import mysql.connector;

cnx = mysql.connector.connect(user="root", password="SECRET")

cur = cnx.cursor()

cur.execute("SELECT * FROM test.derived_annotation_test")

old_value = 0;

for row in cur.fetchall():

if old_value > 0:

print "{0}, {1}, {2}, {3}, {4}".format(row[1], row[2], row[3], row[4], row[4] - old_value)

else:

print "{0}, {1}, {2}, {3}".format(row[1], row[2], row[3], row[4])

old_value = row[4]

虽然可能有更有效的方法来读取列值,但我编写了代码来访问行中的每一列,以向您展示结果如何作为列表返回(就像数组一样)。我只需访问从 0 开始的列号,就可以得到该列的值。注意,volts列的值是表中的第五列,因此用[4]来引用。下面显示了运行该脚本的输出:

$ python ./derived_select_example.py

94.88, 95.665, 46.07, 68.42,

94.88, 95.665, 46.07, 68.42, 0.0

95.23, 96.015, 46.56, 68.52, 0.1

94.97, 95.755, 46.42, 68.62, 0.1

这里我们看到代码打印出了blood_oxygenblood_oxygen_correctedweightvolts的数据。第一行没有值,因为我们没有以前的值来计算增量。剩余的行将最后一个值之后的增量或变化显示为第五个值。

使用 INSERT 语句的派生列和计算列

现在我们来看一下INSERT方法。我们将使用先前获得的电压数据。在这种情况下,我们已经在代码中的某个地方存储了旧值,这样当我们保存新值时,我们就包括了那里的计算。下面的代码是一个例子,说明如何形成包含计算的INSERT语句。虽然公式很简单,但该示例显示了在插入数据时如何填充派生列或计算列。

回想一下,计算是在客户机上执行的,而触发器示例是在服务器上执行的。我离开去做一个练习,把这个例子转换成一个触发器。在我们看到 SQL 代码之前,让我们将计算列添加到表中。有一个名为ALTER TABLE的强大命令,可以用来添加或删除列,等等。我们使用它来添加新列,如下所示:

ALTER TABLE derived_annotation_test ADD COLUMN avg_weight float AFTER weight;

现在我们添加新数据。回想一下 Arduino 示例,我们想简单地将重量值除以 4,得到传感器测得的每件物品的平均重量。下面是另一个 bulk insert 语句。请注意,我们只是在“平均体重”栏的空白处加入了这个公式。

INSERT INTO derived_annotation_test (blood_oxygen, weight, avg_weight, volts)

VALUES (95.46, 46.49, (46.49/4.00), 69.62), (94.96, 46.18, (46.18/4.00), 70.12),

(94.62, 46.11, (46.11/4.00), 70.52);

快速检查表中的数据,确认行中插入了正确的值。

mysql> SELECT * FROM derived_annotation_test;

+----+--------------+------------------------+--------+------------+-------+

| id | blood_oxygen | blood_oxygen_corrected | weight | avg_weight | volts |

+----+--------------+------------------------+--------+------------+-------+

|  5 |        94.88 |                 95.665 |  46.07 |       NULL | 68.42 |

|  7 |        94.88 |                 95.665 |  46.07 |       NULL | 68.42 |

|  8 |        95.23 |                 96.015 |  46.56 |       NULL | 68.52 |

|  9 |        94.97 |                 95.755 |  46.42 |       NULL | 68.62 |

| 10 |        95.46 |                 96.245 |  46.49 |    11.6225 | 69.62 |

| 11 |        94.96 |                 95.745 |  46.18 |     11.545 | 70.12 |

| 12 |        94.62 |                 95.405 |  46.11 |    11.5275 | 70.52 |

+----+--------------+------------------------+--------+------------+-------+

7 rows in set (0.00 sec)

这里我们看到只有最后三行有平均重量列。回想一下,我们将新列添加到已经包含数据的表中。除非添加了触发器,否则现有行的新列的值将保持为空(技术上来说是NULL)。

数据解释

有时数据以不可用的形式生成,或者需要一些翻译或解释才能使用。我们已经看到了一个数据解释的例子。回想一下第 1 章中的工厂监控示例,我们有一个传感器,根据产生的值,它可以指示几种状态中的一种。因此,我们可以将数据值解释为这些状态之一,并因此存储该状态。在这种情况下,我们从一系列值中创建一个值,而不是计算一个新值。

让我们看一个类似的例子,但这次我们将看到从不同类型的传感器生成的数据。在这种情况下,被模拟的传感器是诸如由 Sparkfun ( http://sparkfun.com/products/10221 )出售的液位传感器。这是一种测量电阻的传感器,可用于确定从传感器顶部到液体表面的距离。更具体地说,输出与液位成反比。液体越低,测得的电阻越高。液体越低,测得的电阻越小。

现在,假设我们在一个池塘监控解决方案中使用这种传感器,想要测量过滤池内的水位。此外,我们从经验和观察中得知,根据水位的不同,池塘的状态会发生如下变化:

  • 如果水位低至 6 英寸或低于固定点,过滤器可能需要清洗。
  • 如果水位在 3-6 英寸之间,则池塘水位较低。
  • 如果水位在 1-3”之间,则池塘处于正常状态。
  • 如果水位高于 1 英寸,则说明池塘中的水过多,水溢出了过滤元件。

因此,我们需要检查特定范围内的电阻值,并存储该范围的枚举值。我们将命名范围(CLEANLOWNORMALHIGH)。现在让我们看看如何使用 Python 脚本存储一行数据。请记住,我们总是希望存储原始值,这样,如果枚举需要调整(范围改变),我们可以改变现有的数据,而不会使其不可用。

代码实现

在这个例子中,我们使用 Python 代码来确定使用哪个枚举。以下代码节选自另一个使用连接器/Python 库的 Python 脚本。不要太担心库的机制;相反,请注意我是如何使用 Python 代码来实现枚举的。

从传感器读取的值以欧姆为单位,在 300 到 1500 +/-10%的范围内。那么我们如何知道使用什么范围呢?我们安装传感器并进行一些测量吗?我们可以这么做。事实上,我们可以使用一个高容器,慢慢地往里面注水,同时观察传感器的数值。这是一种有效的测试传感器的好方法,但是还有一种更快的方法。

大多数制造商提供所谓的数据表来描述设备的性能。在这种情况下,制造商会提供一个图表,指示特定液位的预期值( http://cdn.sparkfun.com/datasheets/Sensors/ForceFlex/eTape%20Datasheet%2012110215TC-8_040213.pdf )。根据这些数据,我们可以确定以下范围与观察到的水位相对应。表 4-2 显示了完整的数据。

表 4-2。

Liquid-Level Enumerated Values for Pond Monitoring

| 传感器顶部的深度,单位为英寸 | 值范围 | 结果 | | --- | --- | --- | | 0 < 1 | 1500 及以上 | 高的 | | 1 < n < 3 | 1150 到 1500 | 标准 | | 3 < n < 6 | 1150 到 700 | 低的 | | 6 < n | 700 及以下 | 干净的 |

请注意一些范围是如何重叠的。只有使用或实验才能确定实际值,但这应该是一个良好的开端。现在让我们看看 Python 代码(参见清单 4-11 )。这里我们正在构建一个INSERT语句,它包含了从执行INSERT语句之前的代码中得到的数据。

Listing 4-11.Derived Values Example (Python)

import mysql.connector;

from random import random, randint

def read_sensor():

return 500 + randint(0,1500)

strInsert = "INSERT INTO pond VALUES (null, {0}, '{1}')"

cnx = mysql.connector.connect(user="root", password="SECRET", database="test")

cur = cnx.cursor()

# Calculate enumerated value for sensor.

water_level = read_sensor()

if water_level > 1500:

state = 'CLEAN'

elif water_level > 1150:

state = 'LOW'

elif water_level > 700:

state = 'NORMAL'

else:

state = 'HIGH'

cur.execute(strInsert.format(water_level, state))

cnx.commit()

cur.close()

cnx.close()

花点时间通读代码。不要太担心连接器库。相反,请关注用于设置状态值的代码。这相当简单,确定的值通过参数替换用于INSERT语句。

为了测试代码,我们需要创建一个测试表,如下所示。注意,我添加了我们将需要的两列以及一个自动递增的列,以便于识别行。

CREATE TABLE pond (

``id int(11) NOT NULL AUTO_INCREMENT,

``water_level int NOT NULL,

``state char(12) DEFAULT NULL,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1;

接下来,我们可以多次运行脚本来创建几行。精明的 Python 开发者会看到,我用随机生成的值模拟了传感器读取代码。显然,这将创建相当多的值变化很大的行。也就是说,在实际的池塘中,水位不会从一个时刻到另一个时刻如此剧烈地波动。然而,代码足以测试和演示枚举值的技术。插入几行后,我们可以看到如下结果:

mysql> SELECT * FROM pond;

+----+-------------+--------+

| id | water_level | state  |

+----+-------------+--------+

|  8 |        1054 | NORMAL |

| 10 |        1117 | NORMAL |

| 11 |        1278 | LOW    |

| 12 |        1316 | LOW    |

| 13 |        1451 | LOW    |

| 14 |        1688 | CLEAN  |

+----+-------------+--------+

6 rows in set (0.00 sec)

花点时间确保这些值与基于表 4-2 中数据的枚举值相匹配。我鼓励您多次运行该脚本,直到您看到每个州的示例。提示:您可能希望修改read_sensor()方法,为每个范围返回一个随机数,而不是从 0 到 1500 的范围。

数据库注意事项

有几种方法可以处理数据库中的数据解释。我将讨论两种这样的方法:在保存数据时使用枚举值来确定状态(仍然需要一点代码),以及在保存或更新数据时使用一种高级技术来自动确定状态。是的,另一个触发器!

到目前为止,我们知道触发器是非常强大的,在这种情况下,我们可以包含一组类似的代码来确定来自传感器的状态值。事实上,它类似于 Python 的例子。让我们看看触发器内部的代码,重点看解释是如何完成的。下面显示了触发代码:

DELIMITER //

CREATE TRIGGER set_pond_state BEFORE INSERT ON pond FOR EACH ROW

BEGIN

IF NEW.water_level > 1500 THEN

SET NEW.state = 'CLEAN'

ELSEIF NEW.water_level > 1150 THEN

SET NEW.state = 'LOW'

ELSEIF NEW.water_level > 700 THEN

SET NEW.state = 'NORMAL'

ELSE

SET NEW.state = 'HIGH'

END IF;

END //

DELIMITER ;

现在让我们使用批量插入测试触发器。这里,我们将插入四个新行,它们的值是专门为产生四种状态之一而选择的。

INSERT INTO pond VALUES (null, 1501, null), (null, 1151, null), (null, 701, null), (null, 600, null);

MySQL 中有一个有趣的系统变量。它被命名为last_insert_id,存储自动递增字段使用的最后一个值。如果您练习了 Python 示例,您将已经创建了表并插入了几行。我们可以使用last_insert_id来查找自动递增机制的最后一个值,而不是显示(返回)所有的行。在这种情况下,该值是最后一次插入所使用的最后一个值,但是由于这是一次大容量插入,因此该值是大容量插入中的第一个值(在这种情况下为 15)。你自己试试。

mysql> select @@last_insert_id;

+------------------+

| @@last_insert_id |

+------------------+

|               15 |

+------------------+

1 row in set (0.00 sec)

下面显示了插入的行:

mysql> SELECT * FROM pond WHERE id >= 15;

+----+-------------+--------+

| id | water_level | state  |

+----+-------------+--------+

| 15 |        1501 | CLEAN  |

| 16 |        1151 | LOW    |

| 17 |         701 | NORMAL |

| 18 |         600 | HIGH   |

+----+-------------+--------+

4 rows in set (0.01 sec)

请注意,我们看到插入的四行,并且已经选择了正确的枚举值。在这一点上,我还应该注意到有一个名为enum的数据类型,您可以使用它来存储表本身中值的字符串。我们将在第 5 章的中看到这个选项的实际应用;但是,如果您熟悉使用enum,我鼓励您更改前面显示的表格和触发器,使用enum代替字符串。

现在,您已经看到了几个注释示例,让我们讨论一个更复杂的聚合来自多个传感器或节点的数据的应用,以及您可能希望在 IOT 解决方案中实现的聚合操作类型。

聚合

为您的 IOT 解决方案聚合数据可能比标注数据更复杂。聚合可以有几种形式。最容易实现的形式是聚合来自多个传感器的数据。从多个节点(其他数据收集器)聚合数据在概念上相似,但在实现上有很大不同。最后,更复杂的聚合形式是对数据集执行操作,如统计或计数函数。

在本节中,我们将从较高的层次来探讨这些聚合形式。因为每一个的实现都可能很复杂,所以我只给出一般的概述,而不是具体的代码解决方案。也就是说,我们将在第 8 章中看到这些聚合形式的实现。因此,以下部分讨论了每种形式的策略、实践和一般描述。

来自多个传感器的数据

IOT 解决方案从多个来源或传感器收集数据是正常的。有时,传感器用于观察不同的事物(事件、物体等),但大多数情况下,几个源或传感器用于观察单个事物或对象。也就是说,您可能有一个 Arduino 板形式的数据收集节点,从几个监控某些东西的传感器读取数据。或者,您可能有一个 Arduino 读取多个传感器来监控多个对象。

接下来的问题是如何存储这些数据?您应该像在本章前面的示例中看到的那样将传感器值存储为一个集合,还是应该单独存储这些值?有两种方法:基于传感器定时存储数据和基于特定时间段存储数据。

传感器驱动的数据

基于传感器读取时间生成的数据意味着数据以数据可用性所决定的时间间隔存储。此外,在收集数据时,独立地使用数据(显示、列表或挖掘信息等)。也就是说,使用数据的时间对数据没有意义。

虽然存储一组对应于单个对象的传感器值是很自然的,但是如果传感器以不同的时间间隔产生值,您可能必须考虑一次存储一个值,而不是等待所有传感器报告。事实上,不同的传感器在不同的时间间隔被读取并不罕见。这可能是因为你所观察的事物的性质,以及观察的时机有助于产生知识。

例如,考虑工厂监控解决方案。您可能希望每小时读取一次温度(甚至更频繁),因为室外温度在一小时内可能会在某些区域快速变化,或者您可能希望在室内受控环境中每隔几小时读取一次温度。然而,对于土壤湿度,您可能希望每天读取两次数值,因为土壤湿度可能不会快速变化,尤其是在受控气候下。

假设您决定每小时读取一次温度值,每六小时读取一次土壤湿度值。给定这些时间间隔,你如何处理这六个温度值?每个土壤湿度值有六个温度值。你是平均温度值还是扔掉其中的五个?

显然,丢弃五个传感器读数是潜在的信息损失。在这种情况下,您可能会丢失温度变化的数据。例如,如果温度在第一个小时变化了 4 度(在我所在的地区并不罕见),但在接下来的五个小时内只变化了 1 度,保存最后一个值会模糊温度变化的时间,更重要的是会丢失温度快速变化的时间事件。即使取平均值也会丢失数据和模糊知识。知识的丧失可能不明显,需要一点思考。表 4-3 显示了我们可以收集的数据类型示例。

表 4-3。

Sensor Data Frequency and Loss of Knowledge

| 小时 | 温度 | 土壤湿度 | | --- | --- | --- | | one | Twenty-four point five |   | | Two | Twenty-four point seven |   | | three | Twenty-four point nine |   | | four | Twenty-five point two |   | | five | Twenty-five point four |   | | six | Twenty-five point six | Four hundred and twenty-six | | seven | Twenty-five point eight |   | | eight | Twenty-seven point nine |   | | nine | Thirty point one |   | | Ten | Twenty-nine point three |   | | Eleven | Twenty-eight point nine |   | | Twelve | Twenty-eight point six | Four hundred and ten |

请注意,这里我们看到的是温度值(以摄氏度为单位),但每六小时只有一个土壤湿度值。如果我们只在读取土壤湿度时存储温度读数,我们将会看到数值的巨大变化,并且不知道温度何时变化——只知道自六小时前最后一次读取温度以来温度发生了变化。

例如,注意第 6 小时的温度和土壤湿度。这里我们看到我们分别存储了值(25.6,426)。请注意第 12 小时的值。这里我们存储了值(28.9,410)。虽然土壤湿度没有太大变化,但我们看到温度发生了变化(28.9–25.6 = 3.0)。然而,我们已经失去了在第 7 小时和第 10 小时之间温度变化最大的时刻,甚至失去了第 9 小时温度最高的事实。

相反,如果我们对读取的温度值进行平均,我们将保存第 6 小时(25.05,426)和第 12 小时(28.43,410)的数据。虽然随着时间的推移,我们已经考虑了这些价值,但我们并没有获得更多的信息。是的,我们仍然能探测到间隔期间温度上升的趋势,但是温度最高的那个小时仍然不知道。你也可以说我们已经失去了变化率的知识,甚至失去了准确性,因为我们存储的温度值在读取值时是不准确的。

当您遇到将传感器数据存储为单行会模糊知识的情况时,您需要将数据划分到两个表中,而不是一个表中。图 4-2 显示了一个解决方案的示例,我们可以以不同的速率保存传感器数据,但仍将其与单一事物相关联。

A978-1-4842-1293-6_4_Fig2_HTML.jpg

图 4-2。

Collecting data from multiple sensors at different rates

注意这里我们有三张桌子。基表或包含核心信息的表被命名为plants。我们看到一个单独的表存储土壤湿度数据,另一个存储温度。这些表共享一个公共列id,用于唯一识别读取传感器数据的工厂。也就是说,我们可以检索给定植物的所有温度或土壤湿度值(列idtemperaturesoil_moisture表的外键)。请注意,我们还为每一行存储了时间戳,因此我们知道传感器数据是何时存储的,因此可以绘制一段时间内的数据或确定变化率。

Note

4-2 中的图取自 MySQL Workbench 中的可视化数据库编辑器( http://dev.mysql.com/downloads/workbench/ )。

通过制作三个表而不是一个表,我们保留了从数据中获取知识的潜力,这些数据在不同的时间间隔被读取。事实上,这个例子显示了数据库的强大。当然,您可以在基于文件的解决方案中实现这个解决方案,但是数据库使它更容易查看,甚至更容易设置。

区间驱动数据

在一些解决方案中,以固定或特定的时间间隔存储数据更为重要。在这些解决方案中,重点是收集在特定时间使用的数据。因此,该解决方案很可能不会存储数据,除非或直到数据被请求。一些解决方案使数据动态可用,但是通常直到用户或一些其他机制发起请求时才存储数据。

例如,考虑一个气象站。如果您自己正在构建一个气象站,您可能会加入一个显示元素来实时显示天气观测的当前值,如温度、气压等。在这种情况下,来自传感器的数据被频繁读取,并且显示元素(例如,仪表)被频繁更新。然而,这些值的数据不太可能被存储。

相反,对于气象站来说,保存的数据通常是特定时间段的一组观测数据。你可能会看到气象站每小时存储一次数据,甚至每小时存储几次。这是因为观测有一个共同的数据元素——收集时间。因此,间隔驱动数据可能需要您构建解决方案,以特定的间隔存储大量数据,而不是以特定传感器或事件的间隔存储。

对于这些解决方案,我们通常将信息存储为一个条目(行)。然而,没有任何东西说我们必须这样做。如果有必要,我们可能仍然希望将数据存储在单独的表(文件)中,但是通常不会出现与获得的数据或知识相关的问题。同样,这是因为数据保存为传感器之间的特定间隔,而不是在传感器数据可用时驱动。

来自多个节点的数据

与来自多个传感器的数据密切相关的是从几个节点收集的数据。该节点不是直接读取传感器,而是从其他设备获取数据。例如,您可能使用 XBee 模块将一个 Arduino 无线连接到几个模块。因为 XBee 模块只能发送数据,所以您需要 Arduino 节点来收集数据并将其存储在本地文件或数据库中。

同样的考虑适用于我们从多个传感器发现的数据。很可能每个 XBee 模块都定期发送数据,并且可能包括多条数据(多个传感器数据)。因此,如何保存数据具有相同的含义——将数据作为一行存储在一起,还是分别存储数据。

到目前为止,原则上与上一种形式没有什么不同。然而,从多个节点收集数据还有另一个可以利用的功能——批量保存数据。回想一下前面的例子,MySQL INSERT SQL 语句允许在一个命令中存储多行数据。通过从多个节点收集数据,我们可以轻松地构建这样的语句。

清单 4-12 显示了一个 Arduino 草图的摘录,该草图从多个 XBee 模块获取数据并将数据保存在 MySQL 数据库中。 10 在这个例子中,我让事情变得相当简单,我们读取从多个数据节点生成的传感器数据,这些数据节点被表示为带有温度传感器的 XBee 模块。该代码旨在向数据库中批量插入数据。我已经将包含的行数设置为 3,但是您可以很容易地将这段代码扩展到您有多少内存来存储数据就有多少行。

那么我们如何知道哪个 XBee 被读取了呢?我们保存模块的地址——所有 XBee 模块都有唯一的地址,因此我们知道温度数据的来源。我们还看到一个在数据节点上执行计算的例子,其中我们以原始传感器形式以及摄氏度和华氏度存储温度。我还应该注意,在连接多个 XBee 模块的情况下,根据 XBee 模块发送数据的时间,可以从同一个模块收集两次数据。

Listing 4-12.Getting Data from Multiple Nodes (Arduino)

String get_data(ZBRxIoSampleResponse *ioSample) {

// Received data from address of data node

int address = (ioSample->getRemoteAddress64().getMsb() << 8) +

ioSample->getRemoteAddress64().getLsb();

// Get and calculate the temperature in C and F

float temp_raw = ioSample->getAnalog(3);

float temp_c = ((temp_raw * 1200.0 / 1024.0) - 500.0) / 10.0;

float temp_f = ((temp_c * 9.0)/5.0) + 32.0;

String strRow = String("(");

strRow += String(address);

strRow += ",";

strRow += String(temp_raw);

strRow += ",";

strRow += String(temp_c);

strRow += ",";

strRow += String(temp_f);

strRow += ")";

return strRow;

}

...

void loop() {

String row[3];

int i;

i = 0;

//attempt to read a packet

xbee.readPacket();

if (xbee.getResponse().isAvailable()) {

// XBee module is communicating, check for IO packet

if (xbee.getResponse().getApiId() == ZB_IO_SAMPLE_RESPONSE) {

// Get the packet

xbee.getResponse().getZBRxIoSampleResponse(ioSample);

row[i] = get_data(``&

i++;

}

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());

}

// Store the data once 3 entries are retrieved.

if (i == 3) {

i = 0;

String strINSERT = String("INSERT INTO test.room_temperatures VALUES ");

strINSERT += row[0];

strINSERT += ",";

strINSERT += row[1];

strINSERT += ",";

strINSERT += row[2];

Serial.println(strINSERT);

// Create an instance of the cursor passing in the connection

MySQL_Cursor *cur = new MySQL_Cursor(``&

cur->execute(strINSERT.c_str());

delete cur;

}

}

哇,这么多代码,而且只是节选!我将代码的三个部分加粗,以突出我所演示的概念。注意get_data()方法。这里我们看到了从通信包中读取数据并产生一个形式为(N,F,F,F)的字符串的代码,其中N是一个整数,F是一个浮点数。我们将在后面的代码中使用这个字符串。第二部分是对get_data()的调用,在这里我们保存在数组中创建的字符串。在前面的部分中,我们检测到三个数据收集器已经发送了数据,我们用一个INSERT语句将信息保存到数据库中。

有一点,使用这样的数据聚合器会使事情变得更复杂。回想一下,有时我们希望存储数据被读取的日期和时间。如果我们实现一个像前面所示的数据聚合器,其中我们不捕获读取值的日期和时间,那么在数据库中设置时间戳列可能会导致日期和时间值不准确。

例如,如果读取所有三组数据需要 10 秒,那么时间戳值不仅会延迟大约 10 秒,而且这三行的时间戳几乎相同。对于某些解决方案来说,这可能没问题,但如果延迟更像是 10 分钟甚至一个小时,这种延迟可能是不可接受的。

因此,如果您想要为数据收集器存储日期和时间信息,您将必须在传感器被数据收集器读取或被数据收集器在接收数据时设置时收集数据。

汇总计算

您可能遇到的最后一种聚合形式包括需要对一组数据进行一些计算的情况。这可以简单到计算总和的平均值,找到最小和最大值,或者执行任何这样的公式或运算。您可以编写代码来处理这些操作,这是一个有效的解决方案。然而,这是数据库服务器擅长的另一个领域。

例如,考虑清单 4-13 中所示的 Python 脚本的代码摘录。这里我们看到了从包含多列数据的文件中读取多行的代码。我们使用 Python 的能力来解密文件,然后对数据执行操作。

Listing 4-13.Aggregate Calculations (Python)

file_log = open("data_log_python.txt", 'a')

temp_tot = 0;

temp_min = 999;

temp_max = 0;

for i in range(0,20):

# read sensors

temp = read_sensor(1)

baro = read_sensor(2)

# add to total

temp_tot = temp_tot + temp

# find min/max

if (temp < temp_min):

temp_min = temp

if (temp > temp_max):

temp_max = temp

print(temp, baro)

file_log.write(strData.format(temp, baro))

# display aggregate values

print "Average Temperature:", temp_tot/20.00

print "Min Temperature:", temp_min

print "Max Temperature:", temp_max

file_log.close()

请注意,我们计算读取值的平均值(20)以及最小值和最大值。除了我们必须计数、合计和检测最小值/最大值之外,代码中没有任何神奇之处。虽然代码并不复杂,但它远不止一行代码。这里显示了一个输出示例:

Average Temperature: 94.0704303696

Min Temperature: 90.2774251101

Max Temperature: 99.8600782018

现在让我们看看如何使用一个叫做函数的 MySQL 特性在数据库中进行同样的操作。在本例中,我对前面显示的 Python 代码使用了相同的数据,将其存储在一个简单的表中。正如您将看到的,对数据库中的数据进行聚合操作很容易。

清单 4-14 显示了特殊函数的用法,您可以在SELECT语句中使用AVGMINMAX函数。这些函数完全符合您的预期。这样的功能还有很多。有关可用函数的完整列表,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/5.7/en/func-op-summary-ref.html )。

Listing 4-14.Aggregate Calculations (SQL)

mysql> SELECT AVG(temperature), MIN(temperature), MAX(temperature) FROM aggregation_test;

+------------------+-------------------+-------------------+

| AVG(temperature) | MIN(temperature)  | MAX(temperature)  |

+------------------+-------------------+-------------------+

| 94.0704303741455 | 90.27742767333984 | 99.86007690429688 |

+------------------+-------------------+-------------------+

1 row in set (0.00 sec)

请注意,我们使用了一个命令来获取这些数据。还有什么比这更简单的呢?另请注意,最终值与 Python 示例输出略有不同。这是因为浮点值的舍入不完美,从更高精度的小数中可以看出这一点。例如,如果我们只想显示两个十进制值,Python 和数据库的结果将是相同的:分别是 94.07、90.28 和 99.86。

显然,数据库中的语句更容易阅读和使用。您所要做的就是用正确的函数形成查询。在接下来的两章中,你会看到更多关于这些陈述的内容。

摘要

数据转换不仅仅是将一种数据类型转换成另一种数据类型。正如您在本章中看到的,在决定如何进行转换之前,您需要考虑许多事情。我们不仅需要考虑我们正在观察的东西,还需要考虑我们期望学到的东西,以及如何解释这些数据以获得进一步的知识。我们需要考虑原始数据以及使数据更具信息性和相关性所需的任何转换。

解释数据之外的另一个考虑是如何注释数据。过多的注释会掩盖我们想要学习的东西,过少的注释会产生错误的解释或导致错过获取知识的机会。类似地,如果我们需要组合来自多个传感器或多个数据收集器的数据,或者需要对数据执行统计或计数操作,那么数据的聚合可能很重要。

在本章中,我们学习了在考虑数据时要问的几个实际问题,以及在 Arduino 和 Python 代码中注释和聚合的几个例子。本章还介绍了在数据库中存储 IOT 数据的注意事项。事实上,我们看到许多示例注释和聚合在数据库中比在代码中更容易实现。

在下一章,我们将深入探讨 MySQL 的数据库存储。您将了解什么是 MySQL,如何安装它,以及如何开始为您的 IOT 数据构建数据库。

Footnotes 1

数据库存储前和数据库存储期间。

2

并非总是如此,但这是我注意到的一种趋势。

3

我说可能是相同的大小,因为可变的大小取决于处理器和平台。在考虑数据类型的内存分配之前,最好查阅您的平台的文档。

4

我自己的经验和训练迫使我保持严格的类型坚持,但我也承认在某些情况下这可能是必要的。即超越琐碎。

5

叫做小端。参见 https://en.wikipedia.org/wiki/Endianness

6

或者更可能是几英寸到几厘米。

7

我避免了古怪但过度使用的温度例子。不客气

8

在这种情况下,我使用一种简单的校准方法,将值移动一个固定的增量。这种性质的校准很少。校准可能基于线性标度(随着数值的增加,误差变得更大或更小),或者可能需要更复杂的公式来校正数值。

9

例如存储事件、存储过程等等。

10

是的,你可以用 Arduino 做到这一点!

五、MySQL 优先

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​5) contains supplementary material, which is available to authorized users.

因此,您正在规划您的 IOT 解决方案,并决定在您的解决方案中内置一个数据库服务器。也许您以前从未使用过数据库系统,或者您作为用户使用过一个数据库系统,但从未需要从头开始建立一个。或者,您可能已经决定发现数据库系统有什么大惊小怪的。无论是哪种情况,你都有了入门所需的核心知识 1 :你知道你要存储什么,数据是什么样子的。

回想一下,我们讨论并看到了一些 IOT 数据的示例,以及在 IOT 解决方案中如何最好地存储这些数据。回想一下,我们想要存储的每种类型的数据都有其利弊。您还了解了如何扩充数据,使其在您的 IOT 解决方案中更有用。如您所见,并不总是数据的发起者(传感器节点或平台)需要进行这种增强。正如您所了解的,拥有更多计算资源的数据聚合器更适合这种操作。但是,您还会看到数据库服务器(有时称为数据节点,因为您可以有多个数据节点)的功能更强大,它可以自动执行数据聚合和注释。

在本章中,您还将看到如何将这些技术应用到实践中,将数据存储到数据库中。更具体地说,您将学习如何使用 MySQL,并在 IOT 解决方案中利用这些知识。我们从一个简短的讨论开始,讨论如何获得 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?

MySQL 在您的系统上作为后台进程运行(或者作为前台进程,如果您从命令行启动它)。像大多数数据库系统一样,MySQL 支持结构化查询语言(SQL)。您可以使用 SQL 创建数据库和对象(使用数据定义语言[DDL]),写入或更改数据(使用数据操作语言[DML]),以及执行各种命令来管理服务器。

要发出这些命令,必须首先连接到数据库服务器。MySQL 提供了一个名为 mysql 3 的客户端应用,使您能够连接到服务器并在其上运行命令。客户端接受 SQL 命令以及一些特定于客户端本身的命令。分号必须终止所有命令。

Tip

要查看客户端中可用命令的列表,请在提示符下键入help并按 Enter 键。

要连接到服务器,必须指定用户帐户和要连接的服务器。如果您连接到同一台机器上的服务器,您可以省略服务器信息(主机和端口),因为这些默认为端口3306上的localhost。使用--user(或-u)选项指定用户。您可以在命令上为用户指定密码,但更安全的做法是指定--password(或-p),并且客户端会提示您输入密码。如果您确实在命令行上指定了密码,您将会得到一个警告提示,鼓励您不要这样做。

在没有--host(或-h)和--port选项的同一台机器上使用 mysql 客户端不使用网络连接。如果您想要使用网络连接进行连接,或者想要使用不同的端口进行连接,则必须使用环回地址。例如,要连接到同一台机器上端口 13001 上运行的服务器,使用命令mysql -uroot -p –h127.0.0.1 --port=13001

清单 5-1 展示了几个使用 mysql 客户端的 SQL 命令的例子。

Listing 5-1.Commands Using the mysql Client

$ mysql -uroot -p

Enter password:

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 4

Server version: 5.7.8-rc-log MySQL Community Server (GPL)

Copyright (c) 2000, 2015, 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> CREATE DATABASE plant_monitoring;

Query OK, 1 row affected (0.01 sec)

mysql> CREATE TABLE plant_monitoring.plants (plant_name char(50), sensor_value int, sensor_event timestamp);

Query OK, 0 rows affected (0.06 sec)

mysql> INSERT INTO plant_monitoring.plants VALUES ('living room', 23, NULL);

Query OK, 1 row affected (0.04 sec)

mysql> SELECT * FROM plant_monitor.plants;

+-------------+--------------+---------------------+

| plant_name  | sensor_value | sensor_event        |

+-------------+--------------+---------------------+

| living room |           23 | 2015-09-22 19:54:01 |

+-------------+--------------+---------------------+

1 row in set (0.01 sec)

mysql> SET @@global.server_id = 111;

Query OK, 0 rows affected (0.00 sec)

mysql>

在本例中,您看到 DML 以CREATE DATABASECREATE TABLE语句的形式出现,DDL 以INSERTSELECT语句的形式出现,还有一个简单的管理命令来设置全局服务器变量。接下来,您将看到创建一个数据库和一个表来存储数据,在表中添加一行,最后检索表中的数据。注意我是如何用大写字母表示 SQL 命令关键字的。这是一种常见的做法,有助于使 SQL 命令更容易阅读,也更容易找到用户提供的选项或数据。

您可以通过键入命令quit退出 MySQL 客户端。在 Linux 和 Unix 系统上,您可以按 Ctrl+D 退出客户端。

MySQL 中有很多可用的命令。幸运的是,你只需要掌握几个比较常见的。以下是您最常使用的命令。<>中包含的部分表示用户提供的命令组件,而[...]表示需要额外的选项。

  • CREATE DATABASE <database_name>:创建数据库
  • USE <database>:设置默认数据库(不是 SQL 命令)
  • CREATE TABLE <table_name> [...]:创建一个表格或结构来存储数据
  • INSERT INTO <table_name> [...]:向表格中添加数据
  • UPDATE [...]:更改特定行的一个或多个值
  • DELETE FROM <table_name> [...]:从表格中删除数据
  • SELECT [...]:从表格中检索数据(行)
  • SHOW [...]:显示对象列表

虽然这个列表只是一个简短的介绍,并不像一个完整的语法指南,但有一个很好的在线参考手册,它非常详细地解释了每个命令(以及更多)。当你对 MySQL 有任何疑问时,你应该参考在线参考手册。你可以在 http://dev.mysql.com/doc/ 找到它。

显示的一个更有趣的命令允许您查看对象列表。例如,您可以看到带有SHOW DATABASES的数据库,带有SHOW TABLES的表列表(一旦您更改为数据库),甚至带有SHOW GRANTS的用户权限。我发现自己经常使用这些命令。

Tip

如果使用 mysql 客户端,必须用分号(;)或\G结束每个命令。

如果您认为 MySQL 不仅仅是几个简单的命令,那么您绝对是正确的。尽管 MySQL 易于使用且启动时间快,但它是一个成熟的关系数据库管理系统(RDBMS)。比你在这里看到的要多得多。有关 MySQL 的更多信息,包括所有高级特性,请参见参考手册。

Mysql—What Does It Mean?

MySQL 这个名字是一个专有名称和一个缩写的组合。SQL 是结构化查询语言。“我的部分”不是所有格形式——它是一个名称。在这种情况下,My 是创始人女儿的名字。至于发音,MySQL 专家发音为“My-S-Q-L”而不是“my sequel”

如何获取和安装 MySQL

MySQL 服务器可用于多种平台,包括大多数 Linux 和 Unix 平台、Mac OS X 和 Windows。要下载 MySQL 服务器,请访问 http://dev.mysql.com/downloads/ ,点击社区,然后点击 MySQL 社区服务器。这是 MySQL 的 GPLv2 许可证。 4 页面会自动检测你的操作系统。如果您想为另一个平台下载,可以从下拉列表中选择。

下载页面将列出几个可供下载的文件。根据您的平台,您可能会看到几个选项,包括压缩文件、源代码和安装包。大多数人会选择在笔记本电脑或台式电脑上安装安装包。图 5-1 显示了 Debian 和 Ubuntu 平台的 APT 库的一个例子。

A978-1-4842-1293-6_5_Fig1_HTML.jpg

图 5-1。

Download page for Ubuntu Linux

最受欢迎的平台之一是微软视窗系统。Oracle 为 Windows 提供了一个名为 Windows Installer 的特殊安装包。这个包包括社区许可下所有可用的 MySQL 产品,包括 MySQL 服务器、工作台、实用程序、Fabric 和所有可用的连接器(用于连接 MySQL 的程序库)。这使得在 Windows 上安装成为一站式、一次安装的事情。图 5-2 显示了 Windows installer 的下载页面。以下几段演示如何在 Windows 10 上安装 MySQL。你将在下一章看到如何在像 Raspberry Pi 这样的单板计算机上安装 MySQL。

A978-1-4842-1293-6_5_Fig2_HTML.jpg

图 5-2。

Download page for Windows Installer

首先选择与您的 Windows 版本匹配的 Windows Installer 32 位或 64 位安装包。下载文件后,单击文件开始安装。请注意,一些浏览器(如新的 Edge 浏览器)可能会询问您是否要启动安装。您可能需要回复允许安装的对话框。

第一步是同意许可。图 5-3 显示了安装对话框的许可协议面板。

A978-1-4842-1293-6_5_Fig3_HTML.jpg

图 5-3。

License agreement

显示的许可证是社区版的 GPLv2 许可证。阅读许可 5 并同意后,选择“我接受许可条款”复选框,然后单击“下一步”。

下一个面板显示设置或安装类型。大多数人会选择 developer 选项,因为它会安装所有的 MySQL 组件和应用,并设置在本地机器上运行 MySQL 的默认设置。您可以选择不同的选项,并在右侧的文本中了解更多信息。图 5-4 显示了设置类型面板。做出选择后,单击下一步。

A978-1-4842-1293-6_5_Fig4_HTML.jpg

图 5-4。

Setup type

下一个面板将检查任何必需的组件。例如,如果您在不包含 Python 或 Visual Studio 的机器上安装,您将得到一个警告,如图 5-5 所示。要继续,您必须解决每个问题。也就是说,在解决需求之前,按钮将不可用。解决这些问题后,单击下一步。

A978-1-4842-1293-6_5_Fig5_HTML.jpg

图 5-5。

Check Requirements page

下一个面板将显示所有可供安装的软件包。如果您不想安装其中的一个或多个,您可以单击每个并选择不安装它们。图 5-6 显示了所有包装都标记为待安装的示例。一旦您对安装选项感到满意,请单击执行。

A978-1-4842-1293-6_5_Fig6_HTML.jpg

图 5-6。

Preparing for installation

面板会随着每次安装的进度进行更新,如图 5-7 所示。

A978-1-4842-1293-6_5_Fig7_HTML.jpg

图 5-7。

Installing packages

随着安装的进行,每个软件包旁边会出现不同的符号。在某些情况下,安装程序可能会下载额外的软件包。你可以在图 5-7 中看到这一点。注意,下载了 Connector/Python 2.1.2。您可以单击“显示详细信息”按钮来查看安装的更多详细信息。

一旦所有选中的包都被安装,你会看到配置面板,如图 5-8 所示。该面板根据您选择的软件包显示不同配置选项的列表。如果您选择安装所有内容,您将看到一个类似下图的面板。单击下一步继续。

A978-1-4842-1293-6_5_Fig8_HTML.jpg

图 5-8。

Product configuration

每种产品的配置过程都不同。比如配置 MySQL 服务器,从联网开始有几个步骤,如图 5-9 所示。此面板允许您选择配置类型(服务器如何启动和运行)以及网络细节。在此页面上,您可以选择服务器将监听连接的 TCP/IP 端口。如果要配置其他参数,请选择“显示高级选项”复选框。做出选择后,单击“下一步”进入下一步。

A978-1-4842-1293-6_5_Fig9_HTML.jpg

图 5-9。

Type and Networking page

下一个面板是账户和角色面板,如图 5-10 所示。此面板允许您设置初始用户帐户以及 root 密码。强烈建议您为 root 帐户选择一个强密码。您也可以通过点按“添加用户”按钮来设置具有不同角色的附加用户帐户。选择设置后,单击“下一步”进入下一步。

A978-1-4842-1293-6_5_Fig10_HTML.jpg

图 5-10。

Accounts and Roles page

下一个面板允许您控制 MySQL 在 Windows 机器上的启动方式。图 5-11 显示了细节。请注意,您可以将服务器配置为作为 Windows 服务启动,在启动时自动启动 MySQL(或不启动),以及服务器将使用什么类型的帐户。我强烈建议保留默认设置,除非您知道如何设置帐户来运行服务。单击“下一步”进入下一步。

A978-1-4842-1293-6_5_Fig11_HTML.jpg

图 5-11。

Windows Service page

如果您在第一个配置面板中检查了高级设置,您将看到高级选项面板,如图 5-12 所示。此面板允许您打开常规日志(用于记录服务器反馈语句)、查询日志(用于记录所有查询)和二进制日志(用于复制和备份)。如果你计划在复制设置中使用服务器(我将在第 7 章的中讨论),你应该打开二进制日志。在复制设置中,服务器 ID 在所有服务器中必须是唯一的,您可以在此面板上进行设置。选择设置后,单击下一步。

A978-1-4842-1293-6_5_Fig12_HTML.jpg

图 5-12。

Advanced Options panel

5-13 所示的下一个面板显示了配置的进度。

A978-1-4842-1293-6_5_Fig13_HTML.jpg

图 5-13。

Server configuration execution

如果您选择将 MySQL 作为 Windows 服务启动,您将会看到列出的第二组语句,如图 5-14 所示。最后,如果您选择安装示例和示例数据库,您将看到另一个对话框面板,显示安装示例数据库的进度。所有步骤完成后,单击完成。

A978-1-4842-1293-6_5_Fig14_HTML.jpg

图 5-14。

Configuring examples

除了作为一站式安装机制之外,我最喜欢 Windows Installer 的一点是能够再次使用安装程序进行更改。也就是说,您可以在另一个时间运行安装程序来安装不同的软件包,甚至删除您不再需要的软件包。这是在 Windows 上安装和配置 MySQL 的一种便捷方式。

Note

其他平台的安装程序采用了与 Windows Installer 类似的机制。例如,大多数安装包中都有配置步骤。

现在你知道了如何设置 MySQL,让我们来讨论如何使用 MySQL 存储和检索数据。

如何存储和检索数据

既然您已经知道了 MySQL 是什么以及它是如何使用的,那么在开始构建您的第一个数据库服务器之前,您需要对 RDBMSs 和 MySQL 有更多的了解。本节讨论 MySQL 如何存储数据(以及存储在哪里),它如何与其他系统通信,以及管理新的 MySQL 服务器所需的一些基本管理任务。

Note

我将在第 6 章中向你展示如何在 Raspberry Pi 和类似的板上安装 MySQL。

What Is A Relational Database Management System?

RDBMS 是一种基于数据关系模型的数据存储和检索服务,由 E. F. Codd 于 1970 年提出。这些系统是结构化数据的标准存储机制。大量的研究致力于改进 Codd 提出的基本模型,正如 C. J. Date 在《数据库关系模型:回顾和分析》中所讨论的那样。理论和实践的这种演变最好地记录在第三个宣言中。 7

关系模型是存储库(数据库)的直观概念,可以通过使用一种称为查询语言的机制来检索、更新和插入数据,从而方便地查询存储库。许多供应商已经实现了关系模型,因为它具有完善的系统理论、坚实的数学基础和简单的结构。最常用的查询机制是 SQL,它类似于自然语言。虽然关系模型中不包括 SQL,但它提供了关系模型在 RDBMSs 中的实际应用的一个组成部分。

数据表示为关于某个事件或实体的相关信息(属性或列,有时称为字段)。属性值集以元组的形式形成(有时称为记录或行)。元组存储在具有相同属性集的表中。然后,表可以通过键、属性和元组的约束与其他表相关。

表可以有称为索引的列的特殊映射,允许您以特定的顺序读取数据。索引对于快速检索与索引列的值相匹配的行也很有用。

MySQL 存储数据的方式和位置

MySQL 数据库系统通过一种有趣的编程隔离机制存储数据,这种机制称为存储引擎,由处理程序接口控制。处理程序接口允许在 MySQL 服务器中使用可互换的存储组件,以便解析器、优化器和各种组件可以使用公共机制在磁盘上存储数据时进行交互。这也称为可插拔存储引擎。

Note

MySQL 支持几种存储引擎。默认情况下,大多数都被设计为将数据写入磁盘。但是,内存存储引擎将数据存储在内存中,但不是永久性的。也就是说,当计算机重新启动时,数据会丢失。您可以使用内存存储引擎进行快速查找表。事实上,一种优化技术是在启动时使用内存存储引擎创建查找表的副本。

这对你意味着什么?这意味着您可以选择不同的数据存储机制。您可以在下面的代码示例所示的表CREATE语句中指定存储引擎。请注意命令中的最后一行:这是如何指定存储引擎的。去掉这个子句会导致 MySQL 使用默认的存储引擎。对于本书中的例子,MySQL 5.5 默认使用 MyISAM 存储引擎。

Tip

在 MySQL 版本 5.6 中,默认存储引擎从 MyISAM 更改为 InnoDB。

CREATE DATABASE bvm;

CREATE TABLE bvm.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 上存在哪些存储引擎?您可以通过发出SHOW STORAGE ENGINES命令来发现支持哪些存储引擎,如清单 5-2 所示。如你所见,有很多可供选择。我将介绍一些可能与规划 IOT 解决方案相关的内容。

Note

下面几节展示了如何在典型的类 Linux(实际上是类 Unix)平台上使用 MySQL。我发现大多数 IOT 解决方案将使用这些平台的形式,而不是 Windows 10,但这可能会在未来发生变化。目前,我主要在这些平台上探索 MySQL,而不是 Windows。然而,所示的许多示例可以在 Windows 上执行,尽管使用的是不同的命令集。

Listing 5-2.Available Storage Engines

mysql> SHOW STORAGE ENGINES \G

*************************** 1\. row ***************************

Engine: FEDERATED

Support: NO

Comment: Federated MySQL storage engine

Transactions: NULL

XA: NULL

Savepoints: NULL

*************************** 2\. row ***************************

Engine: MRG_MYISAM

Support: YES

Comment: Collection of identical MyISAM tables

Transactions: NO

XA: NO

Savepoints: NO

*************************** 3\. row ***************************

Engine: CSV

Support: YES

Comment: CSV storage engine

Transactions: NO

XA: NO

Savepoints: NO

*************************** 4\. row ***************************

Engine: BLACKHOLE

Support: YES

Comment: /dev/null storage engine (anything you write to it disappears)

Transactions: NO

XA: NO

Savepoints: NO

*************************** 5\. row ***************************

Engine: MyISAM

Support: YES

Comment: MyISAM storage engine

Transactions: NO

XA: NO

Savepoints: NO

*************************** 6\. row ***************************

Engine: InnoDB

Support: DEFAULT

Comment: Supports transactions, row-level locking, and foreign keys

Transactions: YES

XA: YES

Savepoints: YES

*************************** 7\. row ***************************

Engine: ARCHIVE

Support: YES

Comment: Archive storage engine

Transactions: NO

XA: NO

Savepoints: NO

*************************** 8\. row ***************************

Engine: MEMORY

Support: YES

Comment: Hash based, stored in memory, useful for temporary tables

Transactions: NO

XA: NO

Savepoints: NO

*************************** 9\. row ***************************

Engine: PERFORMANCE_SCHEMA

Support: YES

Comment: Performance Schema

Transactions: NO

XA: NO

Savepoints: NO

9 rows in set (0.00 sec)

mysql>

从 5.6 版本开始,MySQL 默认使用 InnoDB 存储引擎。以前的版本默认使用 MyISAM。InnoDB 是一个完全事务性的、ACID 8 存储引擎。事务是一批语句,在将任何更改写入磁盘之前,这些语句必须全部成功。典型的例子是银行转账。如果您考虑一个需要从一个帐户中扣除一笔金额,然后将该金额存入另一个帐户以完成资金转移的系统,您不会希望第一个帐户成功而第二个帐户失败,或者相反!

将语句包装在一个事务中可以确保在所有语句都正确无误地完成之前,不会将任何数据写入磁盘。在这种情况下,事务用一个BEGIN语句指定,并以一个COMMIT保存更改或一个ROLLBACK撤销更改结束。InnoDB 将其数据存储在一个文件中(还有一些用于管理索引和事务的附加文件)。

MyISAM 存储引擎针对读取进行了优化。MyISAM 作为默认引擎已经有一段时间了,并且是第一批可用的存储引擎之一。事实上,服务器的很大一部分是专用于支持 MyISAM 的。它与 InnoDB 的不同之处在于,它不支持事务,并且以索引顺序访问方法格式存储数据。这意味着它支持快速索引。如果您不需要事务,并且希望能够移动或备份单个表,那么您应该选择 MyISAM 而不是 InnoDB。

您可能需要考虑的另一个存储引擎是归档,尤其是对于传感器网络。该引擎不支持删除(但是您可以删除整个表),并且针对磁盘上的最小存储进行了优化。很明显,如果你在一个像 Raspberry Pi 这样的小系统上运行 MySQL,小系统总是更好!无法删除数据可能会限制更高级的应用,但大多数传感器网络只是存储数据,很少删除数据。在这种情况下,您可以考虑使用归档存储引擎。

还有 CSV 存储引擎(其中 CSV 代表逗号分隔值)。此存储引擎创建文本文件,以纯文本形式存储数据,其他应用(如电子表格应用)可以读取这些数据。如果您将传感器数据用于统计分析,CSV 存储引擎可能会使获取数据的过程更容易。

那么,这些数据都在哪里呢?如果您查询 MySQL 服务器并发出命令SHOW VARIABLES LIKE "datadir";,您会看到所有存储引擎用来存储数据的磁盘位置的路径。对于 InnoDB,这是位于数据目录中的磁盘上的一个文件。InnoDB 也创建一些管理文件,但是数据存储在单个文件中。对于除了NDBMEMORY之外的大多数其他存储引擎,表的数据存储在 data 目录下的一个以数据库名称命名的文件夹中。清单 5-3 展示了一个来自 Mac OS X 机器的例子。您可能需要在自己的机器上使用不同的路径。

Listing 5-3.Finding Where Your Data Is Located

mysql> SHOW VARIABLES LIKE 'datadir'

+---------------+------------------------+

| Variable_name | Value                  |

+---------------+------------------------+

| datadir       | /usr/local/mysql/data/ |

+---------------+------------------------+

1 row in set (0.00 sec)

mysql> quit;

bye

$ sudo ls -lsa /usr/local/mysql/data

rwxr-x---    58 _mysql  wheel       1972 Feb  6 15:05 .

drwxr-xr-x   17 root    wheel        578 Jan 20 16:38 ..

-rw-rw----    1 _mysql  wheel          0 Feb  6 15:04 Chucks-iMac.local.err

-rw-rw----    1 _mysql  wheel          5 Feb  6 15:00 Chucks-iMac.local.pid

drwx------    6 _mysql  wheel        204 Oct 17 15:16 bvm

-rw-rw----    1 _mysql  wheel    5242880 Feb  6 15:00 ib_logfile0

-rw-rw----    1 _mysql  wheel    5242880 Feb  6 15:00 ib_logfile1

-rw-rw----    1 _mysql  wheel  815792128 Feb  1 17:16 ibdata1

-rw-rw----    1 _mysql  wheel   52428800 Feb  1 17:16 ibdata2

drwxr-x---   77 _mysql  wheel       2618 Jan  8 15:24 mysql

drwx------   38 _mysql  wheel       1292 Nov 27 08:46 sakila

drwx------  192 _mysql  wheel       6528 Oct 22 12:17 test

drwx------    6 _mysql  wheel        204 Dec 18 17:05 world_innodb

$ sudo ls -lsa /usr/local/mysql/data/bvm

drwx------   6 _mysql  wheel   204 Oct 17 15:16 .

drwxr-x---  58 _mysql  wheel  1972 Feb  6 15:05 ..

-rw-rw----   1 _mysql  wheel  5056 Oct 17 15:24 books.MYD

-rw-rw----   1 _mysql  wheel  1024 Oct 17 15:25 books.MYI

-rw-rw----   1 _mysql  wheel  8780 Oct 17 15:16 books.frm

-rw-rw----   1 _mysql  wheel    65 Oct 17 15:15 db.opt

该示例首先向数据库服务器查询数据目录的位置(它位于该计算机上受保护的文件夹中)。如果您发出一个列表命令,您可以看到由前缀ibibd标识的 InnoDB 文件。您还可以看到许多目录,所有这些目录都是该服务器上的数据库。之后是一个数据库文件夹的列表。注意扩展名为.MY?的文件:这些是 MyISAM 文件(数据和索引)。.frm文件是服务器创建和维护的配置文件。

Tip

如果您想通过复制文件将数据从一个服务器复制到另一个服务器,请确保也复制了.frm文件!这对于 MyISAM 和 Archive 来说很容易,但对于 InnoDB 来说要困难得多。在 InnoDB 的情况下,你必须复制所有的数据库文件夹和 InnoDB 文件,以确保你得到所有的东西。

虽然在您的 IOT 解决方案中,您不太可能需要一个数据库节点的事务存储引擎,比如运行 MySQL Server 的 Raspberry Pi,但是 MySQL 5.6 有一个,并且默认情况下是打开的。更有可能的情况是对表使用 MyISAM 或归档引擎。

有关存储引擎及其选择和特性的更多信息,请参见在线 MySQL 参考手册“存储引擎”( http://dev.mysql.com/doc/ )一节。

MySQL 配置文件

MySQL 服务器可以使用与配置 Raspberry Pi 类似的配置文件进行配置。在 Windows 上,MySQL 配置文件位于安装文件夹中,命名为my.ini。在其他系统上,它位于/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 参考手册。当您在 Raspberry Pi 上设置 MySQL 时,您将更改这个文件。

如何在 Windows 上启动、停止和重启 MySQL

在计算机上使用数据库和配置 MySQL 时,您可能需要控制 MySQL 服务器的启动和关闭。安装 MySQL 的默认模式是在启动时自动启动,在关机时自动停止,但是您可能希望更改这一模式,或者您可能需要在更改参数后停止并启动服务器。此外,当您更改配置文件时,需要重新启动服务器才能看到更改的效果。

您可以使用通知程序托盘应用或通过 Windows 服务控制面板来启动、停止和重启 MySQL 服务器。只需选择 MySQL 服务,右键单击即可停止或启动该服务。这将执行一个受控的关机和启动,如果你需要这样做。

创建用户和授予访问权限

在使用 MySQL 之前,您需要了解另外两个管理操作:创建用户帐户和授予数据库访问权限。MySQL 可以用GRANT语句执行这两项操作,如果用户不存在,它会自动创建一个用户。但是更迂腐的方法是首先发出一个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 用户密码:

SET PASSWORD FOR sensor1@"%" = PASSWORD("secret");

对于主机使用通配符%也要小心。虽然创建单个用户并让用户从任何主机访问数据库服务器变得更加容易,但这也使得恶意用户更容易访问您的服务器(一旦他们发现了密码)。

另一个考虑是连接性。与 Raspberry Pi 一样,如果您将一个数据库连接到您的网络,而该网络又连接到 Internet,那么您的网络或 Internet 上的其他用户就有可能访问该数据库。不要让他们轻易得逞——更改您的 root 用户密码,并为您的应用创建用户。

第二个命令允许访问数据库。您可以授予用户许多权限。该示例显示了您最有可能向传感器网络数据库用户提供的集合:读取(SELECT)、添加数据(INSERT)和更改数据(UPDATE)。有关安全性和帐户访问权限的更多信息,请参见在线参考手册。

该命令还指定要授予权限的数据库和对象。因此,可以给用户一些表的读(SELECT)权限,给另一些表的写(INSERTUPDATE)权限。这个例子让用户可以访问room_temp数据库中的所有对象(表、视图等等)。

如上所述,您可以将这两个命令合并成一个命令。你可能会在文献中更经常地看到这种形式。下面显示了组合语法。在这种情况下,您需要做的就是将IDENTIFIED BY子句添加到GRANT语句中。酷!

GRANT SELECT, INSERT, UPDATE ON room_temp. * TO  'sensor1'@'%' IDENTIFIED BY 'secret'

常见的 MySQL 命令和概念

学习和掌握数据库系统需要训练、经验和极大的毅力。精通所需的主要知识是如何使用常见的 SQL 命令和概念。本节通过介绍最常见的 MySQL 命令和概念来完成 MySQL 入门。

Note

本节介绍高级命令和概念,而不是重复引用《参考手册》、 9 。如果您决定使用任何命令或概念,请参考在线参考手册,了解更多详细信息、完整的命令语法和其他示例。

MySQL 命令

本节回顾了最常见的 SQL 和 MySQL 特有的命令,您需要了解这些命令,以便充分利用您的 IOT 数据库。虽然您已经看到了其中的一些功能,但是本节提供了一些额外的信息来帮助您使用它们。

Note

用户提供的变量的大小写敏感性(例如,last_nameLast_Name)在不同的平台上是不一致的。例如,Windows 上的区分大小写行为与 Mac OS X 上的不同。MySQL 遵循该平台的区分大小写策略。查看您的平台的在线参考手册,了解区分大小写如何影响用户提供的变量。

创建数据库和表

您需要学习和掌握的最基本的命令是CREATE DATABASECREATE TABLE命令。回想一下,MySQL 之类的数据库服务器允许您创建任意数量的数据库,您可以用逻辑方式添加表和存储数据。

要创建一个数据库,使用CREATE DATABASE后跟一个数据库名称。如果您正在使用 MySQL 客户端,您必须使用USE命令切换到特定的数据库。客户端焦点是在启动时(在命令行上)或通过USE命令指定的最新数据库。您可以通过首先引用数据库名称来覆盖它。例如,SELECT * FROM db1.table1将执行,而不管默认的数据库设置。但是,省略数据库名称会导致 mysql 客户端使用默认数据库。下面显示了创建和更改数据库焦点的两个命令:

mysql> CREATE DATABASE plant_monitoring;

mysql> USE plant_monitoring;

Tip

回想一下,如果您想查看服务器上的所有数据库,可以使用SHOW DATABASES命令。

创建表格需要 yes,CREATE TABLE命令。该命令有许多选项,不仅允许您指定列及其数据类型,还允许您指定附加选项,如索引、外键等。还可以使用CREATE INDEX命令创建一个索引(参见下面的代码)。以下显示了如何创建一个简单的表格来存储工厂传感器数据。

CREATE TABLE plant_monitoring.plants (

``plant_name char(30) NOT NULL,

``sensor_value float DEFAULT NULL,

``sensor_event timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

``sensor_level char(5) DEFAULT NULL,

PRIMARY KEY plant_name (plant_name)

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

注意这里我指定了表名(plants)和四列(plant_namesensor_valuesensor_eventsensor_level)。我使用了几种数据类型。对于plant_name,我使用了一个最多包含 30 个字符的字符字段,一个浮点数据类型用于sensor_value,一个时间戳值用于sensor_event,另一个字符字段用于包含 5 个字符的sensor_level

TIMESTAMP数据类型在 IOT 解决方案或任何您想要记录事件或行动的日期和时间的时候特别有用。例如,知道何时读取传感器值通常是有帮助的。通过向表中添加一个TIMESTAMP列,您不需要计算、读取或格式化传感器甚至聚合节点上的日期和时间。

还要注意,我指定将sensor_name列定义为一个键,这将创建一个索引。在这种情况下,它也是主键。PRIMARY KEY短语告诉服务器确保表中存在且只有一行匹配列的值。通过重复关键字,可以指定几个要在主键中使用的列。注意,所有主键列都不允许空值(NOT NULL)。

如果您不能确定唯一标识一行的一组列(并且您想要这样的行为——有些人喜欢没有这种限制的表,但是一个好的 DBA 不会),那么您可以为 integer 字段使用一个称为AUTO INCREMENT的人工数据类型选项。当用于某一列(必须是第一列)时,服务器会为插入的每一行自动增加该值。这样,它就创建了一个默认主键。有关自动递增列的更多信息,请参见联机参考手册。

Tip

最佳实践表明,在某些情况下,在字符字段上使用主键并不是最佳选择,例如表中的每一列都有很大的值或者有许多唯一值。这可能会降低搜索和索引的速度。在这种情况下,您可以使用 auto increment 字段来人工添加一个更小的主键(但有点神秘)。

可用的数据类型比上一个示例中显示的多得多。您应该查阅联机参考手册,以获得数据类型的完整列表。请参见“数据类型”一节如果你想知道一个表格的布局或“模式”,使用SHOW CREATE TABL E 命令。

像数据库一样,您也可以使用SHOW TABLES命令获得数据库中所有表的列表。

获得结果

您需要知道的最常用的基本命令是从表中返回数据的命令(也称为结果集或行)。为此,您可以使用SELECT语句。这个 SQL 语句是数据库系统的核心。所有对数据的查询都将使用该命令执行。 10

SELECT语句允许您指定想要从数据中选择哪些列。该列表作为语句的第一部分出现。第二部分是FROM子句,它指定了要从中检索行的表。

Note

FROM子句可以用来用JOIN操作符连接表。在后面的小节中,您将看到一个简单的连接示例。

指定列的顺序决定了结果集中的显示顺序。如果你想要所有的列,使用星号(*)代替。清单 5-4 展示了生成相同结果集的三条语句。也就是说,在每个的输出中将显示相同的行。事实上,为了简单起见,我使用了一个只有四行的表。

Listing 5-4.Example SELECT Statements

mysql> SELECT plant_name, sensor_value, sensor_event, sensor_level FROM plant_monitoring.plants;

+------------------------+--------------+---------------------+--------------+

| plant_name             | sensor_value | sensor_event        | sensor_level |

+------------------------+--------------+---------------------+--------------+

| fern in den            |       0.2319 | 2015-09-23 21:04:35 | NULL         |

| fern on deck           |         0.43 | 2015-09-23 21:11:45 | NULL         |

| flowers in bedroom1    |        0.301 | 2015-09-23 21:11:45 | NULL         |

| weird plant in kitchen |        0.677 | 2015-09-23 21:11:45 | NULL         |

+------------------------+--------------+---------------------+--------------+

4 rows in set (0.00 sec)

mysql> SELECT * FROM plant_monitoring.plants;

+------------------------+--------------+---------------------+--------------+

| plant_name             | sensor_value | sensor_event        | sensor_level |

+------------------------+--------------+---------------------+--------------+

| fern in den            |       0.2319 | 2015-09-23 21:04:35 | NULL         |

| fern on deck           |         0.43 | 2015-09-23 21:11:45 | NULL         |

| flowers in bedroom1    |        0.301 | 2015-09-23 21:11:45 | NULL         |

| weird plant in kitchen |        0.677 | 2015-09-23 21:11:45 | NULL         |

+------------------------+--------------+---------------------+--------------+

4 rows in set (0.00 sec)

mysql> SELECT sensor_value, plant_name, sensor_level, sensor_event FROM plant_monitoring.plants;

+--------------+------------------------+--------------+---------------------+

| sensor_value | plant_name             | sensor_level | sensor_event        |

+--------------+------------------------+--------------+---------------------+

|       0.2319 | fern in den            | NULL         | 2015-09-23 21:04:35 |

|         0.43 | fern on deck           | NULL         | 2015-09-23 21:11:45 |

|        0.301 | flowers in bedroom1    | NULL         | 2015-09-23 21:11:45 |

|        0.677 | weird plant in kitchen | NULL         | 2015-09-23 21:11:45 |

+--------------+------------------------+--------------+---------------------+

4 rows in set (0.00 sec)

请注意,前两条语句以相同的顺序生成相同的行和相同的列,但是第三条语句虽然生成相同的行,但以不同的顺序显示列。

您还可以使用列列表中的函数来执行计算和类似操作。一个特殊的例子是使用COUNT()函数来确定结果集中的行数,如下所示。关于 MySQL 提供的函数的更多例子,请参阅在线参考手册。

SELECT COUNT(*) FROM plant_monitoring.plants;

SELECT语句中的下一个子句是WHERE子句。您可以在这里指定用于限制结果集中行数的条件。也就是说,只有那些符合条件的行。这些条件基于列,可能相当复杂。也就是说,您可以基于计算、连接结果等来指定条件。但是为了回答一个问题,大多数条件将是一列或多列上的简单等式或不等式。例如,假设您想要查看传感器读数小于 0.40 的植物。在这种情况下,我们发出以下查询并接收结果。注意,我只指定了两列:工厂名称和从传感器读取的值。

mysql> SELECT plant_name, sensor_value FROM plant_monitoring.plants WHERE sensor_value < 0.40;

+---------------------+--------------+

| plant_name          | sensor_value |

+---------------------+--------------+

| fern in den         |       0.2319 |

| flowers in bedroom1 |        0.301 |

+---------------------+--------------+

2 rows in set (0.01 sec)

您还可以使用其他子句,包括用于对行进行分组以进行聚合或计数的GROUP BY子句,以及用于对结果集进行排序的ORDER BY子句。让我们从聚合开始,快速地看一下每一个。

假设您想要计算每个传感器在表中读取的传感器值的平均值。在这种情况下,我们有一个包含各种传感器随时间变化的传感器读数的表。虽然该示例只包含四行(因此可能没有统计信息),但是该示例非常清楚地展示了聚合的概念,如清单 5-5 所示。请注意,我们收到的只是四个传感器读数的平均值。

Listing 5-5.GROUP BY Example

mysql> SELECT plant_name, sensor_value FROM plant_monitoring.plants WHERE plant_name = 'fern on deck'

+--------------+--------------+

| plant_name   | sensor_value |

+--------------+--------------+

| fern on deck |         0.43 |

| fern on deck |         0.51 |

| fern on deck |        0.477 |

| fern on deck |         0.73 |

+--------------+--------------+

4 rows in set (0.00 sec)

mysql> SELECT plant_name, AVG(sensor_value) as avg_value FROM plant_monitoring.plants WHERE plant_name = 'fern on deck' GROUP BY plant_name;

+--------------+-------------------+

| plant_name   | avg_value         |

+--------------+-------------------+

| fern on deck | 0.536750003695488 |

+--------------+-------------------+

1 row in set (0.00 sec)

注意,我在列列表中指定了 average 函数AVG(),并传入了我想要求平均值的列的名称。MySQL 中有许多这样的函数可以用来执行一些强大的计算。显然,这是数据库服务器中存在多少功率的另一个示例,这将需要网络中典型的轻量级传感器或聚合器节点上的更多资源。

还要注意,我用关键字AS重命名了平均值列。您可以使用它来重命名任何指定的列,这将更改结果集中的名称,如清单所示。

子句的另一个用途是计数。在这种情况下,我们用COUNT()替换了AVG(),得到了与WHERE子句匹配的行数。更具体地说,我们想知道每个工厂存储了多少传感器值。

mysql> SELECT plant_name, COUNT(sensor_value) as num_values FROM plant_monitoring.plants GROUP BY plant_name;

+------------------------+------------+

| plant_name             | num_values |

+------------------------+------------+

| fern in den            |          1 |

| fern on deck           |          4 |

| flowers in bedroom1    |          1 |

| weird plant in kitchen |          1 |

+------------------------+------------+

4 rows in set (0.00 sec)

现在,假设我们想要查看按传感器值排序的结果集的结果。我们将使用为面板上的蕨类植物选择行的相同查询,但是我们使用ORDER BY子句按照传感器值以升序和降序对行进行排序。清单 5-6 显示了每个选项的结果。

Listing 5-6.ORDER BY Examples

mysql> SELECT plant_name, sensor_value FROM plant_monitoring.plants WHERE plant_name = 'fern on deck' ORDER BY sensor_value ASC;

+--------------+--------------+

| plant_name   | sensor_value |

+--------------+--------------+

| fern on deck |         0.43 |

| fern on deck |        0.477 |

| fern on deck |         0.51 |

| fern on deck |         0.73 |

+--------------+--------------+

4 rows in set (0.00 sec)

mysql> SELECT plant_name, sensor_value FROM plant_monitoring.plants WHERE plant_name = 'fern on deck' ORDER BY sensor_value DESC;

+--------------+--------------+

| plant_name   | sensor_value |

+--------------+--------------+

| fern on deck |         0.73 |

| fern on deck |         0.51 |

| fern on deck |        0.477 |

| fern on deck |         0.43 |

+--------------+--------------+

4 rows in set (0.00 sec)

正如我提到的,SELECT陈述比这里显示的要多得多,但是我们在这里看到的会让你走得更远,尤其是在处理大多数中小型 IOT 解决方案的典型数据时。

添加数据

现在您已经创建了一个数据库和表,您将希望向表中加载或插入数据。您可以使用INSERT INTO语句来实现。这里我们指定表格和行的数据。下面是一个简单的例子:

INSERT INTO plant_monitoring.plants (plant_name, sensor_value) VALUES ('fern in den', 0.2319);

在这个例子中,我通过指定名称和值为我的一个工厂插入数据。你想知道其他的柱子呢?在这种情况下,其他列包括一个时间戳列,它将由数据库服务器填充。所有其他列(只有一列)将被设置为NULL,这意味着没有值可用、值缺失、值不为零或值为空。 11

请注意,我在该行的数据之前指定了列。当您希望插入的列数少于表中包含的列数时,这是必要的。更具体地说,关闭列列表意味着您必须为表中的所有列提供数据(或NULL)。此外,列出的列的顺序可以不同于它们在表中的定义顺序。关闭列列表将导致根据列数据在表中的显示方式对其进行排序。 12

您也可以使用逗号分隔的行值列表,使用相同的命令插入几行,如下所示:

INSERT INTO plant_monitoring.plants (plant_name, sensor_value) VALUES ('flowers in bedroom1', 0.301), ('weird plant in kitchen', 0.677), ('fern on deck', 0.430);

这里我用相同的命令插入了几行。请注意,这只是一种简化机制,除了自动提交之外,与发出单独的命令没有什么不同。 十三

更改数据

有时,您需要更改或更新数据。您可能需要更改一列或多列的值,替换多行的值,或者更正数字数据的格式甚至比例。为了更新数据,我们使用UPDATE命令。

您可以更新特定的列,更新一组列,对一列或多列执行计算,等等。我通常不太需要更改 IOT 解决方案中的数据,但有时在传感器读取代码出错或类似数据输入问题的情况下,这可能是必要的。

更有可能的是,您或您的用户想要重命名数据库中的对象。例如,假设我们确定甲板上的植物实际上不是蕨类植物,而是一种外来开花植物。 14 在本例中,我们希望将所有植物名称为“fern on deck”的行改为“flowers on deck”以下命令执行更改:

UPDATE plant_monitoring.plants SET plant_name = 'flowers on deck' WHERE plant_name = 'fern on deck'

注意这里的关键操作符是SET操作符。这告诉数据库为指定的列分配一个新值。您可以在命令中列出多个 set 操作。

注意,我在这里使用了一个WHERE子句来将UPDATE限制到一组特定的行。这就是你在SELECT语句中看到的同一个WHERE子句,它做同样的事情;它允许您指定限制受影响的行的条件。如果不使用WHERE子句,更新将应用于所有行。

Caution

别忘了WHERE条款!发出不带WHERE子句的UPDATE命令将影响表中的所有行!

删除数据

有时,您最终会得到需要删除的表中的数据。也许您使用了测试数据,并希望去除虚假的行,或者也许您希望压缩或清除您的表,或者希望消除不再适用的行。要删除行,使用DELETE FROM命令。

我们来看一个例子。假设您有一个正在开发的工厂监控解决方案,您发现您的一个传感器或传感器节点读取的值太低,原因是编码、布线或校准错误。在这种情况下,我们希望删除传感器值小于 0.20 的所有行。以下命令可以做到这一点:

DELETE FROM plants WHERE sensor_value < 0.20;

Caution

别忘了WHERE条款!发出不带WHERE子句的DELETE FROM命令将永久删除表中的所有行!

注意我在这里使用了一个WHERE子句。也就是说,一个条件语句来限制被操作的行数。您可以使用您想要的任何列或条件;只要确保你有正确的!我喜欢在SELECT语句中首先使用相同的WHERE子句。例如,我将首先发出下面的命令来检查我是否要删除我想要的行,并且只删除那些行。请注意,这是同一个WHERE子句。

SELECT * FROM plants WHERE sensor_value < 0.20;

MySQL 概念

除了前面显示的命令之外,您还可以考虑使用其他概念。虽然每个都有一个 SQL 命令,但我在这里将它们作为一个概念列出,因为它们比简单的对象创建或数据检索更复杂。

索引

创建表时不使用任何排序。也就是说,表是无序的。虽然 MySQL 每次都会以相同的顺序返回数据,但是没有隐含的(或可靠的)顺序,除非您创建一个索引。我这里所指的排序并不像你在排序时所想的那样(在SELECT语句的ORDER BY子句中是可能的)。

相反,索引是服务器在执行查询时用来读取数据的映射。例如,如果一个表上没有索引,并且希望选择某列中值大于某个值的所有行,则服务器必须读取所有行来查找所有匹配项。但是,如果我们在该列上添加了一个索引,服务器将只能读取那些符合标准的行。

我应该注意到有几种形式的索引。这里我指的是一个聚集索引,索引中列的值存储在索引中,允许服务器只读取索引,而不读取行来测试标准。

要创建索引,您可以在CREATE TABLE语句中指定索引,或者发出一个CREATE INDEX命令。下面是一个简单的例子:

CREATE INDEX plant_name ON plants (plant_name);

该命令在plant_name列上添加一个索引。观察这对表格的影响。

CREATE TABLE plants (

``plant_name char(30) DEFAULT NULL,

``sensor_value float DEFAULT NULL,

``sensor_event timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

``sensor_level char(5) DEFAULT NULL,

KEY plant_name (plant_name)

) ENGINE=InnoDB DEFAULT CHARSET=latin1

像这样创建的索引不会影响表中行的唯一性,换句话说,确保存在且只有一行可以被特定列的特定值访问。我所指的是主键(或主索引)的概念,这是在创建表时使用的一个特殊选项,如前所述。

视图

视图是一个或多个表的结果的逻辑映射。它们可以像查询中的表一样被引用,这使它们成为创建数据子集的强大工具。您用CREATE VIEW创建一个视图,并给它起一个类似于表格的名字。下面显示了一个简单的例子,其中我们创建了一个测试视图来从表中读取值。在这种情况下,我们限制了视图的大小(行数),但是您可以为视图使用各种各样的条件,包括组合来自不同表的数据。

CREATE VIEW test_plants AS SELECT * FROM plants LIMIT 5;

在中小型数据库解决方案中通常不会遇到视图,但是我将它们包括在内是为了在您决定进行额外的分析并希望将数据组织成更小的组以便于阅读时让您了解它们。

扳机

另一个高级概念(以及相关的 SQL 命令)是使用事件驱动的机制,当数据发生变化时会“触发”该机制。也就是说,您可以创建一组简短的 SQL 命令(过程),这些命令将在插入或更改数据时执行。

触发器将在几种事件或条件下执行。您可以在更新、插入或删除操作之前或之后设置触发器。触发器与单个表相关联,其主体是一个特殊的构造,允许您对受影响的行进行操作。下面是一个简单的例子:

DELIMITER //

CREATE TRIGGER set_level BEFORE INSERT ON plants FOR EACH ROW

BEGIN

IF NEW.sensor_value < 0.40 THEN

SET NEW.sensor_level = 'LOW'

ELSEIF NEW.sensor_value < 0.70 THEN

SET NEW.sensor_level = 'OK'

ELSE

SET NEW.sensor_level = 'HIGH'

END IF;

END //

DELIMITER ;

该触发器将在每次插入表之前执行。在复合语句(BEGIN中可以看到...END),我们根据sensor_value的值将名为sensor_level的列设置为LOWOKHIGH。要了解这一点,请考虑下面的命令。FOR EACH ROW语法允许触发器作用于事务中的所有行。

INSERT INTO plants (plant_name, sensor_value) VALUES ('plant1', 0.5544);

由于我们提供的值小于中间值(0.70),我们期望触发器为我们填充sensor_level列。下面显示了触发器触发时发生的情况:

+-------------+--------------+---------------------+--------------+

| plant_name  | sensor_value | sensor_event        | sensor_level |

+-------------+--------------+---------------------+--------------+

| plant1      |       0.5544 | 2015-09-23 20:00:15 | OK           |

+-------------+--------------+---------------------+--------------+

1 row in set (0.00 sec)

这展示了一种有趣而强大的方法,可以利用数据库服务器的能力创建派生列,并节省传感器或聚合器节点的处理能力。我鼓励您考虑这个以及类似的强大概念,以利用数据库服务器的强大功能。

简单连接

数据库系统最强大的概念之一是在数据之间建立关系的能力(因此得名关系型)。也就是说,一个表中的数据可以引用另一个(或多个)表中的数据。最简单的形式称为主从关系,其中一个表中的一行引用或关联到另一个表中的一行或多行。

一个常见的(也是经典的)主从关系的例子来自订单跟踪系统,其中一个表包含订单的数据,另一个表包含订单的行项目。因此,我们只存储一次订单信息,如客户号和发货信息,并在检索订单时合并或“连接”这些表。

让我们看一个来自示例数据库的名为world的例子。你可以在 MySQL 网站上找到这个数据库( http://dev.mysql.com/doc/index-other.html )。请随意下载它和任何其他示例数据库。它们都展示了数据库系统的各种设计。您还会发现练习查询数据很方便,因为它包含了许多简单的行。

Note

如果要运行以下示例,需要安装示例文档中描述的世界数据库( http://dev.mysql.com/doc/world-setup/en/world-setup-installation.html )。

清单 5-7 展示了一个简单连接的例子。这里发生了很多事情,所以花点时间检查一下SELECT语句的各个部分,尤其是我是如何指定JOIN子句的。您可以忽略LIMIT选项,因为它只是限制了结果集中的行数。

Listing 5-7.Simple JOIN Example

mysql> SELECT Name, Continent, Language FROM Country JOIN CountryLanguage ON Country.Code = CountryLanguage.CountryCode LIMIT 10;

+-------------+---------------+------------+

| Name        | Continent     | Language   |

+-------------+---------------+------------+

| Aruba       | North America | Dutch      |

| Aruba       | North America | English    |

| Aruba       | North America | Papiamento |

| Aruba       | North America | Spanish    |

| Afghanistan | Asia          | Balochi    |

| Afghanistan | Asia          | Dari       |

| Afghanistan | Asia          | Pashto     |

| Afghanistan | Asia          | Turkmenian |

| Afghanistan | Asia          | Uzbek      |

| Angola      | Africa        | Ambo       |

+-------------+---------------+------------+

10 rows in set (0.00 sec)

这里我使用了一个JOIN子句,它接受两个指定的表,这样第一个表使用特定的列及其值连接到第二个表(ON指定匹配)。数据库服务器所做的是从表中读取每一行,并只返回那些列中的值指定匹配的行。一个表中不在另一个表中的任何行都不会被返回。

Tip

但是您可以检索那些具有不同连接的行。有关更多详细信息,请参见内部和外部连接的在线参考手册。

还要注意,我只包括了几个专栏。在本例中,我从Country表中指定了国家名称和大陆,从CountryLanguage表中指定了语言列。如果列名不是惟一的(相同的列出现在每个表中),我就必须用表名来指定它们,比如Country.Name。事实上,总是以这种方式限定列被认为是一种好的做法。

这个例子中有一个有趣的异常,我觉得有必要指出来。事实上,有些人会认为这是一个设计缺陷。注意,在JOIN子句中,我指定了表格和每个表格的列。这是正常且正确的,但是请注意,两个表中的列名并不匹配。虽然这真的没有关系,并且只需要一点额外的输入,但是一些 DBA 会认为这是错误的,并且希望在两个表中使用相同的公共列名。

连接的另一个用途是检索公共数据、存档数据或查找数据。例如,假设您有一个表,其中存储了不变(或很少变)的事物的详细信息,如与邮政编码相关联的城市或与标识号相关联的名称(例如,SSN)。您可以将这些信息存储在一个单独的表中,并在需要时将数据连接到一个公共列(和值)上。在这种情况下,公共列可以用作外键,这是另一个高级概念。

外键用于维护数据完整性(也就是说,如果一个表中的数据与另一个表相关,但这种关系需要保持一致)。例如,如果您想确保在删除主行时所有的细节行也被删除,您可以在主表中声明一个外键,指向细节表的一列(或多列)。有关外键的更多信息,请参见联机参考手册。

关于连接的讨论只涉及最基本的内容。事实上,连接可以说是数据库系统中最困难和最容易混淆的领域之一。如果您发现您想要使用联接来组合几个表或扩展数据,以便从几个表提供数据(外部联接),您应该花一些时间来深入研究数据库概念,如 Clare Churcher 的书《数据库设计入门》(Apress,2012)。

其他高级概念

MySQL 中有更多可用的概念和命令,但有两个可能会引起人们的兴趣,那就是PROCEDUREFUNCTION,它们有时被称为例程。我在这里介绍这些概念,以便如果您想探索它们,您可以理解它们是如何在高层次上使用的。

假设您需要运行几个命令来更改数据。也就是你需要在计算的基础上做一些复杂的改变。对于这些类型的操作,MySQL 提供了存储过程的概念。存储过程允许您在调用该过程时执行复合语句(一系列 SQL 命令)。存储过程有时被认为是一种主要用于定期维护的高级技术,但它们在更简单的情况下也很方便。

例如,假设您想要开发您的 IOT 解决方案,但是由于您正在开发它,您需要定期重新开始,并且想要首先清除所有数据。如果只有一个表,存储过程不会有太大帮助,但是假设有几个表分布在几个数据库中(对于较大的 IOT 解决方案来说并不少见)。在这种情况下,存储过程可能会有所帮助。

Tip

在 MySQL 客户端中输入带有复合语句的命令时,您需要临时更改分隔符(分号),以便行尾的分号不会终止命令条目。例如,在用复合语句编写命令之前使用DELIMITER //,使用//结束命令,用DELIMITER ;将分隔符改回来。这仅在使用客户端时。

由于存储过程可能相当复杂,如果您决定使用它们,在尝试开发自己的存储过程之前,请阅读联机参考手册的“创建过程和创建函数语法”一节。创建存储过程的内容远不止这一部分。

现在假设您想执行一个复合语句并返回一个结果—您想将它用作一个函数。您可以使用函数通过执行计算、数据转换或简单的翻译来填充数据。因此,函数可用于提供值来填充列值、提供聚合、提供日期操作等等。

您已经看到了几个函数(COUNTAVG)。这些被认为是内置函数,在线参考手册中有一整节专门介绍它们。但是,您也可以创建自己的函数。例如,您可能希望创建一个函数来对您的数据执行一些数据规范化。更具体地说,假设您有一个传感器,它产生一个特定范围内的值,但是根据该值和来自不同传感器或查找表的另一个值,您想要对该值进行加、减、平均等操作来校正它。您可以编写一个函数来实现这一点,并在触发器中调用它来填充计算列的值。

Tip

对计算值使用新列,以便保留原始值。

What About Changing Objects?

您可能想知道当您需要修改表、过程、触发器等时该怎么做。放心吧,你不必从头开始!MySQL 为每个对象提供了一个ALTER命令。也就是说,有一个ALTER TABLEALTER PROCEDURE等等。关于每个ALTER命令的更多信息,参见在线参考手册“数据定义语句”一节。

规划 IOT 数据的数据库存储

既然您已经知道了如何获取、安装和使用 MySQL,那么是时候关注如何应用您在前面章节中学到的知识,并为存储 IOT 数据建立一个数据库了。召回 IOT 数据可以是任何形式的传感器数据、个人信息、代码、事件日期、设备标识等。

我以举例的方式提出这个主题。更具体地说,我觉得最好是演示数据库设计,而不是通过花言巧语来规定实践和政策。我认为这些例子展示了许多您在为您的 IOT 解决方案设计自己的数据库时可能会遇到的概念和构造。让我们深入了解工厂监控解决方案的完整设计。

由于我在前几节中使用了这些表的一些原语,下面的内容可能看起来很熟悉。但是,本节包含一个完全开发的数据库设计。这只是我可能用过的一种设计。如果您考虑实施自己的工厂监控解决方案,我建议您考虑替代方案。

Correct Database Design: Am I Doing This Right?

设计数据库没有任何错误的方法。虽然有些 DBA 会对这种说法感到畏缩,但是只要您能够以合理的性能实现所有的目标,并且不会丢失数据,您就应该认为您的设计是可行的。毕竟,即使是最复杂和专业设计的数据库也会经历常规的和进化的变化。也就是说,你不必一开始就做对。您可以随时调整您的数据库,以满足您不断增长和成熟的 IOT 解决方案需求。

示例 1:工厂监控系统

让我们探索一种 IOT 解决方案,用于监控植物的环境温度和土壤湿度。在这种情况下,解决方案被设计为支持任意数量的工厂(目标数据对象)。这种 IOT 解决方案的一个关键组成部分是,所有收集的数据都存储在所使用的传感器中,大约每小时读取一次。

在下面的例子中,我向您展示了设计数据库时我喜欢使用的四个基本步骤。你可能会发现其他哲学有更多的步骤和更严格的过程(这很好),但是对于爱好者和业余爱好者,我推荐使用这些简化的步骤。这并不意味着你不能设计任何其他方式,只是这种方法应该为大多数工作。事实上,如果你有设计数据库的经验,你应该会看到与你自己的方法的相似之处。

步骤 1:描述数据

设计数据库时,您应该做的第一件事是尽可能完整地描述数据。你应该用英语来描述数据,甚至可以把它写在一张纸上。这样做有助于您从概念上定义数据的样子,以便您可以确定要存储多少个对象,它们由什么组成,以及如何组织它们。

植物监控系统应该存储关于植物的信息。具体来说,我们希望存储告诉我们植物何时需要浇水以及浇水频率的信息。了解植物的名称、位置以及是在室内还是室外也很重要。因此,我们需要存储的数据包括植物名称、位置、室内室外、土壤湿度传感器值、温度传感器值和传感器读数时间。我们还决定要量化土壤湿度的值,以便于编写应用来检测植物何时需要浇水。事实上,我们甚至可以在未来增加自动浇水功能!

因为植物名称总是相同的,所以我们不需要多次存储该信息。因此,我们将创建一个表来存储关于我们正在监控的工厂的信息,并创建另一个表来存储传感器读数。这样,如果你需要改变一个植物的名字(就像我们之前看到的)或者你想改变它的位置,你只需要在一个地方改变它。因为我们将传感器读数存储在不同的表中,所以我们必须选择一个列来连接这些表。因为我们没有为每个工厂分配任何合理的数值,所以我们可以使用自动增量特性来添加一个唯一的键(在本例中是主键)。

步骤 2:设计数据库对象

现在让我们看看这些表是什么样子的。我们将主表(存储工厂信息的表)命名为plants,将明细表(传感器读数)命名为readings。我们将这些表放在名为plant_monitoring的数据库中。清单 5-8 显示了数据库中每个表的布局或模式。

Listing 5-8.Plant-Monitoring Schema

-- A database for storing plant soil moisture and ambient temperature

CREATE DATABASE plant_monitoring;

USE plant_monitoring;

-- This table stores information about a plant.

CREATE TABLE plants (

``id int(11) NOT NULL AUTO_INCREMENT,

``name char(50) DEFAULT NULL,

``location char(30) DEFAULT NULL,

``climate enum ('inside','outside') DEFAULT 'inside',

PRIMARY KEY (id)

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- This table stores values read from sensors. The sensors are read

-- periodically by a sensor node and the values stored as a single row

-- with the date and time added by the database server.

CREATE TABLE readings (

``id int(11) NOT NULL,

``moisture float DEFAULT NULL,

``temperature float DEFAULT NULL,

``event_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

``soil_status enum ('DRY', 'OK', 'WET') DEFAULT NULL

) ENGINE=InnoDB DEFAULT CHARSET=latin1;

DELIMITER //

CREATE TRIGGER set_status BEFORE INSERT ON readings FOR EACH ROW

BEGIN

IF NEW.moisture < 250 THEN

SET NEW.soil_status = 1;

ELSEIF NEW.moisture < 400 THEN

SET NEW.soil_status = 2;

ELSE

SET NEW.soil_status = 3;

END IF;

END //

DELIMITER ;

注意readings表上定义的触发器。回想一下上一节,触发器可以用来为计算列提供数据,这就是我们想要存储在soil_status列中的数据。在这种情况下,我们将干土壤的低阈值设为 250,湿土壤的低阈值设为 400 以上。因此,在 250 和 399 之间,植物土壤被认为是正常的(Ok)。

另外,请注意 SQL 代码中包含的注释。双破折号是 MySQL 客户端会忽略的注释。虽然这个示例数据库相当简单,但添加对数据库对象(表等)的简短描述也无妨,这样您就可以在忘记时记住每个对象的作用,或者作为其他人使用数据库或您的 IOT 解决方案(如定制应用)。

还要注意plants表中的location列。这是一个使用枚举值的例子,这样您就不必为添加到表中的每个工厂重复输入字符串。值从 1 开始,因此您可以为内部指定 1,为外部指定 2,如下所示:

mysql> INSERT INTO plants VALUES(NULL, 'Jerusalem Cherry', 'deck', 2);

Query OK, 1 row affected (0.01 sec)

mysql> select * from plants;

+----+---------------------+----------+---------+

| id | name                | location | climate |

+----+---------------------+----------+---------+

|  1 | Jerusalem Cherry    | deck     | outside |

+----+---------------------+----------+---------+

1 row in set (0.00 sec)

Note

我尽可能为这些列选择描述性的名称。我还加入了一些模糊的东西。你能说出哪些可以用一个更好的名字吗?提示:气候对你意味着什么?显然,为列选择有效的、有意义的名称是一项挑战,也是一种艺术形式。

最后,请注意,虽然我向plants表添加了一个主键,在本例中为AUTO_INCRMENT数据类型,但是我没有向readings表添加一个主键。这样我们可以在readings表中存储任意数量的行,并且数据完全有可能不是唯一的。更准确地说,从传感器读取的值可能与两个或多个读数相同。因此,我在readings表的定义中忽略了唯一性因素。

我还应该注意使用AUTO_INCREMENT数据类型来惟一标识行的结果。虽然从概念上讲,在plants表中的同一位置不能有多于一种植物被命名为相同的植物(即使有三种蕨类植物,你很可能会给它们起不同的名字),但事实上AUTO_INCREMENT是一种人工的唯一性机制,这意味着你很可能两次输入相同的数据,从而产生不同的AUTO_INCREMENT值。因此,在与AUTO_INCREMENT合作时,谨慎一点是明智的。

在设计表格时,有两个方便的工具可以使用。首先,您可以使用SHOW CREATE TABLE命令查看实际的 SQL 命令来重新创建表。事实上,SHOW CREATE可以用于任何物体,如SHOW CREATE TRIGGER。其次,您可以使用EXPLAIN命令,如清单 5-9 所示。

Listing 5-9.Using EXPLAIN

mysql> explain plants;

+------------+----------+------+-----+---------+----------------+

| Field      | Type     | Null | Key | Default | Extra          |

+------------+----------+------+-----+---------+----------------+

| id         | int(11)  | NO   | PRI | NULL    | auto_increment |

| name       | char(50) | YES  |     | NULL    |                |

| location   | char(30) | YES  |     | NULL    |                |

| plant_type | char(30) | YES  |     | NULL    |                |

+------------+----------+------+-----+---------+----------------+

4 rows in set (0.00 sec)

mysql> explain readings;

+-------------+-------------+------+-----+-------------------+-----------------------------+

| Field       | Type        | Null | Key | Default           | Extra                       |

+-------------+-------------+------+-----+-------------------+-----------------------------+

| id          | int(11)     | NO   |     | NULL              |                             |

| moisture    | float       | YES  |     | NULL              |                             |

| temperature | float       | YES  |     | NULL              |                             |

| event_time  | timestamp   | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |

| soil_status | enum('DRY', | YES  |     | NULL              |                             |

| | '好','湿')| | | | |

+-------------+-------------+------+-----+-------------------+-----------------------------+

5 rows in set (0.00 sec)

这里您可以看到一个结果集,它显示了所有列及其数据类型,以及plant monitoring数据库中每个表的选项。注意我们为时间戳字段获得的额外信息。这里它告诉我们,当行被更新时,时间戳将被更新,但它也适用于新行。

但是等等!每个传感器如何知道每个植物的 ID 是什么?嗯,这有点本末倒置。如果您认为每个传感器都可以通过一个小的微控制器板读取和发送数据,那么代码(Arduino 行话中的草图)必须知道来自plants表的 ID。因为我们在表中使用了一个AUTO_INCREMENT列,所以我们必须首先插入关于工厂的数据,然后查询表中的 ID。下面显示了一个示例:

mysql> INSERT INTO plant_monitoring.plants VALUES (NULL, 'fern', 'beside picnic table', 2);

Query OK, 1 row affected (0.01 sec)

mysql> SELECT LAST_INSERT_ID();

+------------------+

| LAST_INSERT_ID() |

+------------------+

|                9 |

+------------------+

1 row in set (0.00 sec)

mysql> SELECT * FROM plant_monitoring.plants WHERE id = 9;

+----+------+---------------------+---------+

| id | name | location            | climate |

+----+------+---------------------+---------+

|  9 | fern | beside picnic table | outside |

+----+------+---------------------+---------+

1 row in set (0.00 sec)

但是请注意,我实际上并不需要查询plants表。相反,我使用了LAST_INSERT_ID()函数,它返回最后生成的AUTO_INCREMENT值。酷!

一旦我们有了这些信息,我们就可以对传感器读取器进行编程,将这些信息发送到数据库,以填充读数表,从而建立与植物表的关系。这意味着当我们查询特定工厂的所有传感器读数时,join 将返回正确的信息。因此,如果我想用id = 9存储关于工厂的数据,我将使用如下的INSERT语句:

INSERT INTO plant_monitoring.readings VALUES (9, 233, 38.4, NULL, NULL);

Database Design First? What About The Application?

我曾经为一个认为应用和用户界面比数据库设计更重要的组织工作过。我想这就是为什么他们最终雇佣了更多的 DBA 而不是开发者。在应用的开发过程中,他们不得不多次重新设计数据库,这导致了交付的重大延迟和一些有趣而严重的错误。

在定义了高级需求之后、实现源代码之前,一定要设计好数据库,包括您想问的问题。这样,您的应用将基于具有良好定义的查询的全功能数据库系统,而不是努力使查询适合某些代码实现。

现在让我们看看如何从数据库中获取信息。

步骤 3:为数据库设计查询或问题

每当设计一个数据库时,我都把它作为一个要点,如果不是一个必需的里程碑的话,来确定我想对数据运行什么查询(换句话说,我想问的问题和我期望的答案)。这些问题是为向用户呈现信息而设计的应用会遇到的问题。请这样想:如果您有一个工厂监控系统,您希望在用户界面上看到什么功能或结果?在设计应用之前定义这些将使应用开发进行得更加顺利。

我们可能希望对工厂监控解决方案执行的查询包括但不限于以下内容。正如您将看到的,其中一些简单易行,而另一些可能需要更复杂的SELECT语句才能实现。我将在接下来的小节中演示其中的一些,剩下的留给您自己去探索。

  • 哪些植物需要浇水?
  • 哪些植物正在经历最高的温度?
  • 每天每株植物的平均土壤湿度是多少?
  • 特定植物在白天能忍受的温度范围是多少?
  • 有多少工厂受到监控?
  • 外面有多少植物?
  • 工厂内部或外部的平均温度是多少?

下面几节将向您介绍如何为其中的三个问题创建查询。我首先给出没有验证的查询,然后将通过相同的逻辑过程向您展示如何用已知的测试数据测试每个查询。然而,对于一些更复杂的查询,我将向您展示它们是如何在上下文中工作的,以便您可以看到各个部分是如何组合在一起的。

示例 1:一个简单的查询

让我们从一个简单的查询开始。这一个包括做一些聚集。回想一下我们想问的一个问题是,“植物内部或外部的平均温度是多少?”在这种情况下,让我们简单一点,只查询室外的植物。正如您将看到的,更改查询来查找内部植物是很简单的。

像所有好的查询设计一样,我们从分解问题开始。对于这个查询,我们需要知道每株植物的平均温度。回想一下,我们需要在AVG()函数中使用一个GROUP BY子句。但是,如果我们希望将行限制为只显示某一天(比如今天)的读数,该怎么办呢?也就是说,我们的应用可能会提供每家工厂当天平均温度的周期性状态。我们可以在一个时间范围内查询数据,但这更多的是一种分析操作——并不是说你不能这样做。为了简单起见,我们将使用今天的阅读材料。

那么,如何查询这类信息呢?显然,我们需要使用某种形式的日期检查。幸运的是,MySQL 提供了许多这样的功能。为此,我们将使用DATE()CURRENT_DATE()函数。它们执行使用event_time TIMESTAMP列确定今天采集的样本的操作。数据库的一个非常酷和强大的特性是,如果我们使用这些函数,我们可以创建查询、视图、函数等等,自动确定今天记录了哪些行。因此,当使用这些函数进行查询时,我们不需要读取、存储和传输日期。酷!

下面的WHERE子句演示了如何做到这一点。注意,我们将event_time列传递给函数,然后由函数确定日期。我们将其与当前日期进行比较,从而只找到传感器读数是在今天获得的那些行。

WHERE DATE(event_time) = CURRENT_DATE()

现在我们只需要做平均。我们之前已经看过一个例子,所以下面的 SQL 命令应该看起来很熟悉——至少在概念上是这样的:

SELECT id, AVG(temperature) as avg_temp FROM readings WHERE DATE(event_time) = CURRENT_DATE() GROUP BY id;

这将为我们提供平均温度和今天读取的每株植物的 ID。剩下的工作就是将这些信息与plants表中的信息相结合,以获得名称、位置和平均温度。以下 SQL 语句显示了如何做到这一点:

SELECT name, location, AVG(temperature) as avg_temp

FROM plants JOIN readings ON plants.id = readings.id

WHERE DATE(event_time) = CURRENT_DATE()

GROUP BY plants.id;

哇,对于这样一个简单的查询,这里发生了很多事情!为了便于阅读,我把每一个子句放在单独的一行上。让我们逐一回顾一下。

首先,我们看到选择的列是工厂namelocation以及平均值temperature。回想一下GROUP BY子句是控制向函数提供哪些行的。在这种情况下,我们按工厂 ID 分组。注意,FROM子句执行了plants表和readings表之间的连接。这样我们就可以从plants表中获得工厂信息,但对与plants表相匹配的readings表中的数据进行计算(平均温度)。因此,一个简单的连接!最后,我们使用前面描述的WHERE子句将数据限制为今天采集的样本。

但我们还没完。我们还没有完全回答这个问题。也就是说,我们还想知道室外设备的平均温度。我们需要做的就是将该条件添加到WHERE子句中,如下所示:

SELECT name, location, AVG(temperature) as avg_temp

FROM plants JOIN readings ON plants.id = readings.id

WHERE DATE(event_time) = CURRENT_DATE() AND plants.climate = 2

GROUP BY plants.id;

现在,我们完成了!要查询内部工厂,只需将climate值更改为 1。回想一下,这是climate列的枚举数据类型的值。

现在让我们看一个更复杂的查询。乍一看,这看起来很简单,但是你会看到它有很多层。

Note

下面只是如何形成查询的一个例子。还有几个其他的,一些更好的,但是我想展示你可以用来解决这个问题的逻辑思维过程。请随意试验和改进下面的例子。

示例 2:复杂的查询

现在让我们看一个更复杂的查询。或者更确切地说,是一种形成简单但实施起来并非微不足道的方法。在这种情况下,考虑查询“哪些植物需要浇水?”为此,我们需要知道哪些植物的水分值低于我们的干/好/湿阈值。因为我们使用计算列,所以我们不必看到实际的传感器值。也就是说,我们可以查询readings表,发现哪些被标记为 DRY。

您可能认为我们只需要查询readings表,以获得今天传感器读数的 DRY 值。这将让你接近,但它可能会导致一些行,但如果你或自动植物浇水系统浇水,或者如果下雨怎么办?你可能只有几个干燥的读数,但有更多的正常或潮湿的读数。在这种情况下,植物可能根本不需要浇水。

您也可以考虑为每个工厂选择最新的传感器读数。事实上,这是大多数人都会做的事情。但这并不包括传感器读取的数值处于边界或产生虚假读数的情况。例如,业余水平的土壤湿度传感器(换句话说,负担得起的传感器)不是 100%准确,可能会产生轻微的不一致的读数。只要你明白你应该回顾几个读数,或者取平均值,或者考虑一个范围的读数相对于另一个范围的读数的百分比,它们对于一个“广泛”的读数来说是非常好的。

我们需要的是一种方法来确定那些比其他值更频繁的植物干旱读数,这需要一点数学知识。你认为这很简单。如果你把它分解成更小的部分,这就是为什么我选择这个例子。让我们把它分成几个部分。我们从我们想要的最基本的陈述开始。

更具体地说,我们想知道所有植物当天的土壤状态。我们将计算soil_status的值,并根据当天的总读数确定发生的百分比。我们可以利用这些信息来决定哪些植物需要浇水。

为了做到这一点,我们将在 MySQL 中使用一个叫做视图的概念。Recall 视图是结果集的逻辑表示,可以像其他SELECT语句中的表一样对待。下图显示了检索当天土壤状态的视图:

CREATE VIEW soil_status_today AS

SELECT id, soil_status, count(soil_status) as num_events FROM plant_monitoring.readings

WHERE DATE(event_time) = CURRENT_DATE() GROUP BY id, soil_status;

还要注意,我使用了一个GROUP BY子句来聚合值,计算每个状态值的数量。让我们来看一个示例结果。注意,我像查询表一样查询视图。

mysql> SELECT * FROM plant_monitoring.soil_status_today;

+----+-------------+------------+

| id | soil_status | num_events |

+----+-------------+------------+

|  1 | DRY         |         10 |

|  2 | OK          |         10 |

|  3 | DRY         |          4 |

|  3 | OK          |          4 |

|  3 | WET         |          2 |

|  4 | OK          |          6 |

|  4 | WET         |          4 |

|  5 | OK          |         10 |

+----+-------------+------------+

8 rows in set (0.01 sec)

所以,这告诉我们,今天,植物 IDs 1 和 3 是干的。但我们还没完!考虑到一天中采集了多个样本。一些植物可能处于稍微干燥的阈值,但没有干燥到传感器读数一致的程度。所以,我们希望植物持续干旱,干旱事件比正常或潮湿多。我们可以使用另一个视图来获取这些信息,但是让我们看看如何使用一个存储函数来实现这一点。下面创建了一个函数,该函数返回给定工厂今天收集的最大样本数:

DELiMITER //

CREATE FUNCTION plant_monitoring.max_samples_today (in_id int)

RETURNS int DETERMINISTIC READS SQL DATA

BEGIN

DECLARE num_samples int;

SELECT COUNT(*) into num_samples FROM plant_monitoring.readings

WHERE DATE(event_time) = CURRENT_DATE() AND readings.id = in_id;

RETURN num_samples;

END //

DELIMITER ;

让我们看看这个函数是如何工作的。让我们使用视图创建一个查询,并计算每个植物的每个值的出现百分比。下面的SELECT语句用一点数学知识完成了这个任务。我包括这些行是为了向您展示它是有效的。

mysql> SELECT *, max_samples_today(id) as max_samples, (num_events/max_samples_today(id)) as percent_occurrence FROM plant_monitoring.soil_status_today;

+----+-------------+------------+-------------+--------------------+

| id | soil_status | num_events | max_samples | percent_occurrence |

+----+-------------+------------+-------------+--------------------+

|  1 | DRY         |         10 |          10 |             1.0000 |

|  2 | OK          |         10 |          10 |             1.0000 |

|  3 | DRY         |          4 |          10 |             0.4000 |

|  3 | OK          |          4 |          10 |             0.4000 |

|  3 | WET         |          2 |          10 |             0.2000 |

|  4 | OK          |          6 |          10 |             0.6000 |

|  4 | WET         |          4 |          10 |             0.4000 |

|  5 | OK          |         10 |          10 |             1.0000 |

+----+-------------+------------+-------------+--------------------+

8 rows in set (0.01 sec)

在这种情况下,我今天正好有十个传感器读数。这是因为我的测试数据(我将在后面的小节中展示)是固定的。当我在家里的实时植物监控解决方案上运行时,我的传感器读数平均每小时一个,每天 18 到 24 个。

注意,我添加了函数调用来获取最大样本数(读数),然后添加了另一列来计算总出现次数的百分比。但是这是很大的工作量。既然我们知道了如何在查询中进行数学运算,我们就可以轻松一点了。

我们现在需要做的就是添加一个百分比检查,比方说超过 50 %,并将行限制在那些带有soil_statusDRY 的行。因此,我们采用前面的查询,并添加几个条件。我们还会将结果中的列限制为仅包含id。以下查询显示了一个示例SELECT语句,用于确定哪些读数指示哪棵或哪些植物今天需要浇水:

mysql> SELECT id FROM soil_status_today WHERE ((num_events/max_samples_today(id)) > 0.50) AND soil_status = 1;

+----+

| id |

+----+

|  1 |

+----+

1 row in set (0.00 sec)

我们快到了。同样,我们将使用前面的查询并连接到plant表来确定哪些工厂需要监控。

SELECT name, location FROM plants JOIN (SELECT id FROM soil_status_today WHERE ((num_events/max_samples_today(id)) > 0.50) AND soil_status = 1) as sub_query on plants.id = sub_query.id;

现在我们知道了今天需要浇水的植物。请注意,我们只是简单地使用了如何从最基本的开始向外查找数据的逻辑分解(就像缩小我们的结果一样)。正如您所看到的,这个查询并没有那么难,因为我们创建了一个函数和一个视图来帮助我们,所以编写类似于这个查询的查询会容易得多。

步骤 4:测试数据库

既然我们已经设计、实现了数据库,并决定了查询(或者至少是我们能想到的所有查询),我们就可以开始构建传感器网络并编写应用了,对吗?没有。我们需要测试这些查询,不仅要确保它们能够工作(也就是说,具有正确的语法),还要确保我们得到了我们期望的结果。这需要有一个已知的数据集来处理。如果您发现在您的应用像病毒一样传播之后,有一个查询没有返回正确的数据,这将对您没有什么好处。

此时,数据可能不完全准确,也不需要实际的实时数据。您可以使用虚构的数据,只要您制作的数据代表表中每列的值范围。也就是说,确保您知道传感器可以读取的最小值和最大值。清单 5-10 显示了我为测试工厂监控解决方案的查询而创建的一些样本数据。

Listing 5-10.Sample Data

INSERT INTO plant_monitoring.plants VALUES (NULL, 'Jerusalem Cherry', 'deck', 2);

INSERT INTO plant_monitoring.plants VALUES (NULL, 'Moses in the Cradle', 'patio', 2);

INSERT INTO plant_monitoring.plants VALUES (NULL, 'Peace Lilly', 'porch', 1);

INSERT INTO plant_monitoring.plants VALUES (NULL, 'Thanksgiving Cactus', 'porch', 1);

INSERT INTO plant_monitoring.plants VALUES (NULL, 'African Violet', 'porch', 1);

INSERT INTO plant_monitoring.readings VALUES (1, 235, 39.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 235, 38.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 230, 38.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 230, 39.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 215, 39.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 215, 39.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 225, 39.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 220, 38.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 222, 38.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (1, 218, 37.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 355, 38.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 350, 38.6, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 366, 38.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 378, 38.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 361, 38.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 348, 37.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 343, 39.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 342, 38.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 358, 36.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (2, 377, 36.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 155, 33.6, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 150, 33.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 166, 33.6, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 278, 32.3, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 261, 31.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 248, 32.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 313, 33.6, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 342, 32.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 458, 31.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (3, 470, 33.4, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 333, 33.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 345, 33.6, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 360, 34.4, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 380, 34.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 395, 33.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 385, 33.4, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 425, 32.3, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 420, 31.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 422, 33.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (4, 418, 32.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 335, 39.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 335, 38.7, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 330, 38.8, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 330, 39.1, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 315, 39.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 315, 39.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 325, 39.2, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 320, 38.9, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 322, 38.5, NULL, NULL);

INSERT INTO plant_monitoring.readings VALUES (5, 318, 37.1, NULL, NULL);

注意,我在最后两列中使用了NULL。这是我做 DBA 时养成的习惯,我总是为表中的每个字段提供一个值。你不必那样做。事实上,只使用您知道的值来指定 SQL 命令也是正确的,但是请记住使用列列表,如下所示:

INSERT INTO plant_monitoring.readings (id, moisture, temperature) VALUES (7,418,32.5);

作为一个简单的测试,让我们做一个简单的连接来找到特定植物的所有读数。例如,我们问这样一个问题,“甲板上植物的湿度读数是多少?”以下查询返回结果:

mysql> SELECT name, location, moisture FROM plants JOIN readings ON plants.id = readings.id WHERE location LIKE '%deck%'

+------+----------+----------+

| name | location | moisture |

+------+----------+----------+

| fern | deck     |      235 |

| fern | deck     |      235 |

| fern | deck     |      230 |

| fern | deck     |      230 |

| fern | deck     |      215 |

| fern | deck     |      215 |

| fern | deck     |      225 |

| fern | deck     |      220 |

| fern | deck     |      222 |

| fern | deck     |      218 |

+------+----------+----------+

10 rows in set (0.00 sec)

注意,我在WHERE子句中使用了一个LIKE函数。我用它是因为我不确定甲板上是否有不止一种植物。例如,可能有一种植物的位置是“桌子旁边的甲板”或“在树下的甲板上”在每一侧使用带有通配符%LIKE,将返回 location 列值中包含“deck”的所有行。酷!花些时间看看清单 5-10 中的样本数据,以确保你看到的是正确的数据。

现在我们有了一些样本数据,让我们看看示例查询的结果。我按照解释上一个查询的顺序遍历结果。

测试示例 1

回想一下这个查询,我们只需要室外植物的平均温度。当我测试一个查询时,我喜欢把它分解成最简单的部分——就像我开发它时一样。这样,我可以验证我得到了每个部分的正确结果。让我们再看一下那个查询。

SELECT name, location, AVG(temperature) as avg_temp

FROM plants JOIN readings ON plants.id = readings.id

WHERE DATE(event_time) = CURRENT_DATE() AND plants.climate = 2

GROUP BY plants.id;

让我们从最基本的数据开始——生活在室外的植物的名称和位置。

Note

我将按部分输入查询,这样您可以更好地阅读结果。如您所见,这种方式在 MySQL 客户端中读取要容易得多,客户端会“等待”直到您键入分号来执行查询。

mysql> SELECT * FROM plants

-> WHERE climate = 2;

+----+---------------------+----------+---------+

| id | name                | location | climate |

+----+---------------------+----------+---------+

|  1 | Jerusalem Cherry    | deck     | outside |

|  2 | Moses in the Cradle | patio    | outside |

+----+---------------------+----------+---------+

2 rows in set (0.00 sec)

所以,我们看到外面有两株植物,一株在甲板上,另一株在院子里。现在,每株植物今天的平均温度是多少?

mysql> SELECT id, AVG(temperature)

-> FROM readings

-> WHERE DATE(event_time) = CURRENT_DATE() GROUP BY id;

+----+--------------------+

| id | AVG(temperature)   |

+----+--------------------+

|  1 |  38.89000015258789 |

|  2 |  38.12999954223633 |

|  3 | 32.859999656677246 |

|  4 |  33.21000003814697 |

|  5 |  38.89000015258789 |

+----+--------------------+

5 rows in set (0.00 sec)

让我们将两者结合起来,以确保我们从测试数据中得到我们所期望的。我们应该首先检查数据,并试图确定我们应该看到什么。在这种情况下,我们应该看到一个工厂的平均温度为 38.89,另一个工厂的平均温度为 38.13(四舍五入)。

mysql> SELECT name, location, AVG(temperature) as avg_temp

-> FROM plants JOIN readings ON plants.id = readings.id

-> WHERE DATE(event_time) = CURRENT_DATE() AND plants.climate = 2

-> GROUP BY plants.id;

+---------------------+----------+-------------------+

| name                | location | avg_temp          |

+---------------------+----------+-------------------+

| Jerusalem Cherry    | deck     | 38.89000015258789 |

| Moses in the Cradle | patio    | 38.12999954223633 |

+---------------------+----------+-------------------+

2 rows in set (0.00 sec)

没错。现在我们知道查询得到了我们想要的结果。另一个复杂的查询呢?

测试复杂查询

对于复杂的查询,我们想知道今天需要浇水的植物。您已经看到了如何将这个查询分解成几个部分来构造 SQL 语句。让我们看看数据库如何生成每个部分的结果,然后组合它们来验证查询。

让我们从我们创建的视图开始。回想一下,这个视图返回今天获取的那些读数的 ID、soil_status和每个soil_status值的计数。也就是说,我们应该看到今天有多个土壤状态值的植物的几行。

mysql> SELECT *

-> FROM soil_status_today;

+----+-------------+------------+

| id | soil_status | num_events |

+----+-------------+------------+

|  1 | DRY         |         10 |

|  2 | OK          |         10 |

|  3 | DRY         |          4 |

|  3 | OK          |          4 |

|  3 | WET         |          2 |

|  4 | OK          |          6 |

|  4 | WET         |          4 |

|  5 | OK          |         10 |

+----+-------------+------------+

8 rows in set (0.00 sec)

太好了。现在我们知道了今天每株植物的土壤湿度状况。注意一些植物有不止一个值,因为它们的土壤湿度在一天中不断变化。我们要的是那些比其他价值更有“干”价值的植物。但是让我们慢一点。

回想一下,我们使用了一个函数来计算今天采集的给定植物的最大样本。让我们使用这个函数来关注前面结果中的工厂 id。在这种情况下,我们看到几行的soil_status值都较高。让我们使用函数返回找到的每个值的出现百分比。在这种情况下,我将查询所有的植物,计算百分比,以便我们可以更容易地看到所有的数据。我们应该看到的是与之前相同的行,只是添加了平均样本。

mysql> SELECT id, soil_status, num_events, (num_events/max_samples_today(id)) as percent_occurrence

-> FROM soil_status_today;

+----+-------------+------------+--------------------+

| id | soil_status | num_events | percent_occurrence |

+----+-------------+------------+--------------------+

|  1 | DRY         |         10 |             1.0000 |

|  2 | OK          |         10 |             1.0000 |

|  3 | DRY         |          4 |             0.4000 |

|  3 | OK          |          4 |             0.4000 |

|  3 | WET         |          2 |             0.2000 |

|  4 | OK          |          6 |             0.6000 |

|  4 | WET         |          4 |             0.4000 |

|  5 | OK          |         10 |             1.0000 |

+----+-------------+------------+--------------------+

8 rows in set (0.00 sec)

太好了,七排!现在,我们有进展了。现在,让我们将该结果的输出限制为那些出现率高于 50%的结果。

mysql> SELECT id, soil_status, num_events, (num_events/max_samples_today(id)) as percent_occurrence

-> FROM soil_status_today

-> WHERE (num_events/max_samples_today(id)) > 0.50;

+----+-------------+------------+--------------------+

| id | soil_status | num_events | percent_occurrence |

+----+-------------+------------+--------------------+

|  1 | DRY         |         10 |             1.0000 |

|  2 | OK          |         10 |             1.0000 |

|  4 | OK          |          6 |             0.6000 |

|  5 | OK          |         10 |             1.0000 |

+----+-------------+------------+--------------------+

4 rows in set (0.00 sec)

请注意,我们所做的只是添加了一个WHERE子句。现在我们已经得到了今天获得的读数的行,这些行中有超过 50%的soil_status的单个值出现。让我们再一次扩展查询并添加soil_status = 'DRY'的条件。

Tip

你注意到那里有什么东西吗?看soil_status = 'DRY'。注意到有什么奇怪的吗?是的,这是一个枚举列,我使用了其中的一个值,而不是像前面的例子那样使用一个数值。如您所见,只要枚举中列出了值,您就可以使用数值或文本值。如果文本与枚举值之一不匹配,将会出现错误。

mysql> SELECT id, soil_status, num_events, (num_events/max_samples_today(id)) as percent_occurrence                    -> FROM soil_status_today

-> WHERE (num_events/max_samples_today(id)) > 0.50 AND soil_status = 'DRY'

+----+-------------+------------+--------------------+

| id | soil_status | num_events | percent_occurrence |

+----+-------------+------------+--------------------+

|  1 | DRY         |         10 |             1.0000 |

+----+-------------+------------+--------------------+

1 row in set (0.00 sec)

完美!我们快到了。现在,我们想知道那株植物的名称和位置。为此,我们添加了一个新的连接来从plants表中获取信息。请注意,我使用土壤状态列的数值。

mysql> SELECT plants.id, name, location, soil_status, num_events, (num_events/max_samples_today(plants.id)) as percent_occurrence

-> FROM soil_status_today JOIN plants ON soil_status_today.id = plants.id

-> WHERE (num_events/max_samples_today(plants.id)) > 0.50 AND soil_status = 1;

+----+------------------+----------+-------------+------------+--------------------+

| id | name             | location | soil_status | num_events | percent_occurrence |

+----+------------------+----------+-------------+------------+--------------------+

|  1 | Jerusalem Cherry | deck     | DRY         |         10 |             1.0000 |

+----+------------------+----------+-------------+------------+--------------------+

1 row in set (0.01 sec)

啊哈!现在,我们有了。嗯,差不多了。我们有太多的信息。但至少我们可以看到信息是正确的。现在,我们只想知道工厂的名称和位置。让我们稍微限制一下输出。

mysql> SELECT name, location

-> FROM soil_status_today JOIN plants ON soil_status_today.id = plants.id

-> WHERE (num_events/max_samples_today(plants.id)) > 0.50 AND soil_status = 1;

+------------------+----------+

| name             | location |

+------------------+----------+

| Jerusalem Cherry | deck     |

+------------------+----------+

1 row in set (0.00 sec)

就在那里!很简单,对吧?嗯,如果这是您的第一次尝试,就不会太多,但是请注意,当我们使用视图、函数和聚合特性等工具时,编写查询要容易得多!

现在我们已经看到了一个完整的工作示例,让我们考虑一下为 IOT 解决方案设计数据库的建议和最佳实践。 十五

建议和最佳做法

让我们回顾一下最佳实践和建议,看看您应该如何为您的 IOT 解决方案构建数据库。本节介绍了一些设计数据库的技巧。也就是说,本节不能涵盖所有关于数据库设计的知识。事实上,还有很多,特别是对于企业级的数据库设计和管理。因此,它需要几个章节,许多页,以及一整本书的许多倍于这一节的大小来公正地对待它。但是,我觉得在结束关于学习在 IOT 解决方案中使用 MySQL 和数据库的讨论时,有必要提供一份参考资料,供您在规划 IOT 数据库设计时参考。我没有特别的顺序列出它们。

  • 使用正确的数据类型:做好准备工作,确定每列使用的最佳数据类型。避免使用所有字符字段的诱惑,这可能会使比较返回错误的结果,或者使您在试图找出视图或函数失败的原因时感到沮丧。
  • 使用最小的数据类型:尽量为你的数据使用最小的数据类型。例如,避免使用宽字符字段。如果需要,您可以随时增加大小(如果您开始截断数据)。同样,使用与您将存储的最大值相匹配的二进制类型。例如,如果不需要高精度或大数值,就不要使用双精度或长整型数据类型。
  • 使用索引:对于超过几十行的数据查询,考虑在经常查询的列上添加索引。如果正在比较或计算的列上有索引,索引可以极大地改善连接和复杂查询(如范围)。
  • 不要存储冗余数据:尽量避免在多个表中存储相同值的诱惑。这可能会使您的查询更容易编写,但会使更新变得困难。也就是说,对于较大的数据库,您如何知道该列的每次出现在哪里?使用主从关系来简化数据。
  • 计划您的查询:在设计表时,始终包括您想向数据库提出的问题。提前准备查询将有助于以后的开发工作。
  • 避免使用 SELECT:虽然获取一个表的所有列很容易,但是在列规范中使用对于大型表或有很多列的表来说可能不是最佳选择。我们在前面的复杂示例中看到了这一点。唯一需要的列是名称和位置。因此,您应该抵制获取所有列的诱惑,而是指定您想要的列。
  • 对固定或很少更改的数据使用查找表:与冗余数据类似,使用静态(或很少更新的)表有助于减少在结果集中传递和存储的额外数据量。存储一个较短的整数或类似的键比在整个表中存储一个或多个字段要有效得多。
  • 尽可能使用数据库服务器的能力:设法将聚合、数学函数的处理,尤其是计算开销大的任务(如日期和字符串操作)卸载到数据库服务器上。不能低估视图、函数、触发器等的力量。精通这些概念可能需要一些时间,但回报可能意味着在您的 IOT 网络节点中使用更小、更便宜的组件。
  • 使用好的、连贯的名称:尽量在数据库对象中使用描述性的、连贯的名称。抵制使用 a、b、c 等作为对象或列名来节省编码击键的诱惑。如果除了你之外的其他人试图弄清楚这些数据描述了什么,他们将会完全迷失。稍微啰嗦比简洁好。最后,避免使用首字母缩写或非标准缩写,因为它们会混淆意思。比如什么是 kdxprt?儿童专家?没有。我不得不问设计师——它的意思是(以一种政治上正确的方式),“前父母。”别这样。
  • 在主表上使用主键:对于任何可以唯一标识的数据,使用可以唯一标识每一行的主键。主键中可以使用多个列。对于列不唯一标识行的表,可以使用AUTO_INCREMENT整数数据类型添加代理或人工主键。
  • 避免宽表:如果你的表包含 20 个或更多的列,很可能它的设计很差。更具体地说,用数据库术语来说,它不是规范化的。再次查看数据,确定所使用的列是否可以移动到另一个表中。寻找本质上被引用、不经常改变或冗余的列。
  • 不要丢弃数据:您应该始终保留表中的原始数据。如果您的数据库存储传感器读数,请存储原始值。如果您需要一个计算列来使阅读或查询更容易,可以像我展示的那样将它们存储在工厂监控数据库中,但要尽量使这些列简单,并使用触发器来设置值,而不是在您的数据或传感器节点上编写代码。这将代码隔离到一个单独的位置,并且一旦经过测试就可以依赖。拥有原始数据将允许您在将来计划查询,而您可能没有考虑过需要原始数据。
  • 避免存储二进制数据:虽然数据库系统允许您存储大型的二进制数据(在 MySQL 中是 BLOB 或二进制大型对象),但对于以这种方式存储许多行的表来说,它们效率不高,也不是一个好的选择,尤其是在它们没有变化的情况下。例如,如果您想要存储与数据项(主表中的一行)相关联的照片,您应该考虑创建一个字段来存储图像的路径,并将其存储在数据库服务器上。虽然这产生了一个新问题——改变路径——但是它从表中删除了 BLOB,并且可以使查询更有效。
  • 规范化您的数据库:规范化对于许多数据库专家来说是一件大事,这是理所当然的,但是对于爱好者和业余爱好者来说,将您的数据库转换成一种更高级的范式(根据关系数据库理论)可能是太多的工作了。也就是说,我鼓励你去争取第三范式, 16 但不要为了达到那里而自毁前程。试图过度规范化可能会很快毁掉一个大型数据库。如果为了获得简单性而不损失效率,少量的欠标准化是允许的。
  • 编码前设计数据库:总是在定义了高级需求之后、实现源代码之前设计数据库。这可能听起来很落后,但这是一个很好的习惯。
  • 测试测试测试!我怎么强调这一点都不为过。花时间测试数据库,更重要的是用已知(测试)数据进行查询,这将为您以后开发查询和显示数据的应用省去很多麻烦。
  • 备份数据:一旦您的解决方案开始运行,就对数据进行备份。如果您没有大量数据,mysqldump、MySQL Utilities、 17 等工具可以以 SQL 形式对您的数据进行逻辑备份,您可以在需要时恢复这些数据。如果您的数据较大,比如千兆字节或更多,您应该考虑商业备份解决方案,如 MySQL Enterprise Backup。
  • 记录您的数据库:如果您的数据库只包含一个或少数几个表和少量数据,这可能看起来是额外的工作,但是想象一下,如果您的解决方案运行多年而没有出现问题,然后有一天您需要添加新功能或解决问题,会发生什么情况。如果您不知道数据库存储了什么或者它是如何产生结果的(视图、触发器等等),您可能会花很多时间去挖掘错误的线索。您可以用多种方式记录数据库。我喜欢将 SQL 语句存储在一个文件中,并为每个对象编写简短的描述。有关示例,请参见本章的示例代码。

摘要

MySQL 数据库服务器是一个强大的工具。鉴于 MySQL 作为互联网数据库服务器在市场上的独特地位,IOT 开发者(以及许多初创公司和类似的互联网公司)选择 MySQL 作为他们的 IOT 解决方案也就不足为奇了。该服务器不仅功能强大且易于使用,还可以作为免费的社区许可证获得,您可以使用它来将您的初始投资控制在预算之内。

在本章中,您了解了使用数据库服务器的一些功能,数据库服务器如何存储数据,如何发出创建数据库和存储数据的表的命令以及检索数据的命令。虽然这一章只介绍了 MySQL 的一个小入门,但是您已经通过示例 IOT 解决方案学习了如何开始使用您自己的 IOT 数据。您自己的 IOT 解决方案很可能在范围上是相似的(但可能不是相同的数据库对象或表布局[模式])。

在下一章中,您将看到如何使用 Raspberry Pi 构建一个数据库节点。您将看到如何利用本章中关于数据存储方式和位置的示例,使用低成本的计算机来构建健壮的 MySQL 服务器。在其他主板上安装 MySQL 的过程是相似的。

Footnotes 1

即使是对数据及其形式的初步了解,对于成功的数据库配置也是至关重要的。

2

根据 GNU ( http://gnu.org/philosophy/free-sw.html ),“自由软件是自由的问题,不是价格的问题。为了理解这个概念,你应该把“免费”理解为“言论自由”,而不是“免费啤酒”。"

3

有时称为 MySQL 监控器、终端监控器,甚至是 MySQL 命令窗口。

4

如果您是 Oracle 的付费客户,并且拥有 MySQL 的订阅或支持协议,请联系您的销售代表了解详细信息。

5

你读过这些,是吗?

6

C.数据库关系模型:回顾与分析。

7

C.J. Date 和 H. Darwen,《未来数据库系统的基础:第三宣言》(雷丁,麻省,Addison-Wesley,2000 年)。

8

http://en.wikipedia.org/wiki/ACID

9

现在,这是一个专业回流可能做不到的技巧。 https://en.wikipedia.org/wiki/Stevie_Starr

10

不包括直接的引擎级查询,如 NoSQL 使用 NDB。

11

https://en.wikipedia.org/wiki/Null_%28SQL%29

12

如果你像我一样是关系数据库专家,那么在数据库系统中,尤其是 SQL 中,像这样的排序概念会让我毛骨悚然。“无序”的概念到此为止!

13

有关多行插入命令的附加条件和差异,请参见联机参考手册。

14

嘿,这是常有的事。虽然这是虚构的,但我和妻子发现了一种我们认为是一回事的植物,当它开始开花时,结果却是另一回事。

15

实际上,遵循任何数据库设计的最佳实践。

16

https://en.wikipedia.org/wiki/Database_normalization

17

http://dev.mysql.com/downloads/utilities/

六、构建低成本的 MySQL 数据节点

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​6) contains supplementary material, which is available to authorized users.

数据节点是 IOT 解决方案中的一个关键组件。您的解决方案可以使用一个或多个数据聚合器将数据发送到云中的数据库服务器或解决方案本身中的一个或多个数据库服务器。如果您的 IOT 解决方案使用定制设计的硬件,您甚至可以在嵌入式计算组件上集成一个数据库服务器。无论选择哪一种,您都需要了解更多关于在解决方案中使用数据库服务器的信息。

至少,您需要知道如何让解决方案中的这些节点或组件向数据库服务器发送数据。这可以是具有微控制器的传感器节点,该微控制器将数据发送到数据聚合器,传感器节点将数据发送到数据聚合器,或者数据聚合器将数据发送到数据库服务器。

虽然数据库服务器有多种选择,包括台式机或服务器计算机,但 IOT 解决方案倾向于使用较小的计算设备,如您在第 3 章中看到的设备,如单板计算机。例如,您可以使用像 pcDuino 这样的小型 PC,像 Raspberry Pi、Beaglebone Black 或 Intel Galileo 这样的单板计算机。

因为 Raspberry Pi 是更受欢迎的选择之一,所以在这一章中我将重点放在 Raspberry Pi 上,但是我包括了关于其他平台的注释,以防你想使用它们。请记住,其中一些平台仍在发展中,可能需要比树莓派更多的工作。在讨论完 Raspberry Pi 之后,您应该会了解到其他平台所需的背景知识。

本章从传感器网络的角度介绍了如何在 IOT 解决方案中使用数据节点,也就是说,该解决方案使用节点网络来分配处理成本或物理分配(例如读取大型农业或工业综合体周围的传感器)。

在本章中,您将学习如何创建一个数据节点(数据库服务器),本章首先简要介绍了 Raspberry Pi,然后介绍了如何使用 Raspberry Pi 建立一个 MySQL 服务器。您还将了解如何从传感器或数据聚合节点连接到数据库服务器。

让我们先来看看树莓派。

介绍树莓派

Raspberry Pi 是一款小型廉价的个人电脑。尽管它缺乏内存扩展能力,也不能容纳诸如 CD、DVD 和硬盘驱动器等机载设备,但它拥有一台简单的个人电脑所需的一切。树莓派已经有了几次迭代。最新版本的树莓派 2B ( http://raspberrypi.org/products/raspberry-pi-2-model-b/ )有四个 USB 端口、一个以太网端口、HDMI 视频,甚至还有一个用于声音的音频连接器。

树莓派 2B 有一个微型 SD 驱动器 2 ,你可以用它将电脑启动到几个 Linux 操作系统中的任何一个。您只需要一台 HDMI 显示器(或带 HDMI 转 DVI 适配器的 DVI)、一个 USB 键盘和鼠标,以及一个 5V 电源,就可以开始运行了!

Note

我用术语micro SD来指代特定的介质,用SD来抽象地指代驱动器或卡。

您也可以使用电脑上的 USB 端口为您的 Raspberry Pi 供电。在这种情况下,您需要一根 USB A 型公线到 micro-USB B 型公线。将 A 型插头插入电脑的 USB 端口,将微型 USB B 型插头插入 Raspberry Pi 电源端口。

在树莓派问世的几年里,它已经有了很多改进。但是最大的改进是在支持领域。Raspberrypi.org组织非常努力地改善新用户的初始体验。有一个易于使用和导航的网站,将所有旧的、难以找到的维基、列表、图表和博客合并到一个中心位置。你可以在 Raspberrypi.org 找到任何你需要的东西。图 6-1 显示了主页面的摘录。

A978-1-4842-1293-6_6_Fig1_HTML.jpg

图 6-1。

Raspberrypi.org main page (courtesy of Raspberrypi.org)

顶部的菜单提供了他们的大量博客的链接,包括示例和操作方法文章,所有可用操作系统和工具的综合下载页面,社区 Raspberry Pi 项目,文档(帮助),论坛,以及教师,学生和开发者的资源。

还有一个到 Raspberry Pi 在线商店的链接(见右边带有 Raspberry Pi 标志的标签),在那里你可以购买电路板、配件、赠品等等。这家商店位于英国,但如果你想找一个离你更近的经销商,这里有网上零售商的链接。

Raspberry Pi 板有几种版本,裸板价格低至 35 美元(新的 Raspberry Pi Zero 为 5 美元)。也可以从 Sparkfun 和 Adafruit 等电子产品供应商那里在线购买。大多数供应商都有一系列经过测试和验证可以与 Raspberry Pi 一起使用的附件。这些设备包括小型显示器、微型键盘,甚至用于安装主板的机箱。

在本节中,您将探索树莓 Pi 2B 的起源,浏览硬件连接,并了解开始使用树莓 Pi 需要哪些配件。

高贵的出身

Raspberry Pi 旨在成为一个探索计算机科学主题的平台。设计者看到了提供廉价、易操作的计算机的需要,这些计算机可以被编程以与诸如伺服电机、显示设备和传感器之类的硬件交互。他们还想打破在个人电脑上花费数百美元的模式,从而让更多的人可以使用电脑。

设计者观察到学生进入计算机科学课程的体验下降。学生们在进入他们的学年时,对计算机系统、硬件或编程很少或没有经验,而不是在编程或硬件方面有一些经验。相反,学生们精通互联网技术和应用。其中一个因素是个人电脑的成本更高,更复杂,这意味着父母不愿意让他们的孩子在家用电脑上做实验。

这给学术机构带来了挑战,它们不得不调整课程,让计算机科学迎合学生的口味。由于学生缺乏兴趣或能力,他们不得不放弃较低级别的硬件和软件课题。学生们不再想学习计算机科学的基础知识,如汇编语言、操作系统、计算理论和并发编程。相反,他们希望学习更高级的语言来开发应用和 web 服务。因此,一些学术机构不再提供基础计算机科学的课程。这可能会导致未来几代计算机专业人员知识和技能的流失。

为了应对这种趋势,树莓派的设计者认为,配备了正确的平台,年轻人可以回到个人电脑的实验中,因为在那个时代,个人电脑需要更大的承诺来学习系统和编程,以满足你的需求。例如,久负盛名的 Commodore 64、Amiga 以及早期的苹果和 IBM 个人电脑提供的软件有限。拥有许多这样的机器后,我在很小的时候就接触到了编程的奇迹和发现。 4

Why Is It Called Raspberry Pi?

这个名字部分来源于设计委员会的贡献,部分是为了延续以水果命名新计算平台的传统(想想吧)。Pi 部分来自 Python,因为设计者希望 Python 成为计算机编程的首选语言。但是,也有其他编程语言可供选择。

Raspberry Pi 试图提供一个鼓励实验的廉价平台。以下部分将探讨更多有关 Raspberry Pi 的信息,包括可用的型号、所需的附件以及在哪里购买主板。

模型

Raspberry Pi 板的选择已经发展到包括四种型号,有几个版本和迭代。我在这里概述了每一种选择。图 6-26-4 显示了每个的表示。

  • Raspberry Pi 2 Model B:第二代主板,具有更快的处理器和 1Gb RAM,但保留了 Model B 的布局。
  • Raspberry Pi 1 型号 B+:具有更多 GPIO 引脚、更多 USB 端口(四个)和一个 micro SD 卡插槽的新一代主板。它也比旧的主板需要更少的能量。
  • Raspberry Pi Model A+:与 Model B 具有相同的新功能,但价格更低,USB 端口更少,没有以太网,占地面积略小。
  • 计算模块开发套件:集成了一个新的、更小的边缘安装的 Raspberry Pi 模块和扩展的 GPIO 引脚(120 个而不是 40 个)。它是为工业应用而设计的。

A978-1-4842-1293-6_6_Fig4_HTML.jpg

图 6-4。

Raspberry Pi Compute Module Development Kit (courtesy of Raspberrypi.org)

A978-1-4842-1293-6_6_Fig3_HTML.jpg

图 6-3。

Raspberry Pi Model A+ (courtesy of Raspberrypi.org)

A978-1-4842-1293-6_6_Fig2_HTML.jpg

图 6-2。

Raspberry Pi 2 Model B and Pi 1 Model B+ (courtesy of Raspberrypi.org) Note

在写这篇文章的时候,树莓派 Zero 发布了。它拥有与最初的 Raspberry Pi 相同的处理器和内存,但缺少以太网和辅助视频端口。这样做是为了将成本保持在令人惊讶的 5.00 美元的低水平,并将外形尺寸缩小到一包口香糖的大小。

What Happened To The $20 Raspberry Pi?

如果你一直在各种媒体上关注 Raspberry Pi 的发布,你可能听说过这款主板的售价仅为 25 美元。然而,大多数零售商列出的树莓派售价为 35 美元或更高。为什么会这样?

简单的答案是 A+型的价格是 25 美元,而 B 型的价格是 35 美元。这是因为型号 B 有一些更多的功能,特别是以太网。如果您不需要以太网或其他 B 专用选项,您可以通过购买 A+来节省一点。

然而,由于供求关系,你可能会看到两种主板(在美国)的平均价格都在 40 美元或以上。明智购物。

6-2 很好的代表了 Model B 系列。圆周率 2 和圆周率 1 很难区分。你必须检查板上的印刷来确定差异。有细微的差别,但很难看出来。幸运的是,由于它们使用相同的布局,大多数 B 型机箱将适合两种板。

本章和其余章节中的示例使用 B 型变型。

董事会之旅

不比一副扑克牌大多少,Raspberry Pi 板包含许多用于连接设备的端口。本节介绍电路板的概况。如果你想跟着你的冲浪板走,拿着它,让树莓派的标志朝上。我将顺时针绕着棋盘工作。

在近侧的中央,您可以看到一个 HDMI 连接器。左边是一个 microUSB 连接器,用于为电路板供电,右边是一个音频端口。众所周知,某些主板上的电源连接器有点脆弱,所以插拔时要小心。使用您的树莓派时,请务必避免给这根电缆带来额外的压力。

HDMI 端口是连接显示器的主要方式。但是,左侧有一个小的带状电缆连接器,称为 DSI 视频连接器。7 英寸树莓派触摸显示屏( http://element14.com/community/docs/DOC-78156?ICID=hp-7inchpidisplay-ban )可以连接到这里,提供真正漂亮的小型平板电脑体验。图 6-5 显示树莓 Pi 触摸显示屏。

A978-1-4842-1293-6_6_Fig5_HTML.jpg

图 6-5。

Raspberry Pi 7" Touch Display (courtesy of Raspberrypi.org)

以太网端口后面还有一个摄像头连接器,用于连接摄像头(对许多应用都很有用)。

Note

一些有进取心的制造商已经为这种新显示器开发了 Shell,包括可以 3D 打印的平板电脑。最新的原型和设计见 http://thingiverse.com

主板底部左侧是底部的微型 SD 卡插槽。

在电路板的另一侧和顶部是通用输入/输出(GPIO)接头(双排引脚),可用于连接传感器和其它器件。

电路板的右侧是大多数连接器的位置。有四个 USB 连接器和一个以太网连接器。连接到 Raspberry Pi 上的 USB 端口的外部供电 USB 集线器可以为一些电路板供电,但建议您使用连接到 micro-USB 连接器的专用电源。

花点时间检查一下电路板的顶面和底面。如您所见,元件安装在两侧。这与大多数只在一面有元件的电路板不同。Raspberry Pi 两面都有组件的主要原因是它使用多层进行跟踪。这使得电路板可以更小,并且可以使用两个表面。这可能是考虑使用 Shell 的最有说服力的理由——保护电路板底部的元件,从而避免短路和电路板故障。

所需附件

Raspberry Pi 是作为裸系统板出售的,没有机箱、电源或外围设备。根据您计划如何使用 Raspberry Pi,您需要一些常见的附件。如果你像我一样一直在积累备用物品,快速翻一翻你的商店可能会找到你需要的大部分物品。

如果你想在控制台模式下使用 Raspberry Pi(没有图形用户界面),你需要一个 USB 电源,一个键盘和一个 HDMI 显示器(或 7 英寸触摸显示器)。在 5V 下运行时,电源的最小额定值应为 700mA 或更大。如果您想使用带有图形用户界面的 Raspberry Pi,您还需要一个定点设备(如鼠标)。

如果你不得不购买这些物品,坚持使用没有额外功能的普通品牌和型号。例如,避免使用最新的多功能键盘和鼠标。很有可能,它们需要的驱动程序并不适用于 Raspberry Pi 的各种操作系统。

你还必须有一个微型 SD 卡。我推荐 8GB 或者更高的版本。回想一下,micro SD 是唯一可用的板载存储介质。您需要将操作系统安装到卡上,并且您创建的任何文件都将存储在卡上。我将在后面的部分演示这一点。

如果您想在应用中使用声音,您还需要一套支持标准 3.5 毫米音频插孔的有源扬声器。最后,如果您想将 Raspberry Pi 连接到互联网,您需要一根以太网电缆或兼容 Raspberry Pi 的 USB Wi-Fi 加密狗。

How Can I Tell If My Device Will Work?

如果你想确保你的设备能和树莓派一起工作,最简单的方法就是尝试一下!如果你不想碰运气,你可以在 http://elinux.org/RPi_VerifiedPeripherals 查看树莓派硬件兼容性列表。该列表包含许多设备和社区中测试过这些设备的各种用户的评论。如果您刚刚开始使用 Raspberry Pi,请寻找需要很少或不需要额外配置或驱动程序的设备。

推荐配件

我强烈建议至少添加小的橡胶或硅胶自粘缓冲垫,以使电路板远离您的办公桌。电路板底部有许多尖尖的插脚,可能会接触到导电材料,从而导致短路,更糟糕的是,可能会出现树莓皮。这些保险杠在大多数家装和五金店都有售。

如果您计划将板从一个房间移动到另一个房间,或者您想要确保您的 Raspberry Pi 受到良好的保护以防意外损坏,您应该考虑购买一个盒子来放置板。有许多箱子可供选择,从简单的卡扣模型到由激光切割丙烯酸树脂甚至轧制铝制成的模型。以下列表包括几个不错的选择,从便宜的到顶级的豪华包都有:

另一个选择是用 3D 打印机打印自己的案例。如果你没有 3D 打印机,你可以在当地的库、社区大学或者朋友的朋友那里找到。如果你友好地提出要求,大多数 3D 打印机爱好者很可能会乐意为你打印一个案例。事实上,有许多这样的案例设计可供下载和打印。图 6-6 显示了我在 3D 打印机上打印的一张。

A978-1-4842-1293-6_6_Fig6_HTML.jpg

图 6-6。

3D-printed Raspberry Pi 2 Model B+ case Tip

如果您计划使用 GPIO 引脚进行试验,或者需要接触电源测试引脚或位于主板内部的其他端口,您可能需要考虑使用自粘式缓冲垫选项,或者订购顶部开口的机箱,以便于接触。如果经常开关,有些箱子容易破损。

除了机箱之外,您还应该考虑购买(或从备件中取出)一个带电源的 USB 集线器。USB 集线器电源模块应为 700–1000 毫安。如果您计划使用消耗大量电力的 USB 设备,如 USB 硬盘驱动器或 USB 软导弹发射器,则需要一个供电集线器。

Caution

因为板子很小,所以很容易把它用在不稳定的地方,比如在行驶中的汽车上或凌乱的桌子上。确保您的 Raspberry Pi 放在安全的地方。电源、HDMI 和 micro SD 卡插槽似乎是最容易受到攻击的连接器。

去哪里买

树莓派在欧洲已经有一段时间了。越来越容易找到,但很少有实体店有树莓酱。幸运的是,许多在线零售商都有它的存货,还有许多已知与树莓派配合使用的配件。以下是一些比较受欢迎的在线零售商,提供了他们的 Raspberry Pi 目录条目链接:

A Raspberry Pi Laptop?

Raspberry Pi 对物理计算做出了重大贡献。它不仅能实现更复杂的传感器节点,还能制造出一台相当轻便的通用计算机。有了合适的显示器、鼠标和存储设备,您就可以完成大多数互联网和一般的工作任务。事实上,有些人已经把他们的家用台式电脑换成了树莓派!

如果你像我一样,你需要能够在任何地方工作, 5 考虑到你必须有单独的显示器和键盘,使用树莓派可能不是很方便。如果你可以带着你的树莓派,那不是很棒吗?现在你可以了!

你需要的是来自摩托罗拉的多余的刺桐。Lapdock 最初设计用于将 Aria 手机用作笔记本电脑,提供了 11.6 英寸 HDMI 显示器、USB 键盘、鼠标、双端口 USB 集线器和扬声器。更重要的是,它是电池供电的,可以轻松地为树莓派供电。Lapdock 具有迷你 HDMI 和迷你 USB 端口,可以连接到 Raspberry Pi,而无需修改 Lapdock。

但有一个问题:你必须购买一个迷你 HDMI 母到母适配器和一个迷你 HDMI 公到 HDMI 公电缆 6 ,并用一根微型 USB 延长线和一根 A 型 USB 线制作自己的弗兰肯斯坦 USB 线。需要定制电缆来允许 Raspberry Pi 使用 USB 键盘和鼠标以及给电路板供电。下图显示了电缆的结构。可以在 www.adafruit.com/blog/2012/09/10/cables-adapters-for-the-atrix-raspberry-pi-laptop/ 找到详细的教程视频。

A978-1-4842-1293-6_6_Figa_HTML.jpg

要制作线缆,从标准 USB 线缆上切下一个普通的 USB A 型连接器, 7 将一根标准微型 USB 延长线一分为二,并拼接电线,如下图所示。您可以放弃未显示连接的末端中的导线。微型 USB 插头将用于为 Raspberry Pi 供电,USB A 型插头将提供与 Lapdock 键盘和鼠标的连接。一旦你做好了这根电缆,你就可以开始工作了。为了获得最佳效果,我推荐一个坚固的盒子来装你的树莓派,以免在运输过程中损坏它。

要使用 Raspberry Pi 笔记本电脑,首先连接 HDMI 电缆,然后连接任何外围设备(如硬盘),然后连接 USB 电缆;打开船坞的盖子。几秒钟之内,您的新笔记本电脑就可以使用了!下图显示了折叠门下 Lapdock 后部的端口,后一张图显示了笔记本电脑的运行情况。

A978-1-4842-1293-6_6_Figc_HTML.jpg

A978-1-4842-1293-6_6_Figb_HTML.jpg

您不仅可以将此解决方案用于您的 Raspberry Pi,其他几个电路板也同样适用。我已经成功地使用 Lapdock 为其他电路板供电。事实上,Lapdock 是一款出色的 pcDuino 笔记本电脑!即使您的主板不支持键盘或鼠标,HDMI 屏幕也很方便。

这个项目最好的部分是成本。你可以在拍卖网站和类似的电子清仓商店找到二手和多余的 Lapdock。例如,在易贝,鳕鱼的价格约为 60 美元。您也可以在网上或大多数电子商店找到这些电缆。然而,女性对女性的迷你 HDMI 适配器有点难找到。我在易贝从中国的经销商那里买了一辆。运输速度惊人的快,而且费用合理。我的手机树莓派(不包括树莓派)价格不到 100 美元。

下一节将介绍一个关于开始使用 Raspberry Pi 的简短教程。如果你已经学会了如何使用 Raspberry Pi,你可以浏览这一部分,看看在启动和运行 Raspberry Pi 方面的最新改进。

树莓派教程

以下部分提供了一个简短的教程,介绍如何开始使用新的 Raspberry Pi,从裸板到完全可操作的平台。许多优秀的作品更详细地讨论了这个主题。如果你发现自己被困住了,或者想知道更多关于开始使用 Raspberry Pi 和 Raspbian 操作系统的信息,请参阅 Peter Membrey 和 David Hows 的《在 Linux 上学习 Raspberry Pi》(a press,2012)。如果您想了解更多关于在硬件项目中使用 Raspberry Pi 的信息,Brendan Horan (Apress,2013)的《实用 Raspberry Pi》是一个很好的资源。

如“所需附件”一节所述,您需要一个微型 SD 卡、一个额定电流为 700mA 或更高的 USB 电源(带公微型 USB 连接器)、一个键盘、一个鼠标(可选)以及一台 HDMI 显示器、一台 HDMI 电视或一台带 HDMI 适配器的 DVI 显示器。然而,在您启动您的 Raspberry Pi 并享受它的光辉之前,您需要为您的 micro SD 卡创建一个启动映像。

选择启动映像(操作系统)

你需要做的第一件事是决定你想要使用哪个操作系统版本。有几个很好的选择,包括标准的 Raspbian“Jessie”变体。每一个都可以作为一个称为图像或卡片图像的压缩文件。你可以在 Raspberry Pi foundation 下载页面上找到推荐图片的列表以及下载每张图片的链接: www.raspberrypi.org/downloads 。以下图片可在网站上找到:

  • Raspbian (Jessie):基于 Debian 的官方操作系统,包含一个图形用户界面(轻量级 X11 桌面环境[LXDE])、开发工具和基本的多媒体功能。
  • Ubuntu Mate:以 Ubuntu 桌面和缩小版的 Ubuntu 操作系统为特色。如果你熟悉 Ubuntu,你会觉得这个版本很舒服。
  • Snappy Ubuntu Core:核心 Ubuntu 系统开发者版;与 Mate 相同,但增加了开发者核心实用程序。
  • 视窗 10 IOT 核心:视窗 10 为 IOT。微软首屈一指的 IOT 操作系统。是的,它看起来和感觉上确实像 Windows,但是没有笨重的图形用户界面。
  • OSMC:开源媒体中心。给自己建一个媒体中心。
  • OpenElec:开放式嵌入式 Linux 娱乐中心。另一个媒体中心选项。
  • PiNet:教室管理系统。教育工作者在课程中使用树莓 Pi 的特别版。
  • RISC OS:非 Linux,类 Unix 操作系统。如果您知道 IBM AIX 是什么,或者您使用过其他 Unix 操作系统,您就会认识到这个怪物。

Tip

如果您刚刚开始使用 Raspberry Pi,您应该使用 Raspbian 图像。本书中的例子也推荐使用这个图像。

还有一些其他的图像选择,包括来自 Adafruit 的 Raspbian 图像的特殊变体。Adafruit 将其图像称为occidentals,包括预装的许多应用和实用程序,包括 Wi-Fi 支持和几个实用程序。一些树莓派的例子——尤其是来自 Adafruit 的例子——需要西方人的形象。您可以在 http://learn.adafruit.com/adafruit-raspberry-pi-educational-linux-distro/overview 找到更多关于图像的信息并下载。

安装启动映像有两种方法。首先,你可以使用名为新的开箱即用软件(NOOBS 8 )的自动化图形用户界面平台,或者你可以从头开始将你的映像安装到一个微型 SD 驱动器上。两者都需要下载并格式化 micro SD 驱动器。

如果你刚刚开始,NOOBS 的解决方案是最简单的。这将需要更长一点的时间(但不会太长)并简化过程。除了格式化微型 SD 卡,一切都是自动化的。我将在下面的小节中介绍这两种选择。

使用 noob

到目前为止,NOOBS 是让你的树莓派运转起来的最好方式。使用 NOOBS,您可以下载一个包含 Raspbian Jessie 的基本安装程序映像。您可以选择安装它或配置 NOOBS 下载其他图像之一,并安装它。但首先,你必须得到 NOOBS 启动图像,并将其复制到您的微型 SD 驱动器。

http://raspberrypi.org/downloads/noobs/ 下载 NOOBS 安装程序开始。您将看到两个选项,一个包含 Raspbian 映像的网络安装程序(有时称为offline installer)或一个不包含任何操作系统的基础映像。如果您想将自动安装程序与尚未包含的操作系统(如 Adafruit 的版本)一起使用,可以使用这个基础映像。

Tip

如果你的下载带宽有限,网上零售商会提供一个预配置的包含 NOOB 的微型 SD 卡。事实上,你经常可以找到任何流行的 Raspberry Pi 操作系统的微型 SD 卡。插上电源就可以走了。它们通常比同样大小的空白卡贵几美元。 9

一旦你下载了安装程序(到目前为止大约 1.4Gb),你将需要格式化一个至少 8Gb 的微型 SD 卡。根据您的桌面平台,您可以使用多种方法来实现这一点。如果您使用 Mac OS X,您可以使用“磁盘工具”来格式化驱动器。或者您可以使用适用于 Windows 或 Mac OS X 的 SD Formatter 4.0 实用程序( http://sdcard.org/downloads/formatter_4/ )。只需下载应用并安装它。然后将您的 micro SD 卡插入读卡器并启动应用。确认选择了正确的媒体后,输入卡的名称(我使用的是 NOOBS ),然后点按“格式化”。图 6-7 显示了 SDFormatter 应用。

A978-1-4842-1293-6_6_Fig7_HTML.jpg

图 6-7。

SDFormatter 4.0 But, I Don’T Have An Sd Card Reader!

您的计算机上必须连接有 SD 卡读写器。有些系统内置了 SD 卡驱动器(联想笔记本电脑、苹果笔记本电脑和台式机等等)。如果您没有 SD 读卡器,您可以在任何出售电子产品或照片设备的地方找到 USB SD 读卡器。大多数阅读器可以接受各种格式,包括 micro SD 或通过 micro SD 转 SD 适配器的 micro SD。

格式化 micro SD 卡后,现在必须将 NOOBS 映像的内容复制到卡上。右键单击您下载的文件,并选择选项来解压缩文件。这将创建一个包含 NOOBS 图像的文件夹。将所有这些文件(不是外部文件夹)复制到 SD 卡并弹出。现在,您已经准备好启动到 NOOBS 并安装您的操作系统。当此过程完成后,安全地取出 SD 卡,并将其插入您的 Raspberry Pi。

现在,您可以连接所有外围设备了。我喜欢把事情简单化,只连接一个显示器、键盘和(对 NOOBS 来说)一个鼠标。如果你想下载 Raspbian 以外的操作系统,你还需要将你的 Raspberry Pi 连接到你的网络。

一旦你的 Raspberry Pi 开机,你会看到各种信息的滚动显示。这是正常的,在 NOOBS 开始前可能会滚动一段时间。当加载 NOOBS 时,你会看到一个类似于图 6-8 的屏幕。

A978-1-4842-1293-6_6_Fig8_HTML.jpg

图 6-8。

NOOBS startup screen

请注意,您将在操作系统列表中看到 Raspbian 映像。要安装它,只需勾选缩略图旁边的复选框,然后单击安装。但是,请注意底部的两个框。这将设置在 NOOBS 使用的语言和键盘。它不影响 Raspbian 的设置。

一旦你开始安装,你会看到一系列的对话框,因为 Raspbian 开始安装。这可能需要一段时间。好消息是对话框提供了很多有用的信息来帮助你开始。您将了解如何登录 Raspbian,配置和定制的技巧,以及如何充分利用您的体验的建议。

安装完成后,在完成的对话框中单击 OK,然后等待 Raspberry Pi 重新启动到 Raspbian。在第一次启动时,您可能会看到 Raspberry Pi 配置对话框。“配置”对话框用于设置您所在地区的时间和日期、启用硬件(如相机板)、创建用户、更改密码等等。图 6-9 显示了配置对话框。

A978-1-4842-1293-6_6_Fig9_HTML.jpg

图 6-9。

Raspbian configuration dialog Tip

您还可以通过打开终端并运行命令raspi-config来运行基于控制台的配置实用程序。

您将看到四个选项卡,可用于更改系统设置。我在下面的列表中简要地解释了每一个,并给出了每一个的推荐设置。完成更改后,单击“确定”关闭对话框。根据您选择的设置,可能会要求您重新启动。

  • 系统:系统的控制板。使用此面板更改 root 密码(强烈推荐)、主机名(可选)、引导类型(如果您想将 Raspberry Pi 设置为无头引导,请使用命令行界面[CLI])和自动登录(不推荐)。
  • 接口:用于启用系统和硬件服务,如摄像头、SSH(推荐)和 GPIO 头的硬件接口。
  • 性能:用于更改处理器的执行方式。你可以选择超频(运行 CPU 更快),但我不建议将这个设置用于将托管数据库或 web 服务器(或两者)的 Raspberry Pi。
  • 本地化:用于设置默认语言、键盘、日期和时间。如果您没有更改任何其他内容,请确保将这些设置为您的本地设置。

Tip

Raspbian Jessie 的默认登录使用用户名pi和密码raspberry。我建议在 Raspberry Pi 配置对话框中进行更改。

要关闭或重新启动 Raspbian,请单击菜单,然后选择关闭。您将看到重启、关机或返回命令行的提示。如果你在命令行,使用命令shutdown -h now关闭系统。

在 Micro SD 卡上安装启动映像

安装启动映像的过程包括选择映像、下载映像,然后将其复制到您的 micro SD 卡。以下部分详细介绍了相关步骤。这是一个手动过程,比 NOOBS 选项稍微复杂一点,但也不过分。

一旦您选择了一个图像并下载了它,您首先解压缩文件,然后将其复制到您的 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 卡。记下磁盘编号。例如,它可能是磁盘 4。

接下来,打开“磁盘工具”并卸载 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 电源。因为树莓派没有开/关开关,所以一通电就会启动。系统启动,然后开始加载操作系统。您会看到一长串语句,这些语句传达了每个子系统在加载时的状态。你不必试图去阅读甚至理解所有出现的行, 10 但是你应该注意任何错误或警告。当引导序列完成时,你会看到一个命令提示符,如图 6-10 所示。

A978-1-4842-1293-6_6_Fig10_HTML.jpg

图 6-10。

Example boot sequence11

可能会提示您输入用户名和密码。默认用户简单来说就是 pi,密码是 raspberry(无引号,全小写)。在提示符下输入,Raspberry Pi 会显示如图 6-11 所示的配置菜单。如果您没有看到这个,您可以通过键入命令raspi-config来启动它。

A978-1-4842-1293-6_6_Fig11_HTML.jpg

图 6-11。

Raspberry Pi configuration menu

配置菜单显示了使用 Raspberry Pi 时您可能想要设置的常见初始选项列表。为了方便起见,它在第一次引导时加载。您可以使用上下箭头键在选项中导航,并使用 Tab 键选择操作按钮。菜单项简要描述如下:

  • 扩展文件系统:使用 SD 卡上的全部空间。
  • 更改用户密码:更改 Pi 用户的密码。如果您的 Raspberry Pi 将连接到网络,尤其是可以从互联网访问,请使用此选项。
  • 引导选项:启用/禁用 GUI 窗口系统的引导。
  • 过扫描:更改视频信号发送到显示器/电视的方式。如果 Raspberry Pi 的输出图像没有填满可用的显示区域,请使用此选项。
  • 国际化选项:更改键盘映射(特定于国家/语言)、时区和语言,设置特定于国家/语言的时间、货币、日期等显示模式。
  • 启用相机:打开/启用相机模块。
  • 添加到 Raspberry Pi:将您的 Raspberry Pi 添加到 Pi 地图,这是世界上树莓 Pi 的全球指标。
  • 超频:更改系统的 CPU 计时设置(也称为速度)。仅限专家。正常的 Raspberry Pi 使用不需要这样。
  • 高级选项:启用/禁用各种系统服务,如 SSH。
  • 关于raspi-config:获取如何使用该工具的信息。

Note

配置菜单的未来版本将包括附加选项。一旦你将你的 Raspberry Pi 连接到互联网并执行了更新选项,查看菜单寻找新选项是个好主意。

第一次引导系统时,您应该使用其中的一些选项。至少,您应该将根文件系统设置为使用整个 SD 卡空间,更改键盘设置,设置您的地区和时区,如果您希望能够远程登录,还应该启用 SSH 服务器。

当您首次初始化 SD 卡上的映像时,该过程不会使用全部可用空间。“扩展文件系统”选项可以帮您做到这一点。在某些情况下,操作完成后系统会重新启动,所以在执行此选项之前,请确保您没有运行其他程序。

通过设置键盘、区域设置和时区,您可以像在 PC 上一样使用 Raspberry Pi。特别是,你的键盘会有你期望的特殊符号;日期、时间和类似的值将正确显示。你的时钟将被设置为正确的当地时间。这些操作可能不需要重新启动。你应该在认真使用 Raspberry Pi 之前设置这些。

在以后的引导中,系统将会启动,并且一旦您登录,它将处于终端模式(除非您选择了在窗口环境中启动的选项)。从这里,您可以使用命令行实用程序探索系统,或者使用startx启动图形用户界面。在继续之前,请花些时间研究一下这个系统。如果您想重启配置会话,请使用命令sudo raspi-config

一旦您的 Raspberry Pi 正在运行,并且您已经花时间探索和学习了系统管理的基础知识,您就可以开始尝试硬件了。

Sd Card Corruption

想象一下这个场景。您正在创建文件、下载文档等等。您的生产力很高,并且您正在享受您的新的低成本、超级酷的树莓派。现在想象一下电源线不小心被踢出墙外,你的树莓 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

既然您已经知道如何设置和运行您的 Raspberry Pi,现在让我们来看看如何将它转变为您的 IOT 解决方案的数据库服务器。

MySQL 安装和设置

是时候弄脏你的手,在你毫无戒心的树莓派上施展魔法了!让我们从给它添加一个 USB 硬盘开始。根据数据的大小,您可能需要认真考虑这样做。

也就是说,如果您的数据很小(从不超过几兆字节),您可以从启动映像 SD 卡使用 MySQL。但是,如果您想确保不会耗尽空间,并使您的数据与您的启动映像分开(这总是一个好主意),您应该安装一个在启动时自动连接的 USB 驱动器。本节详细解释了如何做到这一点。

Tip

请确保您使用高质量的 USB 集线器来托管您的外部驱动器。如果你使用的是传统的主轴驱动,这一点尤为重要,因为它会消耗更多的能量。将外部驱动器直接连接到 Raspberry Pi 可能会剥夺它的电源并导致无尽的沮丧。症状包括随机重启(总是令人惊喜)、命令失败、数据丢失等等。请务必为您的外围设备和 Raspberry Pi 提供充足的电源。

使用什么样的磁盘由您决定。你可以使用 USB 闪存驱动器,如果它有足够的空间和足够的速度(大多数新型号足够快),应该可以正常工作。如果您有额外的固态硬盘或者想要将功耗和热量降至最低,您也可以使用固态硬盘(SSD)。另一方面,你可能有一个额外的硬盘可以使用。本节的示例使用安装在典型 USB 硬盘驱动器 Shell 中的剩余 250GB 笔记本电脑硬盘驱动器。

Tip

使用外部硬盘驱动器(SSD 或传统的主轴驱动器)比访问闪存驱动器上的数据要快得多。它通常每单位(千兆字节)更便宜,或者,正如我提到的,可以很容易地从盈余。

对驱动器进行分区和格式化

在使用与 Raspberry Pi 不兼容的文件系统的新驱动器或现有驱动器之前,必须对驱动器进行分区和格式化。我发现在我的台式电脑上做这个更容易,建议你也这样做。因此,以下假设外部驱动器具有单个 FAT(或 FAT32)分区。这并不重要,因为为了获得最佳性能,我们将删除它并使用 ext4 文件系统创建一个新的分区。

首先,将驱动器连接到树莓派。然后使用fdisk -l命令查看连接的可用磁盘,确定连接了哪些驱动器。如果你有一个标准的 Raspbian 映像,你应该看到你的硬盘被列为/dev/sda。如果您使用不同的映像或您的设备有不同的标签,请在以下步骤中使用您系统中的地址。

一旦您识别了磁盘,再次启动fdisk,将设备作为一个选项,如清单 6-1 所示。

Listing 6-1.Partitioning a Hard Disk on Raspberry Pi

pi@raspberrypi ∼ $ sudo fdisk /dev/sda

Welcome to fdisk (util-linux 2.25.2).

Changes will remain in memory only, until you decide to write them.

Be careful before using the write command.

Command (m for help): m

Help:

Generic

d   delete a partition

l   list known partition types

n   add a new partition

p   print the partition table

t   change a partition type

v   verify the partition table

Misc

m   print this menu

x   extra functionality (experts only)

Save & Exit

w   write table to disk and exit

q   quit without saving changes

Create a new label

g   create a new empty GPT partition table

G   create a new empty SGI (IRIX) partition table

o   create a new empty DOS partition table

s   create a new empty Sun partition table

Command (m for help): p

Disk /dev/sda: 111.8 GiB, 120034123776 bytes, 234441648 sectors

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: gpt

Disk identifier: 790E7C68-F089-45C7-A9E9-D7C2CA56BB31

Command (m for help): n

Partition number (1-128, default 1): 1

First sector (34-234441614, default 2048):

Last sector, +sectors or +size{K,M,G,T,P} (2048-234441614, default 234441614):

Created a new partition 1 of type 'Linux filesystem' and of size 111.8 GiB.

Command (m for help): w

The partition table has been altered.

Calling ioctl() to re-read partition table.

Syncing disks.

这里发生了很多事情。我用粗体突出显示了这些步骤。请注意,首先我显示了该实用程序的帮助菜单,并根据提示使用了m命令。接下来,我使用p命令打印分区表,验证那里没有分区。如果你已经定义了分区并且想要删除它们,你可以使用d命令来完成。

Caution

如果您的驱动器上有一个分区包含您想要保留的数据,请立即中止,并首先将数据复制到另一个驱动器。以下步骤将擦除驱动器上的所有数据!

然后使用命令n创建一个新的分区,并接受缺省值,使用所有的空闲空间。为了检查您的工作,您可以使用p命令来打印设备分区表和元数据。它显示(并确认)新的分区。

如果你担心你可能犯了一个错误,不要惊慌!关于fdisk的伟大之处在于,它不会写或改变磁盘,直到你用wwrite命令告诉它。在示例中,您发出w命令来写入分区表。要查看可用命令的完整列表,您可以使用h命令或运行man fdisk

Tip

对于所有的 Linux 命令,您可以使用命令man <application>查看手册文件。

下一步是用 ext4 文件系统格式化驱动器。这很简单,只需要一个命令:mkfs (make file system)。你把设备名传给它。如果你回忆一下,这是/dev/sda1。即使您创建了一个新分区,它仍然是第一个分区,因为驱动器上只有一个分区。如果您尝试使用不同的分区,请确保使用正确的编号!该命令可能需要几分钟时间来运行,具体取决于驱动器的大小。以下示例显示了该命令的实际效果:

pi@raspberrypi ∼ $ sudo mkfs.ext4 /dev/sda1

mke2fs 1.42.12 (29-Aug-2014)

Creating filesystem with 29304945 4k blocks and 7331840 inodes

Filesystem UUID: 0285ba01-3880-4ee5-8a19-7f47404f1500

Superblock backups stored on blocks:

32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,

4096000, 7962624, 11239424, 20480000, 23887872

Allocating group tables: done

Writing inode tables: done

Creating journal (32768 blocks): done

Writing superblocks and filesystem accounting information: done

现在您有了一个新的分区,并且已经正确格式化了。下一步是将驱动器与引导映像上的挂载点相关联,然后在引导时连接该驱动器,这样每次启动 Raspberry Pi 时,您都不必做任何事情来使用该驱动器。

设置自动驱动安装

Linux 中的外置驱动器通过mount连接(挂载),通过umount断开(卸载)。与某些操作系统不同,不先卸载就拔掉 USB 驱动器通常不是个好主意。同样,您必须先安装驱动器,然后才能使用它。本节说明了安装驱动器并使驱动器在每次引导时自动安装所需的步骤。

我首先讨论安装驱动器并为自动安装做好准备的预备步骤。这些包括在/media文件夹下创建一个文件夹来挂载驱动器(称为挂载点),更改文件夹的权限以允许访问,以及执行一些可选步骤来调整驱动器。

pi@raspberrypi ∼ $ sudo mkdir -p /media/HDD

pi@raspberrypi ∼ $ sudo chmod 755 /media/HDD

pi@raspberrypi ∼ $ sudo tune2fs -m 0 /dev/sda1

tune2fs 1.42.12 (29-Aug-2014)

Setting reserved blocks percentage to 0% (0 blocks)

pi@raspberrypi ∼ $ sudo tune2fs -L MYSQL /dev/sda1

tune2fs 1.42.12 (29-Aug-2014)

pi@raspberrypi ∼ $ sudo mount /dev/sda1 /media/HDD

这些命令很容易识别,是基本的文件和文件夹命令。然而,使用tune2fs (tune file system)的调优步骤用于首先重置用于特权访问的块数(这节省了一点空间),然后将驱动器标记为 MYSQL。同样,这些是可选的,如果你愿意,你可以跳过它们。

Tip

可以用sudo umount /dev/sda1卸载驱动器。

此时,驱动器可以访问并准备好使用。你可以切换到/media/HDD文件夹,创建文件或者做任何你想做的事情。现在,让我们来完成为自动挂载设置驱动器的任务。

最好的方法是通过驱动器的通用唯一标识符(UUID)来引用它。这被分配给这个驱动器,并且只分配给这个驱动器。您可以告诉操作系统将具有特定 UUID 的驱动器挂载到特定的挂载点(/media/HDD)。

还记得之前的/dev/sda设备名称吗?如果您将驱动器插入另一个集线器端口,或者更好的情况是,如果有其他驱动器连接到您的设备,并且您卸载然后再装载它们,则下次引导时设备名称可能会不同!UUID 帮助您确定哪个驱动器是您的数据驱动器,使您不必将驱动器插入特定的端口,并允许您使用其他驱动器,而不必担心如果驱动器被赋予不同的设备名称会破坏您的 MySQL 安装。

要获得 UUID,使用blkid(块 ID)应用。

pi@raspberrypi ∼ $ sudo blkid

/dev/mmcblk0: PTUUID="000575b3" PTTYPE="dos"

/dev/mmcblk0p1: LABEL="RECOVERY" UUID="0761-F2EA" TYPE="vfat" PARTUUID="000575b3-01"

/dev/mmcblk0p3: LABEL="SETTINGS" UUID="13062706-1a48-47bc-9f3a-0ded961267e4" TYPE="ext4" PARTUUID="000575b3-03"

/dev/mmcblk0p5: SEC_TYPE="msdos" LABEL="boot" UUID="07D7-3A9D" TYPE="vfat" PARTUUID="000575b3-05"

/dev/mmcblk0p6: LABEL="root" UUID="c9d8e201-90e5-4d6b-9c8f-92d658fec13c" TYPE="ext4" PARTUUID="000575b3-06"

/dev/sda1: LABEL="MYSQL" UUID="0285ba01-3880-4ee5-8a19-7f47404f1500" TYPE="ext4" PARTUUID="ab7357a8-536a-4015-a36d-f80280c2efd1"

注意粗体的那一行。哇哦!那是一大串。UUID 是一个 128 字节(字符)的字符串。为下一步复制它。

要设置自动驱动器映射,您可以使用一个称为文件系统静态信息的特性(fstab)。这包括位于系统上的/etc文件夹中的一个文件。你可以随意编辑这个文件。如果你来自 Linux 或者 Unix 的老学校,你可能会选择使用 vi。??由此产生的文件如下:

pi@raspberrypi ∼ $ sudo nano /etc/fstab

proc            /proc           proc    defaults          0       0

/dev/mmcblk0p5  /boot           vfat    defaults          0       2

/dev/mmcblk0p6  /               ext4    defaults,noatime  0       1

UUID=0285ba01-3880-4ee5-8a19-7f47404f1500 /media/HDD ext4 defaults.noatime 0 0

# a swapfile is not a swap partition, no line here

#   use  dphys-swapfile swap[on|off]  for that

您添加的行以粗体显示。在这里,您只需添加 UUID、挂载点、文件系统和选项。就这样!您可以使用以下命令重启您的 Raspberry Pi,并在消息滚动时观察屏幕。最终,您会看到驱动器已安装。如果有任何错误,您可以在启动序列中看到它。

$ sudo shutdown –r now

现在您已经准备好构建 MySQL 数据库服务器了!下一节详细介绍了使用 Raspberry Pi 实现这一点所需的步骤。

安装 MySQL 服务器

将 Raspberry Pi 转变成 MySQL 数据库服务器很容易。本节将向您展示如何安装 MySQL,以及如何将它的默认数据目录从您的引导映像移动到您在上一节中连接的新的外部驱动器。

所涉及的步骤包括更新您的能力库(软件包管理器),然后安装 MySQL。虽然这个过程相当漫长,但我觉得最好向您展示整个过程,以防您的基本图像不同或遇到错误。

安装 MySQL

要安装 MySQL 或基础映像中没有的任何软件,您必须连接到互联网。如果您尚未这样做,请使用以太网端口或无线网络设备将您的 Raspberry Pi 连接到互联网。

您可能还记得,您使用的是基于 Debian 的 Raspbian Jessie 发行版。如果您使用其他发行版,它可能有不同的包管理器,本节中的命令可能不起作用。在这种情况下,您应该能够为您的发行版找到类似的命令。

让我们从更新包管理器包头开始。这总是一个好主意,尤其是如果您使用的是几个月前发布的发行版。命令apt-get update告诉系统从已知的主机发行版下载最新的头文件。这可以确保您获得正在安装的任何软件的最新版本。

之后,安装软件就像告诉 aptitude 安装一样简单。诀窍是知道正确的名字。在这种情况下,您要寻找的是mysql-server。清单 6-5 展示了更新 aptitude 和安装 MySQL 的步骤。(为了简洁起见,我省略了一些行。)除了输入命令之外,还会要求您回复询问是否可以下载 MySQL 及其先决条件的提示,并输入 MySQL root 用户的密码。

Note

当您在示例中看到密码 secret 时,它被用作您选择的任何密码的占位符,而不是明确的单词 secret。

让我们从用sudo apt-get update命令更新包管理器开始,如下所示:

pi@raspberrypi ∼ $ sudo apt-get update

Get:1http://archive.raspberrypi.org

Get:2http://mirrordirector.raspbian.org

Get:3http://archive.raspberrypi.org

Get:4http://mirrordirector.raspbian.org

Get:5http://archive.raspberrypi.org

Get:6http://archive.raspberrypi.org

Get:7http://archive.raspberrypi.org

Get:8http://mirrordirector.raspbian.org

Get:9http://mirrordirector.raspbian.org

...

Fetched 9,194 kB in 1min 11s (129 kB/s)

Reading package lists... Done

接下来,让我们用如下所示的sudo apt-get install mysql-server命令安装 MySQL。开始 MySQL 安装后,可能会提示您设置 MySQL 的 root 用户密码。请务必选择一个您容易记住的密码。

pi@raspberrypi ∼ $ sudo apt-get install mysql-server

Reading package lists... Done

Building dependency tree

Reading state information... Done

将安装以下额外的软件包:

libaio1 libdbd-mysql-perl libdbi-perl libhtml-template-perl libmysqlclient18 libterm-readkey-perl mysql-client-5.5 mysql-common

mysql-server-5.5 mysql-server-core-5.5

Suggested packages:

libclone-perl libmldbm-perl libnet-daemon-perl libsql-statement-perl libipc-sharedcache-perl mailx tinyca

将安装以下新软件包:

libaio1 libdbd-mysql-perl libdbi-perl libhtml-template-perl libmysqlclient18 libterm-readkey-perl mysql-client-5.5 mysql-common

mysql-server mysql-server-5.5 mysql-server-core-5.5

0 upgraded, 11 newly installed, 0 to remove and 3 not upgraded.

Need to get 8,121 kB of archives.

After this operation, 88.8 MB of additional disk space will be used.

Do you want to continue? [Y/n] y

Get:1http://mirrordirector.raspbian.org/raspbian/

Get:2http://mirrordirector.raspbian.org/raspbian/

Get:3http://mirrordirector.raspbian.org/raspbian/

...

Setting up libaio1:armhf (0.3.110-1) ...

Setting up libmysqlclient18:armhf (5.5.44-0+deb8u1) ...

Setting up libdbi-perl (1.631-3+b1) ...

Setting up libdbd-mysql-perl (4.028-2+b1) ...

Setting up libterm-readkey-perl (2.32-1+b2) ...

Setting up mysql-client-5.5 (5.5.44-0+deb8u1) ...

Setting up mysql-server-core-5.5 (5.5.44-0+deb8u1) ...

Setting up mysql-server-5.5 (5.5.44-0+deb8u1) ...

151001 12:58:57 [Warning] Using unique option prefix key_buffer instead of key_buffer_size is deprecated and will be removed in a future release. Please use the full name instead.

151001 12:58:57 [Note] /usr/sbin/mysqld (mysqld 5.5.44-0+deb8u1) starting as process 18455 ...

Setting up libhtml-template-perl (2.95-1) ...

Setting up mysql-server (5.5.44-0+deb8u1) ...

Processing triggers for libc-bin (2.19-18+deb8u1) ...

Processing triggers for systemd (215-17+deb8u2) ...

What If It Doesn’T Work?

尽管可能性极小,但如果一切都不靠谱,你可以用下面的命令删除 MySQL 安装包。它们会卸载每个软件包,并删除安装过程中创建的任何文件。

sudo apt-get autoremove mysql-server mysql-server-5.5

sudo apt-get purge mysql-server mysql-server-5.5

一旦你这样做了,你可以再次尝试安装步骤,并纠正你的错误。

现在 MySQL 已经安装好了,让我们使用 MySQL 控制台并尝试连接到服务器。命令是mysql –uroot –p<password>,其中<password>是您安装 MySQL 时提供的密码。清单 6-2 显示了与新 MySQL 服务器的成功连接。我执行了一些命令来进行测试,并为下一步收集信息。请注意,MySQL 控制台显示了 MySQL 服务器的版本以及平台的简称。在这种情况下,我连接到 Debian 平台上的 MySQL 5.5.28-1 服务器。

Listing 6-2.Connecting to MySQL

pi@raspberrypi ∼ $ mysql -uroot -p

Enter password:

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 43

Server version: 5.5.44-0+deb8u1 (Raspbian)

Copyright (c) 2000, 2015, 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 DATABASES;

+--------------------+

| Database           |

+--------------------+

| information_schema |

| mysql              |

| performance_schema |

+--------------------+

3 rows in set (0.00 sec)

mysql> SHOW VARIABLES LIKE '%dir%'

+-----------------------------------------+----------------------------+

| Variable_name                           | Value                      |

+-----------------------------------------+----------------------------+

| basedir                                 | /usr                       |

| binlog_direct_non``_

| character_sets_dir                      | /usr/share/mysql/charsets/ |

| datadir                                 | /var/lib/mysql/            |

| innodb_data_home_dir                    |                            |

| innodb_log_group_home_dir               | ./                         |

| innodb_max_dirty_pages_pct              | 75                         |

| lc_messages_dir                         | /usr/share/mysql/          |

| plugin_dir                              | /usr/lib/mysql/plugin/     |

| slave_load_tmpdir                       | /tmp                       |

| tmpdir                                  | /tmp                       |

+-----------------------------------------+----------------------------+

11 rows in set (0.00 sec)

mysql>

在这个例子中,我发出了SHOW DATABASES命令来查看数据库列表,发出了SHOW VARIABLES命令来显示所有包含名称dir的变量。注意最后一个命令的datadir输出:这是数据的位置。

在下一节中,您将告诉 MySQL 使用外部驱动器来存储您的数据库和数据。

Moving the Data Directory to the External Drive

回想一下,您希望使用 MySQL 来存储您的传感器数据。因此,传感器数据的量可能会增长,并且随着时间的推移可能会消耗大量空间。您可以使用外部驱动器来保存数据,而不必冒填满启动映像 SD(通常只有几千兆字节)的风险。这一节将向您展示如何告诉 MySQL 更改其保存数据的默认位置。

所涉及的步骤需要停止 MySQL 服务器,更改其配置,然后重新启动服务器。最后,测试更改以确保所有新数据都保存在新位置。首先停止 MySQL 服务器。

$ sudo /etc/init.d/mysql stop

您必须为新数据目录创建一个文件夹。

$ sudo mkdir /media/HDD/mysql

现在,您将现有的数据目录及其内容复制到新文件夹中。注意,您只复制数据,而不是整个 MySQL 安装,这是不必要的。

$ sudo cp -R /var/lib/mysql/*  /media/HDD/mysql

$ chown -R mysql mysql /media/HDD/mysql/

接下来编辑 MySQL 的配置文件。在这种情况下,您将datadir行改为datadir = /media/HDD/mysql。注释掉bind-address行以允许从网络上的其他系统访问 MySQL 也是一个好主意。

$ sudo vi /etc/mysql/my.cnf

还有最后一步。您必须将所有者和组更改为安装时创建的 MySQL 用户。以下是正确的命令:

$ sudo chown -R mysql:mysql /media/HDD/mysql

现在你重启 MySQL。

$ sudo /etc/init.d/mysql start

您可以通过连接到 MySQL,创建一个新的数据库,然后检查新文件夹是否是在外部驱动器上创建的,来确定这些更改是否有效,如清单 6-3 所示。

Listing 6-3.Testing the New Data Directory

pi@raspberrypi ∼ $ mysql -uroot -p

Enter password:

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 43

Server version: 5.5.44-0+deb8u1 (Raspbian)

Copyright (c) 2000, 2015, 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> CREATE DATABASE TESTME;

Query OK, 1 row affected (0.00 sec)

mysql> SHOW DATABASES;

+--------------------+

| Database           |

+--------------------+

| information_schema |

| mysql              |

| performance_schema |

| test               |

| testme             |

+--------------------+

5 rows in set (0.00 sec)

mysql> quit

您可以通过使用以下命令显示文件结构来检查数据库是否已创建:

$ sudo ls -lsa /media/HDD/mysql

您应该在mysql文件夹中看到的是每个数据库的单独文件夹。实际上,您应该看到为新创建的数据库创建的文件夹,表示为文件夹testme。现在,你有了它——一个运行在 Raspberry Pi 上的新的 MySQL 数据库服务器!

What About Overheating?

对树莓 Pi 过热的担忧主要是针对那些试图超频和其他冒险修改的人;您应该担心您的树莓 Pi 是否持续运行。通常情况下,数据库服务器 24/7 全天候运行,只有在维护时才将其关闭。

如果你担心过热,你可以以合理的价格(大约 15 美元)为你的树莓 Pi 的主要组件添加散热器。然而,如果将树莓 Pi 放在一个允许散热的 Shell 中,并且放在一个气候可控的环境中,我还没有看到它无限期运行的任何问题。这个问题的确切答案已经由创始人之一自己提供了(见 www.youtube.com/watch?v=Sz8NMp4MgG0 )。

现在我们已经配置好了 Raspberry Pi,安装并运行了 MySQL,让我们看看如何将设备连接到数据库服务器来保存数据。但首先,让我讨论一下树莓派的一些替代品。

其他平台

虽然我重点介绍了 Raspberry Pi 来演示如何构建低成本的 MySQL 数据库服务器,但 Raspberry Pi 并不是唯一的选择。事实上,您可以使用任意数量的低成本计算机和嵌入式平台来托管 MySQL 服务器。在本节中,我将介绍三种替代方案:BeagleBone Black、pcDuino 和 Intel Galileo。

虽然所有这些的过程都是相似的,但还是有一些小的差异和其他需要考虑的事情。因此,我将在接下来的章节中简要介绍这两种方法。之前已经阅读了 Raspberry Pi 教程,您应该能够轻松完成以下任务。

比格犬骨黑

回想一下第 3 章中的内容,黑色比格犬骨是原始比格犬骨的低价版本。它配有预配置的板载可引导 Linux 操作系统。像 Raspberry Pi 一样,它拥有许多端口,包括用于连接存储设备的 USB 端口。

开机前,请务必将 BeagleBone Black 连接到您的网络。我使用了板载以太网端口,发现它足以远程访问电路板。你可能遇到的唯一问题是发现你的板正在使用哪个 IP 地址。我建议使用端口扫描仪。大多数平台都有许多这样的应用。使用那个 IP 地址,用ssh root@<IP address>遥控你的 BeagleBone Black。

Note

BeagleBone Black 上的默认 root 密码为空。

Installing MySQL

BeagleBone Black 上的 MySQL 安装类似于前面的 Raspberry Pi 示例,只是命令略有不同,但我们以相同的顺序执行它们。下面显示了您执行的命令。我们必须首先更新本地包和包头。使用以下命令来完成此操作:

$ opkg update

接下来,我们用以下命令安装 MySQL:

$ opkg install mysql5

这个过程需要一段时间,从下载一些包和支持库开始。安装完成后,发出以下命令启动 MySQL:

$ /etc/init.d/mysqld start

根据 BeagleBone Black 预装操作系统的日期,您可能会看到类似以下内容的错误:

/etc/init.d/mysqld: line 3: /etc/default/rcS: No such file or directory

BeagleBone - mysqld error.png

问题出在启动 MySQL 的脚本中。我们用下面的命令编辑该文件,并注释掉文件中的第三行(更确切地说,是显示/etc/default/rcS的那一行)。只需在第一列中放一个#来注释掉它。

$ vi /etc/init.d/mysqld

保存文件,然后重新启动 MySQL,如下所示:

$ /etc/init.d/mysqld``sta

Note

BeagleBone Black 默认安装上没有sudo。要关机,只需使用shutdown –h now命令。

Configuring the Hard Drive

在 BeagleBone Black 上配置硬盘使用与 Raspberry Pi 上相同的命令。也就是说,您使用fdisk创建一个分区,使用mkfs创建文件系统。

pcDuino

pcDuino 是一款独特的电路板。最新版本支持 A20 或更高版本的处理器(多核,比其他主板稍快)。该板还有许多连接器,包括用于支持 SATA 硬盘的板载 SATA 端口。也许最有趣的,事实上也是这个名字的原因是该板支持 Arduino 兼容的屏蔽。正如您在第 3 章中看到的,这允许我们在单个设备上开发我们的 Arduino 解决方案。

然而,真正好的部分是 pcDuino 的板载可启动操作系统是 Ubuntu 12 的一个版本。这意味着它的行为类似于 Ubuntu 的完整安装。事实上,作为一个 Ubuntu 的普通用户(Mac OS X 的第二选择),我发现自己在 pcDuino 上就像在家一样,尤其是当我使用我的 Lapdock 连接到 pcDuino 时。

也就是说,通过将 pcDuino 连接到显示器、键盘和鼠标,它的操作方式与笔记本电脑非常相似。它也几乎一样快!此外,pcDuino 3B 的板载 Wi-Fi 功能使得使用该板非常方便(房间内没有以太网电缆)。

因为 pcDuino 运行 Ubuntu,所以在 pcDuino 上安装 MySQL 的过程与在大多数其他 Ubuntu 机器上安装 MySQL 的过程是一样的。您甚至可以使用在互联网上其他地方找到的相同文档和示例。有关 pcDuino 特定文档,请参见 http://linksprite.com/?page_id=874

Tip

pcDuino 的默认用户是密码为ubuntuubuntu

安装 MySQL

在 pcDuino 上安装 MySQL 需要以下命令。我们从更新包和包头开始,如下所示:

$ sudo apt-get update

接下来,我们用以下命令安装 MySQL:

$ sudo apt-get install mysql-server

在安装过程中,您必须选择两次 root 用户密码,但仅此而已!您剩下要做的就是根据您的需要配置 MySQL。例如,您可能想要编辑my.cnf文件,并按照上一章所述设置您的数据库。

Configuring the Hard Drive

在 pcDuino 上配置硬盘也与其他平台类似。由于平台有点不同,我将向您介绍这个过程。

首先将 USB 驱动器插入 USB 端口。当驱动器被识别后(访问 LED 可能会闪烁几次,然后稳定亮起),执行fdisk来创建分区,执行mkfs来格式化,然后挂载驱动器。最后,使用我们在 Raspberry Pi 上使用的相同命令来设置驱动器,使挂载持久化,并将 MySQL 数据库datadir转移到驱动器,如前所示。

英特尔伽利略

英特尔 Galileo 是英特尔 Einstein 平台的姐妹主板,它是另一种包含 Arduino 屏蔽接口的主板。与运行 Linux 操作系统(如 Yun)的 pcDuino 和较新的 Arduino 主板一样,英特尔 Galileo 提供从 Linux 或通过串行连接到您的台式计算机对 Arduino 的访问。因此,像 pcDuino 一样,您可以使用它来编写 Arduino 草图以及在您的 IOT 解决方案中嵌入节点。

英特尔 Galileo 的板载可启动 Linux 是最小的,但我建议下载最新的英特尔 SD 卡 Linux 映像(目前位于 http://intel.com/support/galileo/sb/CS-035101.htm ),将其解压缩并安装在可启动的 micro SD 驱动器上(您可以在 https://software.intel.com/en-us/creating-bootable-micro-sd-card-for-intel-galileo-board 找到说明),然后从 micro SD 卡启动 Galileo。

您可以通过以太网电缆将 Galileo 直接连接到您的网络,并且像 BeagleBone Black 示例一样,使用端口扫描器来定位 IP 地址。不幸的是,Galileo 上没有 HDMI 显示端口,所以使用显示器和键盘不是一个选择。

此外,Galileo 的 Linux 映像有点偏瘦,构建的包不像其他平台那样完整。事实上,我们将不得不使用另一个包存储库来安装 MySQL。

安装 MySQL

英特尔 Galileo 上的 MySQL 安装与 BeagleBone Black 几乎相同,但正如您将看到的,它并不干净。一个不同之处是,默认的 SD 卡 Linux 映像没有可用的基础 MySQL 包。幸运的是,AlexT 14 为我们做了这项工作。您所需要做的就是修改包的位置并执行更新。为了完整起见,下面显示了所有步骤。我建议从英特尔的 SD 卡 Linux 映像干净启动开始。

Note

Galileo 上的 SD 卡 Linux 映像没有 root 密码。

我们首先用下面的命令和数据更新名为SSS的包位置文件。把这个粘贴到空文件中。

$ vi /etc/opkg/base-feeds.conf

src/gz allhttp://repo.opkg.net/galileo/repo/all

src/gz clantonhttp://repo.opkg.net/galileo/repo/clanton

src/gz i586http://repo.opkg.net/galileo/repo/i586

然后,我们更新本地包和包头。使用以下命令来完成此操作:

$ opkg update

接下来,我们用以下命令安装 MySQL:

$ opkg install mysql5

这个过程需要一段时间,从下载一些包和支持库开始。如果出现错误,您可能需要使用以下命令覆盖uclibc文件,然后重新启动安装:

$ opkg install --force-overwrite uclibc

安装完成后,您应该能够如下启动 MySQL 服务器。如果您看到如下错误,您可能需要做一些额外的工作。在安装失败后,我在至少一个 Galileo 板上看到了这种情况。你可能不需要这些步骤,但我把它们包括在内,以防你被这个问题卡住。发生的情况是安装在创建特殊用户mysql之前失败了,这对于大多数 Linux 安装来说是常见的。但是,这很容易解决。

$ /etc/init.d/mysqld start

$ 010101 00:27:46 mysqld_safe Logging to '/var/log/mysqld.err'.

chown: unknown user mysql

010101 00:27:46 mysqld_safe Starting mysqld daemon with databases from /var/mysql

010101 00:27:47 mysqld_safe mysqld from pid file /var/lib/mysql/mysqld.pid ended

要解决这个问题,只需如下创建mysql用户并再次启动服务器。如果你想了解更多关于这个过程的内容,请参阅在线 MySQL 参考手册中源代码安装部分的预配置步骤( http://dev.mysql.com/doc/refman/5.6/en/installing-source-distribution.html )。

$ groupadd mysql

$ useradd -r -g mysql mysql

$ chown -R mysql /var/lib/mysql

$ chgrp -R mysql /var/lib/mysql

$ mysql_install_db --user=mysql

$ /etc/init.d/mysqld``s

Configuring the Hard Drive

Galileo 上的硬盘配置与其他平台类似。由于平台有点不同,我将向您介绍这个过程。

首先将 USB 驱动器插入 USB 端口。当驱动器被识别后(访问 LED 可能会闪烁几次,然后稳定亮起),执行fdisk来创建分区,执行mkfs来格式化,然后挂载驱动器。最后,使用我们在 Raspberry Pi 上使用的相同命令来设置驱动器,使挂载持久化,并将 MySQL 数据库datadir转移到驱动器,如前所示。

Does It Matter What Version Of Mysql I Use?

你可能想知道你应该使用哪个版本的 MySQL。虽然 MySQL 的最新版本是 5.7,但您不太可能在 Linux 和类似操作系统的软件包列表或存储库中找到这个版本。幸运的是,大多数软件包和存储库都有可用的 MySQL 5.1 或 5.5,这对大多数 IOT 解决方案来说应该足够了,并且您不太可能需要 MySQL 的最新特性。

但是,如果您确实需要最新的兼容性或一致性特性,您可能需要直接从 Oracle 下载源代码并在本地构建安装。或者您可以为您平台搜索预编译的二进制文件,甚至是安装包。虽然它们很少见,但我不时会遇到这些包裹。不要害怕向你的董事会支持论坛上的人寻求帮助。很可能有人已经创造了你需要的东西。

MySQL 客户端:如何连接和保存数据

您已经看到了如何使用 MySQL 客户端连接到 MySQL 服务器。该工具是一个交互式工具,我们可以在其中执行查询,但它对保存来自传感器或数据节点的数据没有帮助。我们需要的是一种叫做connector的东西。连接器是一个编程模块,用于允许我们的草图(来自 Arduino)或脚本或程序向数据库服务器发送数据。连接器还允许我们查询数据库服务器以从服务器获取数据。

我将介绍您在开发自己的 IOT 解决方案时可能会遇到的两个主要连接器。我将每一个都作为一个教程,您可以使用它来跟随您自己的硬件。我首先介绍一个用于 Arduino 的连接器(Connector/Arduino),然后介绍一个用于编写 Python 脚本的连接器(Connector/Python)。

Database Connectors For Mysql

MySQL 有很多数据库连接器。Oracle 为各种语言提供了许多数据库连接器。以下是可从 http://dev.mysql.com/downloads/connector/ 下载的当前数据库连接器:

  • 连接器/ODBC:符合标准 ODBC
  • 连接器/网络:Windows。Net 平台
  • 连接器/J: Java 应用
  • 连接器/Python: Python 应用
  • 连接器/C++:标准化的 C++应用
  • 连接器/C(libmysql): C 应用
  • PHP 的 MySQL 本地驱动程序(mysqlnd): PHP 5.3 或更新的连接器
  • 连接器/Arduino: Arduino 草图

如您所见,几乎所有您可能遇到的编程语言都有一个连接器——现在甚至还有一个用于 Arduino 的连接器!

Introducing Connector/Arduino

借助专门为 Arduino 设计的新数据库连接器,您可以将 Arduino 项目直接连接到 MySQL 服务器,而无需使用中间计算机或基于 web 的服务。直接访问数据库服务器意味着您可以将从项目中获取的数据存储在数据库中。您还可以检查存储在服务器上的表中的值。该连接器允许您将 IOT 解决方案保存在本地,甚至可以断开与互联网或任何其他外部网络的连接。

将数据保存在数据库中不仅可以保存数据供以后分析,还意味着您的项目可以将数据提供给更复杂的应用。更好的是,如果您的项目使用大量数据进行计算或查找,您可以将数据存储在服务器上,只检索计算或操作所需的数据,而无需占用 Arduino 上的大量内存。显然,这为 Arduino 项目开辟了一条全新的道路!

数据库连接器被命名为 Connector/Arduino。它在为 Arduino 平台构建的库中实现了 MySQL 客户端通信协议(称为数据库连接器)。此后,在讨论一般概念和特性时,我会提到 Connector/Arduino,并将实际的源代码称为 Connector/Arduino 库、连接器或简称为库。

为使用该库而编写的草图(程序)允许您对 SQL 语句进行编码以插入数据,并运行小型查询以从数据库返回数据(例如,使用查找表)。

您可能想知道,内存和处理能力有限的微控制器怎么可能支持将数据插入 MySQL 服务器的代码。您可以这样做,因为与 MySQL 服务器通信的协议不仅广为人知且有据可查,而且是专门设计为轻量级的。这是 MySQL 吸引嵌入式开发者的小细节之一。

要与 MySQL 通信,Arduino 必须通过网络连接到 MySQL 服务器。为此,Arduino 必须使用以太网或 Wi-Fi 屏蔽,并连接到可以连接到数据库服务器的网络或子网(您甚至可以通过互联网连接)。该库与大多数新的 Arduino 以太网、Wi-Fi 和支持标准以太网库的兼容克隆屏蔽兼容。

Note

兼容性与其说是硬件要求,不如说是软件库限制。也就是说,如果您使用的网络设备使用附带的以太网库或基于标准Ethernet.Client类的库,您的硬件应该是兼容的。一些较新的低成本以太网模块可能不兼容。

你可以用连接器/Arduino 做很多事情。接下来是连接器入门的简短入门。如果你需要更多的帮助或者想要更深入地了解这个库以及更多的例子,从 https://github.com/ChuckBell/MySQL_Connector_Arduino/blob/master/extras/MySQL_Connector_Arduino_Reference_Manual.pdf 下载参考手册。

What About Memory?

连接器/Arduino 是作为 Arduino 库实现的。尽管该协议是轻量级的,但是库确实会消耗一些内存。事实上,该库需要大约 20KB 的闪存来加载。因此,它需要 ATmega328 或具有 32KB 闪存的类似处理器。

这看起来似乎没有太多空间来编写您的解决方案,但事实证明,对于大多数传感器来说,您真的不需要那么多空间。如果你这样做了,你可以升级到一个新的更大内存的 Arduino。例如,最新的 Arduino,Due,有 512KB 的内存用于程序代码。基于此,仅仅 20KB 的开销是微不足道的。

该库是开源的,许可为 GPLv2,归 Oracle 公司所有。因此,您打算共享的对库的任何修改都必须符合 GPLv2 许可。虽然它不是 Oracle 或 MySQL 官方支持的产品,但您可以使用 GPLv2 下的库。

Tip

有一个讨论连接器的 MySQL 论坛。 http://forums.mysql.com/list.php?175。如果你被卡住了,需要一些帮助,检查论坛可能的答案。

安装连接器/Arduino

有两种方法可以获得和安装连接器/Arduino。第一种也是推荐的方法是使用库管理器来搜索和安装库。从任意草图中,单击草图➤包括库➤管理库菜单。这将打开库管理器。在过滤器搜索框中输入MySQL,然后选择连接器,最后点击安装。几秒钟之内,新库就安装完毕,可以使用了。酷吧。图 6-12 显示了选择安装 MySQL 连接器/Arduino 库的库管理器对话框。

A978-1-4842-1293-6_6_Fig12_HTML.jpg

图 6-12。

Arduino Library Manager dialog

如果您不想使用 Library Manager,或者因为使用了不同的 IDE 或编辑器而无法使用,可以从 GitHub 网站( https://github.com/ChuckBell/MySQL_Connector_Arduino )下载。

要手动安装连接器,首先导航到 GitHub 上的连接器/Arduino 页面( https://github.com/ChuckBell/MySQL_Connector_Arduino )。最新版本总是可供下载的版本。该文件被命名为MySQL_Connector_Arduino-master.zip。查看页面右侧,单击按钮下载并保存到您的计算机。下载完成后,解压缩文件。您将在解压缩文件的位置看到一个新文件夹。

您需要将文件夹复制或移动到您的Arduino/Libraries文件夹中。将重命名为MySQL_Connector_Arduino的文件夹及其内容放入 Arduino 库文件夹中。你可以通过检查 Arduino 环境的首选项来找到这个位置,如图 6-13 所示。

A978-1-4842-1293-6_6_Fig13_HTML.jpg

图 6-13。

Arduino Preferences dialog Tip

如果您在 Arduino 应用运行时将一个库复制到您的Libraries文件夹,您必须重新启动它以检测新的库。

既然已经安装了连接器/Arduino 库,就可以开始编写支持数据库的草图了!在进入库源代码之前,让我们首先检查一下使用库的一些限制。

Wait! I Have Version 1.0. Can’T I Use That?

如果您已经发现了 Connector/Arduino 库,并且一直在使用 1.0.4 或更早的版本,您将需要升级到较新的版本才能使用本书中的示例。这是因为在 1.1 版本中做了很多改变,使得它与旧版本不兼容。

但不要绝望,因为旧版本仍在 Launchpad 中,并将在那里保留一段时间。最重要的是,新版本不会与您现有的任何草图产生冲突。也就是说,您现有的草图不会受到安装新库的影响。

然而,如果你想在你现有的草图中使用最新的版本,你将不得不改变一些东西。请参阅位于库源代码的 extras 文件夹中的参考手册中的“对以前版本的更改”。

使用连接器/Arduino

让我们从一个简单的草图开始,这个草图设计用来在 MySQL 的一个表中插入一行。你在创造一个“你好,世界!”草图(但保存在数据库表中)。所有支持数据库的草图共享相同的公共构建块。这些包括设置要使用的数据库、使用一组特定的包含文件创建草图、连接到数据库服务器以及执行查询。本节将介绍创建和执行启用数据库的草图所需的基本步骤。

Tip

该库包括许多示例草图,可帮助您快速入门。在你探索掌握库的过程中检查例子。你会发现如何使用 WiFi 连接,甚至如何从草图中的变量构建复杂查询的例子。

您首先需要的是一台数据库服务器!首先创建一个数据库和一个表来存储数据。在这个实验中,您创建一个简单的表,它有两列:一个文本列(char)用于存储消息,一个TIMESTAMP列用于记录保存行的日期和时间。我发现TIMESTAMP数据类型是存储传感器数据的绝佳选择。您很少会不想知道样本是何时采集的!最重要的是,MySQL 使它易于使用。事实上,您只需要向服务器传递一个令牌NULL值,它自己生成并存储当前时间戳。

清单 6-4 显示了一个 MySQL 客户端(名为mysql)会话,它创建数据库和表,并手动向表中插入一行。草图将从您的 Arduino 执行一个类似的INSERT语句。通过发出一个SELECT命令,你可以看到每次表被更新。

Listing 6-4.Creating the Test Database

$ mysql -uroot -psecret

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 102

Server version: 5.6.14-log Source distribution

Copyright (c) 2000, 2011, 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> CREATE DATABASE test_arduino;

Query OK, 1 row affected (0.00 sec)

mysql> USE test_arduino;

Database changed

mysql> CREATE TABLE hello (source char(20), event_date timestamp);

Query OK, 0 rows affected (0.01 sec)

mysql> GRANT ALL ON *.* to 'root'@'%' IDENTIFIED BY 'secret'

mysql> INSERT INTO hello VALUES ('From Laptop', NULL);

Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM hello;

+-------------+---------------------+

| source      | event_date          |

+-------------+---------------------+

| From Laptop | 2013-02-16 20:40:12 |

+-------------+---------------------+

1 row in set (0.00 sec)

mysql>

开始新的草图

是时候开始写你的草图了。打开你的 Arduino 环境,创建一个名为hello_mysql的新草图。以下部分详细介绍了一个典型的支持 MySQL 数据库的草图的各个部分。您从所需的包含文件开始。

包括文件

要使用连接器/Arduino 库,请记住它需要一个以太网屏蔽,因此需要以太网库。连接器/Arduino 库需要使用MySQL_Connection库进行连接,使用MySQL_Cursor库进行查询。因此,您必须按顺序包括其中的每一项。下面显示了一个支持 MySQL 数据库的草图至少需要包含的所有库头文件。现在开始输入这些。

#include <Ethernet.h>

#include <MySQL_Connection.h>

#include <MySQL_Cursor.h>

初步设置

设置好包含文件后,接下来必须处理一些初步声明。这些包括对以太网库和连接器/Arduino 的声明。

以太网库要求您设置服务器的 MAC 地址和 IP 地址。MAC 地址是一串十六进制数字,不需要任何特殊的东西,但是它在网络上的机器中应该是唯一的。它使用动态主机控制协议(DHCP)来获取 IP 地址、DNS 和网关信息。服务器的 IP 地址是使用IPAddress类定义的(正如您所期望的,它将值存储为一个由四个整数组成的数组)。

另一方面,以太网类也允许您为 Arduino 提供一个 IP 地址。如果您为 Arduino 分配 IP 地址,该地址对于它所连接的网段必须是唯一的。请务必使用 IP 扫描仪,以确保您选择的 IP 地址尚未被使用。

下面显示了 10.0.1.X 网络中节点的这些语句:

/* Setup for Ethernet Library */

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(10, 0, 1, 23);

接下来,您需要为 Connector/Arduino 设置一些变量。您需要定义对连接类的引用。我们稍后动态分配 cursor 类,以便更好地管理内存。连接类需要一个参数,该参数是Ethernet.Client类的实例。如果你使用 Arduino 以太网屏蔽,你可以简单地使用EthernetClient类。如果您使用另一个库或 WiFi 盾,您将使用适当的客户端。例如,将 WiFiClient 用于 Arduino WiFi 盾。

您还需要一些字符串用于草图中使用的数据。至少包括一个用户 ID 字符串、一个密码字符串和一个您使用的查询字符串。最后一个字符串是可选的,因为您可以在查询调用中直接使用文字字符串,但是为查询语句生成字符串是一个很好的实践。这也是使查询参数化以便重用的最佳方式。

以下是完成草图声明所需的语句示例:

/* Setup for the Connector/Arduino */

EthernetClient client;

MySQL_Connection conn((Client *)&client);

char user[] = "root";

char password[] = "secret";

char INSERT_SQL[] = "INSERT INTO test_arduino.hello VALUES ('Hello from Arduino!', NULL)";

请注意INSERT语句。您包括一个字符串来表明您正在从您的 Arduino 运行查询。您还包括了NULL值,这样服务器将为该行创建时间戳,如前面的手动执行所示。

连接到 MySQL 服务器

预备工作到此结束;让我们写点代码吧!接下来,更改setup()方法。这是应该放置连接到 MySQL 服务器的代码的地方。回想一下,每次 Arduino 启动时,这个方法只被调用一次。下面显示了所需的代码:

void setup() {

Ethernet.begin(mac_addr);

Serial.begin(115200);

while (!Serial);

delay(1000);

Serial.println("Connecting...");

if (conn.connect(server_addr, 3306, user, password))

delay(500);

else

Serial.println("Connection failed.");

}

代码首先调用以太网库来初始化网络连接。回想一下,当您使用Ethernet.begin()方法时,只传递 MAC 地址,如示例所示,这会导致以太网库使用 DHCP 来获取 IP 地址。如果您想手动分配 IP 地址,请参见 http://arduino.cc/en/Reference/EthernetBegin 中的Ethernet.begin()方法文档。

接下来是对串行监控器的调用。虽然不完全必要,但是包含它是一个好主意,这样您就可以看到由 Connector/Arduino 编写的消息。如果您在连接或运行查询时遇到问题,请确保使用串行监控器,以便可以看到库发送的消息。

现在调用delay()方法。您发出一秒钟的等待命令,以确保您有时间启动串行监控器,并且不会错过调试语句。如果您需要更多时间来启动串行监控器,请随意尝试更改该值。

延迟之后,您向串行监控器打印一条语句,表明您正在尝试连接到服务器。连接到服务器是对名为connect()的连接器/Arduino 库的一次调用。您传递 MySQL 数据库服务器的 IP 地址、服务器监听的端口以及用户名和密码。如果这个调用通过,代码将进入下一个delay()方法调用。

在发出额外的 MySQL 命令之前,需要这种延迟来减缓执行速度。与前面的延迟一样,根据您的硬件和网络延迟,您可能不需要此延迟。如果您强烈反对使用延迟来避免延迟问题,您应该进行试验。另一方面,如果连接失败,代码会通过 print 语句告诉您连接已经失败。

运行查询

现在是运行查询的时候了。我们首先实例化一个MySQL_Cursor类的实例,并传入连接实例。这将动态地分配类(想想代码)。然后我们调用execute()方法,并传入我们想要运行的查询。由于没有返回结果(因为我们正在运行一个INSERT,我们可以关闭连接并删除实例。下面按顺序显示了所有这些步骤。

将这段代码放在成功连接后执行的分支中。下面显示了前面重写的条件语句,其中包含了运行插入查询的方法调用:

if (conn.connect(server_addr, 3306, user, password))

{

delay(500);

/* Write Hello to MySQL table test_arduino.hello */

// Create an instance of the cursor passing in the connection

MySQL_Cursor *cur = new MySQL_Cursor(&conn);

cur->execute(INSERT_SQL);

delete cur;

}

else

Serial.println("Connection failed.");

}

请注意,您只需调用一个名为execute()的方法,并向其传递您之前定义的查询。是的,就是这么简单!

测试草图

现在,除了loop()方法之外,您已经拥有了完成草图所需的所有代码。在这种情况下,您让它成为一个空方法,因为您没有做任何重复的事情。清单 6-5 显示了完成的草图。

Tip

如果您在让连接器工作时遇到问题,请参见 MySQL 连接器参考手册 15 中的“连接器/Arduino 故障排除”部分,然后返回到这个项目。

Listing 6-5.“Hello, MySQL!” Sketch

/**

* Example: Hello, MySQL!

*

* This code module demonstrates how to create a simple database-enabled

* sketch.

*/

#include <Ethernet.h>

#include <MySQL_Connection.h>

#include <MySQL_Cursor.h>

/* Setup for Ethernet Library */

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(10, 0, 1, 23);   // The IP address of your database server

/* Setup for the Connector/Arduino */

EthernetClient client;

MySQL_Connection conn((Client *)&client);

char user[] = "root";

char password[] = "secret";

char INSERT_SQL[] = "INSERT INTO test_arduino.hello VALUES ('Hello from Arduino!', NULL)";

void setup() {

Ethernet.begin(mac_addr);

Serial.begin(115200);

while (!Serial);

Serial.println("Connecting...");

if (conn.connect(server_addr, 3306, user, password))

{

delay(500);

/* Write Hello, World to MySQL table test_arduino.hello */

// Initiate the query class instance

MySQL_Cursor *cur_mem = new MySQL_Cursor(&conn);

// Execute the query

cur_mem->execute(INSERT_SQL);

delete cur_mem;

Serial.println("Query Success!");

}

else

Serial.println("Connection failed.");

}

void loop() {

}

在您点击按钮编译和上传草图之前,让我们讨论一下可能出现的几个错误。如果 MySQL 服务器的 IP 地址或用户名和密码错误,您可能会在串行监控器中看到如图 6-14 所示的连接失败。

A978-1-4842-1293-6_6_Fig14_HTML.jpg

图 6-14。

Failed connection

如果您的 Arduino 连接到 MySQL 服务器,但查询失败,您会在串行监控器中看到如图 6-15 所示的错误。

A978-1-4842-1293-6_6_Fig15_HTML.jpg

图 6-15。

Failed query

请务必仔细检查您的 MySQL 服务器的源代码和 IP 地址,以及选择的用户名和密码。如果您在连接时仍然遇到问题,请参见 MySQL 连接器参考手册 16 中的“连接器/Arduino 故障排除”一节,了解确保您的 MySQL 服务器配置正确的测试内容列表。

仔细检查服务器安装和草图中的信息后,编译草图并上传到 Arduino。然后启动串口监控器,观察连接 MySQL 服务器的过程。图 6-16 显示了代码的完整和成功执行。

A978-1-4842-1293-6_6_Fig16_HTML.jpg

图 6-16。

Correct serial monitor output Note

在例子中,我连接到一个旧版本的 MySQL,它安装在我的 BeagleBone Black board 上。该草图将连接到 MySQL 5.0 及更高版本的任何版本。

哇,是这个吗?不是很有趣,是吗?如果你在你的串行监控器中看到如图 6-16 所示的语句,请放心 Arduino 已经连接到 MySQL 服务器并向其发出查询。要进行检查,只需返回到mysql客户机并在表上发出 select。但是首先,多次运行草图,在表中发布几个插入。

有两种方法可以做到这一点。首先,你可以在你的 Arduino 上按下RESET。如果让串行监控器运行,Arduino 会按顺序显示消息,如图 6-17 所示。第二,可以再次上传草图。在这种情况下,串行监控器关闭,您必须重新打开它。这种方法的优点是每次都可以更改查询语句,从而将不同的行插入到数据库中。现在继续尝试,并检查您的数据库的变化。

A978-1-4842-1293-6_6_Fig17_HTML.jpg

图 6-17。

Results of running the sketch several times

让我们检查一下试运行的结果。为此,您使用mysql客户机连接到数据库服务器,并发出一个SELECT查询。清单 6-6 显示了示例中三次运行的结果。注意每次运行的不同时间戳。正如你所看到的,我运行了一次,然后等了几分钟又运行了一次(我用了我的 Arduino 以太网盾上的RESET按钮),然后马上又运行了一次。很酷,不是吗?

Listing 6-6.Verifying the Connection with the Serial Monitor

$ mysql -uroot -psecret

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 33

Server version: 5.6.14-log Source distribution

Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.

This software comes with ABSOLUTELY NO WARRANTY. This is free software,

and you are welcome to modify and redistribute it under the GPL v2 license

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select * from test_arduino.hello;

+---------------------+---------------------+

| source              | event_date          |

+---------------------+---------------------+

| From laptop         | 2013-02-19 15:17:38 |

| Hello from Arduino! | 2013-02-19 15:18:12 |

| Hello from Arduino! | 2013-02-19 15:28:``3

| Hello from Arduino! | 2013-02-19 15:29:16 |

+---------------------+---------------------+

4 rows in set (0.01 sec)

mysql>

除了这里显示的,您还可以使用连接器做更多的事情。事实上,您可以向数据库服务器查询查找数据;发现变量的值;创建数据库、表、视图。等等。在合理的范围内,你可以做任何你想做的事情。向数据库服务器查询大量行或大数据行可能会超出 Arduino 的内存限制。幸运的是,你将要写的大部分草图将会用简单的INSERT语句保存数据。

Tip

您可以在位于extras文件夹中的连接器/Arduino 参考手册中找到更多如何使用连接器的示例。该文件被命名为MySQL_Connector_Arduino_Reference_Manual.pdf

既然您已经看到了 Arduino 连接器,让我们来看一个更通用的连接器,一个您可以在您的笔记本电脑、台式机、低成本计算机、嵌入式系统等等上使用的连接器,任何可以运行 Python 的东西都可以写入 MySQL 数据库!

连接器/Python 简介

Oracle 的 Python 连接器是一个全功能连接器,为 Python 应用和脚本提供了到 MySQL 数据库服务器的连接。最新版本是 release-2.1.3GA,与 Connector/Arduino 不同的是,Connector/Python 得到了 Oracle 的全面支持和积极维护。

Connector/Python 特性支持从版本 4.1 和更新版本开始的所有当前 MySQL 服务器版本。编写它是为了提供 Python 和 MySQL 之间的自动数据类型转换,使构建查询和解密结果变得容易。它还支持压缩,允许通过 SSL 连接,并支持所有 MySQL SQL 命令。当前版本 2.1.3 增加了 C 库以提高性能。

在 Python 脚本中使用 Connector/Python 包括导入基本模块、启动连接和使用游标执行查询,这与 Connector/Arduino 类似。这并不奇怪,因为我是用 Connector/Python 作为模型编写 Connector/Arduino 的。

然而,与 Connector/Arduino 不同,Connector/Python 没有这样的内存限制,允许您完成大量的处理。事实上,我会将大部分字符串、日期和数学处理转移到 Python 脚本中,而不是在 Arduino 上尝试。

在我们开始讨论如何使用 Connector/Python 编写一些支持 MySQL 数据库的应用之前,我们先来讨论一下如何获得和安装 Connector/Python。

Python? Isn’t That a Snake?

Python 编程语言是一种高级语言,旨在尽可能接近阅读英语,同时简单、易学且功能强大。皮托尼斯塔斯 17 会告诉你设计师们确实达到了这些目标。

Python 在使用前不需要编译步骤。相反,Python 应用(其文件名以.py结尾)是动态解释的。这是非常强大的,但是除非您使用包含自动语法检查器的 Python 集成开发环境(IDE ),否则在执行应用之前不会发现一些语法错误。幸运的是,Python 提供了一个健壮的异常处理机制,可以传达出哪里出错了。

如果您从未使用过 Python,或者您想了解更多,下面是几本介绍这种语言的好书。互联网上也有很多资源,包括位于 www.python.org/doc/ 的 Python 文档页面。

  • 西蒙·蒙克(McGraw-Hill,2013 年)的《树莓派编程》
  • 马格努斯·李·赫特兰德的《从新手到专业人员的 Python 入门》,第二版(2008 年出版)
  • David Beazley 和 Brian K. Jones 的 Python 食谱(O'Reilly Media,2013 年)

有趣的是,Python 是以英国喜剧团 Monty Python 而不是爬行动物命名的。当你学习 Python 的时候,你可能会遇到对 Monty Python 剧集的无聊引用。我对巨蟒小组情有独钟,觉得这些参考资料很有趣。当然,您的里程可能会有所不同。

安装连接器/Python

下载过程与您在服务器上发现的过程相同。可以从 Oracle 的 MySQL 网站( http://dev.mysql.com/downloads/connector/python/ )下载 Connector/Python。该页面将自动检测您的平台,并显示适用于您平台的可用下载。你可能会看到几个选择。请确保选择与您的配置相匹配的选项。

在台式机/笔记本电脑平台上安装

因为大多数平台都安装了 Python,所以您可能不需要做任何准备工作来准备您的系统;只需下载安装程序并安装即可。您可以在产品下载页面( http://dev.mysql.com/downloads/connector/python/ )找到最新的 Python 安装程序。请注意,Connector/Python 需要 Python 2.7(推荐)或 Python 3.3。

6-18 显示了 Ubuntu 平台的典型下载页面。如果您的平台未列出,请使用下拉框选择不同的平台。但是,如果您使用的平台(如 Windows)不包含 Python,您应该首先安装 Python。

A978-1-4842-1293-6_6_Fig18_HTML.jpg

图 6-18。

Connector/Python download page

正如我提到的,Connector/Python 的当前版本是 2.1.3,但是大多数版本 2.0 或更高版本应该适合您的解决方案。最新版本增加了高可用性功能,可用于名为 MySQL Fabric 的企业级高可用性解决方案。任何 2.0 或 2.1 版本都可以用于 IOT 解决方案。

Tip

关于在某些平台上安装的具体注意事项,请参见在线参考手册( http://dev.mysql.com/doc/connector-python/en/connector-python-installation.html )。

在低成本平台上安装

在较小的平台上安装 Connector/Python 要复杂一些,但不会太复杂。简而言之,您通过使用平台的打包机制(apt-getopkg等等)来安装 Connector/Python。下面展示了如何使用 Raspbian Jessie 在 Raspberry Pi 上安装 Connector/Python。其他平台也差不多。

$ sudo pip3 install mysql-connector-python --allow-external mysql-connector-python

Downloading/unpacking mysql-connector-python

mysql-connector-python an externally hosted file and may be unreliable

Downloading mysql-connector-python-2.0.4.zip (277kB): 277kB downloaded

Running setup.py (path:/tmp/pip-build-mm9szi9x/mysql-connector-python/setup.py) egg_info for package mysql-connector-python

Installing collected packages: mysql-connector-python

Running setup.py install for mysql-connector-python

Successfully installed mysql-connector-python

Cleaning up...

注意,我使用 Python 包管理器(来自 PyPi)来获取和安装连接器。这将安装一个旧版本的连接器,但它功能齐全,将满足您的 IOT 解决方案的需要。

检查安装

一旦安装了 Connector/Python,您可以使用下面的简短示例来验证它是否正常工作。通过输入命令python开始。这将打开一个交互式提示,允许您一次输入一行 Python 代码并执行它;这是一个 Python 命令行解释器,在测试小代码片段时非常有用。只需输入以下几行,如示例所示:

$ python

Python 2.7.6 (default, Mar  4 2014, 16:53:21)

[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

>>> import mysql.connector

>>> print mysql.connector.__version__

2.1.3

>>> quit()

您应该看到的是打印的连接器/Python 版本。如果您看到任何关于找不到连接器的错误,请务必检查您的安装以确保它正常工作。一旦您成功地访问了 Connector/Python,您就可以继续看一些例子了。

Tip

如果安装了多个 Python 版本,并且在不同于默认版本的 Python 版本下安装了 Connector/Python,请使用特定于版本的 Python 可执行文件。例如,如果您在 Python3 下安装了 Connector/Python,但是 Python2.7 是默认的,那么使用命令python3来启动解释器。否则,您可能会在导入时看到错误。

使用连接器/Python

让我们从一个简单的例子开始,我们连接到 MySQL 服务器并获得一个数据库列表。在这种情况下,我们首先导入连接器/Python connector类,然后调用connect()方法连接到服务器。为了保持整洁,我们使用字典来存储连接信息。确保您的 MySQL 服务器正在运行,并更改以下示例以匹配您的设置。例如,为服务器提供正确的密码和主机名。

$ python

Python 2.7.6 (default, Mar  4 2014, 16:53:21)

[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin

Type "help", "copyright", "credits" or "license" for more information.

>>> import mysql.connector

>>> server = {

...   'user': 'root',

...   'password': <secret>,

...   'host': '127.0.0.1',

...   'database': 'employees',

...   'raise_on_warnings': True,

... }

>>> cnx = mysql.connector.connect(**server)

如果此时出现错误,请返回并检查您的连接参数。很有可能是您的 MySQL 服务器不可访问、已关闭或者您指定了错误的凭据。如果您没有收到交互式解释器的响应,那也没关系——您已经连接好了,可以开始了!

现在,让解释器运行并添加以下语句。这里我们将打开一个游标对象来执行查询和检索行。我用一种简单的方式编写了这个循环,向您展示了如何遍历所有可用的行(还有其他几种有效的方式来编写这个位)。

>>> cur = cnx.cursor()

>>> cur.execute("SHOW DATABASES")

>>> rows = cur.fetchall()

>>> for row in rows:

...     print row

...

当您得到...(这是一个提示)时,按 Enter 并观察结果,如下所示。根据服务器上的数据库,您的列表可能会略有不同。

(u'arduino',)

(u'employees',)

(u'library',)

(u'mysql',)

(u'performance_schema',)

(u'plant_monitoring',)

(u'test',)

(u'test_arduino',)

(u'world',)

(u'world_innodb',)

但我们还没完。还需要两步。我们需要关闭光标和连接,然后退出解释器。

>>> cur.close()

True

>>> cnx.close()

>>> quit()

恭喜你!您已经编写了第一个支持 MySQL 的 Python 脚本。现在我们知道了基础知识,让我们继续使用源文件而不是交互式解释器来编写更强大的脚本。

大多数 Python 脚本(应用)都是使用名为<something>.py的文件构建的,并从命令行执行,如下所示。我们将使用这个方法来执行下面的例子。因此,对于每个示例,您应该打开一个文件并输入如下所示的文本。

$ python my_script.py

现在让我们看看如何在表格中插入一些数据。在这种情况下,我们只想从文件中读取数据,并将其插入到表中。我会让你发挥你的想象力,你如何可以改变文件读取传感器。事实上,我将在后面的章节中向您展示如何做到这一点。

Note

工作台布局参见第 5 章。如果您执行了来自第 5 章的示例,请确保清空该表,这样您可以在运行该示例时避免键违规。

打开您最喜欢的文本编辑器,输入清单 6-7 中所示的代码。用名称simple_insert.py保存文件。

Listing 6-7.Inserting Data with Connector/Python

import mysql.connector

server = {

'user': 'root',

'password': <secret>,

'host': '127.0.0.1',

'database': 'employees',

'raise_on_warnings': True,

}

cnx = mysql.connector.connect(**server)

cur = cnx.cursor()

# read rows from a file for inserting into plant_monitor table

f = open("plants_data.txt")

lines = f.readlines()

f.close()

# now insert the data

for line in lines:

cols = line.strip('\n').split(",") # comma-separated row

query = "INSERT INTO plant_monitoring.plants (name, location, climate)" \

" VALUES ('{0}','{1}',{2});".format(cols[0], cols[1], cols[2])

print query

cur.execute(query)

cnx.commit()

cur.close()

cnx.close()

这里我们看到与上一个例子相同的启动代码,只是这次我们从一个文件中读取值,并对每个值执行一个INSERT SQL 语句。花点时间研究一下代码及其工作原理。注意,它使用了字符串替换。

注意粗体的那一行。该命令对于确保将行写入表中是必要的。更具体地说,因为我使用了事务存储引擎,并且我的服务器是为事务设置的,所以在我显式提交更改之前,不会写入任何数据。您的服务器可能会有不同的设置,但是在这里添加这个命令没有坏处。您也可以将它添加到循环内部,但是最好将您的提交推到最新的代码块,在这种情况下,它在循环外部。

我们正在阅读的文件只有几行,是第五章中工厂监控系统示例的模型。清单 6-8 显示了文件内容。注意,我把它标为plants_data.txt。如果更改文件名,请确保相应地更改代码。

Listing 6-8.Sample Data

Jerusalem Cherry,deck,2

Moses in the Cradle,patio,2

Peace Lilly,porch,1

Thanksgiving Cactus,porch,1

African Violet,porch,1

要运行该脚本,请从存储文件的文件夹中发出以下命令。确保首先将数据文件放在同一个文件夹中。我展示了运行脚本的结果。

$ python ./simple_insert.py

INSERT INTO plant_monitoring.plants (name, location, climate) VALUES ('Jerusalem Cherry','deck',2);

INSERT INTO plant_monitoring.plants (name, location, climate) VALUES ('Moses in the Cradle','patio',2);

INSERT INTO plant_monitoring.plants (name, location, climate) VALUES ('Peace Lilly','porch',1);

INSERT INTO plant_monitoring.plants (name, location, climate) VALUES ('Thanksgiving Cactus','porch',1);

INSERT INTO plant_monitoring.plants (name, location, climate) VALUES ('African Violet','porch',1);

现在让我们检查一下我们的桌子。如果我们从一个空表开始,我们应该看到以下内容:

mysql> SELECT * FROM plant_monitoring.plants;

+----+---------------------+----------+---------+

| id | name                | location | climate |

+----+---------------------+----------+---------+

| 30 | Jerusalem Cherry    | deck     | outside |

| 31 | Moses in the Cradle | patio    | outside |

| 32 | Peace Lilly         | porch    | inside  |

| 33 | Thanksgiving Cactus | porch    | inside  |

| 34 | African Violet      | porch    | inside  |

+----+---------------------+----------+---------+

5 rows in set (0.00 sec)

除了这里显示的,您还可以使用连接器做更多的事情。事实上,你可以做任何你想做的事情。通常,我使用 Python 脚本在我的数据库和数据库服务器上执行复杂的操作。例如,我可以编写一个脚本来设置我所有的数据库、表、函数等等,这样我就可以在任何我想要的服务器上重新加载一个测试或者开始一个实验;我只是提供不同的连接参数并运行它。

Mysql Utilities: Python-Based Database Administration

如果您是一名 Python 开发者,并且发现自己越来越多地使用 MySQL,尤其是当您发现自己处于管理员的角色时,您可能想看看 Oracle 的 MySQL 实用程序。MySQL Utilities 是一组用于管理 MySQL 服务器的 Python 脚本和 Python 库。您可以做各种各样的事情,从复制用户权限到克隆服务器到发现两个数据库之间的差异。详见 http://dev.mysql.com/downloads/utilities/

您可能还想编写复杂的 Python 脚本来处理来自传感器或 IOT 数据收集器的数据。也就是说,您可以使用 Raspberry Pi 作为数据聚合器来准备存储数据。你甚至可以使用 Python 应用直接从 Raspberry Pi 读取传感器,正如我在我的书Beginning Sensor Networks with Arduino and Raspberry Pi (Apress,2014)中演示的那样。

对于包括执行事务、创建表和运行复杂查询在内的更复杂的示例,请参见 Connector/Python 在线参考手册中的编码示例部分( http://dev.mysql.com/doc/connector-python/en/connector-python-examples.html )。

摘要

本章介绍了 MySQL,并向您提供了一个关于如何设置 Raspberry Pi、安装 MySQL 和使用它的速成课程。您还学习了如何使用 Arduino 草图和另一台机器上的 Python 程序(Raspberry Pi、BeagleBone Black、pcDuino 等)将数据写入数据库服务器。

虽然它没有高可用性、五个九正常运行时间(99.999%)数据库服务器的复杂性,但带有附加 USB 硬盘驱动器的低成本 Raspberry Pi 是一个占用空间非常小的数据库服务器,可以放在任何地方。这很好,因为 IOT 解决方案本质上(通常是必然的)需要小而低的成本。必须构建一个昂贵的数据库服务器通常不是所期望的投资水平。

在下一章,我们将探讨一个高级主题:高可用性。更具体地说,我们将看到如何通过提供冗余以及跨多个数据库节点分离读取(SELECT)和写入(INSERTUPDATEDELETE)的能力来使我们的数据库服务器更加可靠。

Footnotes 1

但可以接受基于 USB 的记忆棒和硬盘。

2

Micro Secure Digital (micro SD):一种小型可移动存储驱动器。 http://en.wikipedia.org/wiki/Secure_Digital

3

我的母校也经历了类似的转变。我哀悼知识的丧失。

4

我的第一台真正的电脑是 IBM PCjr。随后,我在 computer 公司制造了自己的 IBM 个人电脑,配有一个 10MB 的硬盘。啊,那是个人电脑的光辉岁月!

5

像你的沙发、最喜欢的躺椅、露台、咖啡吧等地方。

6

确保选择支持设备感应的电缆。如果连接 Raspberry Pi 时,您的 Lapdock 不通电,很可能是 HDMI 电缆的问题。尝试另一根电缆。

7

在向朋友或配偶“借”电缆之前,一定要获得许可——当你完成修改后,它就不能作为标准电缆使用了。

8

不要和 noob 混为一谈,有点贬义。 https://en.wiktionary.org/wiki/noob

9

操作系统是免费的。你只是为某人为你格式化并安装在卡上的图像的便利而付费。

10

他们走得如此之快;反正你也不可能看懂。基本上,除非有错误,否则它们就是噪音,并且通常出现在显示的最后几行中。

11

Raspberry Pi 图像是用fbgrab生成的。可以用sudo apt-get install fbgrab安装。

12

vi是什么意思?如果您曾经有幸第一次尝试学习它,您可能会认为它意味着“几乎不可能”,因为命令很简洁(根据设计),很难记住。不过说真的,vivim或者Vi Improved文字编辑器的简称。这个名字表明最初的编辑器很可能已经被completely不可能使用了!

13

一个高度技术性的术语,指计算机没有做你认为它们应该做的事情。

14

http://alextgalileo.altervista.org/package-repo-configuration-instructions.html

15

https://github.com/ChuckBell/MySQL_Connector_Arduino/blob/master/extras/MySQL_Connector_Arduino_Reference_Manual.pdf

16

https://github.com/ChuckBell/MySQL_Connector_Arduino/blob/master/extras/MySQL_Connector_Arduino_Reference_Manual.pdf

17

Python 专家经常用这个术语来称呼自己。它是为最狂热和最有经验的 Python 程序员保留的。

七、高可用性 IOT 解决方案

您可能想知道高可用性与 IOT 解决方案有什么关系。也就是说,您可能认为高可用性只适用于大型企业,或者过于昂贵,或者极其复杂。虽然高可用性解决方案确实可以达到这些极端 1 并且达到 100%的正常运行时间是困难和昂贵的,2但高可用性在很大程度上被误解也是事实。这在概念上并不复杂,在较低的可靠性水平上使用更适度的投资也不困难。

高可用性对不同的人来说意味着不同的东西,这也是事实。事实上,如果您阅读致力于高可用性的流行行业杂志、书籍、博客等,您很可能会被不同的观点所迷惑。这是因为有许多方法可以实现高可用性,每种方法都可以解决一个特定的方面或符合特定的体系结构。没有一个单一的高可用性实现(解决方案)可以满足所有可能的需求。

然而,我并不是说高可用性是你可以随便打开的。正如您将看到的,这确实需要一些工作,但取决于您的 IOT 解决方案,它可能会使解决方案在小范围内工作良好,但在扩展或投入现场生产时失败。开发者面临的挑战是知道有哪些工具可用,以及如何使用它们来实现一个或多个高可用性目标。

本章介绍了与 IOT 解决方案相关的高可用性,该解决方案基于商用硬件构建,使用 MySQL 存储和检索数据。因此,重点将是利用 MySQL 中的免费工具和特性来实现高可用性。您将看到向您的解决方案添加高可用性 MySQL 选项的几个可用选项,以及如何设置 MySQL 以实现高可用性的示例。让我们从解释高可用性开始。

什么是高可用性?

如果您认为高可用性大致等同于可靠性,那么高可用性就最容易理解了——使解决方案尽可能易于访问,并且在约定的时间段内能够容忍计划内或计划外的故障。也就是说,这是用户对系统可操作性的期望值。系统越可靠,运行时间越长,就相当于可用性水平越高。

高可用性可以通过多种方式实现,从而产生不同级别的可用性。这些级别可以表示为达到某种更高可靠性状态的目标。本质上,您使用技术和工具来提高可靠性,并使解决方案尽可能长时间地保持运行和数据可用(也称为正常运行时间)。正常运行时间表示为解决方案运行时间的比率或百分比。

Reliability Vs. High Availability: What Is The Difference?

可靠性是对解决方案随时间推移的可操作性的度量,它涵盖了高可用性的主要目标之一。事实上,您可以说,可靠性的最高水平(解决方案始终可运行)是高可用性的定义。因此,要使您的解决方案成为高可用性解决方案,您应该专注于提高可靠性。

您可以通过实践以下工程原则来实现高可用性:

  • 消除单点故障:设计您的解决方案时,尽可能减少组件数量,以免在组件出现故障时导致解决方案无法使用。
  • 通过冗余增加恢复:设计您的解决方案以允许多个活动冗余机制,从而允许从故障中快速恢复。
  • 实施容错:将您的解决方案设计为主动检测故障,并通过切换到冗余或替代机制来自动恢复。

这些原则是实现更高级别的可靠性和高可用性的基础或步骤。即使您不需要实现最大的高可用性(解决方案几乎一直处于运行状态),通过实施这些原则,您至少会使您的解决方案更加可靠,这是一个很好的目标。

对于您自己构建的一个简单的 IOT 项目来说,这些原则似乎不是合理的要求,但是对于较大的 IOT 解决方案来说,它会产生巨大的差异。例如,假设您想采用本书中建议的植物监控解决方案,并将其构建为一个大型苗圃的数百个温室中的植物管理解决方案。有些人可能会告诉你,把所有的数据放在一个地方是不可能的,或者有太多的问题需要解决。然而,这至少是短视的。

相反,这个目标在概念上和实施上都是可以实现的。你确实可以设置数千个工厂监控器 3 并将它们连接在一起,通过互联网从一个应用进行监控。当你期望所有的硬件都一直工作,从不出故障时,问题就来了。也就是说,如果某个主要组件(如数据库服务器)因故障或需要维护而离线,您该怎么办?你如何从中恢复过来?

此外,如果一些中间节点出现故障,您如何让您的解决方案继续运行?对于 IOT 解决方案,这包括关键组件,如应用、数据库服务器、web 服务器,甚至数据收集器或中间节点,这取决于解决方案的关键性质。

通过使用特定的工具或技术来构建解决方案,您可以在解决方案中实现高可用性的几个特征或目标。表 7-1 列出了许多高可用性目标和可能的实现。

表 7-1。

High Availability Goals for IOT Solutions

| 目标 | 技术 | 工具 | | --- | --- | --- | | 从存储介质故障中恢复 | 恢复 | 备份和恢复工具 | | 从数据库故障中快速恢复 | 裁员 | 数据库的多个副本 | | 提高性能 | 缩放比例 | 分裂作者和读者 | | 没有数据收集的损失 | 容错 | 缓存数据和使用冗余节点 |

正如您所看到的,有几个概念和相应的工具或技术可以用来实现不同的目标或高可用性级别。还要注意一些目标有重叠的解决方案。请记住,此列表并没有涵盖每个高可用性目标;相反,它列出了那些提供高可用性功能的目标,这些功能相对容易以最少的投资实现,换句话说,就是您可以使用已知的解决方案和技术在 IOT 解决方案中实现高可用性的目标。我将在下一节更详细地讨论这些目标。

So, What Is Five Nines?

你可能听说过或读到过一个叫做“五个九”的概念,即一年 99.999%的正常运行时间。因此,五个九的解决方案每年最多只允许 5.26 分钟的停机时间。但是“五个九”只是可靠性的一个等级,还包括其他类别,每个类别都与正常运行时间或可靠性的百分比有关。有关可用类别的更多信息,请参见 https://en.wikipedia.org/wiki/High_availability#Percentage_calculation

使用 MySQL 的 IOT 解决方案的高可用性选项

既然您已经了解了高可用性(HA)可以解决的目标或要求,现在让我们讨论一下在您的 IOT 解决方案中实施 HA 的一些选项。因为我们将数据放在数据库服务器中,所以我们将专注于 MySQL 的技术和工具。但是,您可以在数据库服务器之外做一些事情来帮助实现更好的可靠性。

Can Mysql Really Reach High Availability?

您不仅可以使用 MySQL 实现高可用性,还可以使用许多选项来实现高可用性,有些选项来自第三方供应商,还有 Oracle 的一些工具。甚至 MySQL 本身也是用高可用性的基本构件设计的。然而,MySQL 的特性以及用于高可用性的工具和解决方案允许您定制 MySQL 来提供您所需要的可靠性。有关 MySQL 高可用性解决方案的详细信息,请参阅 Bell、Kindahl 和 Thalmann (O'Reilly,2014)的 MySQL 高可用性。

以下部分讨论了实现高可用性目标的四个选项。通过实施所有这些,您将在您的 IOT 解决方案中实现一定级别的高可用性。您的成就不仅取决于您如何实现这些选项,还取决于您满足可靠性目标的程度。

恢复

最容易实现的可靠性是从故障中恢复的能力。这可能是组件、节点、数据库服务器或解决方案的任何其他部分出现故障。因此,恢复就是如何以尽可能少的时间和成本让解决方案恢复运行。

但是,可能无法从所有类型的故障中恢复。例如,如果您的一个或多个数据收集器出现故障,恢复可能需要更换硬件,并在停机期间丢失数据。对于其他类型的故障,恢复选项可能允许更快地恢复运行。此外,有些组件更重要,必须是可恢复的,因此您应该努力保护那些更重要的组件,其中数据库是主要的。

例如,如果您的数据由于硬件故障而损坏或丢失,您需要一种尽可能少丢失数据的方法来恢复数据。实现这一点的一种方法是保留数据的频繁备份副本,以后可以恢复这些副本以防止数据丢失。

许多关于备份和恢复数据的各种策略的书籍已经问世。我没有试图解释每一个细微差别、技术和最佳实践,而是向您推荐了许多可用的文本。对于本章和 MySQL 可用的解决方案,理解有两种类型的备份方法(逻辑和物理)就足够了,每种方法都有自己的优点。

逻辑备份通过遍历数据、逐行制作数据副本以及通常将数据从二进制形式转换为 SQL 语句来制作数据副本。逻辑备份的优势在于数据是可读的,甚至可以在恢复数据之前对其进行修改或更正。不利的一面是,对于较大的数据量,逻辑备份往往较慢,并且可能比实际数据占用更多的存储空间(取决于数据类型、索引数量等)。

物理备份从磁盘存储层制作数据的二进制副本。备份通常是特定于应用的;您必须使用制作备份的同一应用来恢复它。优点是备份速度更快,大小更小。此外,执行物理备份的应用具有一些高级功能,如增量备份(仅备份自上次备份以来发生变化的数据)和其他高级功能。对于小型解决方案,逻辑备份可能已经足够,但是随着解决方案(数据)的增长,您可能需要考虑物理备份解决方案。

裁员

可靠性的一个更具挑战性的实现是冗余——让两个或更多的组件在系统中扮演相同的角色。冗余的目标可能只是在需要替换主要组件的情况下准备一个组件。这可能是一个热备用,其中组件主动与主组件并行工作,当检测到故障时,系统会自动切换到冗余组件。冗余最常见的目标是数据库服务器。MySQL 在这方面有一个出色的特性,叫做复制。

对于最基本的用例,即热备用和备份,MySQL 复制并不难设置。为此,您设置了第二个数据库服务器,它可以获得在原始服务器上所做的所有更改的副本。原始服务器称为主服务器或主要服务器,第二个服务器称为从属服务器或辅助服务器。MySQL 复制是一个非常大的主题,在本章的后面我将专门用一节来讨论它。

还可以在您的 IOT 网络中实现冗余,方法是包含冗余节点,包括冗余数据收集器,或者甚至使用多个传感器以防一个传感器出现故障。您不太可能这样做,但这确实是可能的,我已经看到了一些冗余数据节点的例子。例如,您可以使用两个微控制器作为数据节点;一个是主数据库,连接到您的数据收集器以检索数据。同时,第二微控制器周期性地与第一微控制器交换消息(称为握手)。因此,当第二个微控制器没有响应时,您将实现一个基本的信号/返回方法。

另一种可能的冗余措施是在数据聚合器(将数据写入数据库的节点)上编写代码,以检测数据库服务器何时不再响应(连接失败),并切换到将数据写入本地日志。 5 代码会通过尝试重新连接来定期检查数据库服务器。一旦重新连接,它就读取日志并将数据插入数据库。

这是一个很好的策略,也是 DIY IOT 解决方案中最常见的策略之一。然而,这也有不好的一面。如果在数据库服务器上使用日期和时间字段(如时间戳),保存恢复数据的日期和时间将会出错。在这种情况下,当数据写入日志时,您必须保存正确的日期和时间。

在您的 IOT 解决方案中,还可以实现其他冗余。您可以实施冗余电源选项(例如,太阳能、电池),使用多个传感器以防故障,或者在一个传感器出现故障时使用多个通信协议(在 WiFi 出现故障时使用 XBee 模块),等等。没有任何理由不能在您的解决方案中构建冗余。然而,只有你,设计者,才知道哪些节点是最关键的,从而知道哪些节点在发生故障时需要复制。

冗余机制的复杂程度是你可以控制的,取决于你想投入多少。事实上,冗余的复杂程度与实现的工作量或费用有关。

例如,您可以使用一个备用组件,当原始组件出现故障时,可以手动激活该组件,这是一个缓慢的过程,需要手动干预。或者,您可以使用主动组件来代替主组件,主组件仍需要手动干预,但恢复速度更快。或者,您可以编写代码来自动检测故障,并切换到第二个,这是最好的(最快的),但需要更多的编程,因此需要更多的工作(可能要多得多)。

因此,您可以定制您的冗余,以满足您的需求或能力。您可以从简单的离线备件开始,并随着解决方案的发展增加更多的复杂性。

缩放比例

另一个可靠性实现与性能有关。在这种情况下,您希望最大限度地减少存储和检索数据的时间。MySQL 复制是实现可伸缩性的一种很好的方式。您可以通过设计解决方案将数据写入(保存)到主设备(主设备)并从从设备(辅助设备)读取数据来实现这一点。随着应用的增长,您可以添加额外的从机来帮助最小化读取数据的时间。拥有额外的从属服务器允许您的应用同时运行多个实例甚至多个连接(每个从属服务器至少一个)。因此,可伸缩性建立在 MySQL 的冗余特性之上。

通过拆分写和读,可以减轻主机执行许多语句的负担。考虑到大多数应用的读操作比写操作多得多,使用不同的服务器(或几个服务器)来提供读操作的数据并将写操作留给一个主服务器是有意义的。

对于大多数 IOT 解决方案来说,可伸缩性可能不是最迫切的,但对于较大的解决方案或具有大量数据的解决方案来说,可伸缩性可能是提高性能的好方法。我将在下一章展示一个使用 MySQL 的可伸缩性的例子,但是如何设置 MySQL 复制的概念与创建一个热备用是一样的。不同之处在于,您的应用内置了跨复制服务器拆分写入和读取的能力。

当然,还有其他不需要实现 MySQL 复制就能提高性能的方法,但是从长远来看,您可能不会获得太多好处。对于数据收集器比预期慢的情况,您更有可能使用更快的组件来提高性能。

容错

可靠性的最后一个实现,实际上也是大多数高可用性解决方案在正常运行时间方面的区别是容错,即检测故障并从事件中恢复的能力。容错是通过利用恢复和冗余以及添加检测机制和主动切换来实现的。

例如,如果数据收集器脱机,而正在收集的数据很关键,您的解决方案应该检测到数据收集器脱机,并切换到冗余数据收集器。因此,实现这个例子需要冗余的数据收集器。我将在下一章展示一个冗余数据收集器的例子。

您还可以在数据库中实现容错。我们再次利用 MySQL 复制来实现切换。也就是说,当主服务器关闭时,我们使用 MySQL 中的复制命令将主服务器的角色切换到一个从服务器。使用 MySQL 时,有两种类型的主角色变化:切换,即当主服务器仍在运行时,将主服务器的角色切换到从服务器;故障转移,即当主服务器不再运行时,选择从服务器来承担主服务器的角色。也就是说,切换是有意的,而故障转移是被动的。

Oracle 提供了一些工具来帮助您设置自动故障转移。您可以使用 MySQL 实用程序(mysqlfailover)来监控您的主服务器,并在主服务器离线时切换到从服务器。对于拥有许多服务器的大型解决方案,您可以使用 MySQL Fabric 来管理整个服务器群,自动执行故障转移以及其他更复杂的高可用性操作。还有 MySQL 路由,它是 MySQL 的连接路由,允许您设置路由使用的一组特定服务器,以便路由在当前服务器离线(变得不可访问)时自动切换到另一台服务器。你可以在 MySQL 下载页面( http://dev.mysql.com/downloads/ )找到所有这些产品。都是开源产品。

既然我们已经讨论了高可用性对于 IOT 解决方案的一些实现,那么让我们来看一些实现或设置概念的技术演示。

高可用性技术

您可以通过多种方式实现高可用性概念。有太多的方法需要一整本书来解释,甚至是一部分更常见的技术。回想一下,通常没有必要尝试实现每种技术,因为要么你根本不需要它,要么实现的成本大于收益。考虑到这一点,本节将介绍一些您希望在 IOT 解决方案中考虑实施的最常见的技术,从备份和恢复开始。

备份和恢复

回想一下,可靠性最简单的概念是备份/恢复。为了取得成功,您必须计划、执行和审核数据备份和恢复操作。最常被忽视的一个方面是审计。好的备份和恢复应该是可靠的。也就是说,应该检查或审核备份的输出,以确保它是完整的,并且可以成功恢复。因此,您应该通过在测试机器上恢复备份来测试它们,以确保它们在您需要时是可用的。

本节描述了备份 MySQL 数据所需的一些概念和工具。对于那些可能不熟悉备份和恢复系统以及 MySQL 可用解决方案的人,我将详细讨论这些概念。让我们从定义备份和恢复的目标开始。

备份操作必须制作数据的精确副本。此外,备份副本必须一致。也就是说,备份仅包含在拷贝开始之前提交的事务,不包含部分或未提交的数据。恢复操作必须用备份归档中的数据替换系统上的数据,以便生成的数据与归档中的数据相同。

不幸的是,很少有备份解决方案能够满足所有这些备份和恢复标准。这样做的通常是专有的,昂贵且难以维护。下一节将介绍一些备份和恢复 MySQL 数据的经济方法。

幸运的是,有几种产品可以用来对 MySQL 数据进行逻辑和物理备份。事实上,Oracle 提供了几个产品,包括三个开源选项和一个付费选项。表 7-2 列出了 Oracle 提供的用于备份数据的选项。

表 7-2。

Backup Options for MySQL

| 工具 | 类型 | 许可证 | 统一资源定位器 | 笔记 | | --- | --- | --- | --- | --- | | `mysqldump` | 逻辑学的 | 开放源码 | [`http://dev.mysql.com/doc/refman/5.7/en/mysqldump.html`](http://dev.mysql.com/doc/refman/5.7/en/mysqldump.html) | 包含在服务器安装中 | | `mysqlpump` | 逻辑学的 | 开放源码 | [`http://dev.mysql.com/doc/refman/5.7/en/mysqlpump.html`](http://dev.mysql.com/doc/refman/5.7/en/mysqlpump.html) | 包含在服务器安装中 | | `mysqldbexport`和`mysqldbimport` | 逻辑学的 | 开放源码 | [`http://dev.mysql.com/doc/index-utils-fabric.html`](http://dev.mysql.com/doc/index-utils-fabric.html) | 包含在 MySQL 实用程序中 | | MySQL 企业备份 | 物理的 | 收费的 | [`http://dev.mysql.com/doc/index-enterprise.html`](http://dev.mysql.com/doc/index-enterprise.html) | 包含在 MySQL 企业订阅中 | | 存档原件 | 物理的 | 自由的 |   | 操作系统工具 |

请注意,我包括工具的名称、生成的备份类型、许可证,以及一个链接,可以找到关于该工具的更多信息,并根据您的需求制定最佳解决方案。我将在接下来的章节中详细讨论这些问题。

mysqldump 客户端

一个流行的逻辑备份工具是mysqldump客户端应用。它成为 MySQL 服务器的一部分已经有一段时间了,并且会在您安装服务器时自动安装。客户端创建一组 SQL 语句,当您重新运行数据库时,这些语句会重新创建数据库。例如,输出包含创建数据库和表所需的所有CREATE语句,以及重新创建数据所需的所有INSERT语句。您可以使用mysql客户端实用程序来读取文件以恢复它。

这种形式的备份在更改或更正数据时非常有用。只需备份数据库,编辑生成的语句,然后重新运行语句以使更改生效。该技术的一个可能用途是纠正需要分类的数据值(转换标准已更改)或者可能更改数据的校准。

但是,请记住,逻辑备份(在这种情况下是包含 SQL 语句的文件)备份起来会很慢,恢复起来也会更慢。这是因为数据库服务器必须读取每条语句并执行它们。因此,您正在强迫数据库服务器重做最初保存数据的工作。

您可以使用mysqldump来备份您的所有数据库、数据库的特定子集,甚至是给定数据库中的特定表。清单 7-1 展示了一个为特定数据库备份特定表的例子。

Listing 7-1.Using mysqldump to Back Up a Table

$ mysqldump -uroot --password  plant_monitoring plants

Enter password:

-- MySQL dump 10.13  Distrib 5.7.8-rc, for osx10.8 (x86_64)

--

-- Host: localhost    Database: plant_monitoring

-- ---------------------------------------------

-- Server version        5.7.8-rc-log

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;

/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;

/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;

/*!40101 SET NAMES utf8 */;

/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;

/*!40103 SET TIME_ZONE='+00:00' */;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;

/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;

/*!40101 SET @OLD_SQL_MODE=@@SQL``_

/*!40111 SET @OLD_SQL_NOTES= @@SQL_NOTES, SQL_NOTES=0 */;

--

-- Table structure for table plants``

--

DROP TABLE IF EXISTS plants;

/*!40101 SET @saved_cs_client     = @@character_set_client */;

/*!40101 SET character_set_client = utf8 */;

CREATE TABLE plants (

``id int(11) NOT NULL AUTO_INCREMENT,

``name char(50) DEFAULT NULL,

``location char(30) DEFAULT NULL,

``climate enum('inside','outside') DEFAULT 'inside',

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1;

/*!40101 SET character_set_client = @saved_cs_client */;

--

-- Dumping data for table plants``

--

LOCK TABLES plants WRITE;

/*!40000 ALTER TABLE plants DISABLE KEYS */;

INSERT INTO plants VALUES (1,'Jerusalem Cherry','deck','outside'),(2,'Moses in the Cradle','patio','outside'),(3,'Peace Lilly','porch','inside'),(4,'Thanksgiving Cactus','porch','inside'),(5,'African Violet','porch','inside');

/*!40000 ALTER TABLE plants ENABLE KEYS */;

UNLOCK TABLES;

/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;

/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;

/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;

/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

-- Dump completed on 2015-11-23 20:25:07

注意输出的详细程度。如您所见,客户机捕获了大量关于服务器、数据库和表的信息。还要注意,默认情况下使用 bulk insert 语句。是的,这个文件可以通过管道传输到mysql客户端并执行。有许多选项允许您控制客户端的工作方式。如果以 SQL 语句的形式创建备份听起来是您的最佳选择,请参见在线 MySQL 参考手册中的mysqldump ( http://dev.mysql.com/doc/refman/5.7/en/mysqldump.html )。

mysqlpump客户端实用程序

mysqlpump客户端是mysqldump客户端实用程序的更新版本。它经过了彻底的重新设计,提高了备份和恢复的速度。还有一些用于管理备份创建方式的附加选项,以及旧客户端中没有的高级功能。然而,在概念上,它的工作方式与mysqldump相同。清单 7-2 显示了mysqlpump的输出示例。

Listing 7-2.Using the mysqlpump Client Utility

$ mysqlpump -uroot -p plant_monitoring --skip-definer

Enter password:

-- Dump created by MySQL pump utility, version: 5.7.8-rc, osx10.8 (x86_64)

-- Dump start time: Mon Nov 23 20:38:01 2015

-- Server version: 5.7.8

SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;

SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;

SET @OLD_TIME_ZONE=@@TIME_ZONE;

SET TIME_ZONE='+00:00'

SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;

SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;

SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;

SET NAMES utf8;

CREATE DATABASE /*!32312 IF NOT EXISTS*/ plant_monitoring /*!40100 DEFAULT CHARACTER SET latin1 */;

CREATE TABLE plant_monitoring.plants (

``id int(11) NOT NULL AUTO_INCREMENT,

``name char(50) DEFAULT NULL,

``location char(30) DEFAULT NULL,

``climate enum('inside','outside') DEFAULT 'inside',

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1

;

INSERT INTO plant_monitoring.plants```VALUES`

Dump progress: 0/1 tables, 5/0 rows

DELIMITER //

CREATE FUNCTION plant_monitoring.max_samples_today(in_id int) RETURNS int(11)

READS SQL DATA

DETERMINISTIC

BEGIN

DECLARE num_samples int;

SELECT COUNT(*) into num_samples FROM plant_monitoring.readings

WHERE DATE(event_time) = CURRENT_DATE() AND readings.id = in_id;

RETURN num_samples;

END//

DELIMITER ;

;

SET TIME_ZONE=@OLD_TIME_ZONE;

SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;

SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;

SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;

SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;

SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;

-- Dump end time: Mon Nov 23 20:38:01 2015

Dump completed in 549 milliseconds

如您所见,输出是相似的。像mysqldump一样,有许多选项允许你控制客户端如何工作。如果以 SQL 语句的形式创建备份听起来是您的最佳选择,请参见在线 MySQL 参考手册中的mysqlpump ( http://dev.mysql.com/doc/refman/5.7/en/mysqlpump.html )。

MySQL 实用程序数据库导出和导入

MySQL Utilities 是一组用 Python 编写的实用程序,旨在为 MySQL 提供一个解决方案,发展中的开发运营文化(DevOps 6 )可以使用它来自动化许多重复的任务。更具体地说,这些实用程序可以帮助您有效地管理 MySQL。有许多实用工具可用,但本节介绍了两个可用于帮助备份和还原数据的实用工具。当您需要制作数据副本以进行转换(对数据进行批量或有针对性的更改)或制作人类可读的数据副本时,它们可能会很有帮助。

第一个效用是mysqldbexport。该实用程序允许您读取数据库(一个选定的列表或所有数据库)并以几种格式之一生成输出,包括 SQL 语句、逗号或制表符分隔的列表以及网格或垂直输出,类似于mysql客户端显示数据的方式。您可以将它重定向到一个文件供以后使用。然而,这些替代格式需要使用mysqldbimport来恢复它们。第二个效用是mysqldbimport。这个实用程序读取由mysqldbexport实用程序产生的输出。

这两个实用程序都允许您仅导入对象定义和/或数据。这听起来可能与mysqldumpmysqlpump相似,并且在许多方面确实如此,但是这些实用程序具有简化的选项(对客户端实用程序的一个批评是可用的大量选项),并且由于它们是用 Python 编写的,数据库专业人员可以通过直接修改代码来定制导出和导入以满足他们自己的需求。

清单 7-3 展示了一个运行mysqlbackup实用程序的例子。请注意,输出也类似于前面的客户端实用程序。

Listing 7-3.Using MySQL Utilities for Backup

$ mysqldbexport --server=root@localhost:3306 plant_monitoring --format=CSV --no-headers --export=both

# Source on localhost: ... connected.

# Exporting metadata from plant_monitoring

# TABLES in plant_monitoring:

``plant_monitoring,plants,InnoDB,1,id,int(11),NO,,PRI,latin1_swedish_ci,,,,,,,PRIMARY,id,,

``plant_monitoring,plants,InnoDB,2,name,char(50),YES,,,latin1_swedish_ci,,,,,,,PRIMARY,id,,

``plant_monitoring,plants,InnoDB,3,location,char(30),YES,,,latin1_swedish_ci,,,,,,,PRIMARY,id,,

``plant_monitoring,plants,InnoDB,4,climate,"enum('inside','outside')",YES,inside,,latin1_swedish_ci,,,,,,,PRIMARY,id,,

# FUNCTIONS in plant_monitoring:

``max_samples_today,SQL,READS_SQL_DATA,YES,DEFINER,root@localhost,in_id int,int(11),"BEGIN

DECLARE num_samples int;

SELECT COUNT(*) into num_samples FROM plant_monitoring.readings

WHERE DATE(event_time) = CURRENT_DATE() AND readings.id = in_id;

RETURN num_samples;

END","ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION",utf8,utf8_general_ci,latin1_swedish_ci

# PROCEDURES in plant_monitoring: (none found)

# VIEWS in plant_monitoring: (none found)

# TRIGGERS in plant_monitoring: (none found)

# EVENTS in plant_monitoring: (none found)

# GRANTS in plant_monitoring: (none found)

#...done.

# Exporting data from plant_monitoring

# Data for table plant_monitoring.plants:

id`,`name`,`location`,`climate

1,Jerusalem Cherry,deck,outside

2,Moses in the Cradle,patio,outside

3,Peace Lilly,porch,inside

4,Thanksgiving Cactus,porch,inside

5,African Violet,porch,inside

#...done.

与其他客户端一样,MySQL 实用程序的导出和导入工具支持许多控制导出和导入的选项(尽管没有那么多)。您可以在在线 MySQL 实用程序参考手册( http://dev.mysql.com/doc/index-utils-fabric.html )中找到对mysqldbexportmysqldbimport更深入的研究。

MySQL 企业备份

如果您正在寻找一种允许您对数据进行无阻塞、物理层备份的备份解决方案,并且您已经使用 InnoDB 存储引擎作为您的主要数据存储,那么您将需要研究 Oracle 的 MySQL 企业备份产品。MySQL 企业备份是 MySQL 企业版产品的一部分。您可以在 http://dev.mysql.com/doc/index-enterprise.html 找到有关 MySQL 企业版的更多信息。

虽然 MySQL Enterprise Backup 是一个收费应用,但您可以从 Oracle 的 eDelivery 系统下载试用版。为此,进入 https://edelivery.oracle.com/ ,进入 MySQL 企业版,从选择平台下拉列表中选择您的平台;然后点按“继续”并按照提示进行操作。您必须拥有 Oracle web 帐户才能访问该系统。如果您没有这样的帐户,您可以在网站上创建一个。

有关 MySQL 企业备份和其他 MySQL 企业版产品的更多信息,请参见 http://dev.mysql.com/doc/index-enterprise.html

Physical File Copy

最后,备份 MySQL 最简单也是最基本的方法就是简单地复制文件。不幸的是,这需要停止服务器,这可能并不理想。要执行文件复制,请停止服务器并复制服务器上的数据目录和任何安装文件。一种常见的方法是使用 Unix tar命令创建一个归档文件。然后,您可以将该归档文件移动到另一个系统,并恢复数据目录。

数据目录是 MySQL 保存数据库中所有数据的地方。您可以使用以下命令发现它的位置:

mysql> SHOW VARIABLES LIKE 'datadir'

+---------------+------------------------+

| Variable_name | Value                  |

+---------------+------------------------+

| datadir       | /usr/local/mysql/data/ |

+---------------+------------------------+

1 row in set, 0 warning (0.00 sec)

复制完成后,您应该将文件移动到保存位置(不在同一个磁盘上)。要恢复数据,您必须再次停止服务器,然后将文件复制到数据目录并将其解压缩。

Tip

为备份映像使用有意义的文件名总是一个好主意。

这种类型的备份的缺点是它会制作所有数据库和所有数据的完整副本。这对于一般情况来说可能没问题,但对于只恢复几个数据库中的一个可能不是最好的。您不能简单地复制每个数据库的文件(它们存储在文件夹中),因为如果您使用 InnoDB 并且没有打开每个表的文件选项,所有数据都存储在名为ib*的文件中。

此外,根据数据的大小,您的服务器不仅在复制文件时必须脱机,而且在加载任何其他数据(如缓存条目、使用内存表进行快速查找等)时也必须脱机。因此,物理副本备份对于某些安装可能不可行。

Backup and Recovery with the Binary Log

如果您的恢复目标是能够恢复自上次备份以来更改或添加的数据,而您的备份解决方案不提供增量备份或类似的最新备份,您将面临丢失新数据的风险。虽然这听起来像是您需要将备份计划在您能够承受的丢失数据的最短时间内(这总是一个好策略),但是您可以通过将计划的备份与二进制日志相结合来实现最新的恢复。

二进制日志是特殊的文件,包含对数据库所做的所有更改的副本。事实上,二进制日志记录是复制中使用的主要机制,用于从主服务器捕获更改并将其传输到从服务器。您可以通过在配置文件中使用以下选项来打开二进制日志记录(例如,my.cnf)。该选项采用一个文件名前缀,您可以用它来命名二进制日志(这一点很重要,您将在后面的复制入门中看到)。

[mysqld]

log-bin=mysql-bin

由于二进制日志记录了在数据库运行时对数据所做的所有更改,因此通过还原正确的备份并回放二进制日志直到出现故障的适当时刻,可以将服务器还原到某个精确的时间点。这称为时间点恢复( http://dev.mysql.com/doc/refman/5.7/en/point-in-time-recovery.html )。您可以使用名为mysqlbinlog的客户端实用程序重放二进制日志。

然而,这只有在您跟踪二进制日志和最后一次备份的位置时才起作用。最好的方法是在备份之前刷新日志。生成的新文件是恢复的开始(新更改的第一个日志)。

Note

有一种较新的复制类型,它使用全局事务标识符(GTIDs ),这是二进制日志中的特殊标记,用于指定事务的开始和来源。遗憾的是,GTIDs 仅在 MySQL 5.6 及更高版本中可用。默认存储库中可用的大多数 MySQL 版本是 MySQL 版本 5.1 或 5.5,它们不支持 GTIDs。如果要使用 GTIDs,更多详情请参见在线 MySQL 参考手册(http:// dev.mysql.com/doc/refman/5.7/en/replication-gtids.html)。

修复服务器后,您可以还原最新的备份映像,并使用最后一个二进制日志名称和位置作为起点来应用二进制日志。下面介绍了一个使用备份系统执行时间点恢复的过程:

Return your server to an operational state after the event.   Find the latest backup for the databases you need to restore.   Restore the latest backup image.   Apply the binary log with the mysqlbinlog utility using the starting position (or starting date/time) from the last backup.

应用二进制日志后,您的服务器已经恢复,您可以将其恢复服务。当然,这是再做一次备份的好时机! 7

Tip

为了更容易进行时间点恢复,请始终在备份之前刷新日志。

MySQL 复制入门

使用外部驱动器保存 MySQL 数据的最大好处之一是,您可以随时关闭服务器,断开驱动器,将其插入另一个系统,然后复制数据。如果您的 Raspberry Pi 数据库服务器位于一个(物理上)容易到达的位置,并且有时可以关闭服务器,那么这听起来可能很棒。

然而,对于一些 IOT 网络来说,情况可能并非如此。使用像 Raspberry Pi 这样的低成本计算机板作为数据库服务器的好处之一是,服务器可以驻留在数据收集器节点附近。如果 IOT 网络的一部分在一个孤立的区域,你可以通过把树莓派放在相同的位置来收集和存储数据。但是,如果没有网络连接到数据库服务器,这可能意味着要跋涉到一个谷仓或池塘,或者步行几个足球场的长度到一个工厂的内部去得到硬件。

但是,如果您的 Raspberry Pi 连接到网络,您可以使用 MySQL 的一项名为复制的高级功能来制作数据的实时最新副本。这不仅意味着您可以拥有备份,还意味着您可以查询维护副本的服务器,从而减轻您的 Raspberry Pi 的复杂或长时间运行的查询负担。这也意味着如果第一台服务器(主服务器)出现故障,您可以有一个热备用服务器。也就是说,您可以切换到副本(从属)并保持您的应用运行。Raspberry Pi 是一台很酷的小尺寸计算机,但它不是数据仓库。

什么是复制,它是如何工作的?

MySQL 复制是一个易于使用的特性,也是 MySQL 服务器的一个复杂和主要的组件。本节提供了复制的鸟瞰图,目的是解释它是如何工作的以及如何设置一个简单的复制拓扑。有关复制及其众多特性和命令的更多信息,请参见在线 MySQL 参考手册( http://dev.mysql.com/doc/refman/5.7/en/replication.html )。

复制需要两台或更多服务器。必须将一台服务器指定为源服务器或主服务器。主角色意味着对数据的所有数据更改(写入)都发送到主服务器,并且只发送到主服务器。拓扑中的所有其他服务器维护主数据的副本,并且根据设计和要求是只读服务器。因此,当您的传感器发送数据进行存储时,它们会将数据发送给主设备。您编写的使用传感器数据的应用可以从从属服务器读取这些数据。

复制机制使用一种称为二进制日志的技术,该技术以一种特殊的格式存储更改,从而保留所有更改的记录。这些变化然后被运送到从设备,并在那里重新执行。因此,一旦从服务器重新执行更改(称为事件),它就拥有了数据的精确副本。

主服务器维护更改的二进制日志,从服务器维护该二进制日志的副本,称为中继日志。当从设备向主设备请求数据更改时,它从主设备读取事件并将它们写入其中继日志;然后,从属线程中的另一个线程执行中继日志中的那些事件。可以想象,从主服务器上发生更改到从服务器上发生更改会有一点延迟。幸运的是,除了在高流量(大量变化)的拓扑中,这种延迟几乎是不明显的。出于您的目的,当您从从属服务器读取数据时,它可能是最新的。您可以使用命令SHOW SLAVE STATUS检查从设备的进度;在许多其他事情中,它向你展示了奴隶已经落后于主人有多远。您将在后面的小节中看到这个命令的运行。

现在您已经对复制及其工作原理有了一些了解,让我们来看看如何设置它。下一节将讨论如何将 Raspberry Pi 设置为主机,将桌面计算机设置为从机。

如何设置复制

本节演示如何设置从 Raspberry Pi(主)到桌面计算机(从)的复制。这些步骤包括通过启用二进制日志记录和创建用于读取二进制日志的用户帐户来准备主服务器,通过将从服务器连接到主服务器来准备从服务器,以及启动从服务器进程。这一部分以对复制系统的测试结束。

准备母版

复制要求主服务器启用二进制日志记录。默认情况下它是不打开的,因此您必须编辑配置文件并将其打开。使用sudo vi /etc/mysql/my.cnf编辑配置文件,并通过取消注释和更改以下行来打开二进制日志记录:

server-id               = 1

log_bin                 = /media/HDD/mysql/mysql-bin.log

第一行设置主服务器的服务器 ID。在基本复制(5.5 版)中,每台服务器必须有一个唯一的服务器 ID。在这种情况下,您将 1 分配给主服务器;从机将具有一些其他值,例如 2。富有想象力,是吗?

下一行设置二进制日志文件的位置和名称。您将它保存到您的外部驱动器,因为像数据本身一样,二进制日志会随着时间的推移而增长。幸运的是,MySQL 旨在将文件保持在一个合理的大小,并具有允许您截断它并开始一个新文件的命令(这一过程称为旋转)。有关管理二进制日志文件的更多信息,请参见在线参考手册( http://dev.mysql.com/doc/refman/5.5/en/slave-logs-relaylog.html )。

保存编辑后,您可以使用以下命令重新启动 MySQL 服务器:

pi@raspberrypi /etc $ sudo /etc/init.d/mysql restart

[ ok ] Stopping MySQL database server: mysqld.

[ ok ] Starting MySQL database server: mysqld . . ..

[info] Checking for tables which need an upgrade, are corrupt or were

not closed cleanly..

要测试更改,在 MySQL 控制台中发出以下命令。您应该看到新变量已经被设置为ON

mysql> show variables like 'log_bin'

+---------------+-------+

| Variable_name | Value |

+---------------+-------+

| log_bin       | ON    |

+---------------+-------+

1 row in set (0.01 sec)

打开二进制日志记录后,您必须创建一个用户,供从属服务器用来连接到主服务器并读取二进制日志。这个有一个特殊的特权叫做REPLICATION SLAVE。下面显示了创建用户和添加权限的正确的GRANT语句。记住您在这里使用的用户名和密码——您需要它用于从属服务器。

mysql> GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'%' IDENTIFIED BY 'secret'

Query OK, 0 rows affected (0.01 sec)

但是从机还需要一条信息。从机需要知道要读取的二进制日志的名称,以及从文件中的什么位置开始读取事件。您可以使用SHOW MASTER STATUS命令来确定。

mysql> show master status;

+------------------+----------+--------------+------------------+

| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB |

+------------------+----------+--------------+------------------+

| mysql-bin.000001 |      245 |              |                  |

+------------------+----------+--------------+------------------+

1 row in set (0.00 sec)

mysql>

现在您已经有了主服务器的二进制日志文件名和位置以及复制用户和密码,您可以访问您的从服务器并将其连接到主服务器。您还需要知道 Raspberry Pi 的主机名或 IP 地址,以及 MySQL 运行的端口。默认情况下,端口是 3306;但是如果你改变了它,你应该注意到新的值。记下表 7-3 中所有信息。

表 7-3。

Information Needed from the Master for Replication

| 主文件中的项目 | 价值 | | --- | --- | | IP 地址或主机名 |   | | 港口 |   | | 二进制日志文件 |   | | 二进制日志文件位置 |   | | 复制用户 ID |   | | 复制用户密码 |   |
准备奴隶

您想要用作从属服务器的 MySQL 服务器应该与 Raspberry Pi 上的服务器版本相同,或者至少是兼容的服务器。在线参考手册指定了哪些 MySQL 版本可以很好地协同工作。幸运的是,有问题的版本列表非常短。在本节中,您应该在台式机或服务器计算机上安装一台服务器,并确保其配置正确。

将从设备连接到主设备所需的步骤包括发出一个CHANGE MASTER TO命令来连接到主设备,以及发出一个START SLAVE命令来启动服务器上的从设备角色。是的,就是这么简单!回想一下,您需要来自主机的信息来完成这些命令。以下命令显示了一个从设备连接到一个运行在 Raspberry Pi 上的主设备。让我们从CHANGE MASTER TO命令开始。

Listing 7-4.Using the CHANGE MASTER TO Command

Chucks-iMac:∼ cbell$ mysql -uroot -psecret -h 127.0.0.1 --port=13003

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 3

Server version: 5.5.21 Source distribution

Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.

This software comes with ABSOLUTELY NO WARRANTY. This is free software,

and you are welcome to modify and redistribute it under the GPL v2 license

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> CHANGE MASTER TO MASTER_HOST='10.0.1.17', MASTER_PORT=3306, MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=245, 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才能看到它们。清单 7-5 展示了这两个命令的运行。

Tip

对于宽结果,使用\G选项将列视为行(称为垂直格式)。

Listing 7-5.Starting the Slave

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.000001

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>

花点时间费力地读完所有这些行。有几个关键字段你需要注意。这些包括名称中带有error的任何内容,以及state列。例如,第一行(Slave_IO_State)显示了指示从机 I/O 线程状态的文本消息。I/O 线程负责从主服务器的二进制日志中读取事件。还有一个 SQL 线程负责从中继日志中读取事件并执行它们。

对于这个例子,您只需要确保两个线程都在运行(YES)并且没有错误。有关SHOW SLAVE STATUS命令中所有字段的详细说明,请参见在线 MySQL 参考手册( http://dev.mysqlcom/doc )中的“用于控制从属服务器的 SQL 语句”一节

既然从属服务器已经连接并正在运行,让我们检查它上面的那个testme数据库。

mysql> show databases;

+--------------------+

| Database           |

+--------------------+

| information_schema |

| mysql              |

| performance_schema |

+--------------------+

3 rows in set (0.00 sec)

mysql>

等等!它去哪里了?这个例子不是应该复制一切吗?嗯,是也不是。的确,你的奴隶与主人相连,从现在开始,它将复制主人身上发生的任何变化。回想一下,您使用了SHOW MASTER STATUS命令来获取二进制日志文件和位置。这些值是下一个事件位置的坐标,而不是任何先前事件的坐标。啊哈——您在创建了testme数据库之后设置了复制。

你怎么解决这个问题?那得看情况。如果您真的想要复制testme数据库,您必须停止复制,修复主数据库,然后重新连接从数据库。我不会详细介绍这些步骤,但是我在这里列出了它们作为您自己试验的大纲:

Stop the slave.   Go to the master and drop the database.   Get the new SHOW MASTER STATUS data.   Reconnect the slave.   Start the slave.

明白了吗?很好。如果没有,这是一个很好的练习,回去自己尝试这些步骤。

清理主服务器并重启复制后,继续尝试在主服务器上创建一个数据库,并观察从服务器上的结果。清单 7-6 显示了这些命令。请注意,我使用了不同的数据库名称,以防您选择不尝试之前的挑战。

Listing 7-6.Testing Replicaiton of New Database on the Slave

pi@raspberrypi /etc $ mysql -uroot -psecret

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 38

Server version: 5.5.28-1-log (Debian)

Copyright (c) 2000, 2012, 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> 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>

回到从服务器,查看那里列出了哪些数据库,如清单 7-7 所示。

Listing 7-7.Verifying New Database Is on the Slave

Chucks-iMac:mysql-5613 cbell$ mysql -uroot -psecret -h 127.0.0.1 --port=13003

Welcome to the MySQL monitor.  Commands end with ; or \g.

Your MySQL connection id is 14

Server version: 5.5.21 Source distribution

Copyright (c) 2000, 2010, Oracle and/or its affiliates. All rights reserved.

This software comes with ABSOLUTELY NO WARRANTY. This is free software,

and you are welcome to modify and redistribute it under the GPL v2 license

Type 'help;' or '\h' for help. Type '\c' to clear``the

mysql> show databases;

+--------------------+

| Database           |

+--------------------+

| information_schema |

| mysql              |

| performance_schema |

| testme_again       |

+--------------------+

4 rows in set (0.00 sec)

mysql>

成功!现在,您的 Raspberry Pi 数据库服务器正在由您的桌面计算机进行备份。

Is There A Better Way?

如果您想知道是否有一种更简单的方法可以让复制工作起来,而不需要在从属服务器上摆弄命令,那么我有一个好消息。有更好的办法!参见 MySQL 实用程序中的mysqlreplicate实用程序。该实用程序允许您用一个命令设置复制。有关mysqlreplicate的更多信息,请参见位于 http://dev.mysql.com/doc/index-gui.html 的在线 MySQL 实用程序文档。

IOT 节点中的容错

使用 MySQL 实现容错并不太困难,正如您所看到的,可以使用 MySQL 复制来实现冗余、恢复、可伸缩性,并最终实现数据库组件的高可用性。然而,在将数据写入数据库的典型基于微控制器的节点上实现容错可能有点困难。

这是因为您必须编写所有代码来检测故障,并实现冗余机制来从故障中恢复。有时这会导致代码远远超过典型微控制器的存储空间。因此,您应该考虑微控制器上的容错在内存方面的开销。幸运的是,有更多内存的微控制器可供您使用。

您可能想知道为什么要在这个级别实现容错。回忆的目标是保存数据,换句话说,及时收集和存储数据。一些 IOT 解决方案可以承受短时间内丢失或遗漏数据样本的后果,但其他 IOT 解决方案—特别是那些处理时间敏感或健康数据的解决方案—可能会依赖这些数据来提高预测(如诊断)或建议行动方案的准确性。对于这些 IOT 解决方案,您将希望在整个解决方案中实现容错,从传感器到数据收集器,再到数据库服务器,甚至是应用服务器。

您将在下一章看到容错数据收集器的完整实现。正如您将看到的,我警告过的复杂性就在那里,我们将不得不在硬件方面做出一些让步,以使它能够工作。

摘要

高可用性不仅仅适用于为大型组织编写的大型单一应用。在更适中的可靠性水平上实现高可用性也并不困难。事实上,有许多工具可以让您的 IOT 解决方案更加可靠。

在本章中,您学习了什么是高可用性以及如何实现高可用性概念。您还了解了 MySQL 的关键高可用性概念、工具和技术,包括备份、恢复和复制。您还了解了如何为在微控制器上收集数据实现容错。

在下一章中,你将看到用 MySQL 构建 IOT 解决方案的高级技术演示。

Footnotes 1

大型组织非常清楚实现高可用性的高昂成本。

2

因此是不可能的。

3

由于温室通常有许多共享相同土壤的植物,所以一些传感器可以是每个托盘或者甚至每个平台。

4

一些早期的作品涵盖了如此多的材料,它们可以被用作船锚、压舱物,甚至是建筑材料。如果你知道绿条是什么,我见过一套备份文档,装满了一个绿条纸的盒子。

5

一些人称之为日志记录,而另一些人称之为缓存。

6

https://en.wikipedia.org/wiki/DevOps

7

不,真的。如果在您开始使用服务器后不久修复就失败了,您会惊讶地发现这样的备份是多么方便——这种情况在 IT 界太常见了。

八、高可用性技术演示

Electronic supplementary material The online version of this chapter (doi:10.​1007/​978-1-4842-1293-6_​8) contains supplementary material, which is available to authorized users.

了解什么是高可用性以及实现更高级别的可靠性的一些关键目标,将有助于您设计具有更高可靠性的 IOT 解决方案。正如您在前一章中了解到的,要实现这些目标需要做一些工作,但是如果您的解决方案扩展到一组离散的节点之外,那么适度的工作是值得的。

然而,了解这些概念只是解决方案的一部分。你还需要看一些例子,看看这些技术是如何体现的。例如,如何检测数据收集器节点故障并从中恢复,以及如何设置 MySQL 复制以在主节点出现故障时切换到新的主节点?此外,应用如何知道这已经发生了?

我们将使用示例项目来探索这些主题以及更多内容,这些示例项目展示了代码中的技术。有些例子是用 Python 写的,因为它们更有可能在低成本的计算机主板上实现,甚至在个人或服务器计算机上实现。其他例子是为 Arduino 编写的,但也可以适用于其他微控制器板。

Tip

本章中的示例介绍了一些技巧,而不是完整的解决方案。它们更像是成功的秘诀,而不是如何做的示范。尽管如此,本节中的示例是您可以在自己的解决方案中使用的高级技术。

在我们进入代码示例之前,让我们来探索一下让您的数据库服务器更加可靠的主要工具:MySQL 复制。

MySQL Replication Techniques

回想一下,MySQL 复制是一种机制,它允许您将一台服务器指定为主服务器,将一台或多台服务器指定为从服务器,从服务器接收对主服务器所做的所有更改的副本。这允许您将数据从一台服务器复制到许多其他服务器。在前一章中,您了解了如何设置 MySQL 复制,以便在主服务器出现故障时进行热备用或备份和手动恢复。

虽然 MySQL 复制很容易设置 1 并且管理小型复制拓扑通常不是负担,只需要很少的干预,但管理大型复制拓扑并解决可能出现的问题(无论是有意还是无意的)可能更具挑战性。

Note

本章中的复制示例需要 MySQL server 版本 5.6.5 或更高版本。

使用 MySQL 复制的经验将有助于克服您在这个过程中可能遇到的许多正常需求。事实上,快速阅读在线 MySQL 参考手册中的复制部分会有很大帮助。但是,可能会发生一些在文档中不太明显的事情(但是有描述,只是没有被强调)。我将使用本节中的示例介绍这些主题,从故障转移和切换开始。

您还将研究大量高级 MySQL 复制技术、最佳实践,甚至是一些如何在您的 IOT 解决方案中利用 MySQL 复制的示例。让我们从收集一些关键技术开始,以充分利用 MySQL 复制;首先是一个关键的数据库概念,称为事务处理。

事务处理

事务是对数据库的一组操作,您希望使其原子化。也就是说,如果其中一个失败,您希望自事务开始以来的所有更改都被撤销(恢复)。典型的例子是把钱从一个银行账户转移到另一个账户。这需要从第一个账户中扣除金额,并将其添加到第二个账户中——这一行为需要两步。如果其中任何一个失败,这些变化都不会发生;否则,你已经得到了未分配或未转账的资金!

我们用特殊的START TRANSACTIONBEGIN语句开始一个事务。如果所有语句都成功,我们将发出一个COMMIT语句来完成操作,并指示数据库服务器永久地写入更改。如果我们想要恢复更改,我们可以发出一个ROLLBACK语句来撤销所有的更改。

使用 InnoDB 存储引擎时,MySQL 支持事务。这个引擎是 MySQL 中默认的存储引擎,所以事务是默认启用的。

清单 8-1 展示了一个这样的例子。我们从一个只包含两列的测试表开始——一个自动递增列和一个整数列。观察如何使用事务来保存数据。

Listing 8-1.Demonstration of Transaction Processing in MySQL

mysql> USE test;

Database changed

mysql> CREATE TABLE test_trans(id INT AUTO_INCREMENT PRIMARY KEY, value INT);

Query OK, 0 rows affected (0.07 sec)

mysql> INSERT INTO test_trans VALUES (NULL,1), (NULL,2), (NULL,3);

Query OK, 3 rows affected (0.03 sec)

Records: 3  Duplicates: 0  Warnings: 0

mysql> BEGIN;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO test_trans VALUE (NULL,4);

Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM test_trans;

+----+-------+

| id | value |

+----+-------+

|  1 |     1 |

|  2 |     2 |

|  3 |     3 |

|  4 |     4 |

+----+-------+

4 rows in set (0.00 sec)

mysql> DELETE FROM test_trans;

Query OK, 4 rows affected (0.00 sec)

mysql> SELECT * FROM test_trans;

Empty set (0.00 sec)

mysql> ROLLBACK;

Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM test_trans;

+----+-------+

| id | value |

+----+-------+

|  1 |     1 |

|  2 |     2 |

|  3 |     3 |

+----+-------+

3 rows in set (0.01 sec)

mysql> BEGIN;

Query OK, 0 rows affected (0.00 sec)

mysql> INSERT INTO test_trans VALUES (NULL,4), (NULL,5);

Query OK, 2 rows affected (0.00 sec)

Records: 2  Duplicates: 0  Warnings: 0

mysql> DELETE FROM test_trans WHERE id <= 2;

Query OK, 2 rows affected (0.01 sec)

mysql> SELECT * FROM test_trans;

+----+-------+

| id | value |

+----+-------+

|  3 |     3 |

|  5 |     4 |

|  6 |     5 |

+----+-------+

3 rows in set (0.00 sec)

mysql> COMMIT;

Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM test_trans;

+----+-------+

| id | value |

+----+-------+

|  3 |     3 |

|  5 |     4 |

|  6 |     5 |

+----+-------+

3 rows in set (0.00 sec)

注意,我们从插入几行没有事务开始。在这种情况下,MySQL 服务器被设置为自动提交这些语句。这被称为自动提交,可以用autocommit选项来控制。接下来,我们启动一个执行大量语句的事务,然后回滚操作并检查结果。最后,您可以看到一个如何提交的示例。

事务处理可能不明显的一点是这些变化如何影响其他连接。例如,在前面的演示中,如果另一个连接在提交事务之前查询数据,会发生什么情况?该连接将看到原始的、未被更改的数据,因为它还没有被提交。有趣的是,当前连接会在提交之前看到更改,因为更改是该会话的本地更改。你可以在事务中间的SELECT中看到。

事务是在解决方案中使用的一种优秀的高级技术,尤其是在为数据开发复杂的多步语句时。此外,事务是更高级的复制功能(如全局事务标识符)中使用的一个关键概念。

具有全局事务标识符的高级复制

回想一下第 7 章中的,您发现了如何使用一种机制来设置复制,这种机制需要从主服务器识别二进制日志文件和位置,然后使用CHANGE MASTER TO语句在从服务器上指定该信息。这被称为二进制日志文件和位置复制。但是,有一种更新、更好的方法可以提高性能,并使自动故障转移和更容易的切换成为可能。所需的特性称为使用全局事务标识符(GTID)的复制或基于 GTID 的复制。

GTIDs 使服务器能够为每个事件集或组分配一个唯一的标识符,从而可以知道每个从服务器上应用了哪些事件。也就是说,当启用 GTIDs 时,MySQL 服务器的复制组件会将一个特殊事件注入到二进制日志中,该日志包含一个对每个服务器都是唯一的全局唯一标识符和一个事务开始时的序列号。通过这种方式,MySQL 复制可以检测哪些事件与每个事务相关联,甚至可以检测事务是从哪个服务器发送的。最重要的是,复制协议现在可以确定从设备上缺少哪些事务,这样当从设备连接时,只有那些尚未应用到从设备的事务才会被发送。

现在让我们看看如何使用 GTIDs 设置一个主机和几个从机。如果您的服务器上已经有数据,您应该在打开 GTIDs 之前同步所有服务器上的数据。然而,在我们打开 GTIDs 之前,您应该在主服务器上创建一个用户,从服务器可以使用它来连接。该用户必须拥有所有数据库的REPLICATION SLAVE权限。为用户分配一个密码也是一个很好的做法。以下语句显示了如何设置复制用户帐户并授予适当的权限:

CREATE USER 'rpl'@'localhost' IDENTIFIED BY 'rpl'

GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'

现在我们准备设置服务器来使用 GTIDs。为此,您必须为主服务器指定以下选项。您可以将它们放在my.cnf文件中,或者将它们指定为启动命令。

[mysqld]

gtid-mode=ON

enforce-gtid-consistency

在这里,我们打开 GTIDs,然后添加一个额外的选项来加强跨服务器的一致性。如果您计划切换主角色或计划设置故障转移,您将需要使用此选项。

在从机上,我们还需要在my.cnf文件中指定以下选项或指定为启动命令。拓扑中的所有服务器都需要这些选项。此外,从机可以配置为使用一个表来存储主机信息。您可以在每个从属服务器的以下示例配置文件中看到这些选项:

[mysqld]

gtid-mode=ON

enforce-gtid-consistency

master-info-repository=TABLE

report-host= slave1

report-port=13002

Note

如果您计划将您的主服务器用作从服务器,您应该使用为从服务器指定的选项。

一旦所有服务器都重新启动,并且您确认没有错误,您就可以开始配置复制。回想一下第 7 章,我们对每个从机发出CHANGE MASTER TOSTART SLAVE语句。下面是这些语句的一个示例:

CHANGE MASTER TO MASTER_HOST='master', MASTER_PORT=13001, MASTER_USER='rpl', MASTER_PASSWORD='secret', MASTER_AUTO_POSITION = 1;

START SLAVE;

注意这里我们不再需要指定主二进制日志文件和位置。相反,我们只是指示从机使用 GTIDs 提供的自动定位特性。您可以使用SHOW SLAVE STATUS检查从属服务器上的复制状态。您应该看不到任何错误。关于 GTIDs 的更多信息,请参见在线 MySQL 参考手册 http://dev.mysql.com/doc/refman/5.7/en/replication-gtids.html

Tip

使用\G选项查看数值的垂直列表,而不是表格输出。它使得宽行更容易阅读,在这种情况下,输出也更容易被普通人阅读。

既然您已经理解并能够使用 GTIDs,那么您可以开始使用一些高级的复制概念,比如故障转移。

Testing Replication With Mysql Utilities

在接下来的小节中,我将介绍几个使用复制的概念和例子。要运行这些示例,特别是伸缩和故障转移示例,您需要设置一个主服务器和两个或三个从服务器的复制拓扑。以下是您可以在自己的系统上设置测试复制拓扑的步骤。每个步骤和示例的文档都包含在在线 MySQL 实用程序文档( http://dev.mysql.com/doc/mysql-utilities/1.6/en/ )中。

Clone a running (installed) MySQL server with mysqlreplicate. Make at least three or four clones assigning the correct port, server ID, database director, and so on (change all occurrences of 13001 in the command). mysqlserverclone --server=root:secret@localhost:3306 --new-data=/tmp/13001 --new-port=13001 --new-id=1 --root-password=root --del --mysqld="--log_bin=mysql-bin --gtid-mode=on --enforce-gtid-consistency --master-info-repository=table --report-host=localhost --report-port=13001"   Set up the replication user on each server. Use the mysql client to connect to each server in turn and run the following SQL statements: SET @@sql_log_bin=0; CREATE USER 'rpl'@'localhost' IDENTIFIED BY 'rpl' GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost' SET @@sql_log_bin=1;   Once all servers are cloned, set up replication with mysqlreplication. Here I use the server on port 13001 as the master. Run this command once for each slave (change the slave port accordingly). mysqlreplicate --master=root:root@localhost:13001 --slave=root:root@localhost:13002 --rpl-user=rpl:rpl   To see the topology, use mysqlrplshow using the server and the discover slaves login option. mysqlrplshow --master=root:root@localhost:13001 --disco=root:root

这难道不比手动操作简单得多吗?我在这本书的源代码中包含了一个名为setup_topology.txt的文件,它演示了可以用 MySQL 实用程序创建拓扑的命令。只需更改选项以匹配您的系统。

Switchover

有时需要将主设备的角色从原来的主设备切换到其中一个从设备。您可能需要这样做,以便在主服务器上执行维护,从数据丢失中恢复,以及执行需要停止复制一段时间的其他事件。不要强迫您的解决方案完全停止,您可以简单地临时切换主服务器角色,然后在主服务器重新联机时再切换回来。

接下来是使用 MySQL 实用程序执行切换的演示。回想一下,MySQL 实用程序由一组 Python 脚本和库组成,旨在帮助自动化服务器维护。许多实用程序旨在帮助设置和管理复制。清单 8-2 显示了mysqlrpladmin实用程序的执行,以执行从主设备到其中一个从设备的切换。输出显示了所采取的所有步骤。如果您是手动完成的,那么您将不得不自己在每个从服务器上完成每项操作,然后再在主服务器上完成。

Listing 8-2.Switchover Using mysqlrpladmin

$ mysqlrpladmin switchover --demote-master --master=root:secret@localhost:13001 --new-master=root:secret@localhost:13002 --discover-slaves=root:secret

# Discovering slaves for master at localhost:13001

# Discovering slave at localhost:13002

# Found slave: localhost:13002

# Discovering slave at localhost:13003

# Found slave: localhost:13003

# Discovering slave at localhost:13004

# Found slave: localhost:13004

# Checking privileges.

# Performing switchover from master at localhost:13001 to slave at localhost:13002.

# Checking candidate slave prerequisites.

# Checking slaves configuration to master.

# Waiting for slaves to catch up to old master.

# Stopping slaves.

# Performing STOP on all slaves.

# Demoting old master to be a slave to the new master.

# Switching slaves to new master.

# Starting all slaves.

# Performing START on all slaves.

# Checking slaves for errors.

# Switchover complete.

#

# Replication Topology Health:

+------------+--------+---------+--------+------------+---------+

| host       | port   | role    | state  | gtid_mode  | health  |

+------------+--------+---------+--------+------------+---------+

| localhost  | 13002  | MASTER  | UP     | ON         | OK      |

| localhost  | 13001  | SLAVE   | UP     | ON         | OK      |

| localhost  | 13003  | SLAVE   | UP     | ON         | OK      |

| localhost  | 13004  | SLAVE   | UP     | ON         | OK      |

+------------+--------+---------+--------+------------+---------+

# ...done.

为了执行切换,我指定了原始主服务器(回调切换需要运行良好的主服务器)、新的主服务器以及每个从服务器的登录名和密码(称为发现)。我指定的将主服务器保留为从服务器的选项在这里很有意思。在新的主设备接管后,demote-master选项将主设备作为拓扑的从设备返回。这里需要注意的是,这是不可能的,除非你像设置从设备一样设置主设备(用report-hostreport-portmaster-into-repository)。您可以在清单的末尾看到结果,实用程序打印出新拓扑的健康报告。在这里,您可以看到原来的主机(在端口 13001 上)现在是从机。

Failover

故障转移是指主服务器以某种方式出现故障,您需要自动重新配置拓扑来恢复主服务器角色的情况。在使用 GTIDs 之前,自动故障转移在最好的情况下也是有问题的,并且除了最简单的情况之外,对于所有情况都是不可靠的。

为了使用 GTIDs 执行故障转移,我们选择最好的从设备(丢失事件最少的设备,以及与主设备最匹配的硬件),并使其成为所有其他从设备的从设备(也称为从设备提升)。我们称这个从设备为候选从设备。GTID 机制将确保只应用那些没有在候选从设备上执行的事件。通过这种方式,候选从模块成为最新的,因此成为主模块的替代。

同样,您可以使用 MySQL 实用程序来监控主服务器,并自动将其中一个从服务器提升为新的主服务器。mysqlfailover命令行工具通过执行先前的事件序列来执行自动故障转移,并负责将剩余的从设备重定向到新的主设备。图 8-18-3 显示了该实用程序的一系列截图。第一个显示了监控模式下的实用程序。

A978-1-4842-1293-6_8_Fig3_HTML.jpg

图 8-3。

Automatic failover with mysqlfailover (after failover)

A978-1-4842-1293-6_8_Fig2_HTML.jpg

图 8-2。

Automatic failover with mysqlfailover (fault detected)

A978-1-4842-1293-6_8_Fig1_HTML.jpg

图 8-1。

Automatic failover with mysqlfailover (before failover)

请注意,该实用程序显示了许多统计信息,包括拓扑中所有服务器的当前健康报告。该实用程序通过以指定的时间间隔检查主服务器的状态来工作。在这个例子中,我使用了 30 秒的间隔。为了模拟主进程失败,我终止了主进程。图 8-2 显示实用程序检测到故障并执行自动故障转移。

当故障转移完成时,实用程序返回到其监控模式,如图 8-3 所示。自动故障转移真的很简单!是的,如果安装了 Python 2.6 或更高版本以及 Connector/Python,您可以在低成本计算机上运行该实用程序。

请注意,在前面的屏幕截图中,该实用程序是作为控制台运行的。如果您需要将其作为后台进程运行,有一个配套的实用程序允许您将其作为进程或守护程序运行(仅适用于非 Windows 平台)。要了解更多关于mysqlfailover及其配套守护进程的信息,包括所有可用的命令和更多使用示例,请参见位于 http://dev.mysql.com/doc/mysql-utilities/1.6/en/utils-task-autofailover.html 的 MySQL 实用程序在线手册。

复制和数据库维护技巧

如上所述,对于较小的拓扑结构,MySQL 复制相对容易设置和维护。对于 IOT 解决方案,您不太可能遇到有许多服务器的复制拓扑(但您可能会遇到)。回想一下,如果出现问题,MySQL 复制可能需要做一些工作。在这一节中,我将重点介绍一些可能出错的常见情况,并介绍防止出错和从错误中恢复的方法。

Avoiding Errant Transactions

也许新管理员在 MySQL 复制中最常犯的错误是引入错误的事务。错误事务是那些仅适用于一台服务器的语句(或事务),在另一台服务器上执行时无效,或者在执行时导致复制错误。错误的事务通常是由于在主服务器上运行了一个或多个不应被复制的 SQL 语句。对于 GTIDs,当在从设备上执行相同的操作,并且该从设备后来成为主设备或者离开并重新加入拓扑时,可能会发生错误的事务。

例如,如果您在主服务器上执行任何 SQL 命令来创建、更新或删除从服务器上不存在的任何内容,您可能会遇到严重到足以停止从服务器上的复制的错误。回想一下,一旦复制开始,每个 SQL 语句都被写入二进制日志,然后传输到从属服务器。因此,在启用二进制日志记录的情况下,我们在主服务器上执行语句时必须小心。

幸运的是,有一种方法可以省略记录到二进制日志中的语句。有一个名为sql_log_bin的特殊变量,您可以为当前会话(登录用户)关闭它,它不会影响任何其他连接。只需关闭二进制日志,执行语句,然后再次打开它。以下代码显示了如何使用此技术将复制用户帐户添加到已启用二进制日志的服务器中:

SET @@sql_log_bin=0;

CREATE USER 'rpl'@'localhost' IDENTIFIED BY 'rpl'

GRANT REPLICATION SLAVE ON *.* TO 'rpl'@'localhost'

SET @@sql_log_bin=1;

如果在复制服务器上执行维护时使用这种技术,您可以避免错误的事务,从而避免解决从属错误的许多不必要的麻烦。

Resolving Slave Errors

当您在从机上遇到错误时,您将在SHOW SLAVE STATUS输出中看到错误。如果错误是由错误的事务引起的,您必须指示主服务器跳过这些事务,这对于二进制日志文件和基于位置的复制来说很容易,但是对于启用 GTID 的服务器来说可能有点棘手。召回错误的事务通常是主设备的二进制日志中的某个事件,该事件在从设备上运行时不适用或产生错误。

对于二进制日志文件和基于位置的复制,您可以指示从属服务器跳过一些不适用于从属服务器的事件。但是首先,您应该执行以下步骤来诊断问题。此时,从机因出错而停止。

Determine the error and look up the error in the reference manual. Sometimes the error is well known or has a predictable solution.   Determine the event or events causing a problem. A look at the output of SHOW SLAVE STATUS will tell you the binary log file and position of the event. You can use that on the master to see the event with the SHOW BINLOG EVENTS statement. If the event cannot be run (wrong tables, and so on), you must skip the event. If you are not sure, start the slave again and check for errors.   If you must skip the events, issue the following statements to skip and start the slave. The following statement skips the next two events: mysql> SET GLOBAL sql_slave_skip_counter = 2 mysql> START SLAVE;   If this does not solve the problem, you may need to skip more events. However, if you cannot find a restarting point, you may need to do more intensive diagnosis or restore the slave from the latest backup and restart replication.2

对于支持 GTID 的复制,事情就没那么简单了。这里,我们需要识别从属服务器上的错误事务。您可以在从机的SHOW BINLOG EVENTS输出中看到这些。一旦有了 GTID,就可以使用下面的语句在主服务器上的二进制日志中创建一个空事务。这将有效地告诉主设备该事件已经发生,即使它没有在主设备或该事件被传送到的任何从设备上执行。

错误的事务在 GTID 复制中更常见,因为从属服务器都在自己的二进制日志中记录更改。另外,当从设备连接到主设备时,它会与主设备交换 GTIDs 列表。如果名单不一致,就会出现错误的事务。您可以通过发出以下语句来确定是否存在错误的事务。我们从识别母版上的 UUID 开始。

mysql> SHOW VARIABLES LIKE "%uuid%";

+---------------+--------------------------------------+

| Variable_name | Value                                |

+---------------+--------------------------------------+

| server_uuid   | 0f5ae7a2-9968-11e5-8990-08fdde5e3c13 |

+---------------+--------------------------------------+

1 row in set, 1 warning (0.00 sec)

然后,在从属服务器上,运行SHOW SLAVE STATUS并检查executed_gtid_set的结果,如下所示:

Executed_Gtid_Set: 0f5ae7a2-9968-11e5-8990-08fdde5e3c13:1,

130a0dec-9968-11e5-9766-aa87a4a44672:1

从模块上的错误事务是那些没有从主模块的 UUID 开始的 GTIDs。从这个列表中,我们看到有一个 GTID,130 A0 dec-9968-11e 5-9766-aa 87 a4 a 44672:1,它是错误的(不在主设备上,也不是源自主设备)。要跳过这一步,我们需要欺骗主节点忽略该事务。让我们看看这些陈述,然后讨论它们的用法。清单 8-3 显示了创建一个空事务来忽略(跳过)一个错误事务的过程。

Listing 8-3.Skipping Errant Transactions with GTIDs

mysql> SET GTID_NEXT='dd365ccc-9898-11e5-872d-172ea4aa6911:1'

Query OK, 0 rows affected (0.00 sec)

mysql> BEGIN;

Query OK, 0 rows affected (0.00 sec)

mysql> COMMIT;

Query OK, 0 rows affected (0.01 sec)

mysql> SET GTID_NEXT='AUTOMATIC'

Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH LOGS;

Query OK, 0 rows affected (0.08 sec)

mysql> SHOW BINARY LOGS;

+------------------+-----------+

| Log_name         | File_size |

+------------------+-----------+

| mysql-bin.000001 |      1091 |

| mysql-bin.000002 |       314 |

+------------------+-----------+

2 rows in set (0.00 sec)

mysql> PURGE BINARY LOGS TO 'mysql-bin.000002'

Query OK, 0 rows affected (0.04 sec)

哇哦!这是一个复杂的过程。我们首先在主服务器上设置下一个 GTID,然后开始并提交一个事务(一个空事务)。然后,我们将 GTID 编号恢复为自动,并刷新日志。我们这样做是为了清除旧日志。您可以在SHOW BINARY LOGS的输出中看到,我们可以清除旧的二进制日志文件。一旦这个过程完成,我们就可以重新启动从属服务器。

有关此过程的更多信息,请参见在线 MySQL 参考手册 http://dev.mysql.com/doc/refman/5.7/en/replication-gtids-failover.html#replication-gtids-failover-empty

Starting Replication with Existing Data

第 7 章中关于复制的讨论假设您从一张白纸开始复制。但是,如果主服务器上已经有数据,那么在主服务器上启动二进制日志之前,需要备份主服务器上的数据,并在从服务器上恢复它。

但是,如果您已经有一个主服务器和一个从服务器在运行,并且已经运行了一段时间,但是您想要添加另一个从服务器,该怎么办呢?您可以使用最初在设置从属服务器时所做的备份(如果您这样做的话),但是新的从属服务器必须读取自备份以来发生的所有更改。

您也可以备份主服务器,但是您必须暂停写入二进制日志才能这样做。 3 更好的方法是暂时停止现有的从站,等待它读取并应用其中继日志中的事件(使用SHOW SLAVE STATUS语句查看当前状态)。您需要查看的列在这里用实际复制从属服务器的摘录突出显示:

Slave_IO_State: Waiting for master to send event

...

Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates

在这里,您可以看到从机已经从主机(slave_io_state)读取了所有事件,并且从机已经处理了其中继日志(slave_sql_running_state)中的所有事件。

如果您使用二进制日志文件和基于位置的复制,您将需要记录当前主二进制日志文件和位置,如SHOW SLAVE STATUS输出所示。如果您使用 GTIDs,您不需要这些信息。

一旦从属服务器停止并且执行了所有中继日志事件,您就可以进行备份,重新启动原始从属服务器,然后在新从属服务器上恢复数据,最后在新从属服务器上开始复制。

示例:缩放应用

扩展关乎性能。读取扩展是指使用额外的服务器来指导读取操作(SELECT语句),这样就不会有一个服务器承担处理大量读取请求的负担。还有分片扩展,理想情况下,在几个服务器之间划分(分区)数据,以便它们拥有相同的数据量。要访问碎片,您必须使用一种算法(通常是一个简单的散列函数)或专用于每个碎片的一系列值。可以想象,将查询路由到正确的碎片是一个挑战。幸运的是,大多数 IOT 解决方案不需要使用分片来提高性能。相反,一些 IOT 解决方案更有可能受益于读取扩展。

MySQL 的读扩展使用一种复制拓扑,将所有写操作(CREATEINSERTUPDATEDELETE等)发送到主服务器,并将所有读操作发送到其中一个从服务器。您需要的从机数量很大程度上取决于您的应用读取数据的频率。

虽然设置读取扩展并不比设置复制更困难(您不需要做任何特殊的事情),但是编写应用来扩展读取是一个挑战。更具体地说,知道向哪个服务器发送客户机(应用)可以是一种简单的循环或队列机制,或者您可以实现复杂的负载平衡来将读取定向到最少使用的服务器。

对于需要适度提升读取性能的解决方案,您可能只需要一个简单的服务器选择机制。更大、更复杂的解决方案可能需要构建自己的负载平衡器或使用第三方负载平衡器。 4 即使如此,负载平衡机制也可能完全依赖于应用,甚至在某些情况下依赖于访问方法(想想查询类型)。

对于业余爱好者和爱好者的 IOT 解决方案,甚至中小型商业 IOT 解决方案,简单的服务器选择机制是绰绰有余。如果性能开始下降,您只需添加另一台服务器并分散读取。

您可能想知道在哪里或者如何设置读取比例。一种可能的实现是使用低成本计算机板的拓扑结构。另一个是云服务,它提供可以动态创建和销毁的 MySQL 实例。幸运的是,MySQL 在云中的工作方式与它在任何其他设备上的工作方式相同——您获得的云服务器只是一个虚拟化的服务器,而不是真正的硬件。

现在,您已经了解了什么是读取伸缩以及如何使用它来提高性能,让我们来看一个简单的服务器选择代码示例,您可以使用它在几个服务器之间传播读取。让我们从设计开始。

Overview of the Design

我没有设计一个代码解决方案,而是给出了一个可以用来在 MySQL 中实现持久性组件的解决方案。也就是说,我们可以使用数据库服务器作为看门人,以帮助确保两个客户端不会选择同一个从服务器。它可能不是最有效的机制,也不是一个通用的负载平衡器(但它模拟了一个基本的负载平衡器),它提供了一种机制,允许您在每次添加新服务器时无需修改代码就可以使用它。事实上,它会从可用服务器列表中自动选择一个服务器,您甚至不必创建和管理配置文件。 5

关键概念是复制本身的一个特性。我们将在主服务器上使用SHOW SLAVE HOSTS语句来获取从服务器的列表。回想一下,我们要求所有从机使用--report-host--report-port选项来获取出现在列表中的数据。然后,我们使用循环机制来选择列表中的下一个服务器。

另一个关键概念是使用数据库服务器来存储上次使用的服务器 ID。我们创建一个简单的数据库和一个只有一行的表,如下所示。注意,我们存储了服务器 ID、主机名和端口。

CREATE DATABASE read_scaling;

CREATE TABLE read_scaling.current_slave(server_id INT, host CHAR(30), port INT);

INSERT INTO read_scaling.current_slave VALUES (0,NULL,0);

请注意INSERT语句。您将需要插入一个起始行,因为代码被设计为只更新单行,使它变得很小并且易于使用。但是,您可能对表中的那一行感到疑惑。使用过具有多个客户端的应用的精明读者可能想知道如何防止两个或更多的客户端同时冲突和更新行(或不按顺序)。这就是数据库的力量帮助我们的地方。

在计算机编程行话中,我们称程序中必须执行的部分为临界区。在这种情况下,它类似于这方面的事务。MySQL 提供了一个名为LOCK TABLES的 SQL 语句,我们可以用它来锁定表,更新它,然后释放锁。通过这种方式,我们确保一次只有一个客户端可以更新该表,因为WRITE锁阻止所有其他连接读取或写入该表。下面显示了我们将在代码中使用的 SQL 语句:

LOCK TABLES read_scaling.current_slave WRITE;

UPDATE read_scaling.current_slave SET server_id=2,host='test123',port=13001;

UNLOCK TABLES;

但是,等等,数据怎么办——它不会被复制到从属服务器吗?事实上,会的。虽然这真的没有关系,因为我们不会从从属服务器读取数据,但是有一个很好的理由允许复制这些数据。想想看,如果船长倒下了会发生什么。如果您没有复制数据,那么在配置新的主服务器时,您将会丢失数据;你必须重新设置它。但是鉴于它的简单性,这并不是一个真正的问题。所以,允许它复制是没问题的。

What I Don’t Want Certain Data Replicated?

有时可能有您不想复制的数据。在这种情况下,您可以使用 MySQL 内置的复制过滤器来告诉服务器不要复制某些数据。主服务器(二进制日志选项)和从服务器(复制选项)上都有过滤器。它们可以是包容性的,也可以是排他性的。

在主服务器上设置过滤器可以确保与过滤器匹配的数据不会保存在二进制日志中,因此不会传输到从服务器。在从机上设置过滤器可确保数据在传输过程中被从机丢弃。请这样想,如果您希望某些数据永远不被复制,请使用主过滤器。如果您希望一个或多个从属服务器而不是所有服务器都不获取数据,请使用从属过滤器。

二进制日志过滤器包括--binlog-do-db--binlog-ignore-db。复制过滤器包括--replicate-do-db--replicate-ignore-db--replicate-do-table--replicate-ignore-table。请参阅在线 MySQL 参考手册,了解有关设置和维护过滤器的更多信息,以及使用过滤器的陷阱( http://dev.mysql.com/doc/refman/5.7/en/replication-options-binary-log.html )和( http://dev.mysql.com/doc/refman/5.7/en/replication-options-slave.html )。

最后,代码将被编写为一个类,您可以将它放入您的库中,并包含在您想要的任何客户端代码中。出于演示代码的目的,我将包含测试代码,但是您可以删除它并在库中使用该类。这个类被命名为select_read_server,是用 Python 编写的,但也可以很容易地用您选择的任何语言编写。

Write the Code

该类只需要一个名为get_next_server()的公共方法,该方法返回服务器 ID、主机和端口的字典。需要许多 helper 方法,这些方法将是私有的(名称中以下划线字符开头)。

我们需要一种方法来连接到主机,并检索所有从机的列表,并将该信息存储在内存中。我们将这种方法命名为_get_server_list()。接下来,我们需要一个方法来记录所选从属服务器的服务器 ID,以便任何其他客户机都可以读取它。我们将这种方法命名为_set_server_id()。回想一下,我们将使用数据库来存储所选择的服务器 ID,并将使用锁定来防止其他客户机中断对表的更新。因此,_set_server_id()必须执行前面描述的锁定机制。我们还需要一个从表中读取服务器 ID 的方法。我们将这种方法命名为_get_current_server_id()。最后,我们包含了一个简单的循环来演示如何使用该类为下一个从机检索信息。清单 8-4 显示了完整的代码。

Listing 8-4.Simple Read Scaling Server Selector

# Demonstration of a simple round robin read scaling slave selector

#

# Use this class in your own application to choose the next slave

# in the topology.

#

# Note: you must have a database and table setup on the master as

# follows:

#

# CREATE DATABASE read_scaling;

# CREATE TABLE read_scaling.current_slave(server_id INT, host CHAR(30), port INT);

#

# Note: You must also have a replication topology of a master and at least

#       two slaves for the demo output to be meaningful. In fact, running this

#       on a master with no slaves will result in an error.

#

from __future__ import print_function

import mysql.connector as mysql

import operator

import sys

import time

LOCK_TABLE = "LOCK TABLES read_scaling.current_slave WRITE"

SET_SERVER = "UPDATE read_scaling.current_slave SET server_id={0},host='{1}',port={2}"

UNLOCK_TABLE = "UNLOCK TABLES"

GET_CURRENT_SERVER = "SELECT server_id FROM read_scaling.current_slave"

master = {

'user': 'root',

'password': 'root',

'host': 'localhost',

'port': 13001,

}

class select_read_server(object):

def __init__(self):

self.servers = []

self.cur_server_id = -1;

# Get the list of servers from the master and save the host,

# port, and server id.

def _get_server_list(self):

conn = mysql.connect(**master)

cur = conn.cursor()

cur.execute("SHOW SLAVE HOSTS")

# Save only the id, host, and port

for row in cur.fetchall():

server = {

'id': row[0],

'host': row[1],

'port': row[2],

}

self.servers.append(server)

# order the servers by server_id

cur.close()

conn.close()

self.servers.sort(key=operator.itemgetter('id'))

# Set the server in the database

def _set_server(self, id, host, port):

self.cur_server_id = id

conn = mysql.connect(**master)

cur = conn.cursor()

cur.execute(LOCK_TABLE)

query = SET_SERVER.format(id, host, port)

cur.execute(query)

print(">", query)

cur.execute(UNLOCK_TABLE)

cur.close()

conn.close()

# Get the current server id from the database

def _get_current_server_id(self):

# if first time, skip

if self.cur_server_id == -1: return

conn = mysql.connect(**master)

cur = conn.cursor()

cur.execute(GET_CURRENT_SERVER)

self.cur_server_id = cur.fetchall()[0][0]

cur.close()

conn.close()

# Get the next server in the list based on server id.

def get_next_server(self):

self.servers = []

self._get_server_list()   # update the server list

if not self.servers:

raise RuntimeError("You must have slaves connected to use this code.")

self._get_current_server_id()  # get current server in the database

for server in self.servers:

if server["id"] > self.cur_server_id:

# store the current server_id

self._set_server(server["id"], server["host"], server["port"])

return server

# if we get here, we've looped through all rows so choose first one

server = self.servers[0]

self._set_server(server["id"], server["host"], server["port"])

# return the current server information

return server

# instantiate the class - only need this once.

read_server_selector = select_read_server();

# demonstrate how to retrieve the next server and round robin selection

for i in range(0,10):

print(i, "next read server =", read_server_selector.get_next_server())

sys.stdout.flush()

time.sleep(1)

注意,我们有主设备的连接信息,我们用它来获取从设备的信息。另外,请注意,每次请求下一个服务器时,我们都会得到这个列表。通过这种方式,代码将保持列表更新,这样,如果添加或删除了服务器(并且被主服务器删除),连接列表将自动更新。

然而,SHOW SLAVE HOSTS语句的输出是无序的,可能不会按照服务器 ID 的顺序出现。因此,我添加了一个排序方法来确保循环机制按照升序遍历服务器 id。

Tip

如果从属主机意外断开连接,从属主机视图可能需要几秒钟才能更新。因此,如果您计划在生产环境中使用这段代码,您可能需要考虑添加代码来检查从属服务器是否仍然连接。

Test the Sketch

现在让我们测试代码。要在您的系统上运行它,您只需更改主信息以匹配您的设置,并使用python命令执行代码。清单 8-5 显示了一个运行示例。我添加了代码来打印该类的结果,并打印用于更新该表的 SQL 语句,以便您可以看到它确实选择了列表中的下一个服务器,并从列表中的第一个服务器 ID 开始重复循环。

Listing 8-5.Testing the Read Scaling Selector

$ python ./read_scaling_demo.py

> UPDATE read_scaling.current_slave SET server_id=2,host='localhost',port=13002

0 next read server = {'host': u'localhost', 'id': 2, 'port': 13002}

> UPDATE read_scaling.current_slave SET server_id=3,host='localhost',port=13003

1 next read server = {'host': u'localhost', 'id': 3, 'port': 13003}

> UPDATE read_scaling.current_slave SET server_id=4,host='localhost',port=13004

2 next read server = {'host': u'localhost', 'id': 4, 'port': 13004}

> UPDATE read_scaling.current_slave SET server_id=2,host='localhost',port=13002

3 next read server = {'host': u'localhost', 'id': 2, 'port': 13002}

> UPDATE read_scaling.current_slave SET server_id=3,host='localhost',port=13003

4 next read server = {'host': u'localhost', 'id': 3, 'port': 13003}

> UPDATE read_scaling.current_slave SET server_id=4,host='localhost',port=13004

5 next read server = {'host': u'localhost', 'id': 4, 'port': 13004}

> UPDATE read_scaling.current_slave SET server_id=2,host='localhost',port=13002

6 next read server = {'host': u'localhost', 'id': 2, 'port': 13002}

> UPDATE read_scaling.current_slave SET server_id=3,host='localhost',port=13003

7 next read server = {'host': u'localhost', 'id': 3, 'port': 13003}

> UPDATE read_scaling.current_slave SET server_id=4,host='localhost',port=13004

8 next read server = {'host': u'localhost', 'id': 4, 'port': 13004}

> UPDATE read_scaling.current_slave SET server_id=2,host='localhost',port=13002

9 next read server = {'host': u'localhost', 'id': 2, 'port': 13002}

注意,我们调用方法十次来获取下一个服务器。还要注意,代码在循环选择中成功地遍历从属服务器,用每个新选择更新数据库。酷吧。

High Availability IOT Nodes

您已经看到了许多使用 MySQL 复制的技术,您可以利用这些技术在您的 IOT 解决方案中实现高可用性(可靠性)。但是这些包括数据库可用性,而不是 IOT 节点的底层硬件。本节分析了如何在 IOT 节点中实现高可用性的两个示例。

您将看到一个示例,展示如何对数据收集器故障进行冗余和自动恢复,以及一个数据聚合器,它可以在与数据库的连接失败时缓存数据。这些示例使用 Arduino 微控制器,但是这些概念可以应用于其他微控制器,因此您应该能够在自己的解决方案中复制这些概念。

示例:冗余数据采集器

如果你在越来越复杂的 IOT 解决方案中使用微控制器足够长的时间,你最终会遇到的一件事是,有时微控制器会变得不可靠 6 或完全失控。 7 这可能是因为电源故障、不干净的电源、篡改、糟糕的编程、内存损坏,或者只是普通的命运。我有过似乎永远运行的微控制器,也有过几天就变得可疑的微控制器。无论是哪种情况,微控制器都有可能偶尔出现故障。

解决这一问题并确保我们的解决方案更加可靠(在一定程度上具有高可用性)的一个方法是在我们的硬件中构建冗余。我们可以使用两个微控制器的主/从设置来实现这一点。两个微控制器将具有相同类型的传感器(如果不是相同的传感器【8】),但是只有主控制器将数据写入数据库。此外,我们希望使数据收集器冗余,并自动故障转移到从收集器,这样,如果主收集器出现故障,从收集器就会接管,我们不会丢失任何重要的数据。

是的,我们正在讨论创建一个简单的复制协议,供主设备和从设备进行通信。让我们考虑一下我们将如何做这件事。

Overview of the Design

有许多方法可以在主机和从机之间实现简单的协议。在这种情况下,我们将保持简单,使用心跳的概念。这是一条简单的消息,由一台服务器发送,并在另一台服务器上得到确认。这个想法是,如果心跳在一段时间后失败或丢失,第二个服务器可以假设第一个服务器已经失败或变得不可靠,变得可疑,或完全失控。

幸运的是,我们可以通过 Arduino 内置的功能做到这一点:带有有线协议的 I2C 接口总线。I2C 总线使用两个引脚或导线(有时称为双线):串行数据线(引脚 4 上的 SDA)和串行时钟线(引脚 5 上的 SCL)。该接口允许多个设备在启动时通过注册设备号进行连接。事实上,包括传感器在内的大量设备都使用 I2C 接口。

Caution

如果您使用的是 Uno、Leonardo 或具有较新接头布局的类似电路板,SDA 和 SCL 引脚位于引脚 13 (D2 和 D3)上方。类似地,像 Mega 或 Due 这样的较大电路板的 I2C 接口位于不同的位置(D20 和 D21)。请务必检查您的电路板的页眉布局;否则,您可能会面临令人沮丧的调试会话。 9

在 I2C 总线上,一个设备被指定为写设备,另一个被指定为读设备。因此,该代码将主机作为写入方,将从机作为读取方,从主机向从机传递简单的消息。我们将使用这个配置来构建一个简单的心跳算法,通过这个算法,主机以特定的时间间隔发送一条短消息。这有时被称为脉冲来完成比喻。

因此,当心跳在一个脉冲跨度内没有出现时,我们可以检测到主机何时不再通信。这可以通过在一定时间内没有脉搏或心跳消息来检测。我们可以使用该事件来触发故障转移到从机进行数据收集。更具体地说,在宣布主设备死亡之前,从设备上使用超时来等待一定的时间。如果主服务器在预定时间后没有发送消息,从服务器将接管并开始读取数据,并将数据保存到数据库服务器。因此,我们可以使用冗余数据收集器创建一个简单的自动故障转移。

Assemble the Hardware

本例的硬件需要两块 Arduino 板通过 I2C 总线连接在一起。我们给每个都增加了一个 LED,用于可视确认发送和接收的信息。我在主设备上使用红色 LED,在从设备上使用绿色 LED,但是使用你手头上的任何东西——颜色并不重要。图 8-4 显示了如何连接硬件。

A978-1-4842-1293-6_8_Fig4_HTML.jpg

图 8-4。

Redundant data collector (master/slave) Tip

如果您想用单个电源为 Arduino 板供电,可以将一个 Arduino 上的 5V 引脚连接到另一个 Arduino 上的 VIN 引脚。但是,这不能保护您免受电源问题的影响,所以我不建议将其用于生产。

请注意,图中显示了添加的传感器。这是为了说明的目的;您不需要它来执行示例代码。

Standard Headers And Wiring

现在你应该想知道如何让这些电线留在引脚头。Arduino 板的标准接头非常适合原型制作,但不太适合最终解决方案的安装。幸运的是,你可以买到不带接头的 Arduino 板,事实上,一些克隆的 Arduino 板设计有特殊的布局,使它们易于与试验板或硬连线电路一起使用。或者,您可以使用原型屏蔽来构建您的电路。下图描述了两个替代 Arduino 板和一个原型屏蔽,您可以在永久性解决方案中使用,例如将电线焊接到板上。

A978-1-4842-1293-6_8_Figc_HTML.jpg

A978-1-4842-1293-6_8_Figb_HTML.jpg

A978-1-4842-1293-6_8_Figa_HTML.jpg

图片由 sparkfun.com 提供。

Write the Sketch

示例代码经过了简化,使其更易于实验。更具体地说,我没有包含任何用于读取传感器或写入数据库的代码。我已经为您添加这些元素的位置留下了注释。回想一下,在开发复杂的解决方案时,我们总是希望从这样的框架代码开始。如果您将所有内容都集中在一个代码文件中,而某个部分不工作,您可能不容易发现哪个部分导致了问题(或者即使多个部分都失败了)。

我还使用了一个叫做条件编译的编程技巧。这允许我拥有可以以一种方式使用(编译)但如果以另一种方式使用(编译)就会被丢弃的代码段。在这种情况下,我需要一个源文件,但是有条件地使用主文件的一些部分和从文件的其他部分。

为了将部分标记为有条件的,我们使用#if defined子句和其相反的#if not defined子句以及一个#define来确定在我们编译和上传时包含哪些代码。因此,在下面的代码中,您将看到一个#define SLAVE,您可以注释掉它并使代码为主服务器编译。再次,离开#define SLAVE,代码被编译为从代码;注释掉它,你就得到了大师。

与其一次把所有的代码都扔给你,不如让我们仔细检查每一部分,从每个主模块和从模块的setup()代码开始讨论需要做什么。

主机和从机通信所需的代码来自线库。主机和从机都必须使用wire.begin()启动有线协议,但是从机需要注册设备号并提供一个回调(一个在事件触发时调用的特殊方法,在这种情况下是从主机接收的消息)。主机和从机都需要为 LED 设置一个引脚,并启动串行类。因此,setup()代码使用了前面讨论过的条件编译。代码如下所示。请注意,从模块的代码比主模块做得更多,我们有一个不在条件编译块中的部分。该代码针对主机或从机进行编译。

void setup() {

#if defined SLAVE

Wire.begin(address);            // join i2c bus

Wire.onReceive(getHeartbeat);   // register the heartbeat code

startTime = millis();           // initiate timer

elapsedTime = 0;

#else

Wire.begin();

#endif

pinMode(13, OUTPUT);            // turn on onboard LED (pin 13)

Serial.begin(115200);

// TODO: Add any database initialization code here

}

那么,对从机回调是什么?在这里,我们只是收到了来自主人的信息。完成这项工作的代码如下所示,非常简单明了。注意,我们从主机中读取字符,并将它们打印到串行监控器上。我们真的不在乎主人发出什么信息,只要它发出点什么就好。还要注意的是,我们设置了一个计时器,每当主人发送一条消息,计时器就会重新开始计时。

void getHeartbeat(int chars) {

char ch;

while (Wire.available()) {

ch = Wire.read();

}

Serial.println("Master is ok.");

// Reset timer since master is Ok

startTime = millis();

elapsedTime = 0;

blink();  // visual feedback

}

在主机上,我们用下面的代码发送消息。如你所见,我们只是写了一条简单的短信。然而,我们不能在 I2C 公共汽车上广播消息;它必须被定向到特定的设备。这里我们使用一个简单的变量address来设置地址。由于代码来自同一个文件,我们确保主模块和从模块用相同的值编译。

void sendHeartbeat() {

Wire.beginTransmission(address);

Wire.write("hello!");

Wire.endTransmission();

blink();  // visual feedback

}

在这一点上,我应该注意,我们不希望主设备中包含专门针对从设备的代码(这只会浪费宝贵的内存),所以您会看到特定于从设备的代码周围有条件编译块。

那么,奴隶怎么知道主人死了呢?我们使用由elapsedTimestartTimemaxTime变量指定的超时代码。也就是说,我们用millis() 10 记录 Arduino 每次通过我们的循环所用的毫秒数,这样,如果经过的时间超过设置的最大时间,我们就宣布主设备死亡。在这种情况下,我们编写草图(代码),让从机执行与主机相同的代码来读取传感器并将数据保存到数据库。

现在剩下的是与串行监控器通信、定时器到期和 LED 闪烁的细节——所有这些都应该是熟悉的。在任何情况下,清单 8-6 显示了带有自动故障转移的冗余数据收集器的完整代码。

Listing 8-6.Redundant Data Collector with Failover

/**

Example Arduino redundant data collector

This project demonstrates how to create a data

collector master/slave for redundancy. If the master

fails, the slave will take over.

The sketch has both the master and slave code with the

default the slave role. To use the sketch as a master,

comment out the #define SLAVE.

*/

#include <Wire.h>

// TODO: Add MySQL Connector/Arduino include, variables here

#define SLAVE

int address = 8;                // Address on I2C bus

// Blink the LED on pin 13

void blink() {

digitalWrite(13, HIGH);

delay(1000);

digitalWrite(13, LOW);

}

// Record data to database

// Stubbed method for writing data

void recordData() {

// TODO: Complete this code for your sensor read and the write to MySQL.

}

#if defined SLAVE

unsigned long startTime;        // start of simple timer

unsigned long elapsedTime;      // number of milliseconds elapsed

unsigned long maxTime = 10000;  // timeout value (10 seconds)

// Get the heartbeat message from the master

void getHeartbeat(int chars) {

char ch;

while (Wire.available()) {

ch = Wire.read();

}

Serial.println("Master is ok.");

// Reset timer since master is Ok

startTime = millis();

elapsedTime = 0;

blink();  // visual feedback

}

#else

// Send the heartbeat pulse

void sendHeartbeat() {

Wire.beginTransmission(address);

Wire.write("hello!");

Wire.endTransmission();

blink();  // visual feedback

}

#endif

void setup() {

#if defined SLAVE

Wire.begin(address);            // join i2c bus

Wire.onReceive(getHeartbeat);   // register the heartbeat code

startTime = millis();           // initiate timer

elapsedTime = 0;

#else

Wire.begin();

#endif

pinMode(13, OUTPUT);            // turn on onboard LED (pin 13)

Serial.begin(115200);

// TODO: Add any database initialization code here

}

void loop() {

#if defined SLAVE

Serial.println("looping...");

// Check timeout

elapsedTime += millis() - startTime;

Serial.println(elapsedTime);

if (elapsedTime > maxTime) {

// Master is dead. Read and save the sensor data!

Serial.println("Master has died! Oh, my...");

// OPTIONAL: reset master timer to try again.

startTime = millis();

elapsedTime = 0;

// Record data since master is dead.

recordData();

}

#else

recordData(); // read and save data

Serial.print("Sending heartbeat...");

sendHeartbeat();

Serial.println("done.");

#endif

delay(3000);  // wait 3 seconds

}

不要被这些代码弄得不知所措。我知道它相当复杂,甚至可能更复杂,因为它使用了条件编译,但是它让我们不用有两个项目,而不是一个。鉴于主服务器和从服务器需要相同的代码来与 MySQL 服务器和传感器通信,我们可以编写一次代码,省去调试和维护重复代码的麻烦。

在尝试自己运行之前,请花些时间通读这段代码。在继续运行代码之前,再次检查和三次检查您的连接也是一个好主意,这可能有点棘手,所以请仔细阅读下一节。

Test the Sketch

测试草图可能有点吓人,所以让我们来看看我们需要做什么。首先要把草图编好,上传给师傅。我们简单注释掉#define,选择正确的板卡和端口,然后上传草图。接下来,我们从#define中删除注释,为从机选择正确的板卡和端口,然后编译并上传。

我所做的(我强烈建议这样做)是通过两个 USB 接口将主机和从机连接到你的电脑上。这将使设置实验变得容易得多。事实上,您可以通过在 Arduino IDE 中设置端口并启动串行监控器来在两者之间进行切换。但是,请注意,当串行监控器连接时,Arduino 会强制 Arduino 复位。这对于测试来说没问题,但是要知道这意味着可能需要几秒钟才能看到主/从代码工作。

当通过串行监控器连接到主机时,您应该看到类似于图 8-5 所示的输出。虽然它并不有趣,但它可以帮助你确定你的主人是否在发送信息。别忘了我们还连接了一个 LED,这样你就可以看到 LED,它会随着你看到的每条信息反馈而闪烁。

A978-1-4842-1293-6_8_Fig5_HTML.jpg

图 8-5。

Sample debugging output (master)

接下来,当您连接到从服务器时,您应该看到描述从服务器是否已经接收到消息(心跳)的消息,以及经过时间的计数器。如果主服务器失败,您还会看到一条消息。图 8-6 显示了从机的样本输出。

A978-1-4842-1293-6_8_Fig6_HTML.jpg

图 8-6。

Sample debugging output (slave)

遇到问题不要绝望,不要放弃。回去检查你的线路,确保所有线路都正确。一旦你认为这不成问题,再次编译并上传草图,确保你为每个主机和从机使用正确的电路板和端口。将从(或主)草图上传到两块板上很容易做到。在这种情况下,拥有两个独立的草图可能更容易,如果你在保持#define直线上有问题,你可以这样做。

您还应该考虑上传主草图并让它运行,同时上传从草图,然后连接到从草图的串行监控器输出。我发现这有时有助于保持事情正常。毕竟,大多数棘手的代码都在从机上。

最后,确保主板和从板之间没有电源问题或类似的电源不兼容问题。我用莱昂纳多和一个旧得多的电路板进行了测试,一切正常。然而,我发现有些板,尤其是克隆板,在一些 I2C 设备上并不总是能正常工作,因为我们使用从设备作为 I2C 设备,这可能是一个问题来源。

Tip

确保你有注释掉的#define来为大师编译草图。

一旦你开始看到正确的输出,庆祝它的工作!这张草图(无论多小)实现了复杂的心跳和自动故障转移,这是其他具有更强大硬件的解决方案可能无法实现的。然而,正如你所看到的,只要有一点点聪明才智,你就可以用像 Arduino 这样的微控制器这样的简单硬件做出惊人的事情。

现在真正的考验开始了。从笔记本电脑上拔下主设备和从设备的插头,并给它们通电。我会先给主机加电,然后给从机加电。主设备上的 LED 是否闪烁,然后从设备上的 LED 闪烁?只要这些 led 在闪烁,就表示正在发送和接收主心跳。现在你明白我为什么要添加 led 灯了吧——在没有电脑的情况下测试解决方案。如果您没有看到这种情况,请尝试重置或关闭主板,然后再次打开。我发现有时 I2C 总线不太对劲,重启似乎有所帮助。

一旦您对一切正常感到满意,您就可以添加代码来读取传感器并将数据写入 MySQL 数据库,然后在您的 IOT 解决方案中部署带有故障转移的冗余数据收集器。请记住,较小的主板可能没有支持多个大型库所需的内存,因此您可能需要为自己的解决方案使用较大的 Arduino 主板。只要记得找到 SDA 和 SCL 引脚,并正确布线。

现在让我们看看在向数据库写入数据时如何处理错误。

示例:容错数据收集器

使用 MySQL 实现容错并不太困难,正如您所看到的,可以使用 MySQL 复制来实现冗余、恢复、可伸缩性,并最终实现数据库组件的高可用性。然而,在将数据写入数据库的典型基于微控制器的节点上实现容错可能有点困难。

在这一节中,我将展示一个您可以自己构建的项目。该项目是一个简单的植物土壤湿度数据收集器,具有故障检测和恢复功能,可将数据写入数据库。您已经在本书的前面看到了这样一个项目,但是这一次我们添加了故障检测和恢复机制,正如您将看到的那样,这会增加代码的复杂性,甚至需要额外的硬件!

如果考虑数据收集器节点的最基本功能,它应该从一个或多个传感器读取数据,然后将数据传递给数据节点或数据库,甚至将数据本地存储到文件中。那么,我们可以检测出哪些可能的故障呢?显然,编写代码来检测节点本身是否失效是不可行的(至少在没有冗余节点的情况下是不可行的)。因此,我们只考虑数据收集器可以恢复的那些故障:读取传感器失败和存储数据失败。

对于这个项目,我们只关注其中一个错误,特别是到数据库服务器的连接。在这种情况下,我们担心节点在网络上变得孤立,数据库服务器停止运行,或者从数据收集器到数据库的通信出现任何其他形式的中断。我选择这个特殊的错误来演示所需的额外复杂性。正如您将看到的,这并不简单,但也不复杂——只需要一点点工作。

让我们从设计的简要描述开始,然后是所需组件(硬件)的列表。

Overview of the Design

目标是使节点对数据库连接具有容错能力。更具体地说,如果数据库变得不可访问,我们希望将数据本地存储(缓存数据)到 SD 卡,然后,一旦数据库可访问,将缓存的数据保存到数据库。

该数据库是一个简单的事务,它有一个表,设计用来只捕获土壤湿度传感器值和数据以及读取(存储)该值的时间。下面显示了为此项目创建示例数据库所需的 SQL 语句:

CREATE DATABASE plant_moisture;

CREATE TABLE plant_moisture.plants (

``id int(11) NOT NULL AUTO_INCREMENT,

``moisture_raw int(11) DEFAULT NULL,

``date_saved timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

PRIMARY KEY (id)

) ENGINE=InnoDB AUTO_INCREMENT=257 DEFAULT CHARSET=latin1;

请注意,我们创建了一个简单的表,其中包含一个自动递增列、一个土壤湿度列和一个日期存储列。为了简单起见,我放弃了对土壤湿度的更复杂的注释,但是可以随意添加该列和触发器作为练习。

Assemble the Hardware

该项目的硬件将是一个 Arduino、一个土壤湿度传感器、一个以太网屏蔽和一个实时时钟模块。图 8-7 显示了如何连接所有设备。

A978-1-4842-1293-6_8_Fig7_HTML.jpg

图 8-7。

Arduino Mega with Ethernet shield, RTC module, and moisture sensor

但是等等!为什么我们要使用 Arduino Mega?这是我们看到资源密集型故障检测和恢复的一次机会。回想一下 Arduino Uno 和类似的小型主板的内存是有限的。我们正在添加一系列库:用于连接 MySQL 服务器的连接器/Arduino、用于缓存数据的 SD 卡以及用于读取日期和时间的 RTC 模块。还记得连接器/Arduino 需要几个其他的库。这一点,加上检测故障并从中恢复的代码,我们将很容易超过一个 Uno 板的内存。因此,我们使用 Mega 板,它有足够的内存。正如您将在自己的项目中发现的那样(即使不是现在,也是很快),为 Arduino 编写复杂的代码通常需要升级到更大的电路板。

您可能还想知道 RTC 模块。回想一下,我们的数据库有一个时间戳列,用于存储数据和样本存储的时间。还记得数据库服务器会为您填充这个值。但是,如果我们检测到故障,并且过了一段时间(可能是几个小时)后才能重新连接到数据库服务器,则存储的日期值将是不正确的—数据库服务器将使用写入数据库的日期和时间,而不是获取样本的时间。因此,我们需要 RTC 模块获取当前的日期和时间,并连同存储的值一起存储在缓存中。正如您所看到的,缓存数据不仅仅是一个简单的写后读事件!

因为有很多联系,所以让我们检查一下。首先,土壤湿度传感器连接 5V 电源和来自 Arduino 的接地。我还将传感器的信号(数据)线连接到 Arduino 上的模拟引脚 0。到目前为止,我们很好。然而,RTC 的接线与前面例子中的不同。这是因为 SDA 和 SCL 的引脚在 Mega 上的不同位置。在这种情况下,它们分别位于引脚 20 和 21。因此,我们将相应的 RTC 模块 SDA 和 SCL 连接到引脚 20 和 21,然后从 Arduino 获得 5V 电压和接地。我在图中使用试验板,使电源和接地连接更容易。

Why Not Use A Low-Cost Computer Board?

展示这个项目的目的之一是演示如何用一个小型微控制器实现一些相当复杂的功能。虽然 Arduino Mega 比 Raspberry Pi 贵一点,但学习如何在最底层实现故障检测和恢复将有助于您在更强大的主板上实现类似甚至更复杂的机制。事实上,您可以在 Raspberry Pi 上使用 Connector/Python 来实现这个项目,并实现相同级别的冗余。我鼓励你把这作为一个挑战来探索。

现在让我们看看我们的草图所需的代码。

Write the Sketch

草图(代码)有几个部分。我们知道我们将编写一个数据收集器来读取来自传感器的样本,并将数据写入数据库。此外,如果连接不可行,我们将把数据保存到本地文件。当数据库连接重新建立时,我们必须读取写入文件的数据,并将其插入数据库。在我们进入代码之前,我们应该考虑设计目标并确定关键需求。我发现把它们写下来会有帮助,即使它们在你的脑海中已经很清楚了。事实上,我发现列出需求可以帮助您以更有组织的方式计划和实现代码。我在这里列出了主要要求:

  • 所有收集的数据都应保存到数据库中。
  • 应该测试数据库连接,以确保服务器已连接。
  • 如果数据库无法连接,数据应该存储到本地文件中。
  • 保存到本地文件的数据应写入板载 SD 卡。
  • 当数据库已连接且本地文件中有数据时,读取数据并将其插入数据库。
  • 缓存数据应该存储样本收集的正确日期和时间。

让我们检查每一个需求,并确定每个需求的代码应该是什么。但是首先,让我们考虑一个可以用来封装该机制的高级算法。以下是对loop()方法的高级概述,即驱动草图的“主”程序。我们将在本节的后面看到清单 8-7 中的设置。我发现编写这样的算法有助于组织你的草图。它不仅有助于识别所需的主要方法,而且通过将代码保持在一个较高的水平,它为loop()方法中的代码提供了一个易于阅读的模板,并且可以帮助验证在代码中识别了需求。

read_sample()

if <database connection fails> then

cache_data()  // save data to the file

else

dump_cache()  // read data from file and insert into database

write_data()  // save current sample

end if

因此,现在我们看到,我们将需要三个主要方法以及一些手段来检测数据库连接是否失败。让我们从阅读示例开始,逐一讨论这些问题。由于我们使用模拟传感器,我们可以用analogRead()方法读取传感器,如下所示:

value = analogRead(sensorPin);

接下来,让我们考虑检测数据库连接何时失败的需求。一种方法是使用 Connector/Arduino 库中的connected()方法,但是这只会告诉你之前打开的连接是否仍然打开。应该注意的是,即使数据库服务器离线,也需要一些时间让connected()方法返回 false(连接丢失)。

一个更好的机制是每次通过循环尝试一个connect(),然后在数据保存后尝试close()。如果服务器不可达,那么connect()方法会更快地返回。现在,当我们使用connected()方法时,它立即返回数据库服务器没有连接。下面显示了如何在代码中设置这一点的摘录:

// Attempt to connect to the database server

if (!conn.connected()) {

if (conn.connect(server_addr, 3306, user, password)) {

...

}

}

...

conn.close();

您已经看到了如何将数据写入 SD 卡上的文件的示例。回想一下,它需要 SD 库和代码来打开文件,写入数据,然后关闭文件。唯一棘手的部分是,我们必须获取日期和时间字符串,并将其写入文件,保存采样时间。您已经看到了如何从 RTC 中读取日期和时间,因此对这些代码也很熟悉。

您还看到了如何从文件中读取数据,但不是以将数据插入数据库的方式。但是从机械上来说,这个 insert 与前面展示的 write _data()方法原语是一样的。因此,我们可以重用这个方法(编写一次,在多个地方使用)。下面显示了该方法的代码:

bool write_data(int val, const char *dateStr) {

String queryStr = String("INSERT INTO plant_moisture.plants VALUES (NULL,");

queryStr += String(val);

queryStr += ",";

if (dateStr != "NULL") queryStr += "'";  // quote string for values

queryStr += dateStr;

if (dateStr != "NULL") queryStr += "'";  // quote string for values

queryStr += ")";

// write the data here

Serial.println(queryStr);

// Create an instance of the cursor passing in the connection

MySQL_Cursor *cur = new MySQL_Cursor(&conn);

cur->execute(queryStr.c_str());

delete cur;

}

注意这里我写了代码来检测传递的日期字符串是否是NULL(一个字符串,不是 null 概念,甚至不是 0 或\0 终止符)。如果字符串不是NULL,那么这个字符串就是一个日期和时间值,我们必须在产生的INSERT语句中用引号将它括起来。

就这样。我们涵盖了高级算法中的所有方法。我们只需要实现 RTC、SD 卡和 MySQL 连接器/Arduino 所需的启动代码。清单 8-7 显示了这个草图的完整代码。

Listing 8-7.Fault-Tolerant Data Collector

/**

Example Arduino fault tolerant data collector

This project demonstrates how to save data to a

microSD card as a cache should the node fail to

save data to a MySQL database server.

*/

#include <Wire.h>

#include <SD.h>

#include <SPI.h>

#include <Ethernet.h>

#include <MySQL_Connection.h>

#include <MySQL_Cursor.h>

#include "RTClib.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

#define LOGFILE "cache.txt"        // name of log file for caching data

int sensorPin = 0;                 // Pin for reading the sensor

RTC_DS1307 rtc;                    // real time clock

byte mac_addr[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };

IPAddress server_addr(10,0,1,8);   // Supply the IP of the MySQL *server* here

char user[] = "root";              // can be anything but the user must have

char password[] = "secret";        // access rights to connect (host must permit it)

EthernetClient client;

MySQL_Connection conn((Client *)&client);

// Get the date and time from the RTC

String get_datetime() {

DateTime now = rtc.now();

String dateStr = String(now.year());

dateStr += "-";

dateStr += now.month();

dateStr += "-";

dateStr += now.day();

dateStr += " ";

dateStr += String(now.hour());

dateStr += ":";

dateStr += String(now.minute());

dateStr += ":";

dateStr += String(now.second());

return dateStr;

}

// Save the data to the SD card for later processing

// Note: What happens when the disk is full?

bool cache_data(int value) {

File log_file;

String row = String(value);

log_file = SD.open(LOGFILE, FILE_WRITE);

if (log_file) {

// Get datetime

row += ",";

row += get_datetime();

// Save data to the file

log_file.println(row);

log_file.close();

Serial.println("Data cached to log file.");

return true;

}

Serial.println("ERROR: Cannot open file for writing.");

return false;

}

// Save the data to the database

bool write_data(int val, const char *dateStr) {

String queryStr = String("INSERT INTO plant_moisture.plants VALUES (NULL,");

queryStr += String(val);

queryStr += ",";

if (dateStr != "NULL") queryStr += "'";  // quote string for values

queryStr += dateStr;

if (dateStr != "NULL") queryStr += "'";  // quote string for values

queryStr += ")";

// write the data here

Serial.println(queryStr);

// Create an instance of the cursor passing in the connection

MySQL_Cursor *cur = new MySQL_Cursor(&conn);

cur->execute(queryStr.c_str());

delete cur;

}

// Read the cache file on disk and write rows found to the database

bool dump_cache() {

File log_file;

char char_read;

char buffer[128];

int i = 0;

int comma_pos = 0;

int val = 0;

// if no file, no cache!

if (!SD.exists(LOGFILE)) return true;

log_file = SD.open(LOGFILE);

if (log_file) {

// Read one row at a time.

while (log_file.available()) {

char_read = log_file.read();

if (char_read != '\n') {

// look for fist value

if (char_read == ',') {

buffer[i] = '\0'

val = atoi(buffer);

i = 0;

} else {

buffer[i] = char_read;

i++;

}

} else {

buffer[i] = '\0'

i = 0;

// Write the row to the database

write_data(val, buffer);

}

}

// close the file:

log_file.close();

// Now remove the file

if (SD.remove(LOGFILE)) {

Serial.println("Cache emptied. File removed.");

}

return true;

}

// if the file didn't open, print an error:

Serial.println("ERROR: Cannot open file for reading");

return false;

}

void setup() {

Serial.begin(115200);

while (!Serial); // wait for serial to load

// Turn on the SPI for the SD card (Arduino Ethernet)

// Note: required for Arduino Mega!

pinMode(10,OUTPUT);

digitalWrite(10,HIGH);

// Start the RTC

rtc.begin();

if (!rtc.begin()) {

Serial.println("Couldn't find the RTC module.");

while (1);

}

if (! rtc.isrunning()) {

Serial.println("The RTC module is not working.");

} else {

// Comment out this line after the first run, it is only needed for setting

// the date and time for the first time.

rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

}

// Setup the SD card

Serial.print("Initializing SD card...");

if (!SD.begin(SD_PIN)) {

Serial.println("ERROR: Cannot initialize SD card");

return;

}

Serial.println("SD card is ready.");

// Now remove the file

if (SD.remove(LOGFILE)) {

Serial.println("Cache emptied. File removed.");

}

// Setup the network connection

Ethernet.begin(mac_addr);

delay(1000);

// Test the connection

Serial.println("Testing connection...");

if (conn.connect(server_addr, 3306, user, password)) {

delay(1000);

Serial.println("Success!");

conn.close();

} else {

Serial.println("ERROR: Connection failed.");

}

}

void loop() {

int value;

// Read the sensor

value = analogRead(sensorPin);

// Attempt to connect to the database server

if (!conn.connected()) {

if (conn.connect(server_addr, 3306, user, password)) {

delay(1000);

dump_cache();  // read any rows stored in the cache file

}

}

// Save the data to the database if connected

if (conn.connected()) {

Serial.println("Writing data");

write_data(value, "NULL");

} else {

// Database unreachable, save data to the cache file

cache_data(value);

}

conn.close();

delay(2000);       // wait for it...

}

我想提醒大家注意这段代码中的两件事。首先,注意最后一个delay()方法。使用此方法设置您希望草图等待采样的时间。因为我们只是在测试草图,所以您可以保持这个时间很短(2 秒),但是如果您为自己的数据收集器实现了这个代码或一个派生代码,您可能想要更长的时间。其次,注意我添加了许多信息性消息。有些人可能会认为我添加了太多,实际上,如果您将它们删除,会节省一些空间,但是我将它们放在那里,以便我可以看到草图是如何运行的(更准确地说,当数据被缓存、从缓存中读取并写入数据库时)。让我们看看它的行动。

Test the Sketch

现在让我们编译代码并上传到 Arduino Mega。编译代码时,您会注意到它消耗了 36KB 以上的程序存储空间。虽然编译器可能会显示类似下面的内容,并指示您有足够的可用空间,但如果您试图为较小的 Arduino 编译此内容,您将会得到一个错误,指出程序太大。

Sketch uses 36,370 bytes (14%) of program storage space. Maximum is 253,952 bytes.

Global variables use 2,219 bytes (27%) of dynamic memory, leaving 5,973 bytes for local variables. Maximum is 8,192 bytes.

一旦代码正确编译,并且您已经设置了示例数据库,那么继续运行它一段时间。过一会儿,您可以通过断开以太网电缆来模拟数据库故障。让我们看看会发生什么。

如果您打开串行监控器,您将看到我在整个代码中散布的信息消息的输出。清单 8-8 显示了一次运行的摘录。请注意,样本值是相同的。回想一下,我将延迟设置为仅两秒钟,因此样本值不会有太大的变化。在现实世界的实现中,对于受控气候中的植物,您可能希望每小时检查一次土壤湿度,甚至每隔几个小时检查一次。

Listing 8-8.Debug Statements from Fault-Tolerant Data Collector

Connected to server version 5.7.8-rc-log

Writing data

INSERT INTO plant_moisture.plants VALUES (NULL,456,NULL)

Disconnected.

Connected to server version 5.7.8-rc-log

Writing data

INSERT INTO plant_moisture.plants VALUES (NULL,456,NULL)

Disconnected.

Data cached to log file.

Data cached to log file.

Data cached to log file.

Connected to server version 5.7.8-rc-log

INSERT INTO plant_moisture.plants VALUES (NULL,456,'2015-11-27 15:9:8')

INSERT INTO plant_moisture.plants VALUES (NULL,457,'2015-11-27 15:9:21')

INSERT INTO plant_moisture.plants VALUES (NULL,455,'2015-11-27 15:9:34')

Cache emptied. File removed.

Writing data

INSERT INTO plant_moisture.plants VALUES (NULL,456,NULL)

Disconnected.

注意,当我断开以太网电缆时,代码将数据保存到缓存文件中。但是后来当我重新连接电缆时,数据被从缓存中读取并插入到数据库中。再次注意这三个陈述。您看到日期和时间值被保留了吗?回想一下这些数据被保存到文件中。但是请注意,其他的INSERT语句使用NULL来通知数据库使用数据保存时的当前日期和时间。很棒,对吧?

Check the Requirements

我们是如何满足需求的?我喜欢检查需求,因为一旦代码完成并工作,我就对照代码来编写它们。实际上,我在每个阶段都检查需求,但是在草图运行一段时间后检查它们将帮助你检查数据以确保数据被正确保存。让我们仔细检查每个需求(用内嵌的数字标注)。

从测试中,我们看到所有的数据都被保存了(1)。我们做了一个测试,并捕获到数据库服务器的通信丢失(2)。连接断开时,数据会缓存到 SD 卡上的文件中(3) (4)。当重新连接时,任何缓存的数据都被写入数据库(5),并且缓存数据的样本收集日期和时间被保留(6)。

这就是您的第一个带恢复功能的容错数据收集器。虽然我在这个例子中使用了以太网,它通常更可靠,但是您可以修改这个代码来使用其他形式的通信,从而保护您的数据收集不会因为通信机制故障而丢失。

使用这样的技术以及 MySQL 复制冗余,您可以为您的 IOT 解决方案实现令人印象深刻的高可用性。我鼓励您继续为平台(如 Raspberry Pi、Edison 和其他单个计算机主板)上的其他节点探索这样的代码。你会发现这些主板有足够的内存来构建更复杂的容错机制。

Summary

高可用性就是可靠性。解决方案越可靠,高可用性级别就越高。因此,高可用性的概念并不难,但实现起来可能有点困难。幸运的是,MySQL 提供了许多高可用性特性;其中最主要的是 MySQL 复制。

在本章中,您学习了更多关于 MySQL 复制的知识,包括一些在 IOT 解决方案中设置和使用复制的有用提示和技术。您还看到了一些高可用性概念的示例,包括 Python 的简单循环读取扩展解决方案、用于构建具有故障转移功能的冗余数据收集器的硬件,以及如何为数据收集器内置容错功能,以便在数据收集器与数据库之间的通信路径出现故障时不会丢失数据。

如您所知,IOT 解决方案不仅可以实现一定程度的高可用性;你可以从 IOT 网络的最低层到最高层这样做。从硬件到数据库到软件应用,所有这些都可以变得更加可靠,并最终达到高可用性。

Footnotes 1

正如您将看到的,MySQL 实用程序使之变得更加简单。

2

有时,这是在遇到致命错误或数据损坏后使从属服务器重新联机的最简单、最快的方法。因此,一定要定期备份。

3

还有其他招数,但这是最安全的办法。

4

Oracle 还没有针对 MySQL 读取扩展的负载平衡器。但是它在 MySQL 路由产品中有一个连接路由功能。

5

这并不是说配置文件有什么问题,但是如果您曾经遇到过这样的安装,其中有多个配置文件分散在解决方案的几个部分中,那么您会逐渐意识到不需要它们。

6

不要和 hinky 混淆,后者更糟。

7

对反常行为的高度技术性描述。

8

不推荐,但是我在实验中看到过效果很好。我会使用重复的传感器。

9

你能猜到我是怎么知道的吗?没错。我准备双向射击我的列奥纳多,直到我记得针布局是不同的。使用足够多的 Arduino 板,你会发现你也想这样做!

10

这是一个基本的测量时间的方法,虽然有点不准确,但没有实时时钟,但作为一个粗略的计时器已经足够了。

posted @   绝不原创的飞龙  阅读(76)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Blazor Hybrid适配到HarmonyOS系统
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
点击右上角即可分享
微信分享提示