Arduino-IOS-蓝图-全-

Arduino IOS 蓝图(全)

原文:zh.annas-archive.org/md5/500370a47ef1ccfd668d61e6900ecd38

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

那是 2011 年的秋天,我正在开发一些 iOS 应用程序。在我的桌子上放着一块几乎未被使用的 Arduino 板。一个想法闪过我的脑海——将 iOS 平台和 Arduino 集成在一起会非常棒。我可以从任何地方控制几乎一切,从我的家到工业机器。

我开始着手这项工作,最终设计了 Arduino Manager。这是一个基于小部件的通用 iOS 应用程序,它从 Arduino 获取数据,并通过仪表、图表和其他方式展示它。Arduino Manager 允许你通过开关、旋钮、滑块等方式控制 Arduino 板(更多相关信息请参阅apple.co/1NPfL6i)。

本书展示了这些年来我开发的一些基本技术,以使 Arduino 和 iOS 设备协同工作。

我将向你展示如何构建五个令人惊叹的项目,你将学习如何让 Arduino 周围的额外电子设备工作。你还将学习如何使用数字和模拟传感器,编程 Arduino 板,并开发可以与 Arduino 传输数据的 iOS 应用程序。项目描述详细,为你提供学习工具,而不仅仅是复制一些草图或 iOS 代码。

我保证你不会感到无聊。

本书涵盖的内容

第一章,Arduino 和 iOS – 平台和集成,将简要介绍这两个平台,并介绍它们之间的集成方法。此外,你还将学习如何在两个平台上设置开发环境,并为后续章节中的项目做好准备。

第二章,蓝牙宠物门锁,开发了一个项目,可以帮助你通过测量外部光线、监控门是否锁定或未锁定以及根据需要手动操作,在夜间自动锁定宠物门。这是通过使用 iOS 设备完成的。

第三章,Wi-Fi 电源插头,是关于学习如何制作一个可以通过 Wi-Fi 控制的智能电源插头。这并不是基于传统的继电器技术,iOS 应用程序充满了实用功能。

第四章,iOS 引导式漫游者,将教你如何通过语音命令和移动你的 iOS 设备来控制漫游机器人。

第五章,电视设置恒定音量控制器,是为那些对广告音量过大感到厌烦的人准备的。你将学习如何制作一个自动红外控制器,以保持电视机的音量恒定。

第六章,自动车库门开启器,介绍了一个项目,通过靠近车库门即可自动开启,无需触摸 iOS 设备。该项目利用了 iBeacon 技术。

您需要为本书准备的内容

要构建项目的硬件部分,您需要一个 Arduino 板和一些其他电子组件。

要开发 Arduino 固件,您只需要 Arduino IDE,可以从 Arduino 网站免费下载。

要开发 iOS 应用程序,您需要 Xcode 开发环境(可以从 Apple 免费获得)。由于 Xcode 仅适用于 MAC 平台,因此您需要一个基于 Intel 的 MAC 电脑。大多数项目基于蓝牙 4 协议。因此,您需要一个支持此协议的 iOS 设备。

所有内容都在第一章,Arduino 和 iOS – 平台和集成中详细解释。

本书面向对象

本书是面向对 Arduino 和 iOS 平台有基本了解但想学习如何将它们集成的 Arduino 和 iOS 开发者技术指南。本书包含大量外部参考资料,指向额外的文档和学习材料。因此,即使是经验较少的读者也可以提高其在涵盖主题上的知识。

约定

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“通常,浏览器会下载到您的下载文件夹中。”

代码块设置如下:

        if (deviceIdentifier!=nil) {

            NSArray *devices = [_centralManager retrievePeripheralsWithIdentifiers:@[[CBUUID UUIDWithString:deviceIdentifier]]];
            _arduinoDevice = devices[0];
            _arduinoDevice.delegate = self;
        }

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

        if (deviceIdentifier!=nil) {

            NSArray *devices = [_centralManager retrievePeripheralsWithIdentifiers:@[[CBUUID UUIDWithString:deviceIdentifier]]];
            _arduinoDevice = devices[0];
            _arduinoDevice.delegate = self;
        }

新术语重要词汇以粗体显示。屏幕上显示的词汇,例如在菜单或对话框中,在文本中显示如下:“或者,您可以从 Launchpad 运行它,或者通过导航到Finder | 应用程序 | App Store。”

注意

警告或重要提示以如下方框显示。

小贴士

小贴士和技巧显示如下。

读者反馈

我们始终欢迎读者的反馈。让我们知道您对本书的看法——您喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出您真正能从中受益的标题。

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

如果你在一个你擅长的主题上有专业知识,并且你对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有许多事情可以帮助你从购买中获得最大收益。

下载示例代码

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

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果你在我们的书中发现错误——可能是文本或代码中的错误——如果你能向我们报告这个问题,我们将不胜感激。通过这样做,你可以帮助其他读者避免挫败感,并帮助我们改进本书的后续版本。如果你发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择你的书籍,点击错误清单提交表单链接,并输入你的错误详情来报告它们。一旦你的错误清单得到验证,你的提交将被接受,错误清单将被上传到我们的网站或添加到该标题的错误清单部分。

要查看之前提交的错误清单,请访问 www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在错误清单部分。

盗版

在互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果你在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过 <copyright@packtpub.com> 联系我们,并提供疑似盗版材料的链接。

我们感谢你在保护我们的作者和我们提供有价值内容的能力方面的帮助。

问题

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

第一章:Arduino 和 iOS – 平台和集成

本章将简要介绍 Arduino 和 iOS 平台及其之间的集成方法。此外,你将学习如何在两个平台上设置开发环境,并为下一章的项目做好准备。

我们假设你已经对这两个平台和电子学有一些基本知识,并且能够至少使用面包板搭建电路。然而,本章的主要目的是学习如何将这两个平台集成在一起。

Arduino 自其早期起源以来一直是一款开放硬件设备,你可以轻松找到关于它的任何所需信息。相反,iOS 平台在硬件方面并不开放。你无法设计并构建一个与 iOS 设备兼容的硬件设备,除非加入苹果的专用项目(MFi)。该计划要求严格,只有大公司才能满足。

提示

关于 Arduino 和 iOS 的更多信息

关于 Arduino 和 iOS 开发的更多信息可以在www.arduino.ccapple.co/1HThS1O分别找到。

尽管如此,在章节的结尾,你将学习如何在两种平台之间以允许你的应用程序在 iTunes App Store 上销售的方式传输数据。这并不复杂。我们将使用 TCP/IP 或蓝牙 BLE。

本章将涵盖以下主题:

  • 硬件和软件要求

  • Arduino 和开发环境设置

  • iOS 和开发环境设置

  • Arduino 和 iOS 设备之间的通信方法

硬件和软件要求

为了实现本书中的所有项目,你需要一些硬件和软件组件,这些组件可以从你当地的任何商店或通过互联网轻松购买。

Arduino 平台的硬件要求

为了执行本书中的项目,你主要需要 Arduino UNO 和以下额外的硬件组件:

  1. Arduino UNO R3 (bit.ly/1IInOke)。

  2. 一条 USB 线(A 到 B 类型)。

  3. 一个 9V 外部直流电源(可选但推荐)。

  4. 一块官方 Wi-Fi 盾牌 (bit.ly/1UQgq9v, bit.ly/1i5k1Cn)。

  5. 一块来自 Adafruit 的蓝牙 BLE nRF8001 扩展板 (bit.ly/1MvkyJm)。

  6. 一个数字万用表(可选但强烈推荐)。

  7. 一个面包板(越大越好)。

  8. 一些工具(斜口钳、钳子、镊子、剪钳等)。

  9. 一些公对公和公对母的跳线(越多越好;它们永远不够!)

  10. 一些电子组件,将在每一章中展示。

  11. 一个用于第五章项目的巡游机器人,“电视恒定音量控制器”。

  12. 第六章中的项目 自动车库门开启器 的 iBeacon (apple.co/1GXnt7Z)。

小贴士

仅购买正品!

许多 Arduino 产品被伪造,尤其是在网上,售价仅为几美元。除了道德考虑之外,正品将为您提供更多质量和确保正确功能的确信。本书中的项目是在正品上开发和测试的。您已被警告!

官方 Wi-Fi 扩展板

官方 Wi-Fi 扩展板并不便宜,有时与 iOS 一起使用可能会很困难。许多其他产品更便宜。有些是兼容的,有些则不是。如果您选择使用不同的 Wi-Fi 扩展板,请确保它与 Arduino IDE 中包含的 Wi-Fi 库兼容。否则,您将不得不自行修改本书中的 Arduino 代码。您也可以在 bit.ly/1i5k1Cn 购买此扩展板。

Arduino.cc 与 Arduino.org 冲突

在撰写本书时,Arduino 分裂为两家公司——arduino.cc 和 arduino.org。它们正处于法律斗争中,关于两家公司销售的同名产品存在很多混淆。尽管在美国和亚洲的情况正在变得清晰,但在欧洲仍然不可预测。在这本书中,我们将使用 arduino.cc 的产品和开发环境。

蓝牙断路板

我们选择的蓝牙设备已在多个项目中进行了多次测试,表现良好。市场上还有许多其他设备,可以完成完全相同的工作。如果您决定使用不同的蓝牙设备,请确保它是一个 BLE(蓝牙低功耗)设备,也称为蓝牙 4.0、蓝牙低功耗和蓝牙智能。只有当它是蓝牙 BLE 时,您才能轻松编写仅通过蓝牙通信的 iOS 应用程序。

Arduino 平台的软件要求

您所需的一切是 Arduino IDE 1.6.4、IDE 中包含的库以及一些可在网上获得的附加库。当需要时,您将被告知下载它们。

iOS 平台的硬件要求

编写 iOS 应用程序所需的 Apple 开发环境(Xcode)仅适用于 Mac OS X 操作系统(10.10.3 Yosemite 版本)。因此,您需要一个配备最新英特尔处理器的 Mac 计算机来运行 Yosemite。

此外,您需要一个运行 iOS 8.4 且支持蓝牙 4.0 的 iOS 设备(iPhone、iPad 或 iPod touch)。

如果你已经是 iOS(每年约 99 美元)的Apple Developer Program(ADP)成员,你的生活将会变得简单。不幸的是,如果你不是 ADP 的成员,苹果不允许你将使用 Xcode 编写的程序上传到自己的设备。即使你不是 ADP 的成员,你仍然可以在设备模拟器中运行程序,但它将无法模拟蓝牙 BLE 子系统。这将是一个使用蓝牙 BLE 的项目的问题。无论如何,将本书中展示的代码修改为使用 Wi-Fi 而不是蓝牙 BLE 不应该太复杂。

小贴士

Xcode 和新的苹果规则

在本书编写时,苹果宣布了一个新的 iOS 版本(版本 9)和 Xcode 版本(版本 7),它们现在处于测试版本。这些版本将取消一些限制规则,用户即使没有订阅 Apple Developer Program,也应该能够将自编写的应用程序上传到自己的设备。

iOS 平台的软件要求

如前所述,要为 iOS 平台编写应用程序,你需要 Xcode(本书编写时的版本为 6.4)和一些可在网上找到的额外开源库。当需要时,你将被告知下载它们。Xcode 仅在 Mac 的 OS X(版本 10.10.3 Yosemite 或更高版本)上运行。即使本书中的大部分代码可以在 iOS 的早期版本上运行,但你的 iOS 设备必须能够运行 iOS 8.3 及以上版本,因为本书中使用的正是这个版本。

Arduino 和开发环境设置

Arduino 是一个非常基础但功能强大的微处理器板。它允许你获取数字和模拟输入,处理它们,并控制外部设备。

你可以在bit.ly/1IInOke找到有关此设备的所有详细信息。

它与市场上许多其他类似(甚至更强大)的设备的不同之处在于,它有一个非常简单的开发环境(IDE)和大量的库,这允许你只需几小时就能完成项目。这些库中的大多数已经在 IDE 中可用,或者很容易添加。

小贴士

Arduino UNO – Arduino MEGA

在本书中,我们将参考 Arduino UNO。Arduino 还有许多其他具有不同硬件特性、FLASH 和 RAM 内存大小的板。本书中的大多数项目在 Arduino MEGA 上运行时,代码无需更改,硬件只需做少量修改。

其他平台

每隔几周,就会推出一个与 Arduino 兼容的新平台。其中两个与本书中的项目兼容的平台是 RFduino,它支持蓝牙 BLE 而无需任何额外硬件(更多信息,请访问www.rfduino.com)和 Teensy(了解更多关于 Teensy 的信息,请访问www.pjrc.com/teensy)。与这些平台一起工作可能需要进行一些硬件和/或软件的更改。

IDE 安装

安装 IDE 的过程非常快速且简单,尤其是在您不需要在 OS X 上安装额外的驱动程序的情况下。

您可以使用以下步骤在几分钟内准备好开发环境:

  1. 确保您已安装可以打开 ZIP 文件的应用程序。如果您没有此类应用程序,您可以在 App Store 中免费找到它们(The Unarchiver 可能是一个选择,可以从 apple.co/1gT7W2D 下载)。

  2. bit.ly/1gT8u8E 下载 Arduino IDE 1.6.4。

  3. 通常,浏览器会在您的 下载 文件夹中下载文件。打开此文件夹并解压 ZIP 文件(Arduino-1.6.4-macosx.zip)。

  4. Arduino.app 文件移动到您的应用程序文件夹。

开发环境现在已准备好运行。

为了确保一切按预期工作,请将包含的示例之一(闪烁即可)上传到您的 UNO。请参考 bit.ly/1KsUhqv 以正确配置 IDE 中的板和端口。

小贴士

要使用的 IDE 版本

本书中的代码是在 IDE 1.6.4 上测试的,这是撰写本书时的最新版本。即使有新的 IDE 版本可用,您也应该使用建议的版本。不同版本的 IDE 中包含的库通常不同,这可能会导致意外的行为。您可以在完全构建和测试 Arduino 和 iOS 上的项目后测试新的 IDE。

iOS 和开发环境设置

在过去几年中,通常称为智能手机的设备已成为最常用的个人设备。iPhone 是全球使用最广泛的智能手机之一,而在许多情况下,iPad 已取代了个人电脑。

iPhone、iPad 和 iPod touch 运行相同的操作系统,称为 iOS。它们拥有整洁且统一的用户界面,允许用户通过简单的手势(如轻触和双击屏幕)或复杂的手势(如滑动或捏合屏幕)与设备交互。

可以在 iPhone 和 iPad 上运行的应用程序可以轻松编写,并且这两个平台都非常强大,几乎任何应用程序都可以在上面运行。

我们可以直接进入安装开发环境的过程,这样我们就可以开始享受 iOS 和 Arduino 的乐趣了。

Xcode 安装

您可以执行以下步骤在几分钟内安装 Xcode:

  1. 打开 App Store 应用程序。通常,您可以在 Dock 中找到它。或者,您可以从 Launchpad 运行它,或者通过导航到 查找器 | 应用程序 | App Store

  2. 使用您的账户连接到 App Store(浏览 商店 | 登录)。

  3. 定位 Xcode(导航到 类别 | 开发者工具 或搜索它)。

  4. 点击 获取 按钮开始下载。

在很短的时间内,应用程序将被下载并安装(取决于您的互联网连接速度)。

Arduino 与 iOS 设备之间的通信方法

iOS 平台并不那么开放,尤其是从硬件角度来看。苹果要求连接到 iPhone 或 iPad 的硬件设备不仅应符合直接要求,而且还需要苹果本身的认证。为此,他们需要加入苹果的专用计划(MFi:apple.co/1PwSeWO)。加入该计划的要求非常严格,只有大公司才能满足这些要求。

尽管如此,苹果允许使用 TCP/IP 和蓝牙 BLE 与任何外部设备通信。这些是我们将在整本书中使用来通过 Arduino 传输数据的方法。在 Arduino 的一侧,我们必须选择与这两种方法兼容的附加硬件,或者用更技术性的术语来说,这两种协议。

Arduino 提供了以下两种盾以进行 TCP/IP 通信:

  • 以太网盾

  • Wi-Fi 盾

这两种盾都包括一个 SD 卡槽来存储数据。

以太网盾相对便宜且非常可靠。由于它使用以太网连接,它的缺点是需要物理线连接到你的家庭网络或路由器。这减少了你项目的灵活性。

相反,Wi-Fi 盾允许你在家里任何地方安装 Arduino 板,而不需要线缆的尴尬。这就是我们选择在这本书中使用 Wi-Fi 盾的原因。

苹果允许的另一种将数据传输到外部设备的方法是蓝牙 BLE(也称为蓝牙 4.0)。此协议消耗的能源更少,但它与之前的版本不兼容。如果你的 iOS 设备相当新,它将支持蓝牙 BLE。请在苹果网站上的设备页面上检查这一点。

小贴士

支持 BLE 的设备

如果你希望检查设备是否支持 BLE,请访问bit.ly/1blI106

Arduino 没有提供蓝牙 BLE 盾,但其他供应商有。我们选择了 Adafruit 的蓝牙 BLE nRF8001 分线板。

小贴士

蓝牙 4.0

你可以通过访问bit.ly/1Pj9caw了解更多关于蓝牙 4.0 的信息。

TCP/IP 与蓝牙

你可能会想知道 iOS 和 Arduino 的最佳通信方法。没有正确答案。这实际上取决于你的项目和你的需求。以下表格显示了每种方法的主要优缺点:

方法 优点 缺点
Wi-Fi
  • Arduino 可以放在你家里的几乎任何地方。

  • iOS 设备可以无处不在(如果网络设置得当,甚至在世界另一端)。

  • SD 卡可以用来存储数据。

|

  • 这很贵。

  • 即使数据可以以高达 54 Mbps 的速度传输,Wi-Fi 盾在传输数据方面并不那么响应。

  • 功耗如此之高,以至于你不能使用电池来为 Arduino 和 Wi-Fi 盾供电。

|

蓝牙
  • Arduino 几乎可以放在您家的任何地方。

  • 蓝牙设备消耗的电量要少得多。因此,可以使用电池为其供电。

  • 数据传输速度可达 1 Mbps,但无延迟。

|

  • iOS 设备必须靠近 Arduino(大约 100 米;室内则更短)。

  • 如果您希望存储额外的数据,则板上没有 SD 卡可用。

|

摘要

在本章中,您主要学习了将 Arduino 和 iOS 设备集成的基础知识。在接下来的章节中,您将学习如何在实践中编写此集成的代码。

此外,您已安装 Arduino IDE 以编写和上传程序到 Arduino,以及 Xcode 以编写和上传程序到您的 iOS 设备。

所以,放松一下。我们刚刚开始!让我们开始吧!

第二章:蓝牙宠物门锁

这个项目是关于一个宠物门控制器,通过测量外部光照和温度,锁定或解锁你的房屋宠物门。通过 iOS 设备,你可以检查门的状态(锁定或解锁)并手动覆盖 Arduino 上实现的逻辑以手动锁定门。

在这个项目中,你将学习如何与模拟传感器、开关、1-Wire 传感器(用于测量温度)以及连接到 Arduino 的伺服电机进行交互。此外,你还将为 Arduino 连接一个蓝牙 4.0 板,以便与你的 iOS 设备通信。

然后,你将学习如何编写一个蓝牙 iOS 应用程序,用于从 Arduino 发送和接收数据。

在本章的结尾,我们将讨论不同类型的传感器及其与 Arduino 的通信协议,以便学习如何管理最常用的测量技术。

这个项目需要一些 DIY 技能,以便将锁具安装到宠物门上,并将其连接到伺服电机。

本章分为以下部分:

  • 门锁要求:我们将简要回顾项目要求

  • 硬件:我们将描述项目所需的硬件和电子电路

  • Arduino 代码:我们将为 Arduino 编写代码以控制闩锁并与 iOS 设备通信

  • iOS 代码:我们将为 iOS 设备编写代码

  • 如何进一步发展:更多改进项目和学习的想法

  • 不同类型的传感器:对模拟和数字传感器、低级通信协议及其优缺点的快速概述

门锁要求

我们将构建一个自动系统,允许你在以下不同场景下锁定宠物门:

  • 在夜间,当外部光照低于预定义的阈值时

  • 当外部温度对于你心爱的宠物来说过低或过高时,即当温度低于预定义的阈值或高于预定义的阈值时

此外,我们还需要在 iOS 设备上查看外部温度,了解宠物门是否锁定或解锁,并手动覆盖自动行为,手动锁定门。

硬件

在这个项目中,我们必须构建一个基于闩锁和伺服电机的机电装置,用于锁定/解锁宠物门。此外,我们还需要构建一个简单的电子电路来读取传感器。

必备材料和电子元件

要构建锁具,你需要少量硬件,这些硬件可以在你当地的五金店找到:

  • 一个小门闩,最好是平的且没有凹槽。由于它必须由伺服电机操作,因此它必须滑动非常顺畅。

  • 一些螺母和螺栓,用于将闩锁安装到门上。

  • 几厘米的金属线(直径约 2 毫米)。

其他必备元件包括:

  • 5V 供电的微型伺服电机。

  • 通常闭合的磁开关和一个小磁铁。

  • 一个光敏电阻。

  • 电阻:2 × 10K,1 × 4.7K。

  • 一个数字 DS18B20 温度传感器。它比模拟传感器(如 TMP 35)贵,但精度更高,读数几乎不受电压波动和电噪声的影响。

  • Adafruit Bluefruit LE nRF8001 开发板 (www.adafruit.com/product/1697)。

组装闩锁和伺服电机

要组装闩锁,可以参考以下图片。

组装闩锁和伺服电机

小磁铁被粘贴在闩锁上,因此当它完全缩回时,磁开关是打开的。

光敏电阻和温度传感器必须放置在室外,并连接到 Arduino。它们需要防止湿气和灰尘,因此最好将它们放在一个小塑料盒内。盒子必须钻孔,以便空气可以自由流通,帮助温度传感器正确测量。此外,光敏电阻不应暴露在直射光下,以免失明。一个小塑料管可以完成这项工作。

电子电路

以下图片显示了项目所需的电子电路的电原理图:

电子电路

以下图片显示了如何在面包板上安装电路:

电子电路

为了快速参考,以下列表总结了 nRF8001 的接线:

  • SCK 连接到数字引脚 13

  • MISO 连接到数字引脚 12

  • MOSI 连接到数字引脚 11

  • REQ 连接到数字引脚 10

  • RST 连接到数字引脚 9

  • RDY 连接到数字引脚 2

如果你使用的是 Arduino MEGA 而不是 UNO,则 nRF8001 板的接线必须按以下方式更改:

  • SCK 连接到数字引脚 52

  • MISO 连接到数字引脚 50

  • MOSI 连接到数字引脚 51

  • REQ 连接到数字引脚 10

  • RST 连接到数字引脚 9

  • RDY 连接到数字引脚 2

光线是通过光敏电阻和 Arduino 的模拟输入(A0)测量的。光敏电阻是一种电阻随入射光强度增加而减小的器件。在电路中,光敏电阻与 R1 串联。如果光敏电阻上的光增加,R1 上的电压也会增加,并通过 Arduino 的模拟引脚 A0 进行测量。

注意

当光敏电阻未受光照且其电阻非常高时,R1 会强制将 Arduino 输入接地。

在 Arduino 代码中,你可以使用 analogRead 函数获取模拟引脚上的电压值。它返回一个在 0-1023 范围内的值,该值与施加到模拟引脚上的电压成正比。

闩锁位置是通过磁开关和 Arduino 的数字输入(D4)确定的。当闩锁上的磁铁靠近时,磁开关关闭。开关与 R2 串联。当开关关闭时,R2 上的电压几乎为 5V,而当开关打开时,约为 0V。

电子电路

分压器

小贴士

分压器是由两个电阻组成的简单电路。假设电路外部的电流可以忽略不计,V2 约等于 V1R2/(R1+R2)*。

使用 Arduino 数字输入,你可以确定开关是打开还是关闭。

注意

在 Arduino 代码中,你可以使用digitalRead函数获取数字引脚的值。

温度传感器是一个使用 1-Wire 协议的数字传感器,它通过数字引脚读取。

注意

要读取温度传感器的值,你必须使用OneWire库和DallasTemperature库,通过这些库,一个函数可以直接返回摄氏度的温度。

伺服电机需要 PWM 信号来控制。脉冲宽度调制是一种使用数字手段获得模拟结果的技术。它是一个周期为 2 毫秒的方波。正脉冲的宽度决定了伺服电机的旋转(例如,1.5 毫秒的脉冲将使电机旋转到 90 度位置)。幸运的是,伺服库(已包含在 IDE 中)隐藏了复杂性,我们只需调用一个带有期望电机位置的函数即可移动电机。

Arduino 代码

本项目的完整代码可以从这里下载:

www.packtpub.com/books/content/support

为了更好地理解下一段落的解释,你应该在阅读时打开下载的代码。

每个 Arduino 程序几乎总是具有以下结构。

#include <library_1.h>
#include <library_2.h>

#define SOMETHING A_VALUE

// Function prototypes

void callback_1();

// Global variables

boolean  var_1;

// Called only once at power on or reset

void setup() {

...
}

// Called over and over again

void loop() {
...
}

// Callbacks 

void callback_1() {
...
}

// Additional functions

void function_1() {
...
}

指令#include <library_1.h>告诉编译器使用一个库,而指令#define SOMETHING A_VALUE告诉编译器在程序中所有地方用A_VALUE替换SOMETHING。如果你使用定义的值,那么更改会更简单。如果你需要将A_VALUE替换为其他内容,你可以在一个地方而不是在代码的所有实例中这样做。

函数原型通常用于在代码中使用但将主体放在程序源代码末尾的函数。全局变量是那些需要在程序执行期间保留其值的变量。setup函数用于初始化库和变量;它只在板子上电或复位时调用一次。相反,loop函数会反复调用。在loop函数中定义的变量在调用之间会丢失其值;这就是为什么我们需要全局变量的原因。回调函数是在库中发生事件或数据可用于处理时被调用的。相反,从loop中调用的函数有助于使代码更容易阅读、维护和调试。

安装额外的必需库

对于这个项目,我们需要一些库。其中一些已经在 Arduino IDE 中可用,而其他则需要添加(例如,OneWireDallasTemperatureAdafruit_BLE_UART)。

要添加它们,请遵循以下简单步骤:

  1. 选择菜单项Sketch | Include Library | Manage Libraries

  2. 在搜索框中输入OneWire

  3. 选择带有OneWire的行并点击安装(见下图)。

  4. 在搜索框中输入DallasTemp,点击MAX31850然后安装

  5. 在搜索框中输入nRF8001,点击Adafruit nRF8001然后安装。安装额外的必需库

初始化全局变量和库

温度传感器使用 1-Wire 协议与 Arduino 通信,因此我们需要OneWire库。幸运的是,另一个库(DallasTemperature)使得温度读数变得非常简单。我们需要为这两个库创建全局变量:

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

以及一个用于存储传感器地址:

DeviceAddress temperatureSensorAddress;

nRF8001 库的全局变量在一行中:

Adafruit_BLE_UART uart = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);

注意

如果你更改上一行中使用的引脚,你必须在电路布线中也进行更改。

要控制伺服电机,我们需要另一个全局变量:

Servo myservo;

我们还需要两个额外的全局布尔变量:

boolean  iOSConnected;
boolean  manuallyLocked;

第一个变量在 iOS 设备连接到 Arduino 时为真,第二个变量在用户需要无论光照和温度值如何都保持宠物门关闭时为真。

设置代码

我们开始设置串行通信库:

Serial.begin(9600);
while (!Serial); // Leonardo/Micro should wait for serial init

这不是严格必要的,但它是写入控制台所必需的,这可能对调试很有用。

然后我们初始化传感器库:

sensors.begin();

我们还读取设备编号 0(我们电路中唯一的设备)的地址:

if (!sensors.getAddress(temperatureSensorAddress, 0)) 
    Serial.println("Unable to find address for Device 0");

小贴士

1-Wire 地址

每个 1-Wire 设备在制造时都有自己的地址定义,并且不能更改。要对设备执行任何操作,你必须知道其地址。getAddresssearch库函数可以帮助你找到设备的地址。

传感器可以提供不同精度的读数,但我们需要更高的精度时,设备响应就会越慢。就我们的目的而言,我们不需要高精度,因此可以将精度设置为 9 位:

sensors.setResolution(temperatureSensorAddress, 9);

库必须知道伺服电机连接的引脚:

myservo.attach(SERVOPIN);

用于光敏电阻和开关的引脚都需要配置为输入:

pinMode(PHOTORESISTORPIN, INPUT);
pinMode(SWITCHPIN, INPUT);

为了与 nRF8001 板通信,我们需要设置几个回调函数;一个,以知道 iOS 设备何时连接或断开:

uart.setACIcallback(aciCallback);

另一个回调函数用于接收由 iOS 设备发送的数据:

uart.setRXcallback(rxCallback);

当 iOS 设备的数据可用于处理时,nRF8001 库会调用rxCallback

现在我们准备查看代码的主要部分,它实现了控制宠物门和 iOS 通信的算法。

主程序

从项目需求中,我们得出结论,Arduino 程序必须实现一个简单的逻辑:

  • 读取光强度

  • 读取温度

  • 如果光强度高于LIGHT_THRESHOLD,在LOW_TEMPERATURE_THRESHOLDHIGH_TEMPERATURE_THRESHOLD之间的温度下,伺服电机必须移动到UNLOCK_POSITION(180 度),否则移动到LOCK_POSITION(65 度)

此外,当 iOS 设备连接时,它必须接收有关锁存位置(打开或关闭)和外部的温度信息。

在主循环函数中,我们使用以下方法读取光强度:

unsigned int light = analogRead(PHOTORESISTORPIN);

使用以下方法读取温度:

boolean lacthIsOpened = digitalRead(SWITCHPIN);

并且使用以下方法获取锁存位置:

sensors.requestTemperatures();
float temperature = sensors.getTempC(temperatureSensorAddress);

光是一个在 0-1023(2¹⁰)范围内的值,与光强度成正比,如果锁存器打开,则latchIsOpened的值为 true(1),temperature是传感器测量的温度。

提示

模拟引脚上的电压

Arduino 内部的模数转换器(ADC),连接到模拟引脚,使用 10 位将引脚上的电压转换为整数值,这是一个在 0-1023(2¹⁰ 个值)范围内的整数值。

由于模拟引脚上的电压可以在 0V 和 5V(电源电压)之间,因此每个位都有一个值,该值为 5/1024,然后你可以使用此公式计算模拟引脚上的电压:

电压 = analogRead(<模拟引脚>) * 5/1024

如果你通过电脑的 USB 端口给 Arduino 供电,电源电压永远不会正好是 5V,通常会更低。为了获得更好的读数,你应该使用数字万用表测量电源电压,并在之前的公式中将 5 替换为实际的电源电压。

在循环函数中,必须调用pollACI函数,以便通信库可以接管控制以处理从 iOS 连接的设备接收到的数据。

提示

poolACI

如果你有一个长而复杂的程序,你可能需要在代码中添加许多poolACI调用,以便频繁地允许库接管控制以处理通信。否则,你可能会遇到数据丢失。

核心算法在以下几行中:

if (!manuallyLocked) {

    if (aboveThreshold(light, LIGHT_THRESHOLD, 30) && betweenThresholds(temperature, LOW_TEMPERATURE_THRESHOLD, HIGH_TEMPERATURE_THRESHOLD)) {
      Serial.println("Unlocked");
      myservo.write(UNLOCKED_POSITION);
    }  

    if (belowThreshold(light, LIGHT_THRESHOLD, 30) || !betweenThresholds(temperature, LOW_TEMPERATURE_THRESHOLD, HIGH_TEMPERATURE_THRESHOLD)) {
      Serial.println("Locked");
      myservo.write(LOCKED_POSITION);
    }

  }

如果门没有被手动锁定,灯光强度高于LIGHT_THRESHOLD,温度在LOW_TEMPERATURE_THRESHOLDHIGH_TEMPERATURE_THRESHOLD之间,可以使用以下指令将伺服电机移动到UNLOCKED_POSITION

myservo.write(UNLOCKED_POSITION);

否则,它将被移动到LOCKED_POSITION

myservo.write(LOCKED_POSITION);

提示

德摩根定理

这个定理在用任何编程语言编写 if-then-else 语句时非常有用。永远不要忘记它!

not (A or B) = not A and not B

或者

not (A and B) = not A or not B

"A 或 B"的对立条件是"非 A 非 B","A 和 B"的对立条件是"非 A 非 B"。

以下两个函数相当直观:

  • aboveThreshold(…)

  • betweenThresholds(….)

查看下载的代码以获取更多详细信息。

如果manuallyLocked为真,则忽略光和温度读数。此变量在rxCallback函数中通过从 iOS 设备接收到的消息设置:

void rxCallback(uint8_t *buffer, uint8_t len) {

  if (len > 0) {

    // Data received from the iOS device
    // Received only one byte which has value 48 (character 0) or 49 (character 1)

    manuallyLocked = buffer[0] - '0';
    if (manuallyLocked) {
      Serial.println("Manual Lock");
      myservo.write(LOCKED_POSITION);
    }

  }
}

记住,nRF8001 库在从 iOS 设备发送的数据准备好处理时自动调用 rxCallback 函数。

如果门需要上锁,iOS 设备将发送带有 ASCII 字符 1 的一个字节,否则发送 ASCII 字符 0。

ASCII 字符 0 的代码是 48,因此要将它转换为布尔值 false(0),你需要减去 48(或字符'0',它们是相同的)。从 ASCII 字符 1 减去 48,我们得到布尔值 true(1)。

返回主函数。如果 iOSConnected 为真,iOS 设备已连接到 Arduino,并且需要向其传输一些数据。

数据以以下格式发送到 iOS:

s:latch_position;t:temperature

在这里,latch_position 通知 iOS 设备闩锁是开启还是关闭,而 temperature 是外部温度。

要将数据发送到 iOS,我们使用以下代码:

if (iOSConnected) {

    // When the iOS device is connected some data are transferred to it

    char buffer[32];
    char tempBuffer[6];

    // Data sent to iOS
    // s:latch_position;t:temperature

    dtostrf(temperature, 0, 2, tempBuffer);

    snprintf(buffer, 32, "s:%d;t:%s", lacthIsOpened, tempBuffer);
    uart.write((uint8_t *)buffer, strlen(buffer));
  }

函数 snprintf 创建一个按需格式化的缓冲区,然后通过函数 uart.write((uint8_t *)buffer, strlen(buffer)) 发送到 iOS。

提示

snprintf

此函数永远不会写入比第二个参数指示的更多字符。这对于编写安全代码非常重要。实际上,如果你写入的字符多于缓冲区大小,很可能会在用于其他目的的内存位置写入,导致微处理器崩溃。崩溃后,微处理器将从 setup 函数重新启动程序执行。更多详情请访问 bit.ly/1E021nobit.ly/1LijDx5

dtostrf

不幸的是,在 Arduino 上,snprintf 无法处理浮点数,因此我们需要使用 dtostrf 将温度(一个浮点数)转换为字符串,然后再在 snprintf 中使用它。dtostrf 的第二个参数是转换中使用的十进制位数。更多详情请访问:bit.ly/1fmj9HV

我们最后要编写的代码是管理 iOS 设备的连接和断开。这是在 aciCallback 函数中完成的。nRF8001 库在以下事件发生时调用此函数:设备开始广播以被其他蓝牙设备发现,外部设备连接,或连接的设备断开。

void aciCallback(aci_evt_opcode_t event) {

  if (event == ACI_EVT_DEVICE_STARTED)
    Serial.println(F("Advertising started"));

  if (event == ACI_EVT_CONNECTED) {

    iOSConnected = true;

    char buffer[16];
    snprintf(buffer, 16, "m:%d", manuallyLocked);
    uart.write((uint8_t *)buffer, strlen(buffer));
  }

  if (event == ACI_EVT_DISCONNECTED) {
    iOSConnected = false;
  }
}

测试和调整 Arduino 端

一旦将代码上传到 Arduino(更多详情:bit.ly/1JPNAn3bit.ly/1KsUhqv),你就可以开始测试它了。

如果足够的光线到达光敏电阻,并且温度在 2 摄氏度到 33 摄氏度之间,伺服应该移动到开启位置,并且闩锁应该完全缩回。

然后,当你覆盖光敏电阻时,闩锁应该关闭。为了测试温度传感器,你可以使用吹风机将温度提高到 3 摄氏度以上,或者使用冰袋将其降低到 2 摄氏度以下。在这两种情况下,闩锁都应该关闭。

你可以通过更改这些定义轻松更改温度阈值:

#define LOW_TEMPERATURE_THRESHOLD    2
#define HIGH_TEMPERATURE_THRESHOLD   33

光阈值可能需要更多的调整,因为光敏电阻读取的值高度依赖于光敏电阻的特性、安装定位和方向。无论如何,更改:

#define LIGHT_THRESHOLD             700

你应该能够找到适合你需求的值。

你可能还需要根据你组装的方式调整伺服电机的开合位置。为了调整位置,你可以更改:

#define LOCKED_POSITION             180
#define UNLOCKED_POSITION            65

这表示电机的位置角度。

iOS 代码

在本章中,我们将编写 iOS 应用程序,通过该应用程序我们可以连接到 Arduino,了解宠物门是否锁定,读取外部温度,并最终手动锁定它。

本项目的完整代码可以从这里下载:

www.packtpub.com/books/content/support

为了更好地理解下一段落的解释,你应该在阅读时打开下载的代码。

编写应用程序的主要工具是苹果公司提供的 Xcode。我们可以从 Launchpad 启动它,或者在 Finder 中的应用程序文件夹中打开它。

小贴士

今天开始开发 iOS 应用程序

苹果公司提供的 iOS 开发的有用指南可以在以下链接中找到:apple.co/MtP2Aq

Objective-C

开发 iOS 应用程序使用的语言是 Objective-C。它与 C++类似,你可以在以下链接中找到该语言的介绍:apple.co/19FWxfQ

创建 Xcode 项目

第一步是创建一个新项目。Xcode 提供了许多不同的项目模板;我们将使用 Tabbed Application,它有两个标签。我们将使用第一个标签作为主应用程序面板,第二个用于扫描 nRF8001 设备。此操作仅在应用程序首次启动时进行一次。

小贴士

Xcode 概述

你可以在以下链接中找到所有与 Xcode 相关的工作所需信息:apple.co/1UQnMtS

要创建新项目,我们可以按照以下步骤进行(见以下截图):

  1. 前往文件 | 新建 | 项目 …

  2. 在左侧面板中,选择iOS | 应用程序

  3. 在右侧面板中,选择Tabbed Application,然后点击下一步创建 Xcode 项目

  4. 在下一屏中,输入所需信息:

    • 产品名称: PetDoorLocker

    • 组织名称: Your Name

    • 组织标识符: yourname(此信息仅与将应用程序发布到 iTunes Store 并销售有关。可以忽略)

    • 语言Objective-C

    • 设备通用(我们将创建一个可以在 iPhone 和 iPad 上运行的应用程序)

    创建 Xcode 项目

  5. 点击下一步按钮。

  6. 选择您想要存储项目的文件夹(取消选中源代码控制)。

就这些了!

我们现在可以开始编写新的应用程序了。首先要做的是重命名两个视图控制器(FirstViewController 和 SecondViewController)。

  1. 在左侧面板中选择FirstViewController.h,这将打开右侧面板中的文件。

  2. 在行@interface FirstViewController : UIViewController中选择FirstViewController,通过双击FirstViewController

  3. 右键单击并选择重构 | 重命名 …

  4. 输入视图控制器的新的名称:PetDoorLockerViewController

  5. 点击预览然后保存。

  6. 选择SecondViewController.h,按照相同的步骤将其重命名为BLEConnectionViewController

什么是视图控制器?苹果文档说:

视图控制器是应用程序数据与其视觉外观之间的重要联系。每当 iOS 应用程序显示用户界面时,显示的内容都由视图控制器或一组相互协调的视图控制器管理。因此,视图控制器提供了构建应用程序的骨骼框架。

小贴士

设计模式

在继续之前,我们建议您阅读以下链接上的信息:apple.co/1hkUDbU

现在我们已经准备好设计新应用程序的 GUI。

为 BLEConnectionViewController 设计应用程序用户界面

要设计应用程序用户界面,让我们打开Main.storyboard。此文件包含有关 GUI 的所有内容。一旦打开,您应该看到以下截图所示的内容:

为 BLEConnectionViewController 设计应用程序用户界面

让我们从BLEConnectionViewController开始,它将被用于扫描 nRF8001 设备。

  1. 双击视图控制器以选择它。

  2. 点击第二视图标签并删除它,然后选择由 SecondViewController 加载并删除它。

  3. 在右侧打开实用工具面板:视图 | 实用工具 | 显示实用工具。(要打开此面板,您也可以使用以下截图中的绿色圆形图标)。

  4. 在工具导航器中选择Label并将其拖放到空白区域(见以下截图)。为 BLEConnectionViewController 设计应用程序用户界面

  5. 通过双击Label并输入Device来重命名标签。

  6. 现在我们必须设置将锁定标签在所需位置的自动布局约束:

    1. 点击自动布局固定图标(在之前的截图中以红色圆形标记)。

    2. 在“从父视图的 Leading 空间”中输入20,在“到父视图的 Top 空间”中输入30,如以下截图所示。

    3. 输入53作为宽度

    4. 点击添加 3 个约束

    为 BLEConnectionViewController 设计应用程序用户界面

  7. 在上一个标签旁边放置一个新的标签实例,并为其添加自动布局约束:

    1. 选择两个标签(按下命令按钮点击它们)。

    2. 点击对齐图标(在之前的截图中被圈出绿色)。

    3. 选择垂直居中然后添加 1 个约束

    4. 仅选择新的标签并点击自动布局固定点。

    5. 前导空间尾随空间都输入为20

    6. 对于更新框架,选择容器中的所有框架

    7. 点击添加 2 个约束

    8. 再次选择新的标签,并选择视图 | 实用工具 | 显示属性检查器(或点击下一张截图中圈出的红色图标)。

    9. 将字体大小更改为13并将对齐设置为居中(见下一张截图中圈出的绿色区域)。

    为 BLEConnectionViewController 设计应用程序用户界面

    新标签将显示检测到的 nRF8001 的 UUID。现在我们必须添加一个按钮来开始扫描附近的 nRF8001 设备。

  8. 将按钮拖入容器中,双击它,并输入Scan

    1. 选择按钮并点击自动布局固定点。

    2. 到空间输入为45并点击添加 1 个约束

    3. 选择按钮,点击对齐图标,在容器中选择水平居中,对于更新框架,选择容器中的所有框架,然后点击添加 1 个约束

    现在我们必须将 GUI 组件与代码链接起来,以便程序化地操作它们。

  9. 选择BLEConnectionViewController

  10. 点击视图 | 辅助编辑器。一个新的面板打开,显示BLEConnectionViewController.h

  11. 关闭实用工具面板以获得更多空间(点击视图 | 实用工具 | 隐藏实用工具)。

  12. 确保在下一张截图的红色圈出区域中,你看到的是BLEConnectionViewController.m,如果不是,请点击它并更改为所需的文件。

  13. 选择标签标签,保持命令按钮按下,将标签拖到右侧的代码中,在@interface BLEConnectionViewController ()@end之间(见下图画)。为 BLEConnectionViewController 设计应用程序用户界面

  14. 当出现对话框时,将名称输入为deviceUUIDLabel(见下图画)然后点击连接为 BLEConnectionViewController 设计应用程序用户界面

    这将创建一个属性(deviceUUIDLabel),可以用来更改标签属性,例如其文本。

    小贴士

    ARC 和强与弱

    strong属性指示编译器,与属性关联的内存必须保留分配,直到定义它的类被分配。未定义为强引用(而是弱引用)的属性将在它们定义的代码块完成后自动释放。简而言之,只要有一个强引用指针指向对象,该对象就不会被释放。自引入自动引用计数(ARC)以来,iOS 下的内存管理已经简化,但仍对大多数人来说是一个问题。一篇好的介绍可以在以下链接找到:apple.co/1MvuNgw。它是为 Swift(苹果最新推出的编程语言)编写的,但值得一读。

    小贴士

    原子与非原子

    atomic属性将确保从 getter 或 setter 返回的整个值始终是完整的,无论其他线程上的 setter 活动如何。也就是说,如果线程 A 正在 getter 的中间,而线程 B 调用 setter,线程 A 将返回一个实际有效的值给调用者。

    苹果文档指出:“属性的原子性并不等同于对象的线程安全性”。

    访问强属性比访问非原子属性要慢。更多详情请参阅:apple.co/1JeBIdb

    我们现在将扫描按钮链接到一个当按钮被点击时调用的方法:

  15. 选择扫描按钮,并按住控制键,将其拖到右侧面板。

  16. 将名称输入为startScanning,并将类型选择为UIButton(见下图)。这创建了一个新方法,当按钮被按下时会被调用。为 BLEConnectionViewController 设计应用程序用户界面

    为了完成设计,我们必须设置视图控制器的名称,该名称将出现在工具栏中:

  17. 再次打开工具导航器(视图 | 实用工具 | 显示实用工具)。

  18. 选择容器底部的图标(一个带有第二个标签的小正方形)。

  19. 显示属性检查器(视图 | 实用工具 | 属性检查器)。

  20. 标题字段中,输入Configuration

注意

您还可以从图像列表框中选择一个图标。为此,您应该将图标添加到项目中,将其拖到左侧面板中的“支持文件”组。图标应为 32 × 32 像素。

最后,您的视图控制器应该看起来像下一张图片。

为 BLEConnectionViewController 设计应用程序用户界面

为 PetDoorLockerViewController 设计应用程序用户界面

在本节中,我们将设计第一个视图控制器(PetDoorLockerViewController)的界面。我们只会描述如何添加前一章中没有显示的组件。请参考下一张图片以查看视图的全局布局。

  1. 添加一个标签,将其重命名为Door Status,并添加自动布局约束。

  2. 在右侧面板的搜索字段中输入UIView,并将视图拖到标签附近。你可以通过拖动视图边框上的小白色方块来调整视图大小。在属性检查器中,选择浅灰色作为背景颜色。

  3. 选择标签和视图,然后点击对齐图标。选择垂直居中,然后点击添加 1 个约束

  4. 选择视图并点击对齐图标。在容器中选择水平居中,然后点击添加 1 个约束

  5. 选择固定图标,输入48作为高度和宽度,对于更新框架,选择容器中的所有框架,然后点击添加 2 个约束

  6. 添加一个新的标签温度,并将自动布局约束添加到它上面。

  7. 温度附近添加另一个标签,并将自动布局约束添加到它上面,以使其垂直居中对齐温度,并在容器视图中水平居中。

  8. 然后添加一个标签,并添加自动布局约束。

  9. 附近添加一个开关,并添加自动布局约束以使其垂直居中对齐,并在容器视图中水平居中。

  10. 在容器的底部添加一个按钮(称为连接),并添加自动布局约束以使其垂直居中对齐,并在容器视图中水平居中。

你应该结束于以下图片所示的内容。连接按钮在失去与 nRF8001 设备的连接时很有用,你需要手动重新连接到它。

为 PetDoorLockerViewController 设计应用程序用户界面

一旦添加了所有组件,你可以通过以下方式将它们链接到代码:

@interface PetDoorLockerViewController ()

@property (strong, nonatomic) IBOutlet UIView       *doorStatus;
@property (strong, nonatomic) IBOutlet UILabel      *temperature;
@property (strong, nonatomic) IBOutlet UISwitch     *manualLockSwitch;

@end

并使用reconnect函数来拦截当连接按钮被点击时。

如果有疑问,可以使用下载的代码作为参考。

我们最终准备好为两个视图控制器编写代码。

为 BLEConnectionViewController 编写代码

这个控制器的作用是获取 nRF8001 设备的唯一标识符,以便它可以按需连接到该设备。

要处理与蓝牙 4.0 设备的通信,我们需要使用我们添加到视图控制器接口中的类CBCentralManager

@interface BLEConnectionViewController ()

@property (strong, nonatomic) IBOutlet UILabel  *deviceUUIDLabel;

@property (strong, nonatomic) CBCentralManager  *centralManager;

@end

小贴士

Xcode 类参考

如果你需要了解更多关于一个类的信息,你可以按Option + 点击类名以直接访问文档。相反,Command + 点击将带你到源文件。

类的实例化在viewDidAppear方法中,每当与视图控制器关联的视图在设备屏幕上显示时都会调用。

-(void)viewDidAppear:(BOOL)animated {

    _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}

要与CBCentralManager一起工作,我们需要实现一些代理方法,但首先我们必须通知控制器这一点;我们打开BLEConnectionViewController.h文件,并按以下方式更改它:

#import <UIKit/UIKit.h>
#import <CoreBluetooth/CoreBluetooth.h>

@interface BLEConnectionViewController : UIViewController <CBCentralManagerDelegate>

@end

然后我们准备好编写两个代理方法:

  • centralManagerDidUpdateState: 当 iOS 蓝牙子系统的状态发生变化时调用此方法。在此视图控制器中,此方法仅具有信息性目的,但在需要监控蓝牙子系统状态的更复杂项目中非常有用。查看下载的代码以获取有关此方法的更多详细信息。

  • didDiscoverPeripheral: 当发现新的外设时调用此方法:

    - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
    
        [_scanningTimer invalidate];
        _deviceUUIDLabel.text = peripheral.identifier.UUIDString;
    
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    
        [userDefaults setObject:peripheral.identifier.UUIDString
                         forKey:@"PetDoorLockerDevice"];
    
        [userDefaults synchronize];
    }
    

每次发现新的蓝牙外设(在我们的例子中是 nRF8001 板连接到 Arduino)时,都会调用该方法,提供有关外设的信息。我们将在屏幕上显示外设的标识符:

_deviceUUIDLabel.text = peripheral.identifier.UUIDString;

并将其存储在userDefaults中,键为PetDoorLockerDevice,以便在需要时检索它。

小贴士

用户偏好设置

预设是您存储的持久信息,用于配置您的应用程序。您可以使用NSUserDefaults类访问它们。更多详细信息请参阅此处

之后,我们将回到解释:

[_scanningTimer invalidate];

扫描按钮在用户需要检测可用设备时激活startScanning方法。

- (IBAction)startScanning:(UIButton *)sender {

    if (_centralManager.state != CBCentralManagerStatePoweredOn)
        return;

     [_centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:NRF8001BB_SERVICE_UUID]] options:nil];

    _deviceUUIDLabel.text = @"Scanning...";

    _scanningTimer = [NSTimer scheduledTimerWithTimeInterval:(float)5.0 target:self selector:@selector(scanningTimedOut:) userInfo:nil repeats:NO];
}

如果centralManager的状态不是CBCentralManagerStatePoweredOn,则无法执行任何操作。否则,通过调用scanForPeripheralsWithServices开始扫描。

每个蓝牙 4.0 设备都有一个或多个唯一标识的服务;我们寻找 nRF8001 板的服务标识符(NRF8001BB_SERVICE_UUID)。

小贴士

nRF8001 服务和特征

一个蓝牙外设可以提供更多服务,并且对于每个服务,用户都可以读取和/或写入更多特征。nRF8001 板只有一个服务(UUID:6E400001-B5A3-F393-E0A9-E50E24DCCA9E),一个用于接收数据的特征(UUID:6E400002-B5A3-F393-E0A9-E50E24DCCA9E)和一个用于发送数据的特征(UUID:6E400003-B5A3-F393-E0A9-E50E24DCCA9E)。

NRF8001BB_SERVICE_UUID 的值在视图控制器代码的开头定义:#define NRF8001BB_SERVICE_UUID @"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"

一旦找到具有该服务的服务器,iOS 将调用didDiscoverPeripheral,iOS 设备停止扫描。不幸的是,扫描过程会一直运行,直到找到设备。因此,如果外设未找到,iOS 设备会持续消耗电量。为了克服这个问题,我们需要一个计时器。它定义在视图控制器接口中:

@interface BLEConnectionViewController ()

@property (strong, nonatomic) IBOutlet UILabel      *deviceUUIDLabel;

@property (strong, nonatomic) CBCentralManager      *centralManager;
@property (strong, nonatomic) NSTimer               *scanningTimer;

@end

并通过以下方式实例化:

_scanningTimer = [NSTimer scheduledTimerWithTimeInterval:(float)5.0 target:self selector:@selector(scanningTimedOut:) userInfo:nil repeats:NO];

如果没有停止,计时器将在 5 秒后调用scanningTimedOut方法。在此方法中,我们可以停止centralManager的扫描和耗电:

-(void) scanningTimedOut:(NSTimer *)timer {

    [_centralManager stopScan];
    _deviceUUIDLabel.text = @"No device in range";
}

如果找到具有所需服务的外设,iOS 将调用didDiscoverPeripheral方法:

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {

    [_scanningTimer invalidate];
    _deviceUUIDLabel.text = peripheral.identifier.UUIDString;

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [userDefaults setObject:peripheral.identifier.UUIDString
                     forKey:@"PetDoorLockerDevice"];

    [userDefaults synchronize];
}

我们必须停止scanningTimer,这是此行[_scanningTimer invalidate];的目的,并将外设的 UUID 保存到用户默认设置中。

现在是时候在你的设备上运行应用程序了。

小贴士

运行您的应用程序

您可以在以下位置找到运行应用程序所需的所有信息,无论是在模拟器上还是在物理设备上:developer.apple.com/library/ios/documentation/IDEs/Conceptual/AppDistributionGuide/LaunchingYourApponDevices/LaunchingYourApponDevices.html

打开 Arduino 板电源并启动应用程序,然后点击配置标签,然后点击扫描按钮。几秒钟后,您应该在设备标签附近看到一个由字母和数字组成的长字符串。这就是 nRF8001 设备的 UUID。设置配置已完成!

如果出现问题,将显示消息没有设备在范围内。在这种情况下,请仔细检查以下内容:

  1. nRF8001 已正确连接到 Arduino。

  2. 您已将正确的代码上传到 Arduino。

  3. Arduino 板已上电。

  4. IDE 控制台显示消息设置完成开始广播

  5. 在您的 iOS 设备上激活了蓝牙(轻触设置,然后蓝牙以激活它)。

编写 PetDoorLockerViewController 的代码

在本节中,我们将编写应用程序的主体部分,该部分允许您监控宠物门是否锁定或解锁,读取外部温度,并在需要时锁定宠物门。

注意

由于此视图控制器的代码较为复杂,并且我们希望为其他主题节省空间,因此我们将解释整个代码,但不会逐步指导您编写它。请参阅下载的代码以查看代码的完整内容。

我们需要在视图控制器接口中添加三个定义和一些额外的属性:

#define NRF8001BB_SERVICE_UUID                      @"6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define NRF8001BB_CHAR_TX_UUID                      @"6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define NRF8001BB_CHAR_RX_UUID                      @"6E400003-B5A3-F393-E0A9-E50E24DCCA9E"

@interface PetDoorLockerViewController ()

…

@property (strong, nonatomic) CBCentralManager      *centralManager;
@property (strong, nonatomic) CBPeripheral          *arduinoDevice;
@property (strong, nonatomic) CBCharacteristic      *sendCharacteristic;

@end

我们还需要在PetDoorLockerViewController.h中添加一个代理@interface PetDoorLockerViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate>

设置CBCentralManager实例与我们在上一个控制器中所做的方式完全相同。

方法centralManagerDidUpdateState相当不同:

- (void)centralManagerDidUpdateState:(CBCentralManager *)central {

    NSLog(@"Status of CoreBluetooth central manager changed %ld (%s)", central.state, [self centralManagerStateToString:central.state]);

    if (central.state == CBCentralManagerStatePoweredOn) {

        [self connect];
    }
}

一旦蓝牙子系统准备就绪(其状态为CBCentralManagerStatePoweredOn),应用程序开始尝试连接到 nRF8001 板,调用[self connect],这是一个我们很快就会向您展示的方法。

每次视图控制器在屏幕上显示时,连接都是从viewDidAppear开始的:

-(void)viewDidAppear:(BOOL)animated {

    [super viewDidAppear:animated];

    [self connect];
}

一旦视图从屏幕消失,连接就会关闭,以减少电池的消耗:

-(void)viewDidDisappear:(BOOL)animated {

    [super viewDidDisappear:animated];

    [self disconnect];
}

现在,让我们更详细地看看连接方法。

-(void)connect {

    if (_arduinoDevice == nil) {

        // We need to retrieve the Arduino peripheral

        NSString *deviceIdentifier = [[NSUserDefaults standardUserDefaults] objectForKey:@"PetDoorLockerDevice"];

        if (deviceIdentifier!=nil) {

            NSArray *devices = [_centralManager retrievePeripheralsWithIdentifiers:@[[CBUUID UUIDWithString:deviceIdentifier]]];
            _arduinoDevice = devices[0];
 _arduinoDevice.delegate = self;
        }
        else {

            …
            …

            return;
        }
    }

    [_centralManager connectPeripheral:_arduinoDevice options:nil];
}

如果arduinoDevice未初始化,我们将使用在扫描阶段存储在用户偏好设置中的 UUID 检索它。外围代理委托的设置很重要,因为我们必须发现外围设备的特征,它们通过代理方法返回。connectPeripheral方法实际上连接到外围设备。如果连接成功,将调用代理方法didConnectPeripheral,然后我们可以开始发现设备提供的服务:

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {

    [peripheral discoverServices:@[[CBUUID UUIDWithString:NRF8001BB_SERVICE_UUID]]];

}

小贴士

发现所有服务

在某些情况下,您可能需要发现外围设备提供的所有服务。为此,您使用:[peripheral discoverServices:nil];

一旦 iOS 发现外围设备的服务,它将调用didDiscoverServices方法,然后我们可以开始发现服务的特征:

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {

    …

    for (int i=0; i < peripheral.services.count; i++) {

        CBService *s = [peripheral.services objectAtIndex:i];
        [peripheral discoverCharacteristics:nil forService:s];
    }
}

对于每个提供的服务,iOS 会调用didDiscoverCharacteristicsForService方法(请参阅下载的代码)。在这个方法中,我们将发送数据到 nRF8001 设备的特征存储在属性sendCharacteristic中,并调用此方法:

[peripheral setNotifyValue:YES forCharacteristic:characteristic];

现在,我们将使用接收数据的特征作为参数。现在每次特征发生变化(从 nRF80001 设备发送数据)时,都会调用didUpdateValueForCharacteristic方法,并接收可用数据(请参阅下载的代码)。

当有可用数据时,会调用dataReceived方法,并处理接收到的数据:

-(void)dataReceived:(NSString *)content {

    // Messages has the following formats:
    //
    //  1) m:0|1
    //
    //  2) s:0|1;t:temperature

    NSArray *messages = [content componentsSeparatedByString:@";"];

    for (int i=0; i<messages.count; i++) {

        NSArray *components = [messages[i] componentsSeparatedByString:@":"];

        NSString *command = components[0];
        NSString *value   = components[1];

        if ([command isEqualToString:@"m"]) {
            _manualLockSwitch.on = [value boolValue];
        }

        if ([command isEqualToString:@"s"]) {

            BOOL doorUnlocked = [value boolValue];

            if (doorUnlocked) {

                _doorStatus.backgroundColor = [UIColor greenColor];
            }
            else {

                _doorStatus.backgroundColor = [UIColor redColor];
            }
        }

        if ([command isEqualToString:@"t"]) {
            _temperature.text = value;
        }
    }
}

我们可以接收两种类型的消息:

  • m:0|1

  • s:0|1; t:temperature

当 iOS 设备连接到 Arduino 时,它会接收到第一条消息,告知您门是否已被手动锁定(m:1)或未锁定(m:0)。有了这个信息,我们可以设置手动开关的位置:

_manualLockSwitch.on = [value boolValue];

第二条消息包含两种类型的信息:如果闩锁是打开的(s:1)或关闭的(s:0)以及外部温度。第一种用于更改doorStatus视图的背景颜色:

BOOL doorUnlocked = [value boolValue];

if (doorUnlocked) {

    _doorStatus.backgroundColor = [UIColor greenColor];
}
else {

    _doorStatus.backgroundColor = [UIColor redColor];
}

温度信息用于设置temperature标签的值:

if ([command isEqualToString:@"t"]) {
    _temperature.text = value;
}

当按下manualLockSwitch开关时,会调用switchChanged方法,在那里我们可以向 Arduino 传输数据:

- (IBAction)switchChanged:(UISwitch *)sender {

    NSData* data;

    if (sender.on)
        data=[@"1" dataUsingEncoding:NSUTF8StringEncoding];
    else
        data=[@"0" dataUsingEncoding:NSUTF8StringEncoding];

    [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];

}

要向蓝牙设备发送数据,我们使用writeValue方法写入适当的特征。由于它接受NSData值,我们必须使用dataUsingEncoding方法将字符串"0""1"转换为NSData

我们几乎完成了应用程序。一旦 iOS 应用程序连接到 Arduino,我们需要它在发送到后台时断开连接(以节省电池)。当它再次被带到前台时,它会自动重新连接到 Arduino。

为了做到这一点,我们使connect方法公开,并编写一个新的公开disconnect方法。为了使方法公开,我们在PetDoorLockerViewController.h中添加了几行代码:

-(void)connect;
-(void)disconnect;

disconnect方法非常简单:

-(void)disconnect {

    if (_arduinoDevice != nil) {
        [_centralManager cancelPeripheralConnection:_arduinoDevice];
        _doorStatus.backgroundColor = [UIColor lightGrayColor];
    }
}

doorStatus视图的背景颜色设置为浅灰色,我们可以直观地知道 iOS 是否已连接到 Arduino。

我们必须编写的最后一个方法是reconnect,它不需要任何解释:

- (IBAction)reconnect:(UIButton *)sender {

    [self disconnect];
    [self connect];
}

AppDelegate.m文件中,有两个方法,分别是在应用程序进入后台或返回前台时调用的:

  • applicationDidEnterBackground

  • applicationWillEnterForeground

在这些方法中,我们需要一个对PetDoorLockerViewController的引用。我们可以通过主应用程序窗口来获取它。

- (void)applicationDidEnterBackground:(UIApplication *)application {

    UITabBarController *tabController = (UITabBarController *)_window.rootViewController;
    PetDoorLockerViewController *petDorLockerController = tabController.viewControllers[0];

    [petDorLockerController disconnect];
}

- (void)applicationWillEnterForeground:(UIApplication *)application {

    UITabBarController *tabController = (UITabBarController *)_window.rootViewController;
    PetDoorLockerViewController *petDorLockerController = tabController.viewControllers[0];

    [petDorLockerController connect];
}

测试 iOS 应用程序

现在应用程序已经完成,我们可以在 iOS 设备上再次运行它。一旦启动,它应该连接到 Arduino,如果门是锁着的,门状态指示器应该变成红色,如果门是开着的,应该变成绿色,你应该看到从 Arduino 传感器测量的温度。

当你点击锁开关时,门应该立即关闭并忽略光和温度。

如何更进一步

我们开发的应用程序可以通过许多方式改进;以下是一些你可以尝试的建议改进:

  • 通过计算门的开闭次数和方向来检查你的宠物是否在家。一对磁开关应该能够检测宠物门的开启方向。

  • 通过在宠物的项圈上附加 RFID 标签来检测你的宠物,以避免其他宠物进入你的房子。

  • 使用滑块(UISliderView)直接从 iOS 设备设置光阈值和温度阈值。

  • 将温度的数字指示替换为更吸引人的图形指示器,如仪表或温度计。

  • 当你的宠物通过宠物门时通知你。

  • 显示摄氏度和华氏度温度。

不同的传感器类型

在结束本章之前,我们想概述一下现有的传感器类型,它们与 Arduino 的通信协议,以及使用它们的优缺点。

传感器可以根据它们提供的信号类型分为两大类(模拟和数字)。模拟传感器通常提供电压,该电压与其测量的量成正比。这个电压必须通过 ADC 转换成数字。Arduino 提供了六个模拟引脚,每个引脚都有自己的 ADC。我们在项目中使用的光敏电阻是一个典型的模拟传感器。相反,数字传感器直接提供测量量的数值表示,可以直接使用。我们在本项目中使用的温度传感器就是一个数字传感器的例子。

模拟传感器更容易使用且更便宜,但它们对电源电压波动和电路中的电气噪声非常敏感。因此,读数会有很大变化,通常需要在代码中实现数字滤波器来平滑读数。

数字传感器提供非常稳定的读数,并且通常更精确。不幸的是,它们使用不同的低级协议与微处理器通信,这些协议更复杂。在大多数情况下,协议的复杂性被针对每种类型传感器的专用软件库所隐藏,但这使得编码更加复杂,库通常会导致更大的内存消耗,这在微处理器上是一个非常宝贵的资源。

最常用的低级协议是串行外设接口SPI)和集成电路间接口I2C)。另一个广泛用于温度传感器的低级协议是 1-Wire,该项目已经采用了这种协议。

这些协议的完整比较超出了本项目的范围,但你可以通过参考以下表格来了解它们。

协议 架构 所需信号 多主 数据速率 全双工
SPI 两个共享的单向数据信号和共享时钟 SCK、MISO、MOSI 和板上每个设备的 CS 可能,但不是标准 1 Mbps
I2C 共享数据信号和共享时钟信号 SDA 和 SCL 100 kbps、400 kbps 和 3.2 Mbps
1-Wire 一个数据信号 数据 15 kbps

摘要

干得好!你已经到达了本章的结尾,并且从头开始构建了一个项目!

你已经构建了硬件,包括电子电路,并为 Arduino 和 iOS 编写了软件。

在 Arduino 上,你学习了如何使用模拟和数字传感器(1-Wire),如何编写读取它们的代码,如何控制伺服电机,以及如何处理与 iOS 设备的通信。

在 iOS 上,你学习了如何编写一个具有简单用户界面的应用程序,以及该应用程序通过蓝牙 4.0 与 Arduino 进行通信。

我们最终讨论了模拟和数字传感器以及与 Arduino 交换数据时最常用的低级通信协议。

在下一章中,我们将构建另一个项目,该项目使用 Wi-Fi 而不是蓝牙来传输数据。在那个项目中,Arduino 将接受不同的命令并对之做出反应。iOS 应用将有一个表格视图,这是 UIKit 提供的最有用的组件之一。

第三章:Wi-Fi 电源插头

Wi-Fi 电源插头是一种设备,通过它可以控制连接到它的电器,以两种方式打开和关闭:

  • 从你的 iOS 设备手动操作

  • 自动设置定时器

例如,你可以在每天下午 6 点开启你的灌溉系统 30 分钟,但如果看到你的草坪变黄,你可以手动开启系统进行额外的浇水。

现在,这种设备在市场上以合理的价格就可以买到,但自己制作一个将让你了解它是如何工作的,并适应你自己的需求。

与第一章中的项目相比,我们将使用 Wi-Fi 作为通信协议。这将允许你在不在家的时候也能访问设备。

本章分为以下部分:

  • Wi-Fi 电源插头要求:我们将简要设定项目要求

  • 硬件:我们将描述项目所需的硬件和电子电路

  • Arduino 代码:我们将为 Arduino 编写代码以控制外部设备并与 iOS 设备通信

  • iOS 代码:我们将为 iOS 设备编写代码

  • 如何从世界任何地方访问你的电源插头

  • 如何更进一步:更多改进项目和学习的想法

Wi-Fi 电源插头要求

我们将构建一个能够:

  • 通过从 iOS 设备接收命令来打开和关闭连接到它的电器

  • 在特定时间打开和关闭电器,持续预定义的时间

伴随的 iOS 应用程序需要手动控制电源插头和管理定时器。

硬件

正如我们在第一章中提到的,Arduino 和 iOS – 平台和集成,我们需要一个 Wi-Fi 屏蔽器(www.arduino.cc/en/Main/ArduinoWiFiShield)和一个格式化为 FAT16 的微 SD 卡(更多详情请查看:www.arduino.cc/en/Reference/SDCardNotes)。SD 卡用于在 Arduino 断电时永久存储激活时间,因此其大小不是那么重要。

额外的电子组件

在这个项目中,我们需要一些额外的组件:

  • 光隔离器 MOC3041

  • 330 Ω电阻,0.5 W

  • 330 Ω电阻,0.25 W

  • 红色 LED

  • 三相可控硅 BTA08-600

三相可控硅在 600 V 下可承受 8 安培均方根电流,在 220 V 下大约是 1700 瓦。如果你有一个更强大的外部设备,你可以使用同一系列的其他三相可控硅型号(例如,BTA16)。

电子电路

下面的图片显示了我们需要的项目电子电路的电原理图:

电子电路

以下图片展示了如何在面包板上安装电路。

小贴士

不要忘记安装 Wi-Fi 盾牌,并将微型 SD 卡插入其中。

电子电路

小贴士

此电路使用电源线(120 V 或 220 V)。触摸任何带电源线电压的部分可能极其危险,甚至可能致命。即使是经验丰富的专业人士也可能会受伤或死亡,所以务必非常小心。这意味着如果你从未操作过电源线电压,你需要在一个熟练的人的监督下进行。

再次提醒,如果你之前没有这样做过,请避免自己操作电源线。你将是对自己、你的亲属和你的物品可能造成的任何损害的唯一责任人。你已经收到警告!

如果你觉得操作电源线不安全,你仍然可以通过仅使用 LED 和电阻替换电源电路来享受这个项目。

为了给外部设备供电,我们使用晶闸管(bit.ly/1MzmIYs),它允许你使用小电流控制交流负载。由于电源插头使用电源线,周围的电压很高(120-220 V),并且可能会烧毁 Arduino。因此,在低电压电路(Arduino)和高电压电路(晶闸管)之间放置了一个光电隔离器(bit.ly/1TV1JFc)。基本上,它是一个 LED 和一个低功率晶闸管在同一小封装中。当 LED 开启时,由于光电效应会产生小电流,并极化低功率晶闸管的栅极,从而切换它。这里的主要点是光电隔离了 LED 和晶闸管。

晶体管和继电器可以替换晶闸管和光电隔离器,但继电器是一个机电装置,容易在短时间内出现故障。

打开和关闭 Arduino 上的光电隔离器 LED 非常简单,因为它连接到一个数字引脚(在我们的例子中是 8 号引脚),并通过digitalWrite(<PIN>, HIGH | LOW)进行控制。与光电隔离器串联的外部 LED 仅用于监控目的。

小贴士

控制更多设备

如果你需要控制更多设备,你可以复制电源电路(光电隔离器和晶闸管)并将其连接到另一个数字引脚。然后你需要调整 Arduino 代码和 iOS 代码。

Arduino 代码

此项目的完整代码可以从这里下载:

www.packtpub.com/books/content/support

为了更好地理解下一段落的解释,你应该在阅读时打开下载的代码。

电源插头需要在不同的时间激活外部设备,然后关闭它。我们将激活称为开启-关闭周期。每个激活都可以用一个方波表示(见以下图表)。

Arduino 代码

每个激活从其开始时间开始,持续长度(在此期间设备处于开启状态),并在周期时间后重复。一次性激活的周期等于 0。

我们将使用此图来理解 Arduino 代码。

应用程序可以管理存储在全局数组activations中的NUMBER_OF_ACTIVATIONS个激活。每个激活是一个新类型,定义如下:

typedef struct  {
  char                name[21];
  unsigned long       startTime;      // seconds since midnight 1/1/1970
  uint16_t       length;         // minutes
  uint16_t       period;         // minutes
} activation;

为了使代码尽可能简单(并且节省闪存空间),我们将整个数组保存到 SD 文件中,即使不是所有的激活都被设置。如果一个激活被设置,那么它的名称就会被设置。

提示

闪存

闪存存储 Arduino 上运行的程序。Arduino UNO 有 32K 的闪存,其中 0.5K 用于引导加载程序。引导加载程序是一段小代码,允许通过 USB 编程 UNO,而不是使用外部在电路编程器(例如,AVR-ISP 或 STK500)。

设置代码

请参考下载的代码,因为设置代码相当简单,不需要详细解释。

因为我们要处理时间,而 Arduino 没有实时时钟,我们需要从网络上的网络时间协议服务器获取当前时间(bit.ly/1NyCVM7);这就是函数askTime所做的工作。请求通过 UPD(bit.ly/1MzmQHb)发送,答案在 2390 端口接收。当接收到数据包时,它通过readTime函数(见loop函数)转换为 Unix 时间。

提示

Unix 时间

Unix 时间或 Posix 时间定义为自 1970 年 1 月 1 日星期四 00:00:00 协调世界时(UTC)以来经过的秒数(bit.ly/1E6LP3m)。

主程序

让我们从简化版的主循环开始:

void loop() {

  WiFiClient client;

  client = server.available();

  if (client) {
    Serial.println(F("iOS Device connected"));

    // Waits for client disconnection
    while (client.connected()) {

      // Waits for data and process them

      while (client.available()) {

      }

    }

    Serial.println(F("iOS Device disconnected"));
  }

  //Serial.println(millis() / 1000 - lastActivation);
  if (millis() / 1000 - lastActivation >= ACTIVATION_CHECK_INTERVAL && !manualMode) {

    lastActivation = millis() / 1000;
    checkActivations();
  }

  delay(50);
}

提示

闪存中的字符串

通常 Arduino 会将静态字符串存储在 RAM 中。由于 RAM 也用于存储变量,我们可以使用 F()符号将静态字符串移动到闪存中。例如,Serial.println("iOS Device disconnected")会浪费 23 个字节的 RAM 来存储字符串"iOS Device disconnected"。相反,写入Serial.println(F("iOS Device disconnected"))将使字符串存储在闪存中。

变量server代表监听连接的 TCP 服务器。如果一个新的客户端连接并且有可读数据,available将返回一个WiFiClient实例(client),可以用来读取数据。当客户端连接时,(函数connected返回 true),我们检查是否有数据可用。函数available返回可读字节数,所以当它返回一个大于 0 的数字时,我们可以读取数据并处理它们。

函数millis返回自板子开启以来经过的毫秒数,以下指令:

if (millis() / 1000 - lastActivation >= ACTIVATION_CHECK_INTERVAL && !manualMode) {

    lastActivation = millis() / 1000;
    checkActivations();
  }

函数checkActivationsACTIVATION_CHECK_INTERVAL秒被调用一次。

checkActivations函数检查每个激活(具有名称),并根据当前时间开启或关闭设备。

如果当前时间在激活开始时间和激活开始时间加上激活长度之间,设备必须开启。

请注意,开始时间是自 1970 年 1 月 1 日起的秒数。另一方面,长度是以分钟为单位的。

如果设备已开启且当前时间大于开始时间加上激活长度,则必须关闭设备。然后激活周期减去一个周期,以便为下一次激活做好准备。

activations[i].startTime += 60 * activations[i].period;

注意

由于checkActivationsACTIVATION_CHECK_INTERVAL秒调用一次,激活可能会比其原始时间延迟ACTIVATION_CHECK_INTERVAL

现在我们可以查看从 iOS 设备接收到的命令(参考下载的代码)。

每个命令以一个字节开始,表示命令代码,其后跟任何附加数据。

如果第一个字节是:

  • 'A':Arduino 使用sendActivations函数将所有激活发送到 iOS 设备。

  • 'U':其后跟一个字节,表示要更新的激活(idx)的索引,以及sizeof(activation)字节,表示要更新的激活。这些字节通过memcpy((uint8_t *)&activations[idx], (uint8_t *)&inBuffer[2], sizeof(activation))复制到现有的激活中。

  • 'D':其后跟一个字节,表示要删除的激活(idx)的索引。

  • 'S':其后跟一个字节,表示设备的新状态,该状态通过digitalWrite(PHOTOISOLATOR_PIN, HIGH)digitalWrite(PHOTOISOLATOR_PIN, LOW)设置。如果用户强制设置设备状态,程序进入手动模式,并忽略激活。

此外,在 iOS 设备连接之前,Arduino 使用sendStatus函数发送其状态,第一个字节是操作模式(手动或自动),第二个字节是设备状态(开启或关闭)。

我们必须查看的最后一个函数是updateActivations。假设我们设置了一个在下午 1 点开始的持续 1 分钟的激活,每 2 分钟重复一次。在下午 12:59,我们关闭 Arduino,然后在下午 2 点重新启动。由于下午 2 点 > 下午 1:02(现在 > startTime + 60 *周期),激活不再启动。updateActivations正好具有将重复激活时间移位的目的,以便它们可以正确触发。

updateActivations随后从setup函数调用,当用户返回到自动操作时,因为手动操作期间不会检查和正确更新激活。

使用millis函数计算当前时间可能在几天后导致显著的误差,因此我们通过 NTP 服务器定期更新时间:

  // Time synchronization
  if (millis() / 1000 > TIME_SYNC_INTERVAL && !updatingTime) {

    updatingTime = true;
    askTime();
  }

TIME_SYNC_INTERVAL的预定义值每 24 小时更新当前时间。

iOS 代码

在本章中,我们将查看 iOS 应用程序,手动打开和关闭电器,并管理自动操作的激活。

该项目的完整代码可以从这里下载:

www.packtpub.com/books/content/support

为了更好地理解下一段落的解释,您应该在阅读时打开下载的代码。

创建 Xcode 项目

第一步是创建一个新的项目。我们将再次使用模板 Tabbed Application,因为它提供了两个视图控制器。在这个项目中,我们还将添加另一个。

让我们创建一个新的 Tabbed Application 项目,就像上一章中做的那样,并将其命名为PowerPlug。然后:

  1. FirstViewController重命名为PowerPlugViewController

  2. SecondViewController重命名为WiFiConnectionViewController

在本章中,我们将使用一个额外的库(CocoaAsyncSocket,见bit.ly/1NGHDHE),它简化了通过 TCP/IP 套接字进行通信。要安装库,您可以按照以下步骤操作:

  1. 打开 URL bit.ly/1NGHDHE

  2. 点击页面右侧的下载 ZIP按钮。文件CocoaAsyncSocket-master.zip将被下载到下载文件夹中。

  3. 解压下载的文件。

  4. 定位GCDAsyncSocket.hGCDAsyncSocket.m

  5. 将这些文件拖动并放到 Xcode 项目中PowerPlug组中。

  6. 确保此选项设置为如果需要则复制项目(见以下截图)并点击下一步创建 Xcode 项目

添加新的视图控制器

我们现在添加所需的额外视图控制器类和额外的视图控制器图形容器,按照以下步骤进行:

  1. 在左侧面板中选择PowerPlug文件夹,然后右键点击它。

  2. 选择新建文件…

  3. 在左侧面板中选择iOS 源,在右侧面板中选择Cocoa Touch 类,然后点击下一步

  4. 在列表框的子类中,选择UITableViewController

  5. 字段中输入ActivationsTableViewController(参考下一张截图)并点击下一步

  6. 在下一个窗口中点击保存创建 Xcode 项目

  7. 在左侧面板中选择Main.storyboard

  8. 打开实用工具面板(视图 | 实用工具 | 显示实用工具)。

  9. 在实用工具面板的搜索字段中输入UIViewController

  10. 将 UIViewController 拖动到故事板中。

  11. 选择刚刚添加的视图控制器。

  12. 打开身份检查器(视图 | 实用工具 | 显示 身份检查器,或在下一张图片中点击红色圆圈图标)。添加新的视图控制器

  13. 列表框中,选择ActivationsTableViewController。现在视图控制器类和 GUI 已经绑定在一起。

  14. 现在我们必须将新的视图控制器添加到主视图控制器的标签栏中。按下控制键,将鼠标指针从标签栏控制器拖动到ActivationsTableViewController,然后释放(见下一张截图)。添加新的视图控制器

  15. 当出现下一张截图所示的小对话框时,选择关系 切换 | 视图控制器添加新的视图控制器

现在新的视图控制器已添加到标签栏(见下一张截图)。

添加新的视图控制器

我们需要新的视图控制器在标签栏中是第二个。要移动它,您可以单击并拖动它到所需的位置。

不要忘记更改工具栏中的文本,分别改为PowerPlugActivationsConfiguration

注意

在下载的代码中,您会发现每个视图控制器可以使用三个图标。

由于我们需要显示表格中元素的具体信息,我们需要在导航控制器中嵌入 ActivationsTableViewController:

  1. 选择ActivationsTableViewController

  2. 选择编辑器 | 嵌入在… | 导航控制器

最终的结构应该类似于下一张截图所示:

添加新的视图控制器

为存储每个激活的信息添加类

要存储每个激活的信息,我们需要一个名为Activation的类。

提示

模型-视图-控制器

从技术角度讲,我们现在正在创建模型-视图-控制器模式的“模型”。您可以在以下位置找到 MVC 模式的简要介绍:apple.co/1hkUDbU 和完整的讨论:apple.co/1EEpNzL

要创建类:

  1. 在左侧面板中选择PowerPlug组,右键单击它,然后选择新建文件…

  2. 在左侧面板中选择,在右侧面板中选择Cocoa Touch 类,然后点击下一步

  3. 子类列表框中选择NSObject

  4. 类文本字段中输入Activation(见下一图)并点击下一步

  5. 点击保存为存储每个激活的信息添加类

  6. 打开Activation.h文件并输入以下代码:

    @interface Activation : NSObject
    
    @property (nonatomic,strong) NSString   *name;
    @property (nonatomic,strong) NSDate     *start;
    @property                    NSInteger  length;   // minutes
    @property                    NSInteger  period;   // minutes
    
    @end
    

项目现在已准备好进行下一步,我们可以开始处理视图控制器。

设计 WiFiConnectionViewController 的应用程序用户界面

正如我们在上一个项目中做的那样,我们从视图控制器开始,这允许我们输入连接信息。

它由两个标签、两个输入 IP 地址的字段、分配给 Arduino 的 IP 端口和一个更新连接信息的按钮组成。

请参考以下截图来设计它:

为 WiFiConnectionViewController 设计应用程序用户界面

由于这两个字段只接受数字和句点,我们可以设置一个合适的键盘来帮助用户。为此:

  1. 选择一个字段。

  2. 打开身份检查器(视图 | 实用工具 | 显示 身份检查器)。

  3. 键盘类型列表框中选择数字和标点

现在,在WiFiConnectionViewController.m中将用户界面组件链接到以下代码:

@interface WiFiConnectionViewController ()

@property (strong, nonatomic) IBOutlet UITextField *ipField;
@property (strong, nonatomic) IBOutlet UITextField *portField;

@end

and:

- (IBAction)updateConnectionInformation:(UIButton *)sender {

}

不要忘记将UITextFields代理链接到视图控制器。

请参考下载的代码以获取更多详细信息,并将您的结果与提供的应用程序进行比较。

为 PowerPlugViewController 设计应用程序用户界面

此视图控制器管理与电源插头的手动操作,该插头能够手动开关连接设备的电源。

此视图控制器的最终布局如图所示:

为 PowerPlugViewController 设计应用程序用户界面

编号为1的组件是一个UIImageView,其目的是显示连接的电器是开启还是关闭。要添加此组件,只需拖放并选择要显示的图像(在下载的项目中可以找到的LEDdisabled.png),使用属性检查器。图像视图的大小为 60 × 60,可以通过添加适当的布局约束来设置。

编号为2的组件是一个开关按钮(UISwitch),用于手动开关电器。我们可以像处理其他组件一样将此组件添加到容器中。

编号为3的组件是一个按钮(UIButton),用于在连接丢失时重新连接到 Arduino;编号为4的组件是一个UIView,通过其颜色(浅灰色:未连接,绿色:已连接)显示 Arduino 是否已连接。

一旦添加了组件和所需的自动布局约束,您可以将组件链接到以下代码:

@interface PowerPlugViewController ()

@property (strong, nonatomic) IBOutlet UIImageView  *applianceStatus;
@property (strong, nonatomic) IBOutlet UIView       *connectionStatus;
@property (strong, nonatomic) IBOutlet UISwitch     *manualOperationButton;

@end

为 ActivationsTableViewController 设计应用程序用户界面

ActivationsTableViewController 视图控制器以表格形式显示所有现有的激活。

首先,我们必须添加一个类来存储表格中每一行单元格的信息。要添加此类,请遵循前几章中用于从UITableViewCell派生并调用类ActivationTableViewCell的相同步骤(见下一张截图)。

为 ActivationsTableViewController 设计应用程序用户界面

ActivationTableViewCell.h文件中,按照以下方式更改代码:

@interface ActivationTableViewCell : UITableViewCell

@property (strong, nonatomic) IBOutlet UILabel *name;
@property (strong, nonatomic) IBOutlet UILabel *start;
@property (strong, nonatomic) IBOutlet UILabel *end;
@property (strong, nonatomic) IBOutlet UILabel *period;

@end

现在我们可以创建一个自定义单元格来显示电源插头的每个激活信息,并将其与 ActivationTableViewCell 链接。我们将只展示主要步骤,但您可以参考下载的代码以获取所有详细信息。

  1. Main.storyboard中,选择ActivationsTableViewController,然后选择其中的原型单元格

  2. 打开身份检查器,并在列表框中选择激活单元格视图控制器

  3. 选择属性检查器。

  4. 标识符字段中输入activationCell

  5. 选择大小检查器,并在行高字段中选择 66。

  6. 现在,您可以输入标签(UILabel)以显示每个激活的不同信息,更改颜色和字体大小,并添加以单元格原型结束的布局约束,如下一个屏幕截图所示:为 ActivationsTableViewController 设计应用程序用户界面

现在我们必须将单元格组件链接到类 ActivationTableViewCell。

  1. 前往编辑器 | 显示文档大纲(您也可以使用下一个屏幕截图中圈红的图标打开此面板)。

  2. 定位到激活表格视图控制器 场景并展开它(见下一个屏幕截图)。为 ActivationsTableViewController 设计应用程序用户界面

  3. 选择激活单元并右键点击它。

  4. 按住控制键,将类的每个属性拖到相关的图形组件上(见下一个屏幕截图)。为 ActivationsTableViewController 设计应用程序用户界面

我们需要完成最后一步:创建输入和编辑每个激活的视图控制器(ActivationViewController)。

我们开始添加一个从 UIViewController 继承的新类,称为ActivationViewController,然后我们将新的视图控制器拖放到Main.Storyboard中,并在身份检查器中将它的类更改为ActivationViewController

此视图控制器使用了两个新的组件:

  • UIDatePicker:它允许用户选择一个日期。在我们的情况下,是激活必须开始的日期。

  • UISegmentControl:它允许用户轻松选择激活的长度以及激活必须启动的频率。

控制器的最终布局如下截图所示:

为 ActivationsTableViewController 设计应用程序用户界面

组件可以像我们已经学过的那样拖入容器中,我们可以设置它们的布局约束,就像在其他视图控制器中做的那样。组件通常与代码链接:

@property (nonatomic,weak) Activation *activation;

@property (strong, nonatomic) IBOutlet UITextField          *nameField;
@property (strong, nonatomic) IBOutlet UIDatePicker         *date;
@property (strong, nonatomic) IBOutlet UITextField          *length;
@property (strong, nonatomic) IBOutlet UISegmentedControl   *lengthScale;
@property (strong, nonatomic) IBOutlet UITextField          *period;
@property (strong, nonatomic) IBOutlet UISegmentedControl   *periodScale;

@end

为了编辑激活,我们需要在用户点击表格的行时显示 ActivationViewController。为此,我们必须从单元格创建一个 segue 到具有以下步骤的视图控制器:

  1. 按住控制键,将鼠标指针从单元格拖到 ActivationViewController(见下一个屏幕截图)。为 ActivationsTableViewController 设计应用程序用户界面

  2. 当出现黑色对话框时,选择显示为 ActivationsTableViewController 设计应用程序用户界面

您可以在下载的代码中找到有关 ActivationViewController 的所有详细信息。

这个 ActivationsTableViewController 最后完成了。在我们开始编写视图控制器的代码之前,让我们先喘口气,喝杯咖啡。这是一段漫长而复杂的过程,我们实际上应该喝杯咖啡(或者如果你更喜欢的话,喝茶!)。

为 WiFiConnectionViewController 编写代码

此视图控制器的作用是确保用户输入分配给 Arduino 的 IP 地址和 IP 端口以连接到它。

在这个项目中,我们将把信息存储在文件中,而不是像我们在 Pet Door Locker 中做的那样存储在用户偏好设置中。

当点击更新按钮时,执行以下代码:

- (IBAction)updateConnectionInformation:(UIButton *)sender {

    if (![self validateIpAddress:_ipField.text]) {

        …

        return;
    }

    if ([_portField.text integerValue]<0 || [_portField.text integerValue]>65535) {

        …

        return;
    }

    NSMutableDictionary *connectionInformation = [[NSMutableDictionary alloc] init];

    [connectionInformation setValue:_ipField.text forKey:@"IP"];
    [connectionInformation setValue:_portField.text forKey:@"PORT"];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"connection.plist"];

    [connectionInformation writeToFile:path atomically:YES];
}

两个字段的值存储在 NSMutableDictionary (connectionInformation) 中,并存储在一个名为 writeToFile 的文件中。此文件保存在名为 documentsDirectory 的目录中,该目录仅对应用程序可访问。

在保存信息之前,我们需要检查 IP 地址是否格式正确,端口是否在允许的范围内(有关所有详细信息,请参阅下载的代码)。

由于我们设置了文本字段的代理属性,每次用户完成字段的编辑时,以下方法之一将被调用,我们可以使用 resignFirstResponder 来隐藏键盘。

- (void)textFieldDidEndEditing:(UITextField *)textField {

    [textField resignFirstResponder];
}

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

    [textField resignFirstResponder];

    return YES;
}

当我们点击文本字段 Period 时,键盘会覆盖它(至少在像 iPhone 这样的较小设备上)。为了避免这个问题,我们可以使用几个 UITextView 的代理方法将整个视图向上移动。当我们点击字段时,会调用 textFieldDidBeginEditing 方法,当我们退出字段或点击回车时,也会调用 textFieldDidBeginEditing 方法。

textFieldDidBeginEditing 方法中,我们可以翻译字段,通过将转换变换分配给字段来实现:

self.view.transform = CGAffineTransformMakeTranslation(0, -100);

然后我们以这两个附加方法结束:

- (void)textFieldDidBeginEditing:(UITextField *)textField {

    if ([textField isEqual:_period]) {

        self.view.transform = CGAffineTransformMakeTranslation(0, -100);
    }

}

- (void)textFieldDidEndEditing:(UITextField *)textField {

    if ([textField isEqual:_period]) {

        self.view.transform = CGAffineTransformMakeTranslation(0, 0);
    }
}

每次显示此视图控制器时,两个字段都会用现有值填充。

- (void)viewDidLoad {
    [super viewDidLoad];

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"connection.plist"];

    NSDictionary *connectionInformation = [NSDictionary dictionaryWithContentsOfFile:path];

    if (connectionInformation != nil) {
        _ipField.text = [connectionInformation objectForKey:@"IP"];
        _portField.text = [connectionInformation objectForKey:@"PORT"];
    }

}

为 AppDelegate 编写代码

在这个项目中,Arduino 的连接和通信由 AppDelegate 管理。

为了这个目的,我们需要一些属性:

@interface AppDelegate ()

@property (strong, nonatomic) GCDAsyncSocket                    *socket;
@property (strong, nonatomic) NSMutableArray                    *activations;

@property (strong, nonatomic) PowerPlugViewController           *powerPlugViewController;
@property (strong, nonatomic) ActivationsTableViewController    *activationsViewController;

@end

socket 属性(由我们添加的库提供)是一个用于从 Arduino 发送和接收数据的通道。数组 activations 存储了用户创建的所有激活。

当应用启动或进入前台时,它开始与 Arduino 建立连接:

-(void)connect {

    NSError *err = nil;

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *path = [documentsDirectory stringByAppendingPathComponent:@"connection.plist"];

    NSDictionary *connectionInformation = [NSDictionary dictionaryWithContentsOfFile:path];

    if (![_socket connectToHost:[connectionInformation objectForKey:@"IP"]
                         onPort:[[connectionInformation objectForKey:@"PORT"] integerValue]
                          error:&err]) {
        NSLog(@"Connection Failed %@", [err localizedDescription]);

        return;
    }

    [_socket readDataWithTimeout:5 tag:0];
}

如果与 Arduino 的连接成功,将调用代理方法 didConnectToHost

readDataWithTimeout 方法允许您从对端接收数据:当有数据可用时,调用 didReadData 方法。

由于我们设置了超时(5 秒),如果在这么长时间内没有接收到数据,则调用 socketDidDisconnect 方法。

让我们看看每个方法。

- (void)socket:(GCDAsyncSocket *)sender didConnectToHost:(NSString *)host port:(UInt16)port {

    [_powerPlugViewController arduinoConnected];
}

这里,我们只是通知 PowerPlugViewController 实例连接已成功。

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error {

    [_powerPlugViewController arduinoDisconnected];
    _activations = nil;
}

在这里,我们只是通知 PowerPlugViewController 实例 Arduino 已断开连接,并且我们释放了我们正在使用的激活。

从 Arduino 我们可以接收两种类型的消息:

  • 状态:它以字符 'S' 开头,后面跟着两个字节:

    • 第一个字节为 1 如果电器是手动开启的,否则为 0

    • 第二个字节是电器的状态:如果关闭则为 0,如果开启则为 1

  • 激活:它以字符 'A' 开头,后面跟着 ACTIVATION_SIZE_ON_ARDUINO * NUMBER_OF_ACTIVATIONS 个字节,这些字节代表激活

在以下方法中接收来自 Arduino 的消息,并按照之前的规则进行处理。

- (void)socket:(GCDAsyncSocket *)sender didReadData:(NSData *)data withTag:(long)tag {

    NSLog(@"Bytes received %lu",(unsigned long)[data length]);

    if ([data length]<3) {
        return;
    }

    NSString* answerType = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 1)]
                                             encoding:NSASCIIStringEncoding];

    // S<manual><appliance status>
    // A<activations>

    if ([answerType isEqualToString:@"S"]) {

        BOOL manual;
        BOOL status;

        [[data subdataWithRange:NSMakeRange(1, 1)] getBytes: &manual length: 1];
        [[data subdataWithRange:NSMakeRange(2, 1)] getBytes: &status length: 1];

        [_powerPlugViewController updateStatus:manual applianceStatus:status];
    }

    if ([answerType isEqualToString:@"A"]) {

        if (data.length < (ACTIVATION_SIZE_ON_ARDUINO * NUMBER_OF_ACTIVATIONS + 1)) {

            NSLog(@"Error reading data");

            return;
        }

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

            NSData *nameData = [data subdataWithRange:NSMakeRange(1+ACTIVATION_SIZE_ON_ARDUINO*i, 21)];
            NSString *name = [NSString stringWithCString:nameData.bytes encoding:NSASCIIStringEncoding];

            if (name.length>0) {

                NSData *startData = [data subdataWithRange:NSMakeRange(1+ACTIVATION_SIZE_ON_ARDUINO*i+21, 4)];

                NSInteger start=0;
                [startData getBytes: &start length: 4];

                NSData *lengthData = [data subdataWithRange:NSMakeRange(1+ACTIVATION_SIZE_ON_ARDUINO*i+21+4, 2)];

                NSInteger length=0;
                [lengthData getBytes: &length length: 2];

                NSData *periodData = [data subdataWithRange:NSMakeRange(1+ACTIVATION_SIZE_ON_ARDUINO*i+21+4+2, 2)];

                NSInteger period=0;
                [periodData getBytes: &period length: 2];

                Activation *activation = [[Activation alloc] init];
                activation.name = name;
                activation.start = [NSDate dateWithTimeIntervalSince1970:start];
                activation.length = length;
                activation.period = period;

                [_activations addObject:activation];
            }
        }

        [_activationsViewController dataReceived];
    }

    [sender readDataWithTimeout:5 tag:0];
}

如果收到状态消息,PowerPlugViewController 实例会被通知:

 [_powerPlugViewController updateStatus:manual applianceStatus:status];

如果收到激活消息,每个激活都会添加到 _activations 中:

[_activations addObject:activation];

最后,ActivationsViewController 的实例会被通知所有激活都可用:

[_activationsViewController dataReceived];

AppDelegate 也实现了 PowerPlugViewController 和 ActivationsViewController 视图控制器的代理方法。

对于 ActivationsViewController,实现的方法是 getActivationsupdateActivationOfIndexdeleteActivationOfIndex

当 ActivationsViewController 需要显示配置的激活列表时,会调用 getActivations 方法。

-(NSMutableArray *)getActivations {

    if (_activations == nil) {

        _activations = [[NSMutableArray alloc] init];

        NSString *message = @"A";
        [_socket writeData:[message dataUsingEncoding:NSASCIIStringEncoding] withTimeout:-1 tag:0];

        [_activationsViewController dataRequested];

        return _activations;
    }

    return _activations;
}

如果 _activations 为空,则向 Arduino 发送一条消息,它将响应所有激活的列表。消息非常简单:只有一个字节,值为 'A'(激活)。

消息实际上是通过调用 Arduino 发送的:

[_socket writeData:[message dataUsingEncoding:NSASCIIStringEncoding] withTimeout:-1 tag:0];

当激活被更新(或添加)时,ActivationsViewController 会调用 updateActivationOfIndex 方法。当需要删除激活时,ActivationsViewController 会调用 deleteActivationOfIndex 方法。

对于这两个方法的完整实现,请查看下载的代码。

为 PowerPlugViewController 实现的方法是 sendSwitchCommandreconnect,它们非常简单,你可以直接从下载的代码中理解它们。

编写 PowerPlugViewController 的代码

这个视图控制器管理连接到电源插座的电器的手动操作。

首先,我们需要声明这个视图控制器响应的代理方法和消息。

@protocol PowerPlugViewControllerDelegate <NSObject>

-(void)sendSwitchCommand:(BOOL)on;
-(void)reconnect;

@end

@interface PowerPlugViewController : UIViewController 

-(void)arduinoConnected;
-(void)arduinoDisconnected;
-(void)updateStatus:(BOOL)manual applianceStatus:(BOOL)applianceStatus;

@property (strong, nonatomic) id<PowerPlugViewControllerDelegate> delegate;

@end

这个视图控制器非常简单,其方法相当自解释和直观。我们只需要看看 updateStatus 方法:

-(void)updateStatus:(BOOL)manual applianceStatus:(BOOL)applianceStatus {

    _manualOperationButton.on = manual;

    if (applianceStatus)
        _applianceStatus.image = [UIImage imageNamed:@"LEDon.png"];
    else
        _applianceStatus.image = [UIImage imageNamed:@"LEDoff.png"];
}

applianceStatus 为真时,电器被开启,我们将图像 LEDon.png 设置到 imageView (_applianceStatus) 中。这个图像模拟了一个开启的 LED。当电器关闭时,显示的图像是 LEDoff.png,模拟了一个关闭的 LED。

编写 ActivationsTableViewController 的代码

此视图控制器管理激活列表,允许用户添加、删除和更新每个激活。它基于 UITableView 组件,这是 UIKit 中最常用且功能强大的组件之一。

让我们从ActivationsTableViewController.h开始:

@protocol ActivationsTableViewControllerDelegate <NSObject>

-(NSMutableArray *)getActivations;
-(void)updateActivationOfIndex:(uint8_t)index;
-(void)deleteActivationOfIndex:(uint8_t)index;

@end

@interface ActivationsTableViewController : UITableViewController <ActivationViewControllerDelegate>

@property (strong, nonatomic) id<ActivationsTableViewControllerDelegate> delegate;

-(void)dataRequested;
-(void)dataReceived;

@end

到目前为止,你应该能够识别出视图控制器响应的代理协议和消息。

让我们从 UITableView 代理方法的实现开始。第一个是numberOfRowsInSection,当表格需要知道它需要显示多少项时会被调用:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [_delegate getActivations].count;
}

这不需要很多解释。cellForRowAtIndexPath方法由 UIViewTable 为每个要显示的行调用:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    ActivationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"activationCell"];
    if (cell == nil) {
        cell = [[ActivationTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"activationCell"];
    }

    Activation *activation = [_delegate getActivations][indexPath.row];

    cell.name.text = activation.name;

    NSDateFormatter *dateFormatter =  [[NSDateFormatter alloc] init];
    [dateFormatter setDateStyle:NSDateFormatterShortStyle];
    [dateFormatter setTimeStyle:NSDateFormatterMediumStyle];

    NSDate *endDate = [activation.start dateByAddingTimeInterval:60*activation.length];
    cell.start.text = [dateFormatter stringFromDate:activation.start];
    cell.end.text = [dateFormatter stringFromDate:endDate];

    ….

    return cell;
}

调用方法[tableView dequeueReusableCellWithIdentifier:@"activationCell"]我们得到一个单元格,并填充一个激活的值。

如果上一个函数返回 nil,则没有可用的单元格,必须创建一个:

cell = [[ActivationTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"activationCell"];

查看下载的代码以查看完整的方法实现。

当从 UITableView 中删除一行时,会调用以下方法:

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {

        [[_delegate getActivations] removeObjectAtIndex:indexPath.row];

        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];

        [_delegate deleteActivationOfIndex:indexPath.row];
    }
}

方法deleteRowsAtIndexPaths从表中删除行,而代理方法deleteActivationOfIndex创建一个消息,然后发送到 Arduino 以删除一个激活。

在设计界面时,我们从一个表格单元格到 ActivationViewController 创建了一个转场,以便当用户点击表格行时,ActivationViewController 会出现。

在启动 ActivationViewController 之前,会调用prepareForSegue方法:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

    ActivationViewController *activationViewController = (ActivationViewController *)[segue destinationViewController];
    activationViewController.delegate = self;

    NSIndexPath *p = [self.tableView indexPathForSelectedRow];

    NSMutableArray *activations = [_delegate getActivations];
    if (activations==nil)
        return;

    _selectedActivationIndex = p.row;

    [self.tableView deselectRowAtIndexPath:p animated:NO];
}

在这里,我们设置 ActivationViewController 的代理属性,并存储所选行的索引。

提示

转场标识符

对于从视图控制器开始的每个转场,都会调用prepareForSegue方法。通常,不同的目标视图控制器需要不同的初始化代码。在 Interface Builder 中,您可以设置每个转场的标识符,并使用这些说明来区分转场:

if ([segue.identifier isEqualToString:"<identifier>"]) {
….
}

ActivationViewController 需要两个代理方法,这些方法不需要任何解释:

第一项是getActivation

-(Activation *)getActivation {

    return [_delegate getActivations][_selectedActivationIndex];
}

第二个是update

-(void)update {

    [self.delegate updateActivationOfIndex:_selectedActivationIndex];
    [self.tableView reloadData];
}

我们只是指出,[self.tableView reloadData]强制表格视图重新加载数据,从调用numberOfRowsInSection开始,然后为每一行调用cellForRowAtIndexPath

为 ActivationTableViewController 编写代码

此视图控制器除了获取用户输入的值和更新所选激活外,没有做更多的事情。

要了解详细信息,请参阅书中提供的代码。

测试和调整

当您完成应用程序后,您可以按照以下步骤测试系统:

  1. 在 Arduino 草图中将 IP 信息(IP、网关等)和网络信息(SSID、密码)更改以适应您自己的网络配置。您可能需要访问路由器配置页面以获取此信息。

  2. 将草图上传到 Arduino。

  3. 检查 Arduino 控制台是否有任何错误信息。

  4. 将驱动电路连接到电源线和外部设备(一盏灯就可以完成这项工作)。请遵循所有必要的安全措施,以避免任何电击。

  5. 在你的设备或模拟器上运行 iOS 应用程序。

  6. 点击配置并输入你在 Arduino 草图设置的 IP 地址和端口。

  7. 点击电源插座,连接并开关按钮。你应该看到外部设备相应地打开和关闭。然后关闭你的设备。

  8. 点击激活并输入一个激活码。你应该看到你的设备会根据你输入的时间和值自动打开和关闭。

如果你无法从 NTP 服务器获取时间,你可以尝试更改地址。要找到新的地址,你可以查看链接tf.nist.gov/tf-cgi/servers.cgi或向地址time.nist.gov发送ping并使用返回的地址。

如何从世界任何地方访问电源插座

在本节中,你将学习如何从家庭网络外部访问你的 Arduino 板的基础知识。换句话说,你将能够通过移动网络使用 iOS 应用程序访问你家庭路由器/防火墙后面的电源插座。

注意

本节仅提供参考,因为市场上有很多路由器,网络配置也很多,实际上不可能提供完整的指南。无论如何,通过这个简要概述,你应该能够配置自己的设备。

端口转发

下一个图片显示了典型家庭网络的配置。

端口转发

假设你的 IP 地址如下:

  • 路由器 WAN IP:82.61.147.56

  • 路由器 LAN IP:192.168.1.1

  • Arduino 板 IP:192.168.1.4

通常,你的 Arduino 板在内部网络中的 IP 地址对外部网络不可见,至少除非你明确配置你的路由器。这种配置称为 IP 端口转发。

基本上,此配置指示路由器将它在特定端口接收到的流量转发到另一个 IP 地址上的特定端口。

话虽如此,你的端口转发配置必须类似于以下这样:

(82.61.147.56, 230) --> (192.168.1.4, 230)

访问你的路由器配置页面(通常通过浏览器),你应该能够获取到你的互联网服务提供商分配给你的路由器的当前 IP 地址并配置端口转发。阅读你的路由器手册以了解如何设置此配置。

一旦你配置了端口转发,你可以使用以下方式访问 Arduino 板:

  • IP:82.61.147.56(或者更好的是,分配给你自己路由器的实际 IP 地址)

  • 端口:230

小贴士

请注意安全!

一旦你启用了端口转发,任何人都可以访问它。这意味着任何人都可以轻松地访问你的电源插座并控制它。我们应该实现一个协议,该协议验证 iOS 设备,并加密所有交换的消息。不幸的是,Arduino 没有足够的处理能力和内存来完成这项任务。如果你想保持 iOS 设备和 Arduino 之间的通信安全,你必须设置一个虚拟专用网络(bit.ly/1ENoe3o)。有许多路由器提供 VPN,iOS 原生支持它。

动态 DNS

通常,每次你的路由器连接到互联网时,它都会获得一个不同的公共 IP 地址。因此,每次你的路由器重启时,你都需要更改 Arduino Manager 的 IP 地址。这并不实用。

许多动态域名服务DDNS)可以将相同的名称动态地关联到你的路由器 IP 地址,即使地址发生变化。大多数都是免费的,其中包括:NoIP、yDNS 和 FreeDNS。

大多数路由器都配备了可用的 DDNS 客户端,易于配置;否则,你可能需要在连接到你的网络的计算机上安装简单的软件。

请访问服务提供商的网站进行注册,获取安装和配置步骤。此外,检查你的路由器以确定 DDNS 服务的可用性。

一旦你配置了你选择的动态 DNS 服务,假设你选择了一个像powerplug.something.com这样的域名,你可以通过以下方式访问 Arduino:

  • IP: powerplug.something.com

  • 端口: 230

并且你不需要在重启路由器时更改它们。

如何更进一步

我们开发的项目可以通过许多方式改进,以下是一些你可以尝试的改进:

  • 使用更多的 Arduino 引脚和更多的驱动电路控制更多的外部设备。

  • 检查并向用户报告不同激活之间的任何冲突。

摘要

在本章中,你构建了一个设备,允许你手动或自动控制外部设备(如灯、洗衣机、咖啡壶等),按你的喜好开启和关闭,或指定时间开启和关闭。

你已经学会了不使用继电器制作电源电路,并从 Arduino 管理它。

在 Arduino 上,你已经学会了编写程序,读取和写入 SD 卡,使用 Wi-Fi 盾牌与 iOS 设备和外部服务(获取当前时间)进行通信,以及管理外部电源电路以控制外部设备。

在 iOS 上,你已经学会了如何使用 UITableView 设计一个中等复杂的应用程序的用户界面,它是与用户交互最常用的组件之一。你现在能够理解 MVC 模式并利用它在自己的程序中。此外,你已经学会了通过 TCP 套接字处理 TCP/IP 通信。

下一章将介绍如何使用 iOS 设备提供的硬件特性(加速度计)来控制漫游机器人,编写 Arduino 和 iOS 代码。

第四章:iOS 导航车

在本章中,我们将构建可以用来控制导航车机器人的软件。这有什么新意?孩子们已经玩遥控玩具多年了,有时我们仍然会玩这样的玩具!

然而,我们将构建一个可以通过以下三种不同方式由 iOS 设备控制的机器人:

  • 通过手动命令:iOS 控制上的两个滑块:导航车的转向和油门

  • 通过 iOS 设备在空间中的移动:通过左右移动 iOS 设备,你可以控制转向,通过前后移动 iOS 设备,你可以控制油门

  • 通过使用语音命令:通过说出几个语音命令,iOS 设备可以控制转向和油门

很酷,不是吗?让我们迅速开始这段令人着迷的机器人之旅,同时不忘这里学到的技术可以用于许多其他项目。

本章分为以下部分:

  • iOS 导航车需求:我们将简要设定项目需求

  • 硬件:我们将描述项目所需的硬件和电子电路

  • Arduino 代码:我们将编写 Arduino 代码来控制外部设备并与 iOS 设备通信

  • iOS 代码:我们将为 iOS 设备编写代码

  • 如何更进一步:将提供更多想法来改进项目并学习更多

iOS 导航车需求

我们将开发 Arduino 和 iOS 软件来完成以下工作:

  • 通过以下方式从 iOS 设备控制由两个电机驱动的导航车的方向和速度:

    • 使用手动命令

    • 在空间中移动 iOS 设备

    • 使用语音命令

  • 避免障碍

  • 测量斜率和倾斜角度,以便机器人不会翻倒

  • 通过使用蓝牙 BLE 在导航车和 iOS 应用程序之间来回传输命令和信息。

硬件

我们将假设你已经构建了一个像以下网站展示的导航车机器人:

你也可以购买两个电机和两个轮子(更多信息,请访问 bit.ly/1i7oWTGbit.ly/1KeHtdf),并用金属或木材自己制作底盘。

你可以在 eBay (www.ebay.com) 以可承受的价格找到你可能需要的所有东西。

额外的电子组件

在这个项目中,我们需要一些额外的组件:

什么是加速度计?

在本节中,我们将简要讨论加速度计是什么以及如何使用它。

加速度计是一种测量其三个轴上加速度的设备,并返回三个与加速度成正比的电压信号。

加速度计测量的加速度给我们提供了关于加速度计相对于地球重力场的倾斜信息。

要了解加速度与加速度计倾斜的关系,让我们检查以下图示的情况,我们只考虑两个轴(zx):

什么是加速度计?

在图 1 中,加速度计的 z 轴与重力(g)平行,而 x 轴与重力正交。因此,沿 x 轴的加速度为零。在图 2 中,加速度计以角度 α 旋转。沿加速度计 x 轴的加速度是 gx,gx 是加速度计返回的沿 x 轴的加速度。总之,通过旋转加速度计,我们得到沿加速度计 x 轴的加速度,它与旋转本身成正比。同样,对于指向页面内部的 y 轴也是如此。

在这个项目中,我们利用这种特性将加速度计安装到探测车上来测量其在横向和纵向轴上的倾斜。同时,我们使用 iOS 设备的加速度计来测量设备倾斜,以控制探测车的方向和速度。

电子电路

当直流电机通电时,它会朝一个方向旋转,而当电压极性反转时,它会朝相反的方向旋转。

以下电路图展示了如何使用开关来给直流电机供电,以反转电压极性和旋转方向。在以下电路图中,电机按顺时针方向旋转:

电子电路

而在第二种情况下,它按逆时针方向旋转:

电子电路

晶体管可以替换开关,通过电子信号来控制方向。用晶体管替换四个开关的电路称为 H 桥 (bit.ly/1JBmdrE)。

基本上,H 桥是一种电路,允许你通过两个输入信号来控制直流电机的方向。当第一个信号为高,第二个信号为低时,电机朝一个方向运行;当第一个信号为低,第二个信号为高时,电机朝相反方向运行。要控制电机的速度,我们还需要一个 PWM 信号(www.arduino.cc/en/Tutorial/PWM)。TB6612FBG 电机驱动器包含两个 H 桥电路,可以为两个电机提供高达 1A 的电源。

以下表格,源自 TB6612FBG 数据表,描述了如何使用可用的输入信号来控制电机:

输入 输出
IN1 IN2
H H
L H
L H
H L
L H
L L
H/L H/L

要使电机朝一个方向运行,我们必须将 IN1 置低,IN2 置高。PWM 信号控制电机速度。要使电机朝相反方向运行,我们必须将 IN1 置高,IN2 置低,PWM 信号仍然控制电机的速度。

PWM 信号可以通过 Arduino 的analogWrite函数生成。

以下电子电路图是我们项目所需的:

电子电路

如要求,我们需要一个具有模拟输出的距离传感器;随着到前方物体的距离减小,输出电压降低。输出电压与距离之间的关系不是线性的(参见通过访问www.sparkfun.com/datasheets/Components/GP2Y0A21YK.pdf获取的数据表),但在感兴趣的范围内可以线性化。在我们的应用中,我们只需要在漫游车非常接近障碍物时停止它。因此,我们不太关心实际距离。红色 LED 指示障碍物非常接近漫游车。

ADXL345 是一款 3 轴加速度计。它测量其三个轴上的加速度(单位为[m/s²])。当漫游车完全静止且水平时,x 轴上的加速度为 0 m/s²,y 轴上的加速度为 0 m/s²,z 轴上的加速度为 9.8 m/s²,这是由于重力作用。当漫游车未水平时,沿轴读取的值不同。当测量的值超过阈值时,我们知道漫游车将要翻倒。

下图显示了如何在面包板上安装电路:

电子电路

你可以看到我们使用了两个不同的电源,一个用于 Arduino 和电子元件,另一个用于电机。这样做的原因如下:

  1. 电机可能需要与电子元件不同的电压。电机电源可以有不同的电压,高达 15V,这是 TB6612FBG 芯片所需的。

  2. 电机会产生大量的电噪声,可能会干扰电子设备。在这个配置中,电子设备和电机是电隔离的。

  3. 机器人可以运行更长的时间,特别是使用可充电电池。

小贴士

电机布线

如果一个电机以错误的方向旋转,你必须反转其电线。

如何使机器人转向

当两个电机以相同的速度旋转时,机器人直线行驶。要使其向右转,降低右轮的速度。我们降低的速度越多,机器人转得越厉害。这与使机器人向左转的方法完全相同。

也就是说,Arduino 代码必须管理两个电机的旋转速度,以控制电机的方向。

如何安装加速度计

确保加速度计板安装正确位置非常重要,以便正确读取加速度并与下一章中显示的 Arduino 代码一起工作。加速度计板必须用螺母和螺栓紧紧固定在机器人底盘上(最好使用两个螺母和螺栓,每个侧面一个)。

方案如下所示:

如何安装加速度计

Arduino 代码

该项目的完整代码可以从www.packtpub.com/books/content/support下载。

为了更好地理解下几段中的解释,请在阅读时打开下载的代码。

在继续之前,我们需要从 Adafruit 安装以下额外的库:

  • Adafruit ADXL345:这是用来从 ADXL345 加速度计获取测量数据的

  • Adafruit Unified Sensor:这是一个通用的库,需要从上一个库中获取

要将库安装到 Arduino IDE 中,请打开菜单 草图 | 包含库 | 管理库 …。有关更多信息,请参阅第二章, 蓝牙宠物门锁

设置代码

请参考下载的代码,因为设置代码相当简单,不需要详细解释。

请注意,currentSpeedleftSpeedrightSpeed 分别是:机器人的当前速度、用于使机器人向左转的左轮速度降低,以及用于使机器人向右转的右轮速度降低。它们显然都在设置函数中设置为零。

goingForward 变量表示机器人是向前还是向后移动,它最初被设置为是(向前)。

最后几行初始化加速度计,这是库所要求的。xOffsetyOffset变量是关于在漫游车静止和平面时调整加速度计读数的。我们将在测试和调整部分讨论它们。

电机控制函数

在解释主代码之前,我们将查看以下电机控制函数:

  • forward: 这配置电机控制,使漫游车向前移动

  • backward: 这配置电机控制,使漫游车向后移动

  • brake: 这使漫游车停止

  • throttle: 这控制着漫游车的速度和方向

从之前的 TB6612FBG 表格(第 2 行)来看,为了使电机前进,我们需要将 IN1 设置为低,IN2 设置为高。这正是forward函数对两个电机所做的:

void forward() {

  digitalWrite(STBY, HIGH);

  digitalWrite(MR_I1, LOW);
  digitalWrite(MR_I2, HIGH);

  digitalWrite(ML_I1, LOW);
  digitalWrite(ML_I2, HIGH);
}

backward函数类似。从之前的表格第 3 行来看,我们需要将 IN1 设置为高,IN2 设置为低以改变方向,如下所示:

void backward() {

  digitalWrite(STBY, HIGH);

  digitalWrite(MR_I1, HIGH);
  digitalWrite(MR_I2, LOW);

  digitalWrite(ML_I1, HIGH);
  digitalWrite(ML_I2, LOW);
}

在这两个函数中,我们设置STBYHIGH,以防我们之前通过将STBY设置为LOW来停止了漫游车。

再次,通过使用表格(第 5 行),使用以下函数来停止漫游车:

void brake(void) {

  digitalWrite(MR_I1, LOW);
  digitalWrite(MR_I2, LOW);

  digitalWrite(ML_I1, LOW);
  digitalWrite(ML_I2, LOW);

  digitalWrite(STBY, HIGH);
}

throttle函数非常重要,因为它控制着漫游车的速度和方向:

void throttle(int requiredSpeed, int requiredLeftSpeed, int requiredRightSpeed) {

  analogWrite(ML_PWM, requiredSpeed - requiredLeftSpeed);
  analogWrite(MR_PWM, requiredSpeed - requiredRightSpeed);
}

要设置每个电机的速度,我们必须将适当的 PWM 信号设置到 PWM 引脚。

如果你需要使漫游车直线行驶,设置的速度应使两个电机相等。否则,降低位于漫游车侧轮的速度,这是我们想要转向的方向。

主程序

漫游车控制软件的loop函数并不复杂,如下所示:

void loop() {

  uart.pollACI();

  if (iOSConnected) {

    // Check accelerometer
    if (millis() - lastAccelerometerCheck > ACCELEROMETER_CHECK_INTERVAL) {

      char buffer[32];
      char xBuffer[6];
      char yBuffer[6];

      lastAccelerometerCheck = millis();

      sensors_event_t event;

      accel.getEvent(&event);

      event.acceleration.x += xOffset;
      event.acceleration.y += yOffset;

      dtostrf(event.acceleration.x, 0, 2, xBuffer);
      dtostrf(event.acceleration.y, 0, 2, yBuffer);

      snprintf(buffer, 32, "%s:%s", xBuffer, yBuffer);
      uart.write((uint8_t *)buffer, strlen(buffer));

      //        Serial.print("X: "); Serial.print(event.acceleration.x); Serial.print("  ");
      //        Serial.print("Y: "); Serial.print(event.acceleration.y); Serial.print("  ");Serial.println("m/s² ");

    }
  }

  // Reads distance

  distance = 0;
  for (int i = 0; i < 16; i++)
    distance += analogRead(DISTANCEPIN);
  distance = distance / 16;

  if (distance > DISTANCETHRESHOLD) {
    leftSpeed = 0;
    rightSpeed = 0;
    throttle(0, 0, 0);
    digitalWrite(DISTANCEINDICATORPIN, HIGH);
  }
  else {
    digitalWrite(DISTANCEINDICATORPIN, LOW);
  }
}

当 iOS 设备连接后,每经过ACCELEROMETER_CHECK_INTERVAL毫秒,沿xy轴的加速度值都会发送到 iOS 设备。然后,读取距离传感器。如果障碍物的距离大于DISTANCETHRESHOLD,漫游车停止,并且漫游车上的 LED 灯亮起。

由于距离传感器的读数相当多变(就像大多数模拟传感器一样),以下几行使用了 16 次读数的平均值:

for (int i = 0; i < 16; i++)
    distance += analogRead(DISTANCEPIN);
  distance = distance / 16;

要控制漫游车,iOS 功能必须发送以下命令:

  • F: 这用于使漫游车向前移动。

  • B: 这用于使漫游车向后移动。

  • T=<速度>: 这用于以<速度>的速度移动漫游车。速度范围在 0-100 之间。

  • R=<速度>: 这用于通过将右电机当前速度降低到<速度>来使漫游车向右移动。速度范围在 0-100 之间。

  • L=<速度>: 这用于通过将左电机当前速度降低到<速度>来使漫游车向左移动。速度范围在 0-100 之间。

如我们从第二章中的蓝牙宠物门锁项目所知,蓝牙宠物门锁,iOS 控制器的命令在rxCallback函数中被接收:

void rxCallback(uint8_t *buffer, uint8_t len) {

  if (len > 0) {

    char value[32];

    if (buffer[0] == 'F') {

      forward();
      goingForward = true;
    }

    if (buffer[0] == 'B') {

      backward();
      goingForward = false;
    }

    if (buffer[0] == 'T') {

      strncpy(value, (const char *)&buffer[2], len - 2);
      value[len - 2] = 0;

      currentSpeed = map(atoi(value), 0, 100, 0, 255);
      if (currentSpeed == 0) {
        rightSpeed = 0;
        leftSpeed = 0;
      }
    }

    if (buffer[0] == 'R') {

      strncpy(value, (const char *)&buffer[2], len - 2);
      value[len - 2] = 0;
      //Serial.print("Right Speed "); Serial.println(atoi(value));
      rightSpeed = map(atoi(value), 0, 100, 0, currentSpeed);
      leftSpeed = 0;
    }

    if (buffer[0] == 'L') {

      strncpy(value, (const char *)&buffer[2], len - 2);
      value[len - 2] = 0;

      leftSpeed = map(atoi(value), 0, 100, 0, currentSpeed);
      rightSpeed = 0;
    }

    throttle(currentSpeed, leftSpeed, rightSpeed);
  }

}

F(Forward)和 B(Backward)命令处理起来非常简单,因为我们有相应的函数可以调用。

对于 T(hrottle)命令,我们在value变量中获取所需的速度降低值,并将其从 0-100 的范围按比例缩放到 0-255 的范围(currentSpeed = map(atoi(value), 0, 100, 0, 255))。这是 PWM 信号的预期范围。

函数通过调用throttle函数结束,该函数为两个电机设置速度,从而设置 PWM 信号。

对于 R(ight)命令,我们在value变量中获取所需的速度降低值。然后,我们将 0-100 的范围按比例缩放到 0-currentSpeed的范围。实际上,右电机速度降低不能超过实际电机速度。换句话说,当命令值为 255(最大旋转速度)时,右电机的速度降低到 0,探测车向右转。对于 L(eft)命令也是如此。

iOS 代码

在本章中,我们将查看远程控制探测车的 iOS 应用程序。这个应用程序允许我们通过两个滑块手动控制探测车,这两个滑块模拟方向盘和油门。

然而,我们将通过使用 iOS 设备加速度计甚至语音命令将应用程序推进得更远。这些技术也可以成功地应用于许多其他项目。

让我们一步一步来,这样你可以理解每一个重要的细节。我们将从手动控制开始。和往常一样,这个项目的完整代码可以从www.packtpub.com/books/content/support下载。

为了更好地理解下一段落的解释,请在阅读时打开下载的代码。

创建 Xcode 项目

我们将创建一个新的项目,就像之前章节中做的那样。以下是新项目的参数:

  • 项目类型:分页应用

  • 产品名称:探测车

  • 语言:Objective-C

  • 设备:通用

在项目的选项中,我们需要取消选择以下选项:

  • 横屏向右

  • 横屏向左

我们这样做是因为我们打算使用 iOS 加速度计,我们不希望设备旋转时屏幕也旋转(见以下截图)。要访问项目的选项,请执行以下步骤:

  1. 在 Xcode 的左侧窗格中选择项目。

  2. 在右侧窗格中选择通用选项卡。创建 Xcode 项目

我们需要额外的代码和图形组件来通过两个仪表显示探测车的倾斜角度。该库可以从github.com/sabymike/MSSimpleGauge下载。

要安装额外的代码,请执行以下步骤:

  1. 打开前面的链接,点击右侧的下载 ZIP按钮。

  2. 解压下载的 ZIP 文件。

  3. 打开Gauges文件夹,并将文件复制到项目的Rover文件夹中。

  4. 在 Xcode 中选择Rover组,右键单击它。选择将文件添加到"Rover"…。然后,选择您刚刚复制的文件,并点击添加。确保已选中如果需要则复制项(见以下截图):创建 Xcode 项目

  5. 在 Xcode 中,选择您刚刚添加的文件,右键单击它们,选择从选择新建组,然后输入Gauges。这有助于我们保持代码的整洁。

  6. 为了避免编译错误,打开文件MSArcLayer.h,并在#import <QuartzCore/QuartzCore.h>之前添加#import <UIKit/UIKit.h>

我们还需要库接受语音命令(OpenEars),可以从www.politepix.com/openears/下载。

点击下载 OpenEars按钮。要安装库,您必须执行以下步骤:

  1. 解压缩下载的文件。

  2. 在您下载的发行版中,有一个名为Framework的文件夹。将Framework文件夹拖放到 Xcode 中的您的应用程序项目中。

现在我们已经配置了所需的附加库,我们可以开始创建应用程序。

此项目的结构非常接近 Pet Door Locker。因此,我们可以通过以下步骤重用至少一部分用户界面和代码:

  1. 选择FirstViewController.hFirstViewController.m,右键单击它们,并点击删除(见以下截图)。然后,点击移动到废纸篓创建 Xcode 项目

  2. 通过使用相同的程序,删除SecondViewControllerMain.storyboard

  3. 在 Xcode 中打开PetDoorLocker项目。

  4. 选择以下文件,并将它们拖放到Rover项目中:

    • PetDoorLockerViewController.h

    • PetDoorLockerViewController.m

    • BLEConnectionViewController.h

    • BLEConnectionViewController.m

    • Main.storyboard

    确保已选中如果需要则复制项,然后点击完成

    注意

    如果您已经将图标添加到标签栏中,别忘了也将它们拖放过来。

    1. 使用我们在前几章中使用的相同程序,将PetDoorLockerViewController重命名为RoverViewController

    2. 打开Main.storyboard并定位主视图控制器。

    3. 删除以下 GUI 组件:

      • 门状态温度标签标签

      • 开关组件

    4. 将状态视图移至连接按钮附近,并更新其布局约束。

    5. 添加如图所示的 GUI 组件和相关布局约束:创建 Xcode 项目

      在顶部,有两个 UIView,其大小为 64 x 128,类为 MSRangeGauge(在属性检查器中更改它)

  5. 对于节流滑块,转到属性检查器并设置以下值:

    • 最小值:0

    • 最大值:100

    • 当前值:0

    • 滑块拇指:红色或您喜欢的任何颜色

  6. 对于 Steering 滑块,转到属性检查器并设置以下值:

    • 最小值:0

    • 最大值:200

    • 当前值:100

  7. 对于分段控制器,转到属性检查器并将 Segments 设置为 3。

  8. 通过双击每个分段并输入以下值来更改标题:

    • 手动

    • 加速度计

    • 语音

  9. 选择视图控制器容器,并在身份检查器中,将 Class 更改为 RoverViewController

  10. RoverViewController.h 文件中,添加 #import "MSRangeGauge.h"

  11. 将 GUI 组件连接到 RoverViewController 代码,如下所示:

    @property (strong, nonatomic) IBOutlet UIView               *connectionStatus;
    @property (strong, nonatomic) IBOutlet MSRangeGauge         *verticalIndicator;
    @property (strong, nonatomic) IBOutlet MSRangeGauge         *horizontalIndicator;
    @property (strong, nonatomic) IBOutlet UISlider             *throttleSlider;
    @property (strong, nonatomic) IBOutlet UISlider             *steeringWheelSlider;
    @property (strong, nonatomic) IBOutlet UISwitch             *directionSwitch;
    @property (strong, nonatomic) IBOutlet UISegmentedControl   *modeSegment;
    

    注意

    在你的 RoverViewController.m 中,还有一些对旧项目的引用。不要担心这些引用。我们将在以下部分中删除它们。

  12. 将油门滑块连接到方法,如下所示:(IBAction)throttleChanged:(UISlider *)sender

  13. 将转向滑块连接到方法,如下所示:(IBAction)steeringWheelChanged:(UISlider *)sender

  14. 将前后开关连接到方法,如下所示:(IBAction)directionChanged:(UISwitch *)sender

  15. 将模式分段控制器与方法连接,如下所示:(IBAction)modeChange:(UISegmentedControl *)sender

编写 BLEConnectionViewController 的代码

由于我们已经从 PetDoorLocker 项目中复制了这个视图控制器,我们不需要对其进行更改。

我们节省了一些工作!

编写 RoverViewController 的代码

首先,我们必须通过以下步骤从之前的 PetDoorLocker 项目中删除不再需要的多余代码:

  1. 打开 RoverViewController.m

  2. 删除以下行:

    @property (strong, nonatomic) IBOutlet UIView       *doorStatus;
    @property (strong, nonatomic) IBOutlet UILabel      *temperature;
    @property (strong, nonatomic) IBOutlet UISwitch     *manualLockSwitch;
    
  3. 删除代码中关于 _temperature_doorStatus 的行。如有疑问,请参考下载的代码。

  4. 清空 dataReceived 函数;我们稍后会重写它:

    -(void)dataReceived:(NSString *)content {
    
    }
    
  5. 完全删除 switchChanged 函数。

我们现在可以开始编写新的代码来控制我们的漫游车。

让我们从简单的部分开始——从漫游车接收加速度数据。这些数据提供了关于漫游车沿其纵向和横向轴倾斜的信息。

此信息将通过我们在主屏幕中添加的两个仪表(RMRangeGauges)显示。仪表必须在 viewDidLoad 方法中初始化,如下所示:

- (void)viewDidLoad {

    [super viewDidLoad];

    _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];

    _verticalIndicator.transform = CGAffineTransformMakeRotation(M_PI/2);

    _verticalIndicator.minValue = 0;
    _verticalIndicator.maxValue = 200;
    _verticalIndicator.upperRangeValue = 130;
    _verticalIndicator.lowerRangeValue = 70;
    _verticalIndicator.value = 100;
    _verticalIndicator.fillArcFillColor = [UIColor colorWithRed:.9 green:.1 blue:.1 alpha:1];
    _verticalIndicator.rangeFillColor   = [UIColor colorWithRed:.2 green:.9 blue:.2 alpha:1];

    _horizontalIndicator.minValue = 0;
    _horizontalIndicator.maxValue = 200;
    _horizontalIndicator.upperRangeValue = 130;
    _horizontalIndicator.lowerRangeValue = 70;
    _horizontalIndicator.value = 100;
    _horizontalIndicator.fillArcFillColor = [UIColor colorWithRed:.9 green:.1 blue:.1 alpha:1];
    _horizontalIndicator.rangeFillColor   = [UIColor colorWithRed:.2 green:.9 blue:.2 alpha:1];

    …

}

代码非常易于理解,多亏了方法的自文档命名。

由于仪表是水平的,并且没有显示指针垂直方向的功能,我们使用以下指令:

_verticalIndicator.transform = CGAffineTransformMakeRotation(M_PI/2);

通过前面的代码,我们将第一个仪表旋转 90 度,以便更好地指示垂直倾斜。

倾斜数据以字符串形式通过 dataReceived 方法接收:<垂直倾斜>:<水平倾斜>,并将值设置为两个仪表,如下所示:

-(void)dataReceived:(NSString *)content {

    NSArray *components = [content componentsSeparatedByString:@":"];
    if (components.count != 2) {
        return;
    }

    float x = [components[0] floatValue];
    float y = [components[1] floatValue];

    _verticalIndicator.value = 100+20*y;
    _horizontalIndicator.value = 100+20*x;
}

didDisconnectPeripheral 方法中,当蓝牙设备断开连接时调用,我们必须通过添加以下行来重置两个仪表的位置:

    _verticalIndicator.value = 100;
    _horizontalIndicator.value = 100;

由于我们有三种操作漫游车的方式——手动、使用 iOS 加速度计和通过语音命令——我们将把代码编写分成三个不同的部分,以便更好地理解代码。

控制漫游车的手动代码

对于这个场景,我们需要编写管理油门滑块的代码来控制漫游车的速度,控制方向的转向滑块,以及控制其前进或后退运动的开关。

油门滑块的代码相当简单,因为我们只需要以 T=<速度>的形式向漫游车发送一条消息,如下所示:

- (IBAction)throttleChanged:(UISlider *)sender {

    NSInteger throttle = sender.value;

    NSString *msg = [NSString stringWithFormat:@"T=%ld",(long)throttle];

    NSData* data;
    data=[msg dataUsingEncoding:NSUTF8StringEncoding];

    [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];
}

用于控制方向的方 法并不复杂。它根据滑块相对于中间位置的位置发送两条消息——R=<速度>使漫游车向右转,L=<速度>使漫游车向左转:

- (IBAction)steeringWheelChanged:(UISlider *)sender {

    NSInteger steering = sender.value-100;

    NSString *msg;

    if (steering>0) {

        msg = [NSString stringWithFormat:@"R=%ld",(long)steering];
    }
    else {
        msg = [NSString stringWithFormat:@"L=%ld",(long)-steering];
    }

    NSData* data;
    data=[msg dataUsingEncoding:NSUTF8StringEncoding];

    [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];
}

为了完成对两个滑块的管理,我们需要更改didConnectPeripheral方法,以便当 iOS 设备连接到漫游车时,两个滑块重置到初始位置,如下所示:

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {

    _steeringWheelSlider.value = 100;
    _throttleSlider.value = 0;

    [peripheral discoverServices:@[[CBUUID UUIDWithString:NRF8001BB_SERVICE_UUID]]];
}

我们需要编写的最后一个方法是控制前后方向。我们需要向漫游车发送两条简单的消息——F表示前进,B表示后退:

- (IBAction)directionChanged:(UISwitch *)sender {

    NSData* data;

    if (sender.on) {
        data=[@"F" dataUsingEncoding:NSUTF8StringEncoding];
    }
    else {
        data=[@"B" dataUsingEncoding:NSUTF8StringEncoding];
    }

    [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];

    _throttleSlider.value = 0;
    [self throttleChanged:_throttleSlider];
}

为了避免不希望的行为,每次我们切换方向时,通过调用[self throttleChanged:_throttleSlider]方法将速度设置为 0,使漫游车停止。

我们现在准备好进行漫游车的第一次测试。

使用手动驾驶测试漫游车

要进行第一次漫游车测试,你可以使用以下步骤:

  1. 上传 Arduino 代码并检查控制台是否有任何错误信息。如果一切顺利,Arduino 就准备好控制你的漫游车了。

  2. 打开两个电机和 Arduino 本身。

  3. 将 iOS 应用程序上传到你的设备。

  4. 前往第二个标签页以扫描蓝牙 BLE 扩展板。

  5. 转到第一个标签页,并逐步增加油门滑块。你应该能看到漫游车向前移动。

  6. 你可以通过移动转向滑块来使其左右移动。

  7. 转动方向开关,漫游车停止。再次增加速度时,漫游车将朝相反方向移动。

  8. 当上坡时,你会看到垂直和/或水平仪表的指针上下左右移动。

  9. 当遇到障碍物时,漫游车应该在碰撞之前停止。

小贴士

奇怪的漫游车移动

如果漫游车移动到错误的方向,你可能没有正确连接一个或两个电机。交换错误方向旋转的电机线。

奇怪的仪表指示

如果仪表似乎没有根据斜坡移动,现在不必过于担心这个问题。我们稍后会校准它们。现在,我们只需要检查数据是否从漫游车正确传输到 iOS 设备。

小贴士

缺失的仪表指示

如果你没有看到仪表的任何指示,可能是在 ADXL345 设备布线时出现了错误。首先,请再次检查 Arduino 控制台是否有任何错误消息。如果这没有帮助,请从 Arduino 代码(main loop)中的以下两行取消注释:

Serial.print("X: ")
Serial.print(event.acceleration.x); Serial.print("  ");
Serial.print("Y: "); Serial.print(event.acceleration.y);
Serial.print("  ");Serial.println("m/s² ");

再次连接 iOS 设备并检查 Arduino 控制台。如果你能看到打印的数字,那么加速度计正在工作,你必须再次检查 iOS 代码。

机器人撞到障碍物

距离传感器对障碍物的形状、反射率和位置敏感,有时它无法避开它们。这就是为什么在现实世界的机器人中同时使用不同类型的传感器,并在不同的位置使用的原因。为了检查传感器是否正确连接并且按预期工作,你可以在以下行(在main loop中)取消注释:

Serial.print("D: "); Serial.println(distance);

现在,你应该能够在 Arduino 控制台中看到距离。

通过 iOS 加速度计控制机器人的代码

我们现在将通过使用 iOS 加速度计来控制转向和油门来改进我们的应用程序。

正如我们之前所学的,iOS 设备位置的任何偏差都可以被测量并使用,通过我们安装在机器人上的加速度计发送适当的命令。

要访问加速度计信息,我们需要使用CMMotionManager类。首先,我们在RoverViewController.h中添加#import <CoreMotion/CoreMotion.h>。然后,我们创建一个属性,如下所示:

@interface RoverViewController ()

…

@property (strong, nonatomic) CMMotionManager       *motionManager;

…

@end

最后,我们在viewDidLoad方法中初始化它,如下所示:

- (void)viewDidLoad {

    [super viewDidLoad];

…
    _motionManager = [[CMMotionManager alloc] init];
…
}

当在分段控制器上选择第二个按钮时,加速度计被激活,并调用相关的方法:

- (IBAction)modeChange:(UISegmentedControl *)sender {

    _throttleSlider.value = 0;
    _steeringWheelSlider.value = 100;

    if (_modeSegment.selectedSegmentIndex==1) {

        …
        [self useAccelerometer];
    }

    …
}

useAccelerometer方法实际上激活了加速度计:

-(void)useAccelerometer {

    [_motionManager setDeviceMotionUpdateInterval:0.2];
    [_motionManager startDeviceMotionUpdatesUsingReferenceFrame:CMAttitudeReferenceFrameXArbitraryZVertical
                                                        toQueue:[NSOperationQueue mainQueue]
                                                    withHandler:^(CMDeviceMotion *motion, NSError *error) {

                                                        CMQuaternion quat = motion.attitude.quaternion;

                                                        [self sendAccelerometersCommands:quat];
                                                    }];
}

[_motionManager setDeviceMotionUpdateInterval:0.2]方法指示运动管理器每 0.2 秒更新我们的代码,以加速度值。

下一个方法实际上开始更新我们的加速度计值代码,这些值是在处理程序块中接收到的。

幸运的是,iOS 不仅提供了设备三个轴的实际加速度值,还提供了四元数。不要害怕这个名字!它们只是简单地表示 iOS 设备在三维空间中的方向和旋转(如果你喜欢数学,请访问en.wikipedia.org/wiki/Quaternions_and_spatial_rotation)。从它们中,你可以轻松计算出 iOS 设备的俯仰和滚转两个角度(见以下图像)。

如果你喜欢数学,请访问en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles

通过 iOS 加速度计控制机器人的代码

滚转用于改变漫游车的运动方向,俯仰用于控制油门。-(void)sendAccelerometersCommands:(CMQuaternion)quad方法计算这两个角度并生成需要发送到漫游车的消息,就像我们在throttleChangedsteeringWheelChanged中做的那样。

通过 iOS 设备移动来驾驶漫游车

为了测试漫游车,点击加速度计,切换到前进,并垂直握持 iOS 设备。通过在俯仰轴周围移动设备(参见上一张图片),漫游车应该开始向前移动。设备移动得越远,漫游车的速度就越快。通过向后移动设备,漫游车的速度会降低,你可以通过这种移动来停止漫游车。

为了使漫游车向左或向右转,设备必须围绕滚转轴向左或向右转动。

为了使命令更加或更少地响应,你可以尝试更改代码更新的频率,以新的加速度值,如下所示:

[_motionManager setDeviceMotionUpdateInterval:0.2];

不要忘记,增加更新间隔会增加电池消耗。这种权衡严格与你的漫游车和你的需求相关。

通过语音命令控制漫游车的代码

语音识别多年来一直是一个挑战,但现在,你可以在几分钟内将此功能添加到你的应用程序中。

我们已经添加了所需的库。因此,我们可以通过以下步骤开始添加所需的代码:

  1. 打开RoverViewController.h并添加#import <OpenEars/OEEventsObserver.h>

  2. 将接口从@interface RoverViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate>更改为@interface RoverViewController : UIViewController <CBCentralManagerDelegate, CBPeripheralDelegate, OEEventsObserverDelegate>

  3. 打开RoverViewController.m并添加以下导入:

    #import <OpenEars/OELanguageModelGenerator.h>
    #import <OpenEars/OEPocketsphinxController.h>
    #import <OpenEars/OEAcousticModel.h>
    #import <Slt/Slt.h>
    #import <OpenEars/OEFliteController.h>
    
  4. 添加以下属性:

    @property (strong, nonatomic) NSString              *lmPath;
    @property (strong, nonatomic) NSString              *dicPath;
    
    @property (strong, nonatomic) OEEventsObserver      *openEarsEventsObserver;
    @property (strong, nonatomic) OEFliteController     *fliteController;
    @property (strong, nonatomic) Slt                   *slt;
    
  5. 通过在viewDidLoad方法中添加以下代码来初始化属性。words数组包含将被识别的语音命令。其余的代码来自库的文档:

        NSMutableArray *words = [[NSMutableArray alloc] initWithArray:@[@"RIGHT", @"LEFT", @"CENTER", @"FORWARD", @"SLOWFORWARD", @"FASTFORWARD", @"BACKWARD", @"SLOWBACKWARD", @"FASTBACKWARD",@"STOP"]];
    
        _fliteController = [[OEFliteController alloc] init];
        _slt = [[Slt alloc] init];
    
        OELanguageModelGenerator *lmGenerator = [[OELanguageModelGenerator alloc] init];
    
        NSError *err=nil;
        NSString  *name = @"RoverVoiceControl";
    
        err = [lmGenerator generateLanguageModelFromArray:words withFilesNamed:name forAcousticModelAtPath:[OEAcousticModel pathToModel:@"AcousticModelEnglish"]];
    
        if(err == nil) {
    
            _lmPath = [lmGenerator pathToSuccessfullyGeneratedLanguageModelWithRequestedName:name];
            _dicPath = [lmGenerator pathToSuccessfullyGeneratedDictionaryWithRequestedName:name];
    
        } else {
            NSLog(@"Error: %@",[err localizedDescription]);
        }
    
  6. 按以下方式更改modeChange方法(当它们未被使用时关闭语音命令识别和/或加速度计):

    - (IBAction)modeChange:(UISegmentedControl *)sender {
    
        _throttleSlider.value = 0;
        _steeringWheelSlider.value = 100;
    
        if (_modeSegment.selectedSegmentIndex==0) {
            [[OEPocketsphinxController sharedInstance] stopListening];
            [_motionManager stopDeviceMotionUpdates];
        }
    
        if (_modeSegment.selectedSegmentIndex==1) {
    
            [[OEPocketsphinxController sharedInstance] stopListening];
            [self useAccelerometer];
        }
    
        if (_modeSegment.selectedSegmentIndex==2) {
    
            [_motionManager stopDeviceMotionUpdates];
            [self useVoice];
        }
    }
    
  7. 添加以下useVoice方法。它激活语音命令的监听并配置库,以便在识别到语音命令时调用pocketsphinxDidReceiveHypothesis代理方法:

    -(void)useVoice {
    
        [[OEPocketsphinxController sharedInstance] setActive:TRUE error:nil];
    
        [[OEPocketsphinxController sharedInstance] startListeningWithLanguageModelAtPath:_lmPath
                                                                        dictionaryAtPath:_dicPath
                                                                     acousticModelAtPath:[OEAcousticModel pathToModel:@"AcousticModelEnglish"]
                                                                     languageModelIsJSGF:NO];
    
        [[OEPocketsphinxController sharedInstance] setSecondsOfSilenceToDetect:.7];
        [[OEPocketsphinxController sharedInstance] setVadThreshold:3.0];
    
        _openEarsEventsObserver = [[OEEventsObserver alloc] init];
        [_openEarsEventsObserver setDelegate:self];
    }
    
  8. 添加pocketsphinxDidReceiveHypothesis方法,可以从下载的代码中复制。它除了格式化和向漫游车发送命令之外,没有做太多,就像我们已经为其他模式做了那样。我们只需要指出以下内容:

    • hypothesis参数是一个包含已识别命令的字符串。

    • [_fliteController say:hypothesis withVoice:self.slt]调用允许你听到由你的 iOS 设备发音的已识别命令。

      - (void) pocketsphinxDidReceiveHypothesis:(NSString *)hypothesis recognitionScore:(NSString *)recognitionScore utteranceID:(NSString *)utteranceID {
      
          [_fliteController say:hypothesis withVoice:self.slt];
      
          NSString *msg=nil;
      
          if ([hypothesis isEqualToString:@"FORWARD"]) {
      
              [_arduinoDevice writeValue:[@"F" dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];
      
              msg = [NSString stringWithFormat:@"T=%ld",60l];
          }
      
          …
      
          NSData* data;
          data=[msg dataUsingEncoding:NSUTF8StringEncoding];
      
          [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];
      }
      
  9. 要完成应用,我们必须在AppDelegate.m文件中的applicationDidEnterBackground方法中添加几行代码,以便在应用被发送到后台时断开与漫游车的连接:

        UITabBarController *tabController = (UITabBarController *)_window.rootViewController;
        RoverViewController *roverController = tabController.viewControllers[0];
    
        [roverController disconnect];
    

通过语音命令控制漫游车

要尝试此功能,你必须点击“语音”然后说出任何可用的命令。当应用程序识别出语音命令时,它会发音已识别的命令,漫游车将相应地开始移动。

请注意,语音识别需要一些时间。因此,漫游车不太灵敏。语音模式更适合在开阔空间和长时间导航时使用(无论这对漫游车意味着什么!)。

如果你遇到触发语音识别的低背景噪音,你可以将此调用中的值在 1.5-3.5 的范围内提高:

[[OEPocketsphinxController sharedInstance] setVadThreshold:3.0]

要使漫游车稍微更灵敏,你可以尝试减少在语音结束后应用等待尝试识别语音的时间(默认值为 0.7 秒),如下所示:

[[OEPocketsphinxController sharedInstance] setSecondsOfSilenceToDetect:.7];

测试和调整

我们已经测试了漫游车的所有驾驶模式,但我们可能仍然会从安装在漫游车本身的加速度计中获得不可靠的读数。

要校准加速度计的读数,请使用以下程序:

  1. 确保 ADXL345 牢固地安装在漫游车上,并且其轴与漫游车的纵向和横向轴平行。

  2. 将漫游车放置在坚实、平坦的表面上,并使用水平仪确保漫游车处于水平状态。

  3. 在 Arduino 代码的loop函数中注释掉以下行并上传:

          Serial.print("X: "); Serial.print(event.acceleration.x); Serial.print("  ");
          Serial.print("Y: "); Serial.print(event.acceleration.y); Serial.print("  ");Serial.println("m/s² ");
    
  4. 通过 USB 线为 Arduino 供电并打开控制台。

  5. 连接 iOS 设备。加速度读数在控制台上显示在两个轴上。它们应该是 0 或非常接近 0。如果不是这样,读取 10 到 20 次,计算平均值,并将这些值放入setup函数中的xOffsetyOffset变量。

现在,加速度计的读数应该更加一致,iOS 设备上的两个指针可以帮助你在崎岖地形上驾驶漫游车,避免翻车。

如何更进一步

以下是一些改进项目的建议:

  1. 在 iOS 应用程序中显示前方障碍物的距离。

  2. 当漫游车的倾斜超过一定阈值时停止漫游车。

  3. 添加更多距离传感器或将距离传感器安装在伺服电机上,以便漫游车能够检测到其周围的障碍物。

  4. 安装不同类型的传感器以更好地避免障碍物(例如,超声波距离传感器或激光距离传感器)。

  5. 允许 iOS 设备横屏方向。提示——你需要通过使用ViewController类提供的某个方向代理方法(willTransitionToTraitCollection, viewWillTransitionToSize)来获取实际的 iOS 设备方向。

语音识别可以在许多项目中使用,因为它设置非常简单,而且效果相当不错。你可以开始将语音识别添加到 Wi-Fi 电源插头项目中。

如果你需要一个挑战,尝试通过让无人驾驶车意识到自己的位置来使其自主移动。(提示——你可以使用粒子滤波器来完成这个任务,但这是一个非常困难的主题。谷歌汽车就是基于这个以及其他很多技术)。

摘要

你被引入了令人着迷的机器人世界和遥控车辆的世界。你学习了如何编写 Arduino 代码来控制直流电机的速度和旋转方向,使用模拟红外传感器测量距离,以及通过加速度计测量沿三个轴的加速度。

你学习了如何在 iOS 设备上使用新的图形组件,如UISliderUISegmentedControl,并利用 iOS 设备配备的加速度计。此外,你还学习了如何使用一个功能强大且易于使用的语音识别和文本到语音库来改进你的项目。

不要忘记,你现在有一辆可以驾驶的无人驾驶车,其他人不会认为你很奇怪;你并不是像孩子一样玩耍,你正在学习机器人!和你的无人驾驶车一起享受乐趣吧!

这一章节相当长,项目也很复杂,但现在我们可以喘口气了。在下一章中,我们将构建一个非常简单但极其强大的项目。它通过保持几乎相同的音量来控制电视机的音量,即使广告正在播放也是如此。即使项目相当简单,你也会学到很多关于红外发射器和接收器以及数字信号处理的知识。

第五章. 电视恒定音量控制器

我不太看电视,但当我看的时候,我通常会完全放松并入睡。我知道电视不是用来让你入睡的,但它对我就是这样做的。不幸的是,广告的音量非常高,它们会把我吵醒。如果每五分钟就有一个广告把我吵醒,我怎么能放松呢?

你能相信吗?在两个广告之间的一个午睡中,我想出了一个基于 iOS 和 Arduino 的解决方案。

这并不复杂。iOS 设备监听电视的音频,当音频级别超过预设阈值时,iOS 设备会通过蓝牙发送消息到 Arduino,Arduino 控制电视音量,模拟传统的红外遥控器。当音量低于另一个阈值时,也会发生完全相同的情况。最终结果是电视音量几乎保持恒定,不受电视播放内容的影响。这有助于我睡得更久!

你在本章中将学习到的技术以多种不同的方式有用。你可以用红外遥控器实现任何目的,或者你可以控制许多不同的设备,例如 CD/DVD 播放器、立体声音响、Apple TV、投影仪等等,直接从 Arduino 和 iOS 设备上操作。就像往常一样,这取决于你的想象力。

恒定音量控制器要求

我们的目的是设计一个基于 Arduino 的设备,通过模拟传统的遥控器来使电视音量几乎保持恒定,以及一个 iOS 应用程序,该应用程序监控电视并决定何时降低或提高电视音量。

硬件

大多数电视都可以通过红外遥控器控制,它发送信号来控制音量、更换频道以及控制电视的所有其他功能。

红外遥控器使用一个载波信号(通常为 38 KHz),这种信号很容易从噪声和干扰中分离出来。

载波信号通过遵循不同的规则(编码)来打开和关闭,以便传输 0 和 1 的数字值。

红外接收器通过低通滤波器去除载波信号,并通过返回一个清晰的 0 和 1 序列来解码剩余的信号。

小贴士

红外遥控器理论

你可以在bit.ly/1UjhsIY找到有关红外遥控器的更多信息。

我们将通过使用红外 LED 来模拟红外遥控器,它将发送可以被我们的电视接收的特定信号。

另一方面,我们可以通过设计一个解调器和解码器,使用光电晶体管接收红外信号并将其解码成可理解的数字序列。

现在,电子设备非常简单;一个红外接收器模块(Vishay 4938)将处理信号解调、噪声消除、触发和解码的复杂性。它可以直接连接到 Arduino,使一切变得非常简单。

在本章的项目中,我们需要一个红外接收器来发现我们自己的红外遥控器(以及电视)所使用的编码规则。

额外的电子组件

在这个项目中,我们还需要以下额外的组件:

  • Vishay TSAL6100 红外 LED

  • Vishay TSOP 4838 红外接收模块

  • 电阻 100Ω

  • 电阻 680Ω

  • 电解电容 0.1μF

电子电路

以下图片展示了我们为项目所需的电路的电原理图:

电子电路

红外接收器将仅用于捕获电视遥控器的信号,以便我们的电路可以模拟它们。

然而,一个红外 LED 始终用于向电视发送命令。其他两个 LED 将在 Arduino 增加或减少音量时显示。它们是可选的,可以省略。

如往常一样,蓝牙设备用于接收 iOS 设备的命令。

小贴士

在 Arduino 电流限制内供电红外 LED

从 TSAL6100 的数据表中,我们知道正向电压是 1.35V。因此,R1 上的电压降为5-1.35 = 3.65V,Arduino 提供给 LED 的电流大约为3.65/680=5.3 mA。每个引脚允许的最大电流是 40 mA(推荐值为 20 mA)。所以,我们在 Arduino 的限制范围内。如果你的电视远离 LED,你可能需要降低 R1 电阻以获得更多电流(和红外光)。使用新的 R1 值在之前的计算中检查你是否在 Arduino 的限制范围内。有关 Arduino 引脚电流的更多信息,请查看bit.ly/1JosGac

以下图示展示了如何在面包板上安装电路:

电子电路

Arduino 代码

该项目的全部代码可以从www.packtpub.com/books/content/support下载。

为了更好地理解以下段落中的解释,请在阅读时打开下载的代码。

在这个项目中,我们将使用红外遥控库,它帮助我们编码和解码红外信号。

该库可以从bit.ly/1Isd8Ay下载,并按照以下步骤安装:

  1. 导航到bit.ly/1Isd8Ay的发布页面,以获取最新版本并下载IRremote.zip文件。

  2. 解压你喜欢的文件。

  3. 打开查找器,然后是应用程序文件夹(Shift + Control + A)。

  4. 定位 Arduino 应用程序。

  5. 右键单击并选择显示包内容

  6. 定位Java文件夹,然后是libraries

  7. IRremote文件夹(在第 2 步中解压)复制到libraries文件夹中。

  8. 如果 Arduino 正在运行,请重新启动 Arduino。

在这个项目中,我们需要以下两个 Arduino 程序:

  • 一个用于获取你的红外遥控器发送的用于增加和减少音量的代码

  • 另一个是 Arduino 必须运行的用于自动控制电视音量的主程序

让我们从用于获取红外遥控器代码的代码开始。

解码器设置代码

在本节中,我们将参考下载的 Decode.ino 程序,该程序用于发现您遥控器使用的代码。

由于设置代码相当简单,不需要详细解释;它只是初始化库以接收和解码消息。

解码器主程序

在本节中,我们将参考下载的 Decode.ino 程序;主代码接收来自电视遥控器的信号并输出相应的代码,这些代码将被包含在主程序中以模拟遥控器本身。

一旦程序运行,如果您按下遥控器上的任何按钮,控制台将显示以下内容:

For IR Scope: 
+4500 -4350 … 

For Arduino sketch: 
unsigned int raw[68] = {4500,4350,600,1650,600,1600,600,1600,…};

第二行是我们需要的。请参阅 测试和调整 部分以获取如何使用这些数据的详细说明。

现在,我们将查看将在 Arduino 上持续运行的 main 代码。

设置代码

在本节中,我们将参考 Arduino_VolumeController.ino 程序。设置函数初始化 nRF8001 板并配置可选监控 LED 的引脚。

主程序

loop 函数只是调用 polACI 函数以允许正确管理来自 nRF8001 板的传入消息。

该程序接受来自 iOS 设备的以下两条消息(参看 rxCallback 函数):

  • D 用于降低音量

  • I 用于增加音量

以下两个函数通过发送两个 updown 缓冲区通过红外 LED 来执行实际的音量增加和减少:

void volumeUp() {
  irsend.sendRaw(up, VOLUME_UP_BUFFER_LEN, 38);
  delay(20);
}

void volumeDown() {
  irsend.sendRaw(down, VOLUME_DOWN_BUFFER_LEN, 38);
  delay(20);
  irsend.sendRaw(down, VOLUME_DOWN_BUFFER_LEN, 38);
  delay(20);
}

updown 缓冲区,VOLUME_UP_BUFFER_LENVOLUME_DOWN_BUFFER_LEN,是在 Decode.ino 程序的帮助下准备的(参见 测试和调整 部分)。

iOS 代码

在本章中,我们将探讨一个 iOS 应用程序,该应用程序监控电视音量并将音量增加或减少命令发送到 Arduino 板,以保持所需的音量值。

该项目的完整代码可以从 www.packtpub.com/books/content/support 下载。

为了更好地理解以下段落中的解释,请在阅读时打开下载的代码。

创建 Xcode 项目

我们将创建一个新的项目,就像我们在前面的章节中所做的那样。以下是你需要遵循的步骤:

以下是新项目的参数:

  • 项目类型: 选项卡式应用程序

  • 产品名称: VolumeController

  • 语言: Objective-C

  • 设备: 通用

要为此项目设置功能,请执行以下步骤:

  1. 在 Xcode 的左侧面板中选择项目。

  2. 在右侧面板中选择 功能

  3. 打开后台模式选项,并选择音频和 AirPlay(参见图示)。这允许 iOS 设备在 iOS 设备屏幕关闭或应用进入后台时也能监听音频信号:创建 Xcode 项目

由于此项目的结构与 Pet Door Locker 非常相似,我们可以通过以下步骤重用用户界面和代码的一部分(更多详情,请参阅第四章中的 iOS Guided Rover 项目[iOS Guided Rover],我们在这里几乎做了同样的事情):

  1. 选择FirstViewController.hFirstViewController.m,右键单击它们,点击删除,然后选择移动到废纸篓

  2. 使用相同的步骤,删除SecondViewControllerMain.storyboard

  3. 在 Xcode 中打开PetDoorLocker项目。

  4. 选择以下文件,并将它们拖放到此项目中(参见图示)。

    • BLEConnectionViewController.h

    • BLEConnectionViewController.m

    • Main.storyboard

    确保选中如果需要则复制项目,然后点击完成

  5. 复制用于 BLEConnectionViewController 视图控制器的图标。

  6. 创建一个新的视图控制器类,并将其命名为VolumeControllerViewController

  7. 打开Main.storyboard并定位主视图控制器。

  8. 删除所有图形组件。

  9. 打开身份检查器,将更改为VolumeControllerViewController

现在,我们准备好创建新应用所需的内容。

为 VolumeControllerViewController 设计用户界面

这个视图控制器是应用程序的主要视图控制器,并且只包含以下组件:

  • 控制音量开关的开关

  • 设置电视设置所需音量的滑块

一旦添加了组件及其布局约束,最终结果将类似于以下截图:

为 VolumeControllerViewController 设计用户界面

一旦 GUI 组件与视图控制器的代码链接,我们将得到以下代码:

@interface VolumeControllerViewController ()

@property (strong, nonatomic) IBOutlet UISlider     *volumeSlider;

@end

and with:
- (IBAction)switchChanged:(UISwitch *)sender {
…
}
- (IBAction)volumeChanged:(UISlider *)sender {
…
}

为 BLEConnectionViewController 编写代码

由于我们从 Pet Door Locker 项目复制了这个视图控制器,所以我们不需要更改它,除了将用于存储外围设备 UUID 的密钥从PetDoorLockerDevice更改为VolumeControllerDevice

我们保存了一些工作!

现在,我们准备好开始处理 VolumeControllerViewController,这更有趣。

为 VolumeControllerViewController 编写代码

这是应用程序的主要部分;几乎所有事情都发生在这里。

我们需要一些属性,如下所示:

@interface VolumeControllerViewController ()

@property (strong, nonatomic) IBOutlet UISlider  *volumeSlider;

@property (strong, nonatomic) CBCentralManager   *centralManager;
@property (strong, nonatomic) CBPeripheral       *arduinoDevice;
@property (strong, nonatomic) CBCharacteristic   *sendCharacteristic;

@property (nonatomic,strong) AVAudioEngine       *audioEngine;

@property float                                  actualVolumeDb;
@property float                                  desiredVolumeDb;
@property float                                  desiredVolumeMinDb;
@property float                                  desiredVolumeMaxDb;

@property NSUInteger                             increaseVolumeDelay;

@end

其中一些用于管理蓝牙通信,不需要过多解释。audioEngineAVAudioEngine的实例,它允许我们将 iOS 设备麦克风捕获的音频信号转换为数值样本。通过分析这些样本,我们可以获得与电视音量直接相关的信号功率(音量越高,信号功率越大)。

小贴士

模拟-数字转换

将模拟信号转换为表示信号自身在不同时间的振幅的数字序列的操作,称为模拟-数字转换。Arduino 模拟输入执行完全相同的操作。与数字-模拟转换一起,它是数字信号处理的基本操作,以及将音乐存储在我们的设备中并以合理的质量播放。更多详情,请访问bit.ly/1N1QyXp

actualVolumeDb属性存储实际测量为 dB(分贝)的信号音量。

小贴士

分贝(dB)

分贝(dB)是一个对数单位,表示物理量两个值之间的比率。关于信号的功率,其分贝值按照以下公式计算:

为 VolumeControllerViewController 编写代码

在这里,P 是信号的功率,P[0]是参考功率。你可以在bit.ly/1LZQM0m了解更多关于分贝的信息。我们必须指出,如果 P < P[0],则 P[dB]的值将低于零。因此,分贝值通常是负值,0dB 表示信号的最大功率。

desiredVolumeDb属性存储期望的音量,以 dB 为单位,用户通过应用主标签页中的音量滑块来控制此值;desiredVolumeMinDbdesiredVolumeMaxDb是从desiredVolumeDb派生出来的。

代码中最重要的一部分在viewDidLoad方法中(参考下载的代码)。

首先,我们实例化AudioEngine并获取默认的输入节点,即麦克风,如下所示:

    _audioEngine = [[AVAudioEngine alloc] init];
    AVAudioInputNode *input = [_audioEngine inputNode];

AVAudioEngine是一个非常强大的类,它允许数字音频信号处理。我们只是刚刚触及它的功能。

小贴士

AVAudioEngine

你可以通过访问apple.co/1kExe35(AVAudioEngine 的实际应用)和apple.co/1WYG6Tp了解更多关于 AVAudioEngine 的信息。

我们将要使用的AVAudioEngine和其他函数要求我们添加以下导入:

#import <AVFoundation/AVFoundation.h>
#import <Accelerate/Accelerate.h>

通过在我们的输入节点总线上的输入上安装音频节,我们可以获取 iOS 设备所监听的信号的数值表示,如下所示:

[input installTapOnBus:0 bufferSize:8192 format:[input inputFormatForBus:0] block:^(AVAudioPCMBuffer* buffer, AVAudioTime* when) {
…
…
}];

一旦有新的数据缓冲区可用,代码块就会被调用,数据可以被处理。现在,我们可以看看将音频数据样本转换为实际控制电视的命令的代码:

for (UInt32 i = 0; i < buffer.audioBufferList->mNumberBuffers; i++) {

    Float32 *data = buffer.audioBufferList->mBuffers[i].mData;
    UInt32 numFrames = buffer.audioBufferList->mBuffers[i].mDataByteSize / sizeof(Float32);

  // Squares all the data values
    vDSP_vsq(data, 1, data, 1, numFrames*buffer.audioBufferList->mNumberBuffers);

            // Mean value of the squared data values: power of the signal
    float meanVal = 0.0;
    vDSP_meanv(data, 1, &meanVal, numFrames*buffer.audioBufferList->mNumberBuffers);

    // Signal power in Decibel
    float meanValDb = 10 * log10(meanVal);

    _actualVolumeDb = _actualVolumeDb + 0.2*(meanValDb - _actualVolumeDb);

    if (fabsf(_actualVolumeDb) < _desiredVolumeMinDb && _centralManager.state == CBCentralManagerStatePoweredOn && _sendCharacteristic != nil) {

        //printf("Decrease volume\n");

        NSData* data=[@"D" dataUsingEncoding:NSUTF8StringEncoding];
        [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];

        _increaseVolumeDelay = 0;
    }

    if (fabsf(_actualVolumeDb) > _desiredVolumeMaxDb && _centralManager.state == CBCentralManagerStatePoweredOn && _sendCharacteristic != nil) {

        _increaseVolumeDelay++;
    }

    if (_increaseVolumeDelay > 10) {

        //printf("Increase volume\n");

        _increaseVolumeDelay = 0;

        NSData* data=[@"I" dataUsingEncoding:NSUTF8StringEncoding];
                [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];
            }
        }

在我们的案例中,for循环只执行一次,因为我们只有一个缓冲区,我们只使用一个通道。

一个信号(由N个样本表示)的功率可以通过以下公式计算:

为 VolumeControllerViewController 编写代码的截图

这里,v 是第 n 个信号样本的值。

因为功率计算必须在实时进行,我们将使用以下由加速框架提供的函数:

  • vDSP_vsq:此函数计算每个输入向量元素的平方

  • vDSP_meanv:此函数计算输入向量元素的均值

小贴士

加速框架

加速框架是一个用于数字信号处理的必备工具,它可以帮助你节省在实现最常用算法的时间,并且主要提供针对内存占用和性能优化的算法实现。有关加速框架的更多信息,请参阅apple.co/1PYIKE8apple.co/1JCJWYh

最终,信号功率存储在_actualVolumeDb中。当_actualVolumeDb的模数低于_desiredVolumeMinDb时,电视的音量太高,我们需要向 Arduino 发送消息来降低它。别忘了_actualVolumeDb是一个负数;当电视音量增加时,模数会减小这个数。相反,当电视音量降低时,_actualVolumeDb的模数会增加,并且当它高于_desiredVolumeMaxDb时,我们需要向 Arduino 发送消息来增加电视的音量。

在对话中的暂停期间,即使说话的音量没有改变,信号的功率往往会降低。如果没有任何调整,在对话期间会持续不断地向电视发送增加和减少的消息。为了避免这种行为,我们只在信号功率超过阈值一段时间后发送音量增加消息(当_increaseVolumeDelay大于 10 时)。

我们可以看看其他不复杂的视图控制器方法。

当属于视图控制器的视图出现时,会调用以下方法:

-(void)viewDidAppear:(BOOL)animated {

     [super viewDidAppear:animated];

    NSError* error = nil;

    [self connect];

    _actualVolumeDb = 0;
    [_audioEngine startAndReturnError:&error];

    if (error) {
        NSLog(@"Error %@",[error description]);
    }

}

在这个函数中,我们连接到 Arduino 板并启动音频引擎,以便开始监听电视。

当视图从屏幕消失时,会调用viewDidDisappear方法,然后我们断开与 Arduino 的连接并停止音频引擎,如下所示:

-(void)viewDidDisappear:(BOOL)animated {

     [self viewDidDisappear:animated];

    [self disconnect];

    [_audioEngine pause];
}

当开关被操作时调用的方法(switchChanged)相当简单:

- (IBAction)switchChanged:(UISwitch *)sender {

    NSError* error = nil;

    if (sender.on) {
        [_audioEngine startAndReturnError:&error];

        if (error) {
            NSLog(@"Error %@",[error description]);
        }
        _volumeSlider.enabled = YES;
    }
    else {
        [_audioEngine stop];
        _volumeSlider.enabled = NO;
    }
}

当音量滑块改变时调用的方法如下:

- (IBAction)volumeChanged:(UISlider *)sender {

    _desiredVolumeDb = 50.*(1-sender.value);
    _desiredVolumeMaxDb = _desiredVolumeDb + 2;
    _desiredVolumeMinDb = _desiredVolumeDb - 3;
}

我们只设置所需的音量和上下阈值。

用于管理蓝牙连接和数据传输的其他方法不需要解释,因为它们与之前的项目完全一样。

测试和调整

我们现在准备好测试我们新的神奇系统,并花更多的时间看电视(或者睡更多的午觉!)让我们执行以下步骤:

  1. 加载Decoder.ino草图并打开 Arduino IDE 控制台。

  2. 将电视遥控器指向 TSOP4838 接收器,并按下增加音量的按钮。你应该在控制台看到如下内容:

    For IR Scope: 
    +4500 -4350 … 
    
    For Arduino sketch: 
    unsigned int raw[68] = {4500,4350,600,1650,600,1600,600,1600,…};
    
  3. 复制大括号之间的所有值。

  4. 打开Arduino_VolumeController.ino文件,并将以下值粘贴进去:

    unsigned int up[68] = {9000, 4450, …..,};
    
  5. 检查两个向量的长度(例如示例中的 68)是否相同,如有必要进行修改。

  6. 将电视遥控器指向 TSOP4838 接收器,并按下降低音量的按钮。复制值并粘贴如下:

    unsigned int down[68] = {9000, 4400, ….,};
    
  7. 检查两个向量的长度(例如示例中的 68)是否相同,如有必要进行修改。

  8. Arduino_VolumeController.ino上传到 Arduino,并将红外 LED 指向电视。

  9. 打开 iOS 应用程序,扫描 nRF8001,然后转到主标签页。

  10. 点击连接,然后通过触摸滑块设置所需的音量。

  11. 现在,你应该看到蓝色 LED 和绿色 LED 闪烁。电视的音量应该稳定到所需的值。

为了检查一切是否正常工作,请使用遥控器增加电视的音量;你应该立即看到蓝色 LED 闪烁,音量降低到预设值。同样,通过使用遥控器降低音量,你应该看到绿色 LED 闪烁,电视的音量增加。

小憩一下,广告就不会将你吵醒!

如何更进一步

以下是在此项目中可以实施的一些改进:

  1. 改变频道和控制其他电视功能。

  2. 通过拍手来打开或关闭电视。

  3. 添加一个按钮来静音电视。

  4. 在接收到电话时静音电视。

总之,你可以使用你学到的红外技术用于许多其他目的。例如,你可以修改第四章中的漫游项目,“iOS 引导式漫游”,通过红外遥控器控制机器人。查看 IRremote 库提供的其他功能,以了解其他提供的选项。你可以在IRremote库文件夹中找到存储的IRremote.h中的所有可用功能。

在 iOS 端,尝试使用 AV Audio Engine 和用于处理信号的 Accelerate 框架进行实验。

摘要

本章重点介绍了一个简单但实用的项目,并教你如何使用红外线将数据从 Arduino 发送和接收。你在这里学到的基本电路和程序有许多不同的应用。

在 iOS 平台上,你学习了从设备麦克风和 DSP(数字信号处理)捕获声音的非常基础的知识。这让你可以利用 iOS 平台的处理能力来扩展你的 Arduino 项目。

下一章将会非常精彩。你将神奇地打开你的车库门;你甚至不需要触摸你的 iOS 设备就能做到这一点。你还将了解到很多关于 iBeacon 技术的知识。你的想象力将是你的唯一限制!

第六章:自动车库门开启器

这个项目是关于一种基于蓝牙 BLE 通信协议的新兴技术——iBeacon。基本上,iBeacon 是一种持续传输独特编码信号的小型设备。iOS 设备可以检测 iBeacon,以确定其与 iBeacon 的距离是更近还是更远,并触发动作。

小贴士

iBeacon 是苹果的技术

iBeacon 是由苹果公司发明的一项技术,该协议尚未公开(如果你在互联网上找到了任何信息,谷歌是你的朋友)。因此,要使用 iBeacon 设备,你需要一个 iOS 设备和苹果公司提供的 API。

另一个正在出现的标准也可以与 Android 设备一起工作——AltBeacon(更多信息,请访问bit.ly/1KsXD17)。AltBeacon 网站也为 iOS 提供了大量有用的信息。

我们将使用这些技术在我们靠近车库门时立即打开它。你可能想知道这与传统的车库遥控器或 iOS 设备上可用的众多遥控应用程序有什么区别。主要区别是这一切都是自动发生的(自动魔法?);你甚至不需要触摸你的手机。信不信由你,你甚至不需要控制器应用程序运行。

小贴士

没有车库?

这个项目可以用来打开任何类型的门,但你可能需要调整或更换你的锁,使其能够通过电信号控制。或者,你也可以使用这个项目来控制内部/外部灯光。绝对不会感到无聊!

让我们通过更仔细地了解 iBeacon 来开始吧。

iBeacon – 技术概述

iBeacon 是一种小型设备,利用蓝牙 BLE 技术在其周围建立区域。任何支持蓝牙 BLE 的 iOS 设备都可以确定自己是否进入了该区域,并大致估算与 iBeacon 的距离。

我们可以在每件博物馆艺术品附近放置一个 iBeacon,并编写一个 iOS 应用程序,当参观者靠近时立即显示艺术品信息。这是一个典型的 iBeacon 应用示例。

iBeacon 通过三个值唯一标识——UUID(一个 16 字节的通用标识符)、major(2 字节)和 minor(2 字节),这些值通过蓝牙信号持续传输。

小贴士

所有你需要了解的 关于 iBeacon 的信息

你可以在developer.apple.com/ibeacon/developer.apple.com/ibeacon/Getting-Started-with-iBeacon.pdf找到所有你需要了解的信息。

参考下图的下一个图,你可以看到 iBeacon 周围有一个区域(即 iBeacon 区域)。当 iOS 设备穿过这个区域的边界并进入时,注册了这个区域的 iOS 应用程序(使用 iBeacon 的 UUID、major 和 minor)会收到一个“进入”通知;当 iOS 设备穿过区域的边界并退出时,会收到一个“退出”通知。

我们将在以下章节中详细讨论这个问题:

iBeacon – 技术概述

在开阔空间中,iBeacon 区域大约有 30 米宽,但这个大小很大程度上取决于硬件设计、配置、障碍物以及安装方式。

当在区域内时,iOS 设备可以持续监控与 iBeacon 设备的距离。这个距离以远、近或立即返回。苹果没有声明这些距离的实际大小,很可能是由于这些距离实际上受到 iBeacon 的发射功率、iBeacon 与 iOS 设备之间的障碍物、iOS 设备的朝向以及其他因素的影响。无论如何,这些信息可以用来根据与 iBeacon 的距离改变应用程序的行为。

小贴士

iBeacon 距离计算

iOS 设备可以检测到 iBeacon 的信号强度。它可以通过一个描述距离衰减的公式来计算与 iBeacon 的距离。不幸的是,由于 iBeacon 信号有很多波动,并且其传播受到许多因素的影响,这个公式对距离的估计非常不准确。为了在实际应用中使用这个值,必须使用概率技术来获取距离的估计值。我们无法在本书中涵盖这些技术。

iOS 处理 iBeacon 通知的一个有趣特性是,即使应用程序没有运行,进入和退出通知也会被接收,并且它们会启动应用程序。为了节省 iOS 设备的电量,应用程序只运行几秒钟(大约 3 秒钟)然后暂停。然后,应用程序必须在这个短暂的时间间隔内完成每一个操作。

我必须指出,通常情况下,iOS 应用在穿过 iBeacon 区域时几乎立即就会收到进入通知。相反,退出通知可能在穿过区域边界后几分钟才会收到。

汽车库门开启器的要求和设计限制

了解 iBeacon 的能力后,想象一个车库门开启器的工作方式并不困难。iBeacon 安装在车库门后面。一个带有 BLE 板的 Arduino 监听来自 iOS 设备的命令并控制车库门开启器。iOS 应用程序在进入 iBeacon 区域时发送“打开”命令,在退出 iBeacon 区域时发送“关闭”命令。非常简单,不是吗?

不幸的是,我们必须面对接收退出通知时可能出现的长时间延迟。当你开车时,你可以在几分钟内覆盖很大的距离,远离你的车库;iOS 设备无法在这么长时间内连接到 Arduino。

此外,在查看下一张图片后,你可能意识到 iBeacon 区域也可能只覆盖房子的一部分。显然,我们不希望带着 iOS 设备在口袋里在房子周围走动时,车库门会随机开启和关闭。

车库门开启器的要求和设计限制

一个简单的解决方案是在房子的各个地方添加所需的 iBeacon,所有这些 iBeacon 都具有相同的 UUID、major 和 minor,以扩展 iBeacon 区域。

然而,这个解决方案将会太昂贵。我们将利用 iOS 的另一个电源特性来克服这两个问题——地理围栏:

车库门开启器的要求和设计限制

我们可以注册一个地理围栏区域(使用中心点的纬度和经度以及半径),覆盖我们整个房子。一旦穿过地理围栏区域的边界,我们会收到通知(或者更好的是,两个通知——一个在进入区域时,另一个在离开区域时)。

你可能会想知道为什么我们不简单地只使用地理围栏区域来开启车库门。不这样做的原因是地理围栏通知并不非常精确。它们可能有很大的延迟,并且可能不在所有区域工作。通过结合这两种技术,我们得到了我们想要实现的确切结果。

我们最终克服了所有涉及在 iBeacon 区域内跟踪设备进入和从地理围栏区域退出的设计挑战。这就是自动车库门开启器的工作原理:

  1. 让我们假设我们同时位于两个区域之外。当我们穿过地理围栏区域时,不会发生任何事情,因为应用程序只识别退出通知。

  2. 一旦我们穿过离车库几米远的 iBeacon 区域,iOS 设备就会收到通知,并发送一个开启命令到控制门的 Arduino 板,从而打开门。

  3. 门会在短时间内自动关闭。这允许我们将车停放在车库内。

  4. 从现在起,穿过 iBeacon 区域的行为将被忽略。然后,如果我们移动到房子周围时穿过 iBeacon 区域,车库将不会开启。

  5. 当我们离开时,我们最终会穿过地理围栏区域。这个事件再次启用了接收 iBeacon 进入通知。我们处于与步骤 1 中描述的相同状态。

我们可以使用以下状态图来描述应用程序的行为(严格来说,它是一个 Mealy 状态机;更多信息,请访问bit.ly/1hmZs3V):

车库门开启器的要求和设计限制

我们显然不希望其他人打开我们的车库。因此,每个授权用户必须通过使用 PIN(个人识别号码)来识别自己。分配新的 PIN,您允许其他人获取对车库的访问权限。您可以通过从授权列表中移除 PIN 来撤销访问权限。

然后,应用程序必须管理 PIN,我们需要一个主 PIN,它被连接到 Arduino 代码中。只有知道主 PIN 的人才有权管理 PIN。

提示

安全警告!

即使 iOS 应用需要 PIN 才能打开车库门,该应用也不是完全安全的,因为它不提供任何加密机制。任何人都可以通过使用蓝牙协议嗅探器来访问 PIN。这并不容易,但却是可能的。我们已经警告过您!使通信安全可能是学习更多关于加密以及如何在 Arduino 上使用有限的内存和处理器能力实现加密的好机会。

硬件

我们需要的最主要硬件组件是 iBeacon。我们使用的是redbear.net上展示的那个。市场上几乎任何价格都有很多产品。在选择 iBeacon 时,请确保它与 iBeacon Apple 协议兼容,因为有些产品并不兼容。

提示

作为 iBeacon 的 iOS 设备

如果您有两个 iOS 设备,您可以使用其中一个作为通过 iTunes 商店中可用的应用程序运行的 iBeacon。我已经为这个目的发布了自己的应用程序,可以在apple.co/1hmZt80找到。

根据 iBeacon 外壳,它可以安装在车库内部或外部。如果放置在较高的位置会更好。通常,为 iBeacon 供电的电池应该至少可以使用一年。定位时,无论如何都要考虑更换电池。

附加电子元件

在这个项目中,我们需要以下附加元件:

  • 一个 P2N2222 BJT 晶体管(详见文本)

  • 一个 BS170 NMOSFET 晶体管(详见文本)

  • 一个 10K 欧姆电阻(详见文本)

  • 一个 1.5K 欧姆电阻(详见文本)

  • 一个 1N4001 二极管(详见文本)

  • 一个继电器:线圈电压 5V,接触电流最大 1A(详见文本)

电子电路

以下图片显示了项目所需的电子电路的电原理图:

电子电路

通常,继电器电流约为 40 mA,这超过了 Arduino 可以提供的最大电流。有一些继电器消耗的电流更少,但为了避免烧毁 Arduino,我们可以使用晶体管来供电继电器。当继电器关闭时,线圈中储存的能量会以反向电流的形式放电到晶体管上,这可能会损坏晶体管。二极管(反向恢复二极管)短路这个电流,保护晶体管免受损坏。

下面的图示展示了如何在面包板上安装电路:

电子电路

电磁继电器是一种消耗电流的机电设备,容易发生故障。因此,你可以通过使用 N-MOSFET 来使用更可靠的电路。这个替代电路在下面的图中展示:

电子电路

下面的图示展示了如何在面包板上安装电路:

电子电路

R1 电阻下拉 MOSFET 的栅极。因此,当驱动引脚(7)为低电平且引脚在 Arduino 的供电阶段浮动时,它会关闭。

注意

你可能需要将 R1 的值调整到 1K 到 1M 的范围内,以确保当不需要时 MOSFET 不会打开。

小贴士

使用 RFduino 而不是 Arduino

对于这个项目,我们可以使用 RFduino 而不是 Arduino(更多信息,请访问www.rfduino.com)。它与 Arduino 兼容,还包括蓝牙设备和相关的软件栈。此外,它还可以同时充当 iBeacon。一个设备可以满足项目的所有硬件需求。它没有我们将用来存储引脚的 EEPROM,但我们可以将它们存储在闪存中。这个项目的原始版本是在 RFduino 上,并且是一个商业产品。对于这本书,我选择使用 Arduino 来避免购买另一件硬件,因为 RFduino 需要更复杂的代码来同时使用蓝牙接收命令和充当 iBeacon。你可以尝试自己在这个 RFduino 上构建项目。这可以是一个学习更多的好机会。查看 RFduino。它是一个惊人的产品!

Arduino 代码

这个项目的完整代码可以从www.packtpub.com/books/content/support下载。

为了更好地理解下一段落的解释,请在阅读时打开下载的代码。

在这个项目中,我们将使用 EEPROM 来存储引脚。实际上,当没有供电时,这种内存不会丢失其内容。

要存储一个引脚,我们使用第一个字符来表示是否使用,并使用最后五个字符来存储实际的引脚(正好是五个字符长)。引脚在 EEPROM 中按顺序存储,从地址 0 开始。

设置代码

请参考下载的代码。由于设置代码相当简单,不需要详细的解释。

设置代码与其他项目的设置代码没有太大区别。让我们来看看 EEPROM 初始化的部分:

void setup() {

  // EEPROM INITIALIZATION - FIRST TIME ONLY

  for (int i = 0; i < 6*NUMBER_OF_PINS; i++)
    EEPROM[i] = 0;

  // Set the master PIN

  EEPROM[0] = 1;
  EEPROM[1] = '1';
  EEPROM[2] = '2';
  EEPROM[3] = '3';
  EEPROM[4] = '4';
  EEPROM[5] = '5';

…

}

for循环初始化用于初始化引脚的 EEPROM 位置为 0。这个循环只需要在代码第一次执行时执行,否则它会清除存储的引脚。在测试和调整部分,我们将提供更多细节。

最后几行将主 PIN 从位置 1(示例中的主 PIN 为 12345)写入。位置 0 设置为 1,以指示下一个五个位置用于存储 PIN。

主程序

循环函数非常简单。它只检查门是否已经打开,然后通过调用pulseOutput函数(该函数使继电器脉冲 300 毫秒)来关闭CLOSING_DOOR_INTERVAL

Arduino 代码的其余部分用于在rxCallback函数中接收到的消息上做出反应。

每条消息由一个 PIN(五个字符)和一些其他字符组成。该 PIN 将被检查,如果未识别,则消息将被拒绝。

主要消息用于打开车库门:O=1,其中是分配给开门用户的 5 个字符长的 PIN。当它被接收时,它会触发继电器并打开门。

以下所有其他消息都与 PIN 管理相关:

  • P,当 iOS 设备请求当前存储在 Arduino EEPROM 中的 PIN 列表。

  • A,当 iOS 设备需要添加新的 PIN 时。命令后的下一个 5 个字节是实际的 PIN。

  • E,当 iOS 设备需要更新现有的 PIN 时。命令后的第一个字节是需要编辑的 PIN 的索引,其后是 5 个字节的新 PIN。

  • D,当 iOS 设备需要删除现有的 PIN 时。命令后的第一个字节是需要删除的 PIN 的索引。

实现每个命令的功能不需要太多解释。请注意,主 PIN(EEPROM 中的位置 0 处的 PIN)永远不会传输到 iOS 应用程序。要更改主 PIN,必须直接在代码中更改。printPins函数(将所有存储的 PIN 输出)可以帮助你理解函数的工作方式。注释掉代码中已经存在的调用。

iOS 代码

在本章中,我们将查看 iOS 应用程序,该应用程序监控 iBeacon 区域和地理围栏区域,并发送命令打开门。该应用程序还管理可以分配给亲属、访客和朋友的 PIN,以便打开你的车库。

该应用程序还可以像传统遥控器一样手动打开车库门。

该项目的完整代码可以从www.packtpub.com/books/content/support下载。

为了更好地理解下一段落的解释,请在阅读时打开下载的代码。

创建 Xcode 项目

我们将创建一个新的项目,就像在前几章中做的那样。以下是你需要执行的步骤:

以下是新项目的参数:

  • 项目类型:标签应用

  • 产品名称:GarageiBeacon

  • 语言:Objective-C

  • 设备:通用

我们必须为此项目设置一个能力,如下所示:

  1. 在 Xcode 的左侧面板中选择项目。

  2. 在右侧面板中选择 能力

  3. 打开 Background Modes 选项并选择 位置更新(见以下截图):创建 Xcode 项目

由于这个项目的结构非常接近宠物门锁,我们可以通过以下步骤重用一部分用户界面和代码(更多详情,请回到第四章中关于 iOS 引导漫游器的项目[part0033.xhtml#aid-VF2I1 "第四章. iOS 引导漫游器"],iOS 引导漫游器,在那里我们几乎做了与这次相同的事情):

  1. 选择 FirstViewController.hFirstViewController.m,右键点击它们,点击 删除,然后选择 移动到废纸篓

  2. 使用相同的程序,删除 SecondViewControllerMain.storyboard

  3. 在 Xcode 中打开 PetDoorLocker 项目。

  4. 选择以下文件并将它们拖放到此项目中:

    • BLEConnectionViewController.h

    • BLEConnectionViewController.m

    • Main.storyboard

    确保已选择 如果需要则复制项目,然后点击 完成

  5. 复制用于 BLEConnectionViewController 视图控制器的图标。

  6. 创建一个新的视图控制器类,名为 GarageViewController

  7. 打开 Main.storyboard 并定位主视图控制器。

  8. 删除所有图形组件。

  9. 打开 身份检查器并将 更改为 GarageViewController

  10. 为了使位置请求授权正确,我们需要添加一个新文件。通过导航到 文件 | 新建 | 文件… 然后选择 iOS - 资源字符串文件。点击 下一步并输入文件名,InfoPlist。最后,点击 创建

  11. 打开新创建的文件并输入以下行:

    NSLocationAlwaysUsageDescription = "This is required in order to make Garage iBeacon working properly.";
    

现在,我们已准备好创建新的应用程序!

为 BLEConnectionViewController 设计用户界面

我们必须向这个视图控制器添加许多组件以添加我们的个人 PIN 和与地理围栏区域相关的信息。

到目前为止,您应该已经精通添加 UIKit 组件及其相关的布局约束。因此,我们不会在这个主题上花费太多时间。您的最终结果应该类似于以下图片。无论如何,如果您需要,您始终可以参考下载的代码:

为 BLEConnectionViewController 设计用户界面

我们还需要将 PIN 文本字段的代理出口设置为 BLEConnectionViewController,以便知道何时发生变化。您可以通过使用连接检查器来完成此操作。

对于 PIN 文本字段,我们需要屏蔽将要输入的值。为此,请执行以下步骤:

  1. 选择字段。

  2. 打开属性检查器。

  3. 选择 Secure Text Entry 复选框。

将新组件链接到代码,您应该得到以下结果:

@interface BLEConnectionViewController ()

@property (strong, nonatomic) IBOutlet UILabel      *deviceUUIDLabel;

@property (strong, nonatomic) IBOutlet UITextField *pinField;
@property (strong, nonatomic) IBOutlet UITextField *longitudeField;
@property (strong, nonatomic) IBOutlet UITextField *latitudeField;
@property (strong, nonatomic) IBOutlet UISegmentedControl   *houseRegionSizeSegment;

@property (strong, nonatomic) CBCentralManager      *centralManager;
@property (strong, nonatomic) NSTimer               *scanningTimer;

@end

设置房屋位置按钮将用于设置围绕房屋的地理围栏区域中心,而房屋区域大小段将用于设置地理围栏区域的半径。

按钮连接到以下方法:

- (IBAction)startLocating:(id)sender {

}

该段连接到以下内容:

- (IBAction)regionSizeChanged:(UISegmentedControl *)sender {

}

设计 GarageViewController 的用户界面

这个视图控制器是应用程序的主要视图控制器,它应该包含一个手动打开/关闭车库门的按钮,以防万一!

由于这是一个学习项目,我们添加了一些组件,以便用户能够获得更多关于其相对于 iBeacon 和地理围栏区域位置的信息。

GUI 应该看起来像以下截图:

设计 GarageViewController 的用户界面

这次,两个按钮都有一个背景(你可以从下载的代码中复制它;它被命名为 buttonBackground.png)。要添加它,选择按钮,打开属性检查器,然后为背景选择buttonBackground.png(见以下截图):

设计 GarageViewController 的用户界面

不要忘记将文本颜色更改为白色。

你还可以从下载的项目中复制三个 LED 图像(blueLED.pnggrayLED.png)。

一旦将 GUI 组件链接到代码,你应该得到以下结果:

@interface GarageViewController ()

@property (strong, nonatomic) IBOutlet UIImageView  *houseRegionIndicator;
@property (strong, nonatomic) IBOutlet UIImageView  *garageRegionIndicator;
@property (strong, nonatomic) IBOutlet UIImageView *readyToOpenIndicator;

@end

此外,两个按钮分别链接到两个方法,如下所示:

- (IBAction)manualOperation:(UIButton *)sender {
}

和:

- (IBAction)simulateHomeRegionExit:(UIButton *)sender {
}

设计 PinsViewController 的用户界面

我们需要另一个视图控制器来管理 PIN。创建并链接到主视图控制器,就像我们在前面的项目中做的那样,并将其嵌入到导航控制器中。为此,选择新的视图控制器,导航到 Editor | Embed In Navigation Controller。这创建了一个导航栏,我们可以在其中放置一个用于添加 PIN 的按钮(见以下截图中的圆形区域)。

GUI 组件在以下截图中显示。基本上,它们是一个用于显示启用 PIN 的 Table View 和一个可以输入主 PIN 的字段。只有知道主 PIN 的人才能管理其他 PIN:

设计 PinsViewController 的用户界面

在继续下一节之前,执行以下步骤:

  1. 创建一个名为 PinsViewController 的新类,该类继承自 UIViewController。

  2. 在故事板中选择 PinsViewController,打开身份检查器,并将 Class 选择为 PinsViewController。

  3. 打开连接检查器(浏览 View | Utilities | Show Connections Inspector)。

  4. 选择 Table View,将 dataSource 和 delegate 输出口拖到 PinsViewController 类(见以下截图)。这告诉 Table View 向 PinsViewController 请求显示的项目和通知事件:设计 PinsViewController 的用户界面

  5. 将主 PIN 字段的控制委托设置为视图控制器。

  6. 为主 PIN 设置 安全文本输入 复选框。

  7. 将 GUI 组件与代码链接起来,你应该得到以下结果:

    @interface PinsViewController ()
    
    @property (strong, nonatomic) IBOutlet UITableView *tableView;
    @property (strong, nonatomic) IBOutlet UITextField *pinField;
    
    @end
    

    以及以下方法:

    - (IBAction)addPin:(id)sender {
    }
    

为 BLEConnectionViewController 编写代码

由于我们从 Pet Door Locker 项目中复制了这个视图控制器,我们只需要做几处修改。

首先,我们需要打开 BLEConnectionViewController.h 文件并添加以下导入:

#import <CoreLocation/CoreLocation.h>

我们还需要对以下行进行修改:

@interface BLEConnectionViewController : UIViewController < CBCentralManagerDelegate>

@end

将前面的行更改为以下内容:

@interface BLEConnectionViewController : UIViewController <CLLocationManagerDelegate, CBCentralManagerDelegate>

@end

然后,打开 BLEConnectionViewController.m 文件,进行其余的修改。让我们添加一个新属性,如下所示:

@property (strong, nonatomic) CLLocationManager     *locationManager;

位置管理器允许我们通过我们 iOS 设备的 GPS 接收器获取我们房屋的地理坐标。这将用于创建地理围栏区域。

要初始化位置管理器,我们必须将 viewDidAppear 方法更改为以下内容:

-(void)viewDidAppear:(BOOL)animated {

     [super viewDidAppear:animated];

    _locationManager = [[CLLocationManager alloc] init];
[_locationManager requestAlwaysAuthorization];

    _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
}

位置管理器必须经过用户授权才能工作。因此,我们需要发出以下方法调用:

[_locationManager requestAlwaysAuthorization];

调用此方法,iOS 会启动一个包含我们在上一节中添加到 InfoPlist 文件中的消息的授权请求。开始接收您自己房屋位置的所需代码如下:

- (IBAction)startLocating:(id)sender {

    _locationManager.delegate = self;
    _locationManager.distanceFilter = kCLDistanceFilterNone;
    _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    [_locationManager startUpdatingLocation];
}

这段代码不需要任何解释。

一旦 GPS 接收器定位了位置,就会调用以下方法,我们可以存储房屋的经纬度:

- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {

    [manager stopUpdatingLocation];
    CLLocation *currentLocation = [locations objectAtIndex:0];

    _latitudeField.text = [NSString stringWithFormat:@"%f",currentLocation.coordinate.latitude];
    _longitudeField.text = [NSString stringWithFormat:@"%f",currentLocation.coordinate.longitude];

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [userDefaults setObject:[NSNumber numberWithFloat:currentLocation.coordinate.latitude] forKey:@"HouseLatitude"];
    [userDefaults setObject:[NSNumber numberWithFloat:currentLocation.coordinate.longitude] forKey:@"HouseLongitude"];
    [userDefaults synchronize];

    sleep(2);  // To be sure that monitoring of region started - To avoid kCLErrorDomain error 5 
}

请注意,一旦坐标可用,我们就停止位置管理器来更新它们([manager stopUpdatingLocation])。我们这样做是为了节省电池电量,因为我们不再检查坐标来确定我们是否在房屋周围的区域内。请参阅以下部分以了解我们如何获取这些信息。

当我们更改地理围栏区域的大小时,会调用以下方法:

- (IBAction)regionSizeChanged:(UISegmentedControl *)sender {

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [userDefaults setObject:[NSNumber numberWithFloat:sender.selectedSegmentIndex+1] forKey:@"HouseSize"];
    [userDefaults synchronize];
}

这是我们存储区域本身大小的位置。

我们需要编写的最后一个方法是在 PIN 修改后存储个人 PIN:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {

    [textField resignFirstResponder];

    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

    [userDefaults setObject:textField.text forKey:@"GarageiBeaconPIN"];
    [userDefaults synchronize];

    return YES;
}

要在视图控制器启动时初始化文本字段的值,将 viewDidLoad 方法更改为以下内容:

- (void)viewDidLoad {

    [super viewDidLoad];

    _deviceUUIDLabel.text = [[NSUserDefaults standardUserDefaults] objectForKey:@"GarageiBeaconDevice"];
    _pinField.text = [[NSUserDefaults standardUserDefaults] objectForKey:@"GarageiBeaconPIN"];

    _latitudeField.text = [[[NSUserDefaults standardUserDefaults] objectForKey:@"HouseLatitude"] stringValue];
    _longitudeField.text = [[[NSUserDefaults standardUserDefaults] objectForKey:@"HouseLongitude"] stringValue];
}

当视图控制器不在屏幕上显示时,我们可以释放位置管理器和中央管理器,如下所示:

-(void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];

    _centralManager = nil;
    _locationManager = nil;
}

现在,我们已经准备好开始编写 GarageViewController 的代码了,它要有趣得多。

为 GarageViewController 编写代码

由于我们同时使用位置管理器和中央管理器,我们需要更新 GarageViewController.h 文件,添加所需的包含和协议,以以下内容结束:

#import <CoreLocation/CoreLocation.h>
#import <CoreBluetooth/CoreBluetooth.h>

@interface GarageViewController : UIViewController <CLLocationManagerDelegate, CBCentralManagerDelegate, CBPeripheralDelegate>

@end

然后,我们打开 GarageViewController.m 文件,添加代码来管理来自地理围栏区域和 iBeacon 区域的通知,并向 Arduino 发送开启消息。

添加以下属性:

@property (nonatomic,strong) CLLocationManager      *locationManager;
@property (nonatomic,strong) CBCentralManager       *centralManager;

@property (strong, nonatomic) CBPeripheral          *arduinoDevice;
@property (strong, nonatomic) CBCharacteristic      *sendCharacteristic;

@property                     BOOL                  insideHouse;

然后,我们可以添加用于管理与 Arduino 蓝牙通信的代码。此代码几乎与我们在前几章中使用的一样。因此,我们不必在这方面花费太多时间。我们只需指出以下几点:

  • 由于我们没有通过蓝牙从 Arduino 接收任何数据,因此可以删除didUpdateValueForCharacteristic函数,我们也不需要寻找用于接收数据的特征

  • 一旦应用程序连接到 Arduino,它立即发送开启命令

didDiscoverCharacteristicsForService方法与我们用于其他项目的略有不同:

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {

    if (error) {

        NSLog(@"Error %@",[error localizedDescription]);

        return;
    }

    for (CBService *service in peripheral.services) {

        if ([service.UUID.UUIDString isEqualToString:NRF8001BB_SERVICE_UUID]) {

            for (CBCharacteristic *characteristic in service.characteristics) {

                if ([characteristic.UUID.UUIDString isEqualToString:NRF8001BB_CHAR_TX_UUID]) {

                    _sendCharacteristic = characteristic;

                    // Device connected - Sending opening command

                    NSData      *data;
                    NSString    *msg;

                    msg = [[NSString alloc] initWithFormat:@"%@O=1",[[NSUserDefaults standardUserDefaults] objectForKey:@"GarageiBeaconPIN"]];
                    data=[msg dataUsingEncoding:NSUTF8StringEncoding];

                    [_arduinoDevice writeValue:data forCharacteristic:_sendCharacteristic type:CBCharacteristicWriteWithoutResponse];

                    // Disconnects

                    [_centralManager cancelPeripheralConnection:_arduinoDevice];
                }
            }
        }
    }
}

一旦启动视图控制器,我们必须初始化位置管理器,并创建地理围栏区域和 iBeacon 区域,如果它们尚未创建:

- (void)viewDidLoad {

    [super viewDidLoad];

    _centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];

    _locationManager = [[CLLocationManager alloc] init];
    _locationManager.delegate = self;

    _locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    _locationManager.distanceFilter = kCLDistanceFilterNone;
    _locationManager.activityType = CLActivityTypeOther;

    [_locationManager requestAlwaysAuthorization];

    for (CLRegion *region in _locationManager.monitoredRegions) {

        [_locationManager requestStateForRegion:region];
    }

    _arduinoDevice = nil;

    // Monitoring change of UserDefaults

    [[NSUserDefaults standardUserDefaults] addObserver:self
                                            forKeyPath:@"HouseLongitude"
                                               options:NSKeyValueObservingOptionNew
                                               context:NULL];

    [[NSUserDefaults standardUserDefaults] addObserver:self
                                            forKeyPath:@"HouseSize"
                                               options:NSKeyValueObservingOptionNew
                                               context:NULL];

    [self addObserver:self
           forKeyPath:@"insideHouse"
              options:NSKeyValueObservingOptionNew
              context:NULL];
}

一旦位置管理器初始化并由用户授权,将调用以下方法并创建 iBeacon 区域(我们将在本节后面讨论实际的 iBeacon 区域创建):

- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {

    if (status == kCLAuthorizationStatusAuthorizedAlways && _centralManager.state == CBCentralManagerStatePoweredOn) {
        [self createGaregeRegionIfNeeded];
    }
}

然后我们调用一个方法:

[[NSUserDefaults standardUserDefaults] addObserver:self
                                            forKeyPath:@"HouseLongitude"
                                               options:NSKeyValueObservingOptionNew
                                               context:NULL];

此方法在用户默认设置中激活了一个键值观察者,其中存储有关地理围栏区域的信息。

提示

键值观察

关于键值观察的更多信息,请参阅苹果文档apple.co/1PZ6aJm

现在,每次HouseLongitude发生变化时,observeValueForKeyPath方法都会被调用,这使我们能够创建或更新地理围栏区域。请注意,当用户定位房子时,HouseLongitude在 BLEConnectionViewController 中发生变化。KVO 技术允许我们自动保持地理围栏区域的更新。KVO 还设置为HouseSize用户默认属性,以便在用户更改区域大小时更新区域。

我们还可以为insideHouse属性设置 KVO。我们将在稍后讨论这一点。

让我们看看observeValueForKeyPath方法,其中实际上创建了地理围栏区域:

-(void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)anObject change:(NSDictionary *)aChange context:(void *)aContext {

    if ([aKeyPath isEqualToString:@"insideHouse"]) {

        if (_insideHouse)
            _readyToOpenIndicator.image = [UIImage imageNamed:@"grayLED.png"];
        else
            _readyToOpenIndicator.image = [UIImage imageNamed:@"blueLED.png"];

        return;
    }

    CLLocationCoordinate2D center;

    center.latitude = [[[NSUserDefaults standardUserDefaults] objectForKey:@"HouseLatitude"] floatValue];
    center.longitude = [[[NSUserDefaults standardUserDefaults] objectForKey:@"HouseLongitude"] floatValue];

    double radius = [[[NSUserDefaults standardUserDefaults] objectForKey:@"HouseSize"] doubleValue];
    radius = (radius == 0) ? 1 : radius;

    //NSLog(@"Latitude %f Longitude %f Radius %f",center.latitude, center.longitude, radius);

    CLCircularRegion *houseRegion = [[CLCircularRegion alloc] initWithCenter:center
radius:100\. * radius
identifier:@"House Region"];
    houseRegion.notifyOnEntry = YES;
    houseRegion.notifyOnExit = YES;

    [_locationManager startMonitoringForRegion:houseRegion];
    [_locationManager requestStateForRegion:houseRegion];
}

要创建地理围栏区域,我们需要定义其中心,即房子的位置,以及一个手动选择的半径。然后,调用以下:

[_locationManager startMonitoringForRegion:houseRegion];

iOS 知道我们需要在进入或退出地理围栏区域时立即接收通知。因此,我们调用:

[_locationManager requestStateForRegion:houseRegion];

我们请求 iOS 确定我们是在地理围栏区域内还是外部,并调用locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region方法来通知我们(我们将在后面讨论此方法)。

让我们看看如何创建 iBeacon。记住,当位置管理器获得授权并且中央管理器(它管理蓝牙连接和通信)开启时,会调用以下方法以创建 iBeacon 区域:

-(void)createGaregeRegionIfNeeded {

    NSArray *regions = [_locationManager.monitoredRegions allObjects];

    NSPredicate *p = [NSPredicate predicateWithFormat:@"identifier == %@",@"Garage Region"];
    NSArray *garageRegions = [regions filteredArrayUsingPredicate:p];

    if (garageRegions.count == 0 && _centralManager.state == CBCentralManagerStatePoweredOn) {

        NSUUID *beaconUUID = [[NSUUID alloc] initWithUUIDString:@"00000000-0000-0000-0000-0000000000FF"];

        CLBeaconRegion *beaconRegion = [[CLBeaconRegion alloc] initWithProximityUUID:beaconUUID
                                                                               major:0
                                                                               minor:1
                                                                          identifier:@"Garage Region"];
        beaconRegion.notifyEntryStateOnDisplay = YES;

        [_locationManager startMonitoringForRegion:beaconRegion];
        [_locationManager requestStateForRegion:beaconRegion];
    }
}

区域的创建与地理围栏区域的创建非常相似。这次,区域有一个 UUID、一个 major 和一个 minor,而不是一个中心和半径。

一旦创建了 iBeacon 区域,我们就请求 iOS 开始监控它([_locationManager startMonitoringForRegion:beaconRegion]),并立即告诉我们我们是否在区域内部或外部([_locationManager requestStateForRegion:beaconRegion])。

现在,让我们看看代码中最重要的一部分——实际管理区域边界跨越并发送打开车库门命令的代码。

每次我们进入一个区域,都会调用didEnterRegion方法:

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region  {

    UILocalNotification* localNotification = [[UILocalNotification alloc] init];
    localNotification.fireDate = nil;
    localNotification.alertBody = [NSString stringWithFormat:@"Entering %@",region.identifier];
    localNotification.timeZone = [NSTimeZone defaultTimeZone];
    localNotification.soundName = @"Chime.aiff";
    [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];

    if ([region.identifier isEqualToString:@"Garage Region"]) {

        [_locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region];

        if (_insideHouse)
            return;

        NSString *deviceIdentifier = [[NSUserDefaults standardUserDefaults] objectForKey:@"GarageiBeaconDevice"];

        if (deviceIdentifier!=nil && _arduinoDevice==nil) {

            NSArray *devices = [_centralManager retrievePeripheralsWithIdentifiers:@[[CBUUID UUIDWithString:deviceIdentifier]]];
            if (devices.count == 0) {
                return;
            }

            _arduinoDevice = devices[0];
            _arduinoDevice.delegate = self;
        }

        [_centralManager connectPeripheral:_arduinoDevice options:nil];

        [self setInsideHouse:YES];
    }
}

前几行向用户发送本地通知,告知他们已经进入了区域边界。

如果你正在进入 iBeacon 区域,并且你在房子内部(_insideHouse = YES),则不会发生任何事。这意味着如果 iBeacon 区域无法覆盖整个房子,并且如果你通过进入房子内部离开 iBeacon 区域,你不会意外地打开车库门。

如果你不在房子里,应用程序会通过蓝牙连接到 Arduino,并打开车库门。别忘了实际的开启命令是发送到didDiscoverCharacteristicsForService方法的。

现在先忘记[_locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region]

提示

启用本地通知

为了发送本地通知,它们必须得到用户的授权。为此,我们需要在应用程序启动时立即调用[application registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:UIUserNotificationTypeAlert|UIUserNotificationTypeBadge|UIUserNotificationTypeSound categories:nil]]方法。didFinishLaunchingWithOptions方法是我们调用它的地方。

每次我们进入一个区域,都会调用didExitRegion方法:

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {

UILocalNotification* localNotification = [[UILocalNotification alloc] init];
    localNotification.fireDate = nil;
    localNotification.alertBody = [NSString stringWithFormat:@"Exiting %@",region.identifier];
    localNotification.timeZone = [NSTimeZone defaultTimeZone];
    localNotification.soundName = @"Chime.aiff";
    [[UIApplication sharedApplication] presentLocalNotificationNow:localNotification];

    [_locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *)region];

    if ([region.identifier isEqualToString:@"House Region"]) {

        [self setInsideHouse:NO];
    }
}

在发送本地通知后,如果我们正在退出地理围栏区域,我们可以将insideHouse属性设置为NO,这样当我们再次进入 iBeacon 区域时,就会发送开启命令。现在先忘记[_locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *)region]

为什么我们不使用传统的代码(_insideHouse = YES)来设置属性?在viewDidLoad方法中,我们为属性设置了一个观察者,以便每次它改变时,都会调用observeValueForKeyPath。传统的代码不会启动observeValueForKeyPath方法,我们需要使用[self setInsideHouse:NO]代替。当observeValueForKeyPath因为insideHouse改变而被调用时,我们更新readyToOpen的图像,让用户了解应用程序是否会在进入 iBeacon 区域时发送开启命令。为此,我们需要将这些代码行放入observeValueForKeyPath方法中:

    if ([aKeyPath isEqualToString:@"insideHouse"]) {

        if (_insideHouse)
            _readyToOpenIndicator.image = [UIImage imageNamed:@"grayLED.png"];
        else
            _readyToOpenIndicator.image = [UIImage imageNamed:@"blueLED.png"];

        return;
    }

提示

后台工作

iBeacon 技术与地理围栏特别有趣的地方在于,即使在应用在后台运行或根本未运行时,didEnterRegiondidExitRegion也会被调用。不幸的是,当应用未运行时,iOS 会启动它并保持运行一段时间(大约 3 秒)以节省电池。因此,任何需要响应事件的动作都必须非常迅速。在代码中,我们只是连接到 Arduino 并发送一些字节到它,这比 3 秒的时间要少得多。

我们需要编写的最后一个相关方法是didDetermineState,当需要确定设备相对于一个区域的位置(调用[_locationManager startMonitoringForRegion:beaconRegion][_locationManager startMonitoringForRegion:houseRegion])或当 iOS 识别到有变化时会被调用。在这个函数中,我们更新指示器,以视觉方式告知用户他们位于被监控区域之一,如下所示:

- (void)locationManager:(CLLocationManager *)manager didDetermineState:(CLRegionState)state forRegion:(CLRegion *)region {

    switch (state) {

        case CLRegionStateInside:
            NSLog(@"Inside %@",region.identifier);
            break;

        case CLRegionStateOutside:
            NSLog(@"Outside %@",region.identifier);
            break;

        case CLRegionStateUnknown:
            NSLog(@"Unknown %@",region.identifier);
            break;
    }

    if ([region.identifier isEqualToString:@"Garage Region"]) {

        if (state==CLRegionStateInside) {

            _garageRegionIndicator.image = [UIImage imageNamed:@"blueLED.png"];
        }
        else {

            _garageRegionIndicator.image = [UIImage imageNamed:@"grayLED.png"];
        }
    }

    if ([region.identifier isEqualToString:@"House Region"]) {

        if (state==CLRegionStateInside) {

            _houseRegionIndicator.image = [UIImage imageNamed:@"blueLED.png"];
        }
        else {

            [self setInsideHouse:NO];
            _houseRegionIndicator.image = [UIImage imageNamed:@"grayLED.png"];
        }
    }

}

注意,视觉信息(在 iBeacon 和地理围栏区域内,准备打开车库门)并不是严格必需的。我们将其放入应用中是为了让你尝试使用 iBeacon 和地理围栏。

最后两个方法如下:

  • manualOperation:通过发送开启命令手动打开车库门。

  • simulateHomeRegionExit:通过手动将insideHouse属性设置为NO来模拟从地理围栏区域退出。这在调试阶段或如果你希望在不实际开车离开你的房子的情况下了解应用的工作原理时可能很有用(我们确实这样做了很多次!)。

代码非常简单,不需要太多解释:

- (IBAction)manualOperation:(UIButton *)sender {

    NSString *deviceIdentifier = [[NSUserDefaults standardUserDefaults] objectForKey:@"GarageiBeaconDevice"];

    if (deviceIdentifier!=nil && _arduinoDevice==nil) {

        NSArray *devices = [_centralManager retrievePeripheralsWithIdentifiers:@[[CBUUID UUIDWithString:deviceIdentifier]]];
        if (devices.count == 0) {
            return;
        }
        _arduinoDevice = devices[0];
        _arduinoDevice.delegate = self;
    }

    if (_arduinoDevice != nil) {
        [_centralManager connectPeripheral:_arduinoDevice options:nil];
    }
}

- (IBAction)simulateHomeRegionExit:(UIButton *)sender {

    //_insideHouse = NO; // This doesn't fire the KVO !
    [self setInsideHouse:NO];
}

最后两个方法(我们保证!)你需要查看的是以下内容:

  • didRangeBeacons:这个方法在本项目中没有使用,但我们展示了它,因为它可能在另一个 iBeacon 项目中非常有用,因为它可以估算 iOS 设备和每个范围内的 iBeacon 之间的距离。可以通过使用[_locationManager startRangingBeaconsInRegion:(CLBeaconRegion *)region][_locationManager stopRangingBeaconsInRegion:(CLBeaconRegion *)region]分别启动和停止 iBeacon 的测距。

  • monitoringDidFailForRegion:这个方法告诉我们是否在监控任何区域时出现了问题。永远不要忘记实现它。

-(void)locationManager:(CLLocationManager *)manager didRangeBeacons:(NSArray *)beacons inRegion:(CLBeaconRegion *)region {

    if ([beacons count] == 0) {
        return;
    }

    CLBeacon *b = beacons[0];

    if (b.proximity == CLProximityFar) {

        NSLog(@"Far");
    }

    if (b.proximity == CLProximityNear) {

        NSLog(@"Near");
    }

    if (b.proximity == CLProximityImmediate) {

        NSLog(@"Immediate");
    }

    if (b.proximity == CLProximityUnknown) {

        NSLog(@"Unknown");
    }
}

- (void)locationManager:(CLLocationManager *)manager monitoringDidFailForRegion:(CLRegion *)region withError:(NSError *)error {

    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Error",nil)
                                                    message:[NSString stringWithFormat:@"Region Monitoring Failed for the region: %@\n%@",[region identifier],[error localizedDescription]]
                                                   delegate:self
                                          cancelButtonTitle:@"Ok"
                                          otherButtonTitles:nil,nil];

    NSLog(@"%@",[error localizedDescription]);

    [alert show];
}

为 PinsViewController 编写代码

这个视图控制器管理着授权你的亲戚和朋友访问你的车库所需的 PIN 码。它几乎与 Power Plug 项目中的 ActivationsTableViewController 以相同的方式工作。因此,我们不会在这个问题上花费太多时间。

这里的主要区别在于我们只需要输入一个 PIN,为它创建一个屏幕没有意义。我们利用 UIAlertView 的一个特性。通过将其样式设置为UIAlertViewStylePlainTextInput,它将显示一个文本框,我们可以输入 PIN。这非常简单方便。

你应该能够自己编写这个视图控制器,并将你的结果与下载的代码进行比较。让我们试试。

测试和调整

我们现在已准备好测试这个项目并给邻居留下深刻印象。首先,你必须设置 iBeacon 参数。如果你使用 RedLab iBeacon,你可以通过免费从 iTunes Store 获取的 iOS 应用来设置它(itunes.apple.com/it/app/redbear-beacontool/id828819434?l=en&mt=8)。

你必须输入以下值:

  • UUID:00000000-0000-0000-0000-0000000000FF

  • 主要:0

  • 次要:1

  • 广告间隔:250 ms

  • TX 功率:0

注意

仔细检查 UUID。它是一个长字符串,任何错误都会阻止 iBeacon 被应用程序识别。

小贴士

调整 iBeacon 参数

一旦一切按预期工作,你可以尝试降低 TX 功率和/或广告间隔。它们越低,你就能节省更多的电量,iBeacon 在没有更换电池的情况下工作的时间就越长。此外,降低 TX 功率允许你在靠近车库时发送开启命令。让我们进行一些测试,以检测最适合你的值。

如果你使用的是不同的 iBeacon,请询问制造商如何设置参数,以确保它们完全相同。

现在,你应该将 iBeacon 放置在你车库门附近的高处,并保持关闭状态。

将继电器触点或 MOSFET 引脚连接到你的车库门开启器(请参阅本章开头提供的电气图)。市面上有很多不同的型号,所以你需要自己来做。一般建议是将继电器输出(或连接到 MOSFET 的输出)并联到你用来手动开启和关闭车库门的按钮。查看你的车库门开启器说明书以获取更多信息和建议。

在开始测试阶段之前,我们需要设置 Arduino 代码,以便正确清理 EEPROM 并存储主 PIN。为此,请执行以下步骤:

  1. setup函数中,注释掉以下行,这些行清理 EEPROM 并存储主 PIN:

      for (int i = 0; i < 6*NUMBER_OF_PINS; i++)
        EEPROM[i] = 0;
    
      // Set the master PIN
    
      EEPROM[0] = 1;     // Don't change this
      EEPROM[1] = '1';
      EEPROM[2] = '2';
      EEPROM[3] = '3';
      EEPROM[4] = '4';
      EEPROM[5] = '5';
    

    注意

    你可以将主 PIN(12345)更改为你喜欢的代码。

  2. 将代码上传到 Arduino。

  3. 再次注释掉之前的代码并将其上传到 Arduino。现在,EEPROM 已清除,主 PIN 已存储。

现在,当你打开应用时,你会看到一个消息。你必须通过选择允许来响应此消息:

测试和调整

在应用中打开配置标签页,扫描 RF8001,选择您的个人 PIN,将其输入到 PIN 字段中,然后点击设置房屋位置。几秒钟后,您应该看到通过 GPS 获取到的您家的经纬度。

房屋区域大小设置为 100 米。只有当您在屋内时车库门意外打开,或者您有一个非常大的房子(真幸运!),才将其更改为更高的值。

点击PINS标签页,输入主 PIN(如果未在 Arduino 代码中更改,则为12345),然后点击Enter。您应该看到一个空列表。点击添加按钮(+)并输入您之前选择的个人 PIN。

点击主标签页,然后点击打开按钮。现在,您的车库应该打开,然后大约 30 秒后关闭。

注意

要更改关闭延迟时间,您必须更改 Arduino 代码中的 CLOSING_DOOR_INTERVAL 的值。此外,为了操作车库门开启器,Arduino 会短接控制线大约 300 毫秒。如果这对您的设备来说不够,您可以在pulseOutput函数中更改延迟。

现在,我们将测试最令人兴奋的功能——自动打开车库:

  1. 再次打开主标签页。

  2. 您应该看到在房屋区域指示灯已开启,在车库区域指示灯已关闭,以及准备打开指示灯已开启。

  3. 如果准备打开指示灯是关闭的,请点击模拟退出

  4. 关闭您的应用,将其发送到后台或从任务列表中关闭。

  5. 打开 iBeacon。

  6. 您应该在屏幕上看到一个通知,并听到短促的声音,然后您的车库门应该开始打开。

  7. 现在,如果您在屋内或屋外移动,您的车库不应该再打开(准备打开指示灯应该保持关闭状态)。

  8. 驾车离开您的住所,直到您从 iOS 设备中听到声音通知。请安全驾驶。在驾驶时,您不需要看您的 iOS 设备。声音通知会提醒您。

  9. 停车并检查应用。现在,准备打开指示灯应该已开启。

  10. 驾车返回您的住所。一旦您靠近 iBeacon,您应该会听到通知,并且车库门应该开始打开。

现在,您已经准备好让您的邻居们印象深刻了!

如何进一步操作

可以对这个项目进行的改进如下:

  • 直接从应用中设置自动门关闭间隔,而不是更改 Arduino 代码。

  • 在进入和离开车库时开关车库灯。

  • 通过使用汽车发动机产生的噪音而不是手动操作来从内部打开车库门。这需要监听 iOS 设备上的电机噪音并将其与预先录制的电机噪音进行比较。这样做是为了避免意外情况下车库内或外的噪音打开门的情况。对于信号比较,你可以使用两个信号的互相关操作(查看 iOS 中可用的 Accelerate 框架),但你必须注意,获取的信号和预先录制的信号可能长度不同,或者可能存在时间偏移。好吧,这是一个大挑战,但这本书的最后一章,你现在应该已经是 Arduino 和 iOS 编程的大师了。数字信号处理是一门你可能感兴趣的技艺。

  • 通过使用翻板开关、霍尔效应传感器或超声波距离传感器来检查车库门是否有效关闭,并将通知发送到您的手机。您可能需要 WiFi 盾牌来利用可用的物联网服务之一发送通知。

摘要

在构建这个项目的过程中,你学到了很多,特别是在 iOS 方面。你学会了如何创建和管理地理围栏和 iBeacon 区域。这为 iOS 上的许多不同项目打开了大门,无论是与 Arduino 集成还是不集成。此外,你还学会了如何监控属性的变化(KVO),这是一种在 Model-View-Controller 模型帮助下,位于良好编程基础的技术。这可以多次应用。在 Arduino 上,你学会了如何使用 EEPROM 来存储需要永久存储在板上以控制程序行为的信息。

这个项目结束了 Arduino 和 iOS 编程以及这两个平台集成的漫长旅程。

希望你在阅读这本书、编码和构建至少一些提议的项目(或者也许全部!)的过程中感到愉快!主要的是,我希望你更多地了解了 Arduino、iOS 及其集成,从现在开始,你可以设计和构建你自己的项目。

制作一些革命性和改变游戏规则的项目,享受乐趣吧!

posted @ 2025-09-28 09:13  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报