Arduino-安卓蓝图-全-
Arduino 安卓蓝图(全)
原文:
zh.annas-archive.org/md5/20c35b9d87797f174fdd245bc9d90b76译者:飞龙
前言
当直接比较 Arduino 和 Android 时,人们可以看到它们是两个截然不同的平台,具有不同的目标。Arduino 主要专注于将物理日常物品连接到嵌入式微控制器。另一方面,Android 旨在提供必要的操作系统和框架,以操作全球无数智能手机。
这种现实也反映了作者们截然不同的现实,他们来自非常不同的背景和文化;斯蒂芬来自马耳他岛,他带来了医学背景和对技术与医学交叉的激情,而马可则来自法国,拥有电气工程背景。
将 Arduino 和 Android 平台的努力结合在一起,产生了令人难以置信的实用项目,这些项目增强了日常生活。牢记这种动机,使得两位来自截然不同背景的作者聚集在一起共同完成这本书。我们相信技术与现实生活的交汇,并展望了一个技术将继续成为我们日常生活不可或缺部分的未来。
本书涵盖的内容
第一章, 设置您的开发环境,介绍了您将需要采取的必要步骤,以便构建本书的所有项目。您将学习如何设置 Android 开发环境。我们还将构建我们的第一个 Arduino 项目。
第二章, 通过蓝牙控制 Arduino 板,教我们如何首次将 Arduino 和 Android 连接起来。我们将构建一个带有蓝牙低功耗模块的 Arduino 系统,并通过 Android 应用程序控制一个简单的 LED 灯。
第三章, 蓝牙气象站,教我们如何使用 Arduino 和 Android 构建我们的第一个实用应用程序。我们将构建一个气象测量站,并通过我们从头开始构建的 Android 应用程序来可视化测量结果。
第四章, Wi-Fi 智能电源插座,教我们如何构建一个流行的设备的 DIY 版本:无线电源开关。我们将使用 Android 应用程序通过 Wi-Fi 与开关通信,控制它,并测量连接设备的能耗。
第五章, Wi-Fi 远程监控摄像头,介绍了一款强大的 Arduino 板,Arduino Yún,用于构建 DIY 无线监控摄像头。我们还将构建一个 Android 应用程序,用于从 Android 手机远程监控这个摄像头。
第六章, Android 手机传感器,解释了如何反过来操作,并使用我们手机上的传感器来控制 Arduino 板。应用这一原理,我们将使用手机的陀螺仪来控制伺服电机的角度。
第七章, 语音激活 Arduino,教我们如何使用强大的安卓语音 API 通过蓝牙控制 Arduino 板。
第八章, 通过 NFC 控制 Arduino 板,展示了如何使用许多安卓手机中现有的 NFC 芯片来激活连接到 Arduino 板的继电器。
第九章, 蓝牙低功耗移动机器人,利用本书迄今为止所学的一切知识,基于 Arduino 构建一个移动机器人。机器人将通过安卓应用程序通过 Wi-Fi 进行控制。
第十章, 脉搏传感器,专注于一个医疗应用,该应用用于测量心率。我们将连接一个心率传感器到 Arduino,并通过蓝牙低功耗监控测量结果。
您需要为此书准备以下内容
您需要两种软件为此书做准备:您将需要的 Arduino 软件,以及您将需要的安卓软件。对于 Arduino,以下是所有章节中您将需要的:
- Arduino IDE(推荐版本为 1.5.7)
您还需要根据章节需要几个 Arduino 库,但这些库的链接将在相关章节中给出。
在安卓端,您还需要以下内容:
-
Android Studio
-
安卓手机上的 Android 4.3 或更高版本
此书面向的对象
Arduino Android Blueprints 面向那些对 Arduino 或安卓生态系统有了解,并希望开始使用这两个平台构建令人兴奋的应用程序的人。
例如,如果您已经在使用 Arduino 平台,并且想要构建远程控制项目的移动应用程序,那么这本书就是为您准备的。
习惯用法
在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名应如下所示:"您的Arduino文件夹是存储所有草图的地方,您可以在 Arduino IDE 的偏好设置中定义此文件夹。"
代码块应如下设置:
android:textSize="200dp"
android:gravity="center"
任何命令行输入或输出都应如下所示:
/distance
新术语和重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中如下所示:"根据您的设备,此选项可能会有所不同,但从 Android 4.2 及更高版本开始,开发者选项屏幕默认隐藏。"
注意
警告或重要注意事项如下所示。
小贴士
小贴士和技巧如下所示。
读者反馈
我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者的反馈对我们开发您真正能从中获得最大价值的书籍非常重要。
要向我们发送一般反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件主题中提及书籍标题。
如果您在某个领域有专业知识,并且对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南:www.packtpub.com/authors。
客户支持
现在您已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大价值。
下载示例代码
您可以从您在www.packtpub.com的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给您。
下载本书的彩色图像
我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。这些彩色图像将帮助您更好地理解输出的变化。您可以从以下链接下载此文件:www.packtpub.com/sites/default/files/downloads/0389OS_ColorImages.pdf。
勘误
尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现了错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。
查看之前提交的勘误表,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。
侵权
互联网上版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。
请通过 <copyright@packtpub.com> 联系我们,并提供涉嫌盗版材料的链接。
我们感谢您在保护我们作者以及为我们提供有价值内容方面的帮助。
问题
如果您在这本书的任何方面遇到问题,您可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决问题。
第一章. 设置您的开发环境
本书的第一章将向您介绍 Arduino 和 Android 开发的基础知识,以便您确信您拥有在本书其余部分找到的更高级教程所需的基础知识。
在 Arduino 方面,我们将构建一个非常简单的项目,使用继电器模块(这是一个可以用 Arduino 控制的开关)和温度湿度传感器。我们还将了解 Arduino IDE 的基础知识以及 aREST 库的基本命令,这是一个易于控制 Arduino 板的框架。我们将在本书的几个章节中使用这个库,以便从 Android 设备轻松控制 Arduino 板。在本章中,我们将通过将 Arduino 板通过 USB 连接到您的计算机来简单地尝试 aREST 库的命令。
从 Android 开发的角度来看,我们将共同努力设置开发环境,并确保您的计算机和 Android 设备已准备好用于开发目的。我们将从一个简单的 Android 应用程序开始,该应用程序显示传说中的文本,Hello World。
Android Studio 是基于 IntelliJ 的集成开发环境(IDE),由 Android 开发团队全面支持,它将为您提供必要的工具和资源,以确保您开发出功能齐全且美观的 Android 应用程序。
Android Studio目前处于测试阶段,但由谷歌的专门团队定期更新,这使得它成为开发 Android 项目的自然选择。
硬件和软件要求
你首先需要的是一块 Arduino Uno 板。在这本书中,我们将使用这块板来连接传感器、执行器和无线模块,并使它们与 Android 设备交互。然后,我们还需要一个继电器模块。继电器基本上是一个可以从 Arduino 控制的电开关,它可以让我们控制像灯这样的设备。本项目使用的是 Polulu 的 5V 继电器模块,该模块在板上正确集成了继电器,并包含了从 Arduino 控制继电器所需的所有组件。以下是本章中使用的继电器模块的图片:

您还需要一个 DHT11(或 DHT22)传感器,以及一个 4.7K 欧姆的电阻,用于温度和湿度测量。电阻基本上是一个限制流入电气设备的电流的装置。在这里,它确保 DHT 传感器的正确工作是非常必要的。
最后,您还需要一个小型面包板和跳线,以便进行不同的硬件连接。
以下是您为这个项目所需的全部硬件组件列表,以及在网上找到这些组件的链接:
-
Arduino Uno 板(
www.adafruit.com/product/50) -
5V 继电器模块(
www.pololu.com/product/2480) -
DHT11 传感器和 4.7K 欧姆电阻(
www.adafruit.com/product/386)
在软件方面,您将需要我们在本书其余部分也将使用的 Arduino IDE。您可以在arduino.cc/en/Main/Software获取它。
安装 IDE 的过程非常简单;您只需打开文件并遵循屏幕上的说明。
您将需要 DHT11 传感器的库,可以在github.com/adafruit/DHT-sensor-library找到。
您还需要在github.com/marcoschwartz/aREST找到的 aREST 库。
要安装某个库,只需在您的Arduino/libraries文件夹中提取文件夹(如果尚不存在,请创建此文件夹)。您的Arduino文件夹是存储所有草图的地方,您可以在 Arduino IDE 的偏好设置中定义此文件夹。
准备 Android 开发需要我们准备好设计和开发应用程序,以下清单将指导您为任何项目准备基础知识:
-
Java 开发工具包版本 6(或更高)
-
Android Studio
-
Android 软件开发工具包
-
带有蓝牙 SMART 技术的 Android 设备
我们还将共同努力确保您的一切都设置得当。
安装 Java 开发工具包
Android Studio 没有Java 开发工具包(JDK)将无法工作;因此,了解您安装的 Java 版本是必要的(在这种情况下,Java 运行时环境将不足以满足要求)。
检查 JDK 版本
为了兼容性,您必须检查您的 JDK 版本。
Mac
打开终端并输入以下命令:
java –version
这将是屏幕上显示的内容:

Windows
打开命令提示符并输入以下命令:
java -version
这将是屏幕上显示的内容:

安装 Java
如果您尚未安装 Java,或者您的版本低于 6.0,请通过以下定制和缩短的链接安装 Java JDK,并选择适用于您的版本:
将打开以下窗口:

对于这些项目的主要建议是安装 JDK 6.0 或更高版本。
选择适用于您的操作系统的 JDK。在基于 Intel 的 Mac 上,您可以参考以下有用的表格来查看您的 Mac 是 32 位还是 64 位:
| 处理器名称 | 32 位或 64 位处理器 |
|---|---|
| 英特尔酷睿单核 | 32 位 |
| 英特尔酷睿双核 | 32 位 |
| Intel Core 2 Duo | 64 位 |
| Intel 四核 Xeon | 64 位 |
| 双核 Intel Xeon | 64 位 |
| 四核 Intel Xeon | 64 位 |
| Core i3 | 64 位 |
| Core i5 | 64 位 |
| Core i7 | 64 位 |
您可以通过点击屏幕左上角的苹果标志,然后选择关于我的 Mac来检查处理器名称。
在 Windows 的情况下,要查看您的计算机是否运行 32 位或 64 位版本的 Windows,您需要执行以下操作:
-
点击开始按钮。
-
右键点击我的电脑,然后点击属性。如果系统下列出x64 版,则您的处理器能够运行 64 位启用应用程序。
安装 Android Studio
让我们看看如何在 Mac 和 Windows 上安装 Android Studio:
-
访问
developer.android.com的 Android 开发者网站。将出现以下屏幕:![安装 Android Studio]()
-
点击Android Studio;您将被引导到登录页面,您的操作系统版本将自动检测,如图所示:
![安装 Android Studio]()
-
接受软件使用协议的条款和条件:
![安装 Android Studio]()
Mac
双击下载的文件,按照提示操作,然后将 Android Studio 图标拖入您的应用程序文件夹:

Windows
打开下载的文件,然后按照以下Android Studio 安装向导窗口完成安装过程:

设置 Android 软件开发工具包
随着 Android Studio 的引入,设置 Android软件开发工具包(SDK)的过程得到了极大的改进,因为最新的 SDK 作为 Android Studio 安装包的一部分预先安装。为了理解以下章节中详细说明的项目,了解如何在 Android Studio 中安装(甚至卸载)SDK 将非常有帮助。
有多种方法可以访问SDK 管理器。最直接的方法是通过以下 Android Studio 主工具栏:

另一个选项是通过启动菜单,您将面临以下选项:

为了访问 SDK 管理器,您需要点击配置,随后将出现以下屏幕,然后点击SDK 管理器:

上一张截图显示了 SDK 管理器的样子。如果您需要安装任何包,您需要勾选该特定包的复选框,点击安装包,然后最终接受许可证,如图所示:

设置您的物理 Android 设备以进行开发
以下是需要执行以启用您的 Android 设备进行开发的三个主要步骤:
-
在您的特定 Android 设备上启用开发者选项。
-
启用USB 调试。
-
通过安全的 USB 调试将安装的 IDE 授权给计算机(具有 Android 4.4.2 的设备)。
启用开发者选项
根据您的设备,此选项可能会有所不同,但从 Android 4.2 及以上版本开始,开发者选项屏幕默认隐藏。
要使其可用,请转到设置 | 关于手机并点击构建号七次。返回上一屏幕后,您将找到已启用的开发者选项。
启用 USB 调试
USB 调试使 IDE 能够通过 USB 端口与设备通信。这可以在启用开发者选项后激活,通过导航到设置 | 开发者选项 | 调试 | USB 调试来检查USB 调试选项。
通过安全的 USB 调试将安装的 IDE 授权给计算机(具有 Android 4.4.2 的设备)
在通过Android 调试桥接器(ADB)进行任何数据传输之前,您必须在您的手机或平板电脑上接受 RSA 密钥。这通过将设备通过 USB 连接到计算机来完成,这会触发一个名为启用 USB 调试的通知。
选择始终允许来自此计算机,然后点击确定。
硬件配置
对于本书的第一个项目,只需要进行很少的硬件连接。我们只需要将继电器模块和 DHT11 传感器连接到 Arduino 板上。
以下图像总结了本章的硬件连接(DHT 传感器位于面包板的左侧,继电器模块位于右侧):

您需要做的第一件事是将 Arduino 板上的电源连接到面包板侧的电源轨。将 Arduino 的 5V 引脚连接到面包板上的红色电源轨,将 Arduino 的 GND 引脚连接到面包板上的蓝色电源轨。
对于 DHT11 传感器,您首先需要查看传感器的引脚配置,可以通过访问www.rlocman.ru/i/Image/2012/09/06/DHT11_Pins.jpg来实现。
您需要首先连接电源;VCC 引脚连接到面包板上的红色电源轨,GND 引脚连接到蓝色电源轨。您还需要将 DATA 引脚连接到 Arduino 板的 7 号引脚。最后,将 4.7K 欧姆电阻放置在传感器的 VCC 和 DATA 引脚之间。
对于继电器模块,您有三个引脚需要连接:VCC、GND 和 SIG。将 VCC 引脚连接到面包板上的红色电源轨,GND 连接到蓝色电源轨,最后将 SIG 引脚连接到 Arduino 的 8 号引脚。
以下是完全组装好的项目的图片:

学习使用 aREST 库
现在我们硬件已经组装好,我们将了解 Arduino 环境的基础知识,以及如何使用我们将在本书的几个章节中使用来从 Android 手机控制 Arduino 的 aREST 库。
aREST 库将使我们能够简单地通过相同的命令在外部控制 Arduino 板,无论是使用 USB 线、蓝牙还是 Wi-Fi。没有这个库,我们将不得不为这本书的所有章节重写相同的代码。要找到关于 aREST 库的完整文档,您可以访问github.com/marcoschwartz/aREST。
Arduino IDE 的主窗口是您输入代码以编程 Arduino 板的地方。
Arduino 代码文件通常被称为草图。以下截图是 Arduino IDE,其中已经加载了本章的代码:

您基本上会使用两个按钮,您可以在工具栏的左侧找到它们。第一个,带有勾选标记的,可以用来编译代码。第二个将用于将代码上传到 Arduino 板。请注意,如果代码尚未编译,上传按钮在上传之前也会编译代码。
Arduino IDE 的第二个重要窗口被称为串行监视器。这是您可以监控 Arduino 项目正在做什么的地方,使用代码中的Serial.print()语句生成调试输出。您可以通过点击 Arduino IDE 主窗口右上角的图标来访问它。
以下截图显示了串行监视器的样子:

我们现在将在这个书中构建我们的第一个 Arduino 草图。我们想要实现的是简单地控制继电器并从 DHT11 传感器读取数据。为此,您将使用 aREST 库,通过从您的计算机发送命令。在本书的下一章中,我们将使用相同的命令,但通过蓝牙或 Wi-Fi 连接。本节的目标确实是让您熟悉 aREST 库的命令。
以下代码是这部分完整的 Arduino 草图:
// Libraries
#include <aREST.h>
#include "DHT.h"
// DHT sensor
#define DHTPIN 7
#define DHTTYPE DHT11
// Create aREST instance
aREST rest = aREST();
// DHT instance
DHT dht(DHTPIN, DHTTYPE);
// Variables to be exposed to the API
int temperature;
int humidity;
void setup(void) {
// Start Serial (with 115200 as the baud rate)
Serial.begin(115200);
// Expose variables to REST API
rest.variable("temperature",&temperature);
rest.variable("humidity",&humidity);
// Give name and ID to device
rest.set_id("001");
rest.set_name("arduino_project");
// Start temperature sensor
dht.begin();
}
void loop() {
// Measure from DHT
float h = dht.readHumidity();
float t = dht.readTemperature();
temperature = (int)t;
humidity = (int)h;
// Handle REST calls
rest.handle(Serial);
}
让我们按照以下步骤探索这个 Arduino 草图的细节:
-
Arduino 草图首先导入项目所需的库:
#include <aREST.h> #include "DHT.h" -
之后,我们需要定义 DHT11 传感器连接到的引脚以及传感器的类型:
#define DHTPIN 7 #define DHTTYPE DHT11 -
我们还需要创建一个 aREST 库的实例:
aREST rest = aREST(); -
我们还需要创建一个 DHT11 传感器的实例,以便我们可以从它测量数据:
DHT dht(DHTPIN, DHTTYPE); -
最后,我们需要创建两个变量来存储我们的测量值:
int temperature; int humidity; -
在草图的
setup()函数中,我们需要启动串行端口:Serial.begin(115200); -
接下来,我们需要公开我们的两个测量变量,以便我们可以通过 aREST 库通过串行端口访问它们。请注意,我们必须传递这些变量的引用,而不是它们的值,如下面的代码所示:
rest.variable("temperature",&temperature); rest.variable("humidity",&humidity); -
我们还为我们项目设置了一个 ID 和名称。这在这里不起任何作用,但只是为了在有多块板的情况下识别我们的板:
rest.set_id("001"); rest.set_name("arduino_project"); -
最后,我们开始启动 DHT11 传感器:
dht.begin(); -
现在,在草图的
loop()函数中,我们从 DHT11 传感器进行测量,并将这些测量值转换为整数(在 C 语言中称为“类型转换”):float h = dht.readHumidity(); float t = dht.readTemperature(); temperature = (int)t; humidity = (int)h; -
注意,在这里我们将这些数字转换为整数,因为这是 aREST 库唯一支持的变量类型。然而,由于 DHT11 传感器的分辨率有限,我们在这里没有丢失任何信息。最后,我们使用以下代码处理来自外部的任何请求:
rest.handle(Serial);注意
注意,本章的所有代码都可以在书的 GitHub 仓库中找到,链接如下:
现在是时候将草图上传到你的 Arduino 板上了。如果你在编译时遇到任何错误,请确保你已经安装了本章所需的全部 Arduino 库。
完成此操作后,只需简单地打开串行监视器(确保串行速度设置为115200)。请注意,你也可以使用自己的串行终端软件完成同样的操作,例如,可以在freeware.the-meiers.org/找到的 CoolTerm。
现在,我们将测试 aREST 库是否正常工作。让我们按照以下步骤进行:
-
首先,我们将查询板的 ID 和名称。为此,输入以下内容:
/id -
你应该会收到以下回答:
{"id": "001", "name": "arduino_project", "connected": true} -
现在,我们将看到如何控制继电器,因为在这本书中我们将多次这样做。首先,我们需要定义继电器引脚,即 Arduino 板的 8 号引脚,是一个输出。为此,我们可以简单地输入以下内容:
/mode/8/o -
你应该在串行监视器上收到以下回答:
{"message": "Pin D8 set to output", "id": "001", "name": "arduino_project", "connected": true} -
现在,为了激活继电器,我们需要将 8 号引脚设置为
HIGH状态。这是通过以下命令完成的:/digital/8/1 -
你将立即收到一个确认消息,并听到继电器咔哒一声。要再次关闭继电器,只需输入以下代码:
/digital/8/0 -
现在,我们将使用 aREST 库从板上读取数据。例如,要读取温度变量,你可以简单地输入以下代码:
/temperature -
你将收到以下确认消息,其中包含温度值:
{"temperature": 28, "id": "001", "name": "arduino_project", "connected": true} -
你也可以为湿度做同样的事情:
/humidity -
你将收到一条类似的回执:
{"humidity": 35, "id": "001", "name": "arduino_project", "connected": true}
如果一切正常,恭喜!您现在已经了解了我们将贯穿整本书的 aREST 库的基础知识。请注意,目前我们通过串行通信使用这些命令,但本书后面,我们将首先通过蓝牙使用相同的命令,然后通过 Wi-Fi 从 Android 设备控制 Arduino 板。
现在我们已经看到了 aREST 库的工作方式,我们将创建我们的第一个 Android 项目。请注意,在本介绍性章节中,我们不会将两者连接起来;这将在本书的下一章中完成。
创建您的第一个 Android 项目
为了在 Android 应用项目的世界中开始,设置一个非常基础的项目非常有用,该项目将经历 Android 应用开发的两个主要过程:编写应用程序并在 Android 物理设备上测试它。
小贴士
下载示例代码
您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。
设置您的第一个 Hello Arduino 项目
当 Android Studio 启动时,请按照以下截图所示点击新建项目:

在 Android 应用开发中,配置项目是一个重要的步骤。本项目将使用 Android 4.3 作为最低目标 SDK,因为我们打算使用低功耗蓝牙 API,该 API 是在 Android 的这个特定版本中引入的。在这种情况下,我们将项目命名为Hello Arduino并写下您的公司域名,因为应用程序包名的惯例是您选择的域名的反向。
参考以下截图:

对于这个特定的项目,我们将选择最基础的项目,空白活动,如以下截图所示。其他选项提供了我们目前不需要的附加功能。

在以下截图中,我们选择空白活动,并需要为主 Java 文件命名。让我们保持为MyActivity:

一旦您完成所有前面的步骤,您将欢迎来到这个工作区,它提供了项目树、主要代码编辑器和显示用户界面(UI)预览的设备的好概述,如以下截图所示:

在这个特定的项目中,我们不需要修改现有的代码,因此我们将继续构建我们的应用程序并在我们的物理 Android 设备上运行它。
在物理设备上安装您的应用程序
之前,我们已经通过 USB 连接并启用了我们的物理 Android 设备。在 Android Studio 中,我们需要设置配置以运行我们的 Android 应用程序。
这是通过从主工具栏中选择编辑配置来完成的,如下截图所示:

在编辑配置窗口中,我们将点击加号并选择Android 应用程序,在那里我们设置以下配置并通过按确定来确认:

在设置好一切之后,我们就可以运行应用程序了。选择我们之前设置的应用程序配置,并按如下截图所示的播放按钮(绿色三角形):

有创建Android 虚拟设备(AVD)的可能性来安装应用程序。然而,在这个时间点,没有支持蓝牙的虚拟模拟器,而我们需要在本书的许多项目中使用蓝牙。因此,我们将专注于设置运行 Android 4.3 或更高版本的物理 Android 设备。
在下一步中,选择您的物理设备并按确定,如下截图所示:

如果您正确设置了所有内容,您应该会在 Android 设备上看到以下内容:

概述
让我们总结一下本书这一章节我们所做的工作。我们构建了一个非常简单的 Arduino 项目,包括 Arduino 板、继电器模块和温湿度传感器。我们看到了如何将这些组件连接在一起,以便我们可以将继电器作为输出控制,并从传感器读取数据。我们还看到了 aREST 库的基础知识,我们将在整个书中使用它来从 Android 设备控制 Arduino 板。
在 Android 端,我们已经为开发准备了我们的 IDE 和 Android 设备,这将为我们准备本书中为您准备的项目,并帮助我们有一个无缝的体验。我们还有机会编译我们的第一个应用程序,并在我们的 Android 设备上运行它。
在这个阶段,您已经可以重复本章中我们采取的步骤,真正熟悉 Arduino IDE、aREST 库的命令以及 Android 开发环境。我们将在本书的其余部分广泛使用这些工具;因此,您熟悉它们至关重要。
第二章.通过蓝牙控制 Arduino 板
本书第二章将介绍如何组装东西,并编写我们的第一个应用程序来通过低功耗蓝牙(BLE)控制 Arduino 板。我们选择使用 BLE 作为本书所有蓝牙项目的标准,因为它是出版时的最新蓝牙通信标准。与之前的蓝牙模块相比,BLE 模块具有低能耗,因为该标准在短时间脉冲中工作,而不是保持持续连接。此外,BLE 提供低延迟,并且与较老的蓝牙标准具有可比的范围。
我们将连接一个 BLE 模块到 Arduino 以及一个我们将通过 Android 应用程序控制的 LED。然后,我们将编写一个 Arduino 草图,使用 aREST 库,以便我们可以通过蓝牙接收来自智能手机或平板电脑的命令。
Android 应用程序也将能够远程控制该板,我们将有机会通过学习如何包含按钮来切换 LED 的开和关来增强用户体验。
以下将是本章的主要收获:
-
将 BLE 模块连接到 Arduino 板
-
编写 Arduino 草图以在 Arduino 板上启用蓝牙通信
-
编写 Android 应用程序以通过蓝牙向 Arduino 板发送命令
硬件和软件要求
对于这个项目,你首先需要一块 Arduino Uno 板。
然后,你需要一个 BLE 模块。我们选择了 Adafruit nRF8001 芯片,因为它附带了一个不错的 Arduino 库,并且已经存在用于控制该模块的 Android 应用程序示例。
以下是本项目使用的模块的特写照片:

你还需要一个你喜欢的颜色的 LED 灯和一个 330 欧姆电阻。最后,为了进行不同的电气连接,你还需要一个面包板和一些跳线。
以下是你将需要用于此项目的所有硬件部件列表,以及如何在网络上找到这些部件的链接:
-
Arduino Uno 板(
www.adafruit.com/product/50) -
330 欧姆电阻(
www.sparkfun.com/products/8377) -
Adafruit nRF8001 扩展板(
www.adafruit.com/products/1697)
在软件方面,你需要以下内容:
-
Arduino IDE(
arduino.cc/en/Main/Software) -
Arduino aREST 库(
github.com/marcoschwartz/aREST/) -
nRF8001 Arduino 库用于 BLE 芯片(
github.com/adafruit/Adafruit_nRF8001)
要安装某个库,只需将文件夹提取到您的Arduino /libraries文件夹中(如果尚不存在,则创建此文件夹)。要找到您的Arduino文件夹或定义一个新的,您可以在 Arduino IDE 的首选项选项中操作。
配置硬件
我们现在将构建项目的硬件部分。为了帮助您,以下是这个项目的原理图:

现在,我们将执行以下步骤:
-
第一步是将蓝牙模块和 LED 放置在面包板上。
-
然后,将 Arduino 板上的电源连接到面包板:Arduino 板的 5V 连接到红色电源轨,GND连接到蓝色电源轨。
-
我们现在将连接 BLE 模块。首先,连接模块的电源:GND连接到蓝色电源轨,VIN连接到红色电源轨。
-
在此之后,您需要连接负责串行外设接口(SPI)通信的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。
-
然后,将REQ引脚连接到 Arduino 引脚10。最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。
-
对于 LED,只需将电阻放置在面包板上,使其与 LED 串联,连接到 LED 的阳极,即 LED 的最长引脚。
-
然后,将电阻的另一端连接到 Arduino 引脚7。
-
最后,将 LED 的另一端(阴极)连接到蓝色电源轨,即地线。
这是完全组装好的项目的图片:

编写 Arduino 草图
我们现在将编写 Arduino 草图,以便 Arduino 板能够与 BLE 模块通信并通过蓝牙接收来自 Android 的命令。以下是这部分完整的草图:
#define LIGHTWEIGHT 1
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
// Pins
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, // on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
// Create aREST instance
aREST rest = aREST();
// BLE instance
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
void setup(void)
{
// Start Serial
Serial.begin(9600);
Serial.println(F("Adafruit Bluefruit Low Energy nRF8001 Print echo demo"));
// Start BLE
BTLEserial.begin();
// Give name and ID to device
rest.set_id("001");
rest.set_name("my_arduino");
}
aci_evt_opcode_t laststatus = ACI_EVT_DISCONNECTED;
void loop() {
// Tell the nRF8001 to do whatever it should be working on.
BTLEserial.pollACI();
// Ask what is our current status
aci_evt_opcode_t status = BTLEserial.getState();
// If the status changed....
if (status != laststatus) {
// print it out!
if (status == ACI_EVT_DEVICE_STARTED) {
Serial.println(F("* Advertising started"));
}
if (status == ACI_EVT_CONNECTED) {
Serial.println(F("* Connected!"));
}
if (status == ACI_EVT_DISCONNECTED) {
Serial.println(F("* Disconnected or advertising timed out"));
}
// OK set the last status change to this one
laststatus = status;
}
// Handle REST calls
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
}
现在,让我们看看这个草图的细节。它首先导入 nRF8001 BLE 模块和 aREST 库所需的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
我们还将为 aREST 库指定一个名为LIGHTWEIGHT的选项。这意味着 Arduino 板将只返回有限的数据回 Android 手机。当我们从板上读取时,它将返回变量的值,当我们向板上发送命令时,则不返回任何数据。这在使用 BLE 通信时是必需的。这是通过以下代码片段完成的:
#define LIGHTWEIGHT 1
然后,我们将定义 BLE 模块连接到的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
注意,我们没有为 BLE 模块的 SPI 引脚定义引脚,因为它们已经在模块的库中定义了。
在此之后,我们可以创建一个 aREST API 的实例,该实例将用于处理通过蓝牙传入的请求:
aREST rest = aREST();
我们还需要为之前定义的引脚创建 BLE 模块的实例:
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
现在,在草图的setup()函数中,我们将启动串行通信,并打印一条欢迎信息:
Serial.begin(9600);
Serial.println(F("Adafruit Bluefruit Low Energy nRF8001 Print echo demo"));
注意,欢迎信息是通过在消息周围使用F()函数打印的,这直接将字符串变量放入 Arduino 程序内存中。这样做是为了为这个草图节省一些动态内存(RAM)。
我们还将初始化 BLE 模块:
BTLEserial.begin();
最后,我们将为我们的板子分配一个 ID 和名称:
rest.set_id("001");
rest.set_name("my_arduino");
在草图的loop()函数中,我们将检查 BLE 模块的状态:
BTLEserial.pollACI();
然后,我们将获取此状态并将其存储在一个变量中:
aci_evt_opcode_t status = BTLEserial.getState();
如果有设备连接到我们的 BLE 模块,我们将使用 aREST 库处理传入的请求:
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
注意
注意,本章的所有代码都可以在本书的 GitHub 仓库中找到,网址为github.com/marcoschwartz/arduino-android-blueprints。
现在是时候将草图上传到您的 Arduino 板上了。完成此操作后,您可以继续开发 Android 应用程序,通过 BLE 草图控制 Arduino 板。
如何创建一个简单的 Android 应用程序以连接到 BLE 模块
连接 Adafruit BLE 模块将给我们机会:
-
学习如何与现有的开源项目合作
-
分析 Java 并了解
Main活动如何连接到布局文件 -
修改代码以通过蓝牙点亮 LED 并使其工作
对于这个项目,我们将使用一个开源项目,它与我们的 Adafruit 蓝牙模块配合得非常好,并且针对 Android Studio IDE 进行了优化。在本章中,我们还将有机会解释代码的不同部分是做什么用的。
要使项目成功运行,您需要确保您已经安装了第一章中概述的必要 SDK。SDK 可以通过 SDK Manager 获取,通过工具 > Android > SDK Manager访问。
第一步是访问 Tony Dicola 的 GitHub 公共仓库,网址为github.com/tdicola/BTLETest,如图下所示:

在这一点上,您可以选择使用 GitHub 桌面应用程序在桌面克隆,或者下载 ZIP 文件并将其提取到您的桌面上,如图下所示:

双击提取的文件(Windows 和 Mac)。
打开 Android Studio,然后单击导入项目和选择提取的文件夹,如图所示:

为了帮助您在选择过程中,您将能够在您需要选择的文件夹旁边看到一个小的 Android 标志,如图下所示:

在成功导入项目后,你可能需要修改 Gradle 设置文件,以确保它正确编译并成功构建。Gradle 设置文件充当我们 Android 项目的首选项管理器,并允许我们管理我们希望为项目包含哪些库。
你可以通过访问项目树并点击app > src然后点击build.gradle来修改 Gradle 设置文件,如下面的截图所示:

我们的建议是将buildToolsVersion修改为19.1.0。不要被标签页中出现的app所迷惑。正确的设置如下所示:

一旦你在 Gradle 的设置选项中修改了设置,你将被要求同步你的项目设置,你可以通过点击立即同步来完成。一旦 Gradle 设置文件设置好,你就可以继续在支持 BLE 的物理 Android 设备上测试应用程序(设备应运行 Android 4.3 或更高版本)。通过工具栏,点击运行,然后选择运行应用程序,接着选择正确的物理设备,如下面的截图所示:

你可以通过点击文本字段然后点击发送来向蓝牙模块发送以下消息:
-
/mode/7/o /
-
/digital/7/1 /
-
/digital/7/0 /
当你看到前一条消息通过 LED 以正确的响应时,它会根据之前的顺序打开和关闭,我们接着将修改布局文件。
修改 Android 布局文件
修改 Android 布局文件将简化用户体验,并允许我们通过点击按钮来开关 LED。在 Android 布局文件中,我们将添加以下按钮:
-
激活引脚以接受输入
-
打开 LED
-
关闭 LED
前往项目树,如下面的截图所示,并按照以下路径操作:app > src > res > layout > activity_main.xml。双击activity_main.xml文件。

Android 布局文件可以通过设计视图或文本视图进行管理,其中尺寸和属性使用 XML 格式设置。在这种情况下,我们将坚持使用设计视图来修改布局,如下面的截图所示:

在设计视图中,有一个调色板,其中包含定义好的用户界面元素,开发者可以将它们拖放到设计视图中以创建自定义布局。为了遵循正确的设计-开发-分发方法,我们将首先创建一个如何使应用看起来和工作方式的纸版原型,如下面的截图所示。在这个时候,我们的纸版原型既不复杂也不遵循设计原则,但我们希望帮助你熟悉这个过程,以便你能够设计高质量的应用。

以这个纸版原型为我们的指南,我们然后可以开始修改设计。我们将从调整滚动视图区域的大小开始,这显示了 Android 物理设备连接到 BLE 模块时接收到的响应。这将使我们能够可视化我们想要设计的布局。
将按钮添加到界面就像从调色板选项拖放按钮到用户界面一样简单。调色板选项位于设计视图的左侧。在这种情况下,我们将添加以下三个按钮:
-
设置输出
-
开启
-
关闭
如果你双击界面中包含的按钮,你将能够更改文本和 ID。标准的 Java 命名约定推荐使用驼峰命名法;因此,你应该如下标识它们:
-
设置输出按钮
-
文本:
设置输出 -
ID:
setOutputBtn
-
-
开启按钮
-
文本:
开启 LED -
ID:
switchOnBtn
-
-
设置输出按钮
-
文本:
关闭 LED -
ID:
switchOffBtn
-
在布局设置完成后,我们可以继续将布局连接到我们的主活动代码。
将修改后的布局连接到相应的活动
从项目树中,按照以下路径:app > src > main > java > com.tonydicola.bletest.app > MainActivity,如下面的截图所示:

双击MainActivity.java。MainActivity.java的屏幕将如下所示。在接下来的段落中,我们将有机会查看代码并了解它在应用中的作用。代码中包含许多注释(以//////开始的语句),将进一步解释这些代码行的角色。

如果我们快速分析代码,我们可以看到以下结构:
-
包名。
-
一个导入语句。
-
私有和公共变量的声明(可以在整个活动中使用)。
-
BluetoothGattCallback:这是处理回调并执行大部分逻辑的方法。 -
onServicesDiscovered:这是处理蓝牙服务发现的方法。 -
onCharacteristicChanged:这是一个处理任何特性变化的方法。 -
onCreate:这是一个处理主布局及其功能的方法。当活动首次显示时调用onCreate方法,在 Android 应用生命周期中扮演着非常重要的角色。本节的大部分代码将与 Android 布局相关。 -
onResume和onStop:这些是构成 Android 应用生命周期一部分的方法,决定了应用在不同点如何反应。 -
sendClick:这是一个处理当点击发送按钮时将运行哪些过程的方法。 -
parseIDs:这是一个将蓝牙模块的 ID 以字符串格式返回的方法。 -
Boilerplate:这是在创建此项目时主要模板内可用的代码,但它不一定与此项目相关。
理解代码将帮助我们做出正确的修改;我们将从添加以下代码开始,将 UI 元素声明为私有变量:
private Button setoutput;
private Button switchon;
private Button switchoff;
然后我们进入 onCreate 方法,在那里我们将添加识别布局中实际按钮的代码,并且我们还将为每个按钮添加 onClickListener 方法,这允许 Android 应用监听用户与按钮的任何交互并相应地行动。
首先,我们将通过添加以下代码来获取 UI 元素的引用:
setoutput = (Button) findViewById(R.id.setToOutputBtn);
switchon = (Button) findViewById(R.id.switchOnBtn);
switchoff = (Button) findViewById(R.id.switchOffBtn);
在这些对 UI 元素的引用之后,我们将添加一些额外的代码,这将使我们能够向 BLE 模块发送正确的消息,并切换开和关闭灯光:
setoutput.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String setOutputMessage = "/mode/7/o /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + setOutputMessage);
}
else {
writeLine("Couldn't write TX characteristic!");
}
}
});
switchon.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String switchOnMessage = "/digital/7/1 /";
tx.setValue(switchOnMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + switchOnMessage);
}
else {
writeLine("Couldn't write TX characteristic!");
}
}
});
switchoff.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String switchOffMessage = "/digital/7/0 /";
tx.setValue(switchOffMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + switchOffMessage);
}
else {
writeLine("Couldn't write TX characteristic!");
}
}
});
在实现了前面的方法之后,我们现在应该能够构建应用并在我们的物理设备上测试它。最终结果应该如下所示:

现在,您应该能够从 Android 应用中成功切换开和关闭灯光。
如何更进一步
您可以采取一些措施来进一步学习本章所学的内容。您可以使用所学知识控制不仅仅是简单的 LED。例如,您可以连接我们在第一章中使用过的继电器模块,并通过蓝牙控制它。这已经允许您通过您的 Android 手机控制更大的设备,例如灯具。当然,这样的项目需要您采取安全预防措施,这些将在我们将构建此类应用的章节中详细说明。
您还可以通过改进用户界面和学习如何使用更美观的按钮、定制的应用图标以及提升用户体验的通用改进来提升 Android 应用。随着本书的深入,我们将有更多机会在此基础上构建代码,并启用更多功能和能力。
摘要
让我们总结一下本章所学的内容。我们连接了一个 BLE 模块到 Arduino,以及一个简单的红色 LED,我们可以远程控制它。之后,我们编写了一个草图,使得 Arduino 板能够通过蓝牙模块接收命令。
在 Android 端,我们抓住机会对一个现有项目进行了分析、修改,并在我们的物理 Android 设备上运行了最终应用程序。
在下一章中,我们将使用本章所学的内容构建一个无线气象站。我们将把几个传感器连接到 Arduino 板上,并通过与 Arduino 板通过蓝牙通信的 Android 应用程序读取这些传感器的数据。
第三章:蓝牙气象站
在本章中,我们将使用 Arduino 和 Android 构建本书的第一个完整应用。我们将使用 Arduino 构建一个小型气象站,它将通过蓝牙被 Android 应用访问。
在 Arduino 端,我们将使用温度和湿度传感器以及环境光强度传感器构建一个简单的气象站。我们将把一个蓝牙低功耗(BLE)模块连接到项目中,以便 Android 手机可以无线访问测量数据。
我们将开发一个简单的 Android 应用,它具有一个界面,允许我们:
-
通过点击按钮访问气象站执行的所有测量
-
在扩大的文本视图中显示每个测量值
硬件和软件要求
对于这个项目,你首先需要一块 Arduino Uno 板。
然后,你需要一个 BLE 模块。我们选择了 Adafruit nRF8001 芯片,因为它附带了一个不错的 Arduino 库,并且已经存在一些用于控制该模块的 Android 应用示例。这正是我们在上一章中使用过的模块。
对于传感器,我选择了 DHT11 传感器来测量温度和环境湿度。DHT11 是一个易于与 Arduino 集成的数字温度和湿度传感器。有几种 Arduino 的解决方案,但这个传感器被选中是因为它是最容易与 Arduino 接口的之一。为了让传感器与 Arduino 一起工作,我们还需要一个 4.7K 欧姆电阻。
我们还将使用一个串联 10K 欧姆电阻的光电传感器来测量环境光强度。光电传感器基本上是一个电阻,其电阻会根据细胞上的入射光而改变。它将被连接到 Arduino 模拟输入以测量环境光强度。
最后,你需要一个面板和一些跳线来制作不同的连接。
以下是需要为此项目准备的全部硬件部件列表,以及如何在网络上找到这些部件的链接:
-
Arduino Uno 板(
www.adafruit.com/product/50) -
DHT11 传感器和 4.7K 欧姆电阻(
www.adafruit.com/products/386) -
10K 欧姆电阻(
www.sparkfun.com/products/8374) -
Adafruit nRF8001 扩展板(
www.adafruit.com/products/1697)
在软件方面,你将需要通常的 Arduino IDE 和 Arduino aREST 库,该库位于github.com/marcoschwartz/aREST/。
光敏电阻从 DHT11 传感器进行测量,您需要从github.com/adafruit/DHT-sensor-library找到的 DHT 库。
对于 BLE 芯片,您还需要在github.com/adafruit/Adafruit_nRF8001找到的 nRF8001 Arduino 库。
要安装某个库,只需将文件夹提取到您的Arduino /libraries文件夹中(如果尚不存在,则创建此文件夹)。
硬件配置
现在,我们将构建这个项目的硬件。为了帮助您,以下是项目的原理图:

现在,我们将执行以下步骤:
-
第一步是将蓝牙模块、DHT11 传感器和光敏电阻放置在面包板上。
-
然后,将 Arduino 板上的电源连接到面包板:Arduino 板的 5V 连接到红色电源轨,而GND连接到蓝色电源轨。
-
现在,我们将连接 BLE 模块。首先,连接模块的电源:GND连接到蓝色电源轨,VIN连接到红色电源轨。
-
之后,您需要连接负责 SPI 接口的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。
-
然后,将REQ引脚连接到 Arduino 引脚10。最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。
对于 DHT 传感器,这是传感器上每个引脚的功能:
![硬件配置]()
-
您需要首先连接电源:VCC引脚连接到面包板上的红色电源轨,而GND引脚连接到蓝色电源轨。
-
您还需要将DATA引脚连接到 Arduino 板的引脚编号7。
-
最后,将 4.7K 欧姆电阻放置在VCC和传感器DATA引脚之间。
-
对于光敏电阻,将 10K 欧姆电阻串联在光敏电阻上。这意味着光敏电阻的一个引脚应该与电阻的一个引脚接触(在面包板上的同一行)。
-
然后,将电阻的另一端连接到蓝色电源轨,将光敏电阻的另一端连接到面包板上的红色电源轨。
-
最后,将光敏电阻和电阻之间的公共引脚连接到 Arduino 板上的模拟引脚A0。
这是完全组装好的项目的图片:
![硬件配置]()
测试传感器
现在,我们将编写一个简单的 Arduino 草图来测试项目的所有传感器。这将确保在编写使用蓝牙的 Android 应用程序之前,所有连接都已正确完成。这是测试传感器的完整草图:
#include "DHT.h"
// DHT sensor
#define DHTPIN 7
#define DHTTYPE DHT11
// DHT instance
DHT dht(DHTPIN, DHTTYPE);
void setup()
{
// Initialize the Serial port
Serial.begin(9600);
// Init DHT
dht.begin();
}
void loop()
{
// Measure from DHT
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
// Measure light level
float sensor_reading = analogRead(A0);
float light = sensor_reading/1024*100;
// Display temperature
Serial.print("Temperature: ");
Serial.print((int)temperature);
Serial.println(" C");
// Display humidity
Serial.print("Humidity: ");
Serial.print(humidity);
Serial.println("%");
// Display light level
Serial.print("Light: ");
Serial.print(light);
Serial.println("%");
Serial.println("");
// Wait 500 ms
delay(500);
}
让我们更详细地看看这个草图。它首先包含 DHT11 库:
#include "DHT.h"
我们还声明传感器连接到引脚编号 7,并且我们使用的 DHT 传感器是 DHT11 传感器,通过声明常量:
#define DHTPIN 7
#define DHTTYPE DHT11
之后,我们可以声明 DHT 传感器的实例:
DHT dht(DHTPIN, DHTTYPE);
在草图的 setup() 函数中,我们将开始串行通信:
Serial.begin(9600);
我们还将初始化 DHT 传感器:
dht.begin();
在草图中的 loop() 函数中,我们将从传感器执行温度和湿度测量:
float temperature = dht.readTemperature();
float humidity = dht.readHumidity();
我们还将从光敏电阻读取数据,并将此读取值转换为照度百分比。为此,我们必须知道 Arduino 的模拟输入返回一个从 0 到 1,023(10 位)的值。因此,我们需要将输入的读取值除以 1,023。然后,为了得到一个百分比结果,我们将此值乘以 100:
float sensor_reading = analogRead(A0);
float light = sensor_reading/1024*100;
当测量完成后,我们将每个测量值打印到串行端口,以便我们可以可视化数据。例如,以下代码将打印温度:
Serial.print("Temperature: ");
Serial.print((int)temperature);
Serial.println(" C");
我们还将每 500 毫秒重复每个 loop() 函数:
delay(500);
注意
注意:所有本章的代码都可以在本书的 GitHub 仓库中找到,网址为 github.com/marcoschwartz/arduino-android-blueprints。
现在是时候测试这个简单的 Arduino 草图,以检查我们的传感器是否正常工作。将草图上传到 Arduino 板,并打开串行监视器(确保串行速度设置为 9,600)。根据你的环境,你应该在串行监视器中看到类似的结果:
Temperature: 26 C
Humidity: 35%
Light: 75.42%
编写 Arduino 草图
现在我们知道我们的传感器工作正常,我们可以编写最终的草图,允许 Arduino 板被我们稍后将要编写的 Android 应用程序访问。以下是这一部分的完整草图:
// Control Arduino board from BLE
// Enable lightweight
#define LIGHTWEIGHT 1
// Libraries
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
#include "DHT.h"
// Pins
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2
#define ADAFRUITBLE_RST 9
// DHT sensor
#define DHTPIN 7
#define DHTTYPE DHT11
// DHT instance
DHT dht(DHTPIN, DHTTYPE);
// Create aREST instance
aREST rest = aREST();
// BLE instance
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
// Variables to be exposed to the API
int temperature;
int humidity;
int light;
void setup(void)
{
// Start Serial
Serial.begin(9600);
// Start BLE
BTLEserial.begin();
// Give name and ID to device
rest.set_id("001");
rest.set_name("weather_station");
// Expose variables to API
rest.variable("temperature",&temperature);
rest.variable("humidity",&humidity);
rest.variable("light",&light);
// Init DHT
dht.begin();
// Welcome message
Serial.println("Weather station started");
}
void loop() {
// Measure from DHT
float t = dht.readTemperature();
float h = dht.readHumidity();
temperature = (int)t;
humidity = (int)h;
// Measure light level
float sensor_reading = analogRead(A0);
light = (int)(sensor_reading/1024*100);
// Tell the nRF8001 to do whatever it should be working on.
BTLEserial.pollACI();
// Ask what is our current status
aci_evt_opcode_t status = BTLEserial.getState();
// Handle REST calls
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
}
现在,让我们更详细地看看这个草图。其中一些部分与之前测试传感器的草图类似;我们不会再次详细说明这些部分。它首先声明我们想要使用 aREST 库的轻量级模式:
#define LIGHTWEIGHT 1
然后,我们将定义我们想要使用蓝牙芯片的库、aREST 库和 DHT 传感器的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
#include "DHT.h"
然后,我们将定义连接 BLE 模块的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2
#define ADAFRUITBLE_RST 9
我们需要创建 aREST 库的实例:
aREST rest = aREST();
我们还需要创建一个 BLE 模块的实例:
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
在草图中的 setup() 函数之前,我们将声明以下三个变量,这些变量包含来自传感器的测量值:
int temperature;
int humidity;
int light;
然后,在草图的 setup() 函数中,我们将初始化 BLE 模块:
BTLEserial.begin();
之后,我们将为我们的项目设置一个 ID 和名称:
rest.set_id("001");
rest.set_name("weather_station");
我们还必须将不同的测量变量暴露给 aREST API,以便 Android 应用程序可以访问它们:
rest.variable("temperature",&temperature);
rest.variable("humidity",&humidity);
rest.variable("light",&light);
在草图中的 loop() 函数中,我们将轮询 BLE 模块的状态:
BTLEserial.pollACI();
我们还将获取模块的状态并将其存储在一个变量中:
aci_evt_opcode_t status = BTLEserial.getState();
如果这个状态指示蓝牙模块连接到另一个设备,我们将使用 aREST 库处理传入的请求:
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
注意
注意,本章的所有代码都可以在本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
现在是时候将草图上传到你的 Arduino 板上了。完成此操作后,你可以继续开发 Android 应用程序,通过 BLE 草图来控制 Arduino 板。
绘制我们的 Android 应用程序和修改布局文件
我们将开始我们的 BLE 气象站项目,通过在 Android Studio 中创建一个新的空白活动来启动项目。
我们的目标项目将针对最低 SDK 版本 18 和最高 SDK 版本 19。
我们将首先绘制一个纸制原型,展示我们的应用程序如何工作以及基本用户流程,如下面的图像所示。这将帮助我们理解应用程序的工作方式,并促进我们的开发过程。

通过分析前面的图像,我们可以看到这个设计将需要两个TextView对象。上面的TextView对象将显示所有蓝牙回调、状态变化和写入 BLE 模块的特性,而下面的TextView对象将根据哪个按钮被点击显示温度、光线和湿度传感器的输出。
TextView对象将被赋予以下 ID:
-
connectionStatusView -
dataOutputTextView
在布局的下半部分,我们将有三个按钮,反映我们将请求的三个参数,即温度、光线和湿度。我们将按钮命名为如下:
-
温度按钮将被命名为如下:
-
Text:
Temperature -
ID:
temperatureButton
-
-
湿度按钮将被命名为如下:
-
Text:
Humidity -
ID:
humidityButton
-
-
灯光按钮将被命名为如下:
-
Text:
Light -
ID:
lightButton
-
实现主活动中的 Android 布局
在我们开始这个项目之前,我们将启用Auto-Import功能,这将使我们能够更有效地编译我们的项目,并减少我们需要担心的事情。
你可以通过进入首选项选项并选择所有可用选项来启用Auto-Import。在 Mac 和 Windows 上,Auto-Import首选项如下:
-
在 Mac 上,导航到Android Studio > 首选项 > 编辑器 > Auto-Import
-
在 Windows 上,导航到文件 > 设置 > 编辑器 > Auto-Import
在所有必要的设置就绪后,我们将首先创建一个新的项目,在新建项目设置中选择以下内容:
-
Name:
Bluetooth Weather Station -
Minimum SDK:
18 -
Project:
Blank Activity -
Activity Name:
MainActivity -
Domain:
arduinoandroid.com
我们将在第二章通过蓝牙控制 Arduino 板的“通过蓝牙控制 Arduino 板”项目基础上进行扩展,即 Arduino BLE Android 项目将首先从 GitHub 仓库导入arduinoBLE项目并将其克隆到我们的桌面或下载为 ZIP 文件,如第二章通过蓝牙控制 Arduino 板中所述。
一旦导入,我们将打开MainActivity.java,选择import语句下面的所有代码并复制。当所有代码都已复制后,我们将打开我们的当前项目(Android 蓝牙气象站),进入MainActivity.java,删除import语句下面的所有代码,并粘贴代码。
如果你在项目的这个阶段遇到困难,我们的代码将在仓库中以两个阶段提供,一个是包含所有需要修改的必要代码的版本,另一个是完成的项目。所有这些都可以在 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
一旦代码在我们的项目中,我们将通过更改对 UI 元素的引用来反映我们在onCreate()方法中对 Android 布局文件的最新添加:
dataOutput = (TextView) findViewById(R.id.dataOutputTextView);
connectionOutput = (TextView) findViewById(R.id.connectionStatusView);
adapter = BluetoothAdapter.getDefaultAdapter();
temperature = (Button) findViewById(R.id.temperatureButton);
light = (Button) findViewById(R.id.lightButton);
humidity = (Button) findViewById(R.id.humidityButton);
在这个项目中,我们将修改onClickListeners以连接到我们在 Android 布局文件中包含的按钮:
temperature.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String setTempMessage = "/temperature /";
tx.setValue(setTempMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + setTempMessage);
} else {
writeLine("Couldn't write TX characteristic!");
}
}
});
light.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String setLightMessage = "/light /";
tx.setValue(setLightMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + setLightMessage);
}
else {
writeLine("Couldn't write TX characteristic!");
}
}
});
humidity.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
String setHumidityMessage = "/humidity /";
tx.setValue(setHumidityMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeLine("Sent: " + setHumidityMessage);
}
else {
writeLine("Couldn't write TX characteristic!");
}
}
});
我们还将修改处理写入remoteCharacteristics的代码,即writeLine()方法,此外,我们还将添加另一个名为writeSensorData()的方法,它将处理来自我们不同传感器的远程数据:
private void writeLine(final CharSequence text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
connectionOutput.setText("");
connectionOutput.append(text);
connectionOutput.append("\n");
}
});
}
//Implement the method below to output temperature/humidity/light readings to dataOutputView
private void writeSensorData(final CharSequence text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.e(LOG_TAG,text.toString());
output=text.toString().trim();
if (output.length() > 0 && output.length() <=3) {
dataOutput.setText(output);
}
else {
return;
}
}
});
}
在我们能够继续编译项目之前,我们需要处理onCharacteristicChanged方法,以便从传感器数据接收到的数据将被设置为dataOutput文本视图:
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
writeSensorData(characteristic.getStringValue(0));
}
在当前这个时间点,项目将无法正常运作,因为必要的权限尚未实施。用户权限是必要的,因为它允许应用程序访问设备的各种功能。在这种情况下,我们需要在AndroidManifest.xml文件中添加以下两个权限,您可以通过导航到app > src > main > AndroidManifest.xml来找到该文件:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
当我们完成所有这些更改时,我们应该期望基本用户界面看起来如下,传感器数据将在点击不同参数后显示:

提升用户界面
当前用户界面需要进一步改进以使其更友好。人们很容易注意到传感器数据输出需要放大并居中,按钮肯定可以更吸引人。此外,我们想要确保我们的气象站应用在用户当前的应用列表中脱颖而出,因此我们的应用肯定将从图标的变化中受益。
我们将专注于以下主要任务:
-
创建并添加我们自己的 Android 应用图标
-
居中并放大数据输出文本
-
修改按钮并给我们的文本添加一些颜色
创建并添加我们自己的应用图标
我们增强用户体验的第一步之一就是拥有我们自己的图标。
首先,我们将开始下载图像资产。这个资产可以在bit.ly/chapter3-iclauncher公开获取。
您应使用项目树进行导航,然后在app上右键单击,如下截图所示:

在您右键单击app后,通过转到新建 > 图像资产来创建一个新的图像资产,如下截图所示:

您将看到一个资产工作室弹出窗口,它允许您选择您自己的图像文件。出于优化目的,我们建议选择分辨率为 144 像素×144 像素的.png文件。Android Studio 会自动进行所有调整大小和资源创建,以适应不同的屏幕,如下截图所示:

一旦您选择了我们提供的ic_launcher图像文件,您将看到一个显示图标在不同尺寸的屏幕。点击下一步,您将看到以下屏幕:

前一个屏幕警告您之前的文件将被覆盖,并再次向您展示不同分辨率的图像启动器文件。点击完成,然后编译应用,在您的物理设备上启动它,您应该在应用托盘和应用的操作栏中看到如下所示的东西:

这是应用的操作栏将看起来像:

居中并放大数据输出文本
为了编辑显示传感器数据的主文本布局,我们需要打开项目树并导航到布局文件,该文件位于app > src > main > res > layout > activity_main_screen.xml。
一旦进入这种视图,我们建议您使用文本视图修改文本。这将使您获得更精细的控制,并且也会让您习惯于在编程编辑 Android 布局文件时使用的不同约定。
当打开activity_main_screen.xml文件时,我们将看到按钮和文本视图的不同 XML 代码。在此阶段,请注意负责Sensor Data Output TextView 的代码,并添加以下代码:
android:textSize="200dp"
android:gravity="center"
负责显示Sensor Data Output TextView 的整个代码块现在如下所示:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="@+id/dataOutputTextView"
android:layout_gravity="center_vertical"
android:textSize="200dp"
android:gravity="center"
android:text="99" />
在此代码块中,我们暂时使用了占位文本99,以便我们可以近似地看到它使用 Android 布局设计器时的样子。通过这次修改,传感器数据输出现在足够大,用户可以看得到,从而提升了用户体验。
修改按钮并添加一些颜色到我们的文本
最后,我们将通过以下步骤修改我们的按钮并为文本添加一些颜色:
-
我们将遵循以下两个步骤来创建新的按钮:
-
创建一个名为
buttonshape.xml的新 XML 可绘制文件,并将其放入一个名为drawable的文件夹中。 -
我们将接着将可绘制资源文件连接到主 Android 布局文件。
-
-
通过在
res文件夹上右键单击,创建drawable文件夹,该文件夹可通过导航到App>src>main>res找到。 -
在
res文件夹内创建drawable文件夹后,我们需要再次右键单击新的drawable文件夹,点击新建并选择可绘制资源文件,如图所示:![修改按钮并添加一些颜色到我们的文本]()
-
将文件命名为
buttonshape,输入shape作为根元素,然后点击确定,如图所示:![修改按钮并添加一些颜色到我们的文本]()
-
在
buttonshape.xml文件中,我们将添加以下代码:<?xml version="1.0" encoding="utf-8"?> <shape > android:shape="rectangle" > <corners android:radius="10dp" /> <solid android:color="#FFFFFF" /> <padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" /> <size android:width="85dp" android:height="99dp" /> <stroke android:width="2dp" android:color="#4A90E2" /> </shape> -
然后,我们转向
activity_main_screen.xml文件,并在按钮模块中包含以下代码来引用这个可绘制资源:android:background="@drawable/buttonshape" -
我们还将在
activity_main_screen.xml文件中的按钮和 TextView 模块中添加以下代码行,以增加一些趣味性:android:textColor="#4A90E2"
在前面的代码中,#4A90E2指的是应用图标中使用的主体颜色的十六进制代码,以便我们保持与主要用户界面的某种一致性。
最终布局在 Nexus 5 智能手机上看起来如下所示:

需要注意的是,不同的 Android 设备有不同的尺寸。因此,对于您的特定 Android 设备,您可能需要在 Android 布局文件中进行进一步的优化,以改善界面。
如何进一步操作
在 Android 应用程序中,针对改进用户界面流程可以做出大量的改进。目前,服务发现仅通过物理旋转设备来刷新,因为设备旋转时会调用 onResume() 方法。这可以通过在操作栏中添加一个刷新图标并将其连接到代码来轻松改进,这样当图标被点击时就会调用此方法。
此外,进一步的用户界面定制可以使应用程序个性化,以便符合您的喜好;关于这个应用程序,您可以通过查看以下来自 Android 开发者网站的链接来了解可能性:
-
Button 小部件文档在
developer.android.com/reference/android/widget/Button.html -
TextView 文档在
developer.android.com/reference/android/widget/TextView.html
您甚至可以通过实时监控、统计和趋势进一步扩展应用程序。
摘要
在本章中,我们使用 Arduino 和 Android 构建了一个简单的气象站。我们将多个传感器连接到我们的 Arduino 板上,以及一个低功耗蓝牙模块。我们还构建了相应的 Android 应用程序,这样我们只需点击手机的按钮即可访问 Arduino 板上测量的所有数据。
在下一章中,我们将使用不同的技术通过 Android 与 Arduino 板进行交互:Wi-Fi。我们将构建一个智能电源开关,用于远程控制电器,并通过 Wi-Fi 测量电器的功耗。
第四章:Wi-Fi 智能电源插座
在本章中,我们将构建一个非常常见连接对象的开源版本——Wi-Fi 电源插座。实际上,这种插座可以从许多商店购买,通常附带其自己的 iOS 或 Android 应用程序。
在本章中,我们将从头开始构建这样一个电源插座,基于 Arduino。我们将连接一个继电器模块、一个电流传感器和一个 Wi-Fi 模块到 Arduino 板上,以制作我们自己的 Wi-Fi 电源插座。这个电源插座能够开关任何设备,并且会持续测量设备的功耗。
我们将构建一个 Android 应用程序,通过 Wi-Fi 远程开关电源插座。我们还将能够根据请求获取功率输出并在屏幕上显示。
以下将是本章的主要收获:
-
将继电器模块、电流传感器和 Wi-Fi 模块连接到 Arduino
-
通过 Wi-Fi 发送命令控制项目
-
构建一个 Android 应用程序以从手机或平板电脑控制项目
硬件和软件要求
首先,让我们看看本章所需的硬件组件。
我们需要一个 Arduino Uno 板。为了远程控制灯具(在本章中,灯具被用作示例,但当然任何 110V 或 230V 的设备都可以在这里使用),你还需要一个继电器模块。我们使用了 Polulu 的 5V 继电器模块,但你也可以使用你想要的任何 5V 继电器模块。
为了测量连接到插座的设备的即时功耗,你还需要一个电流传感器。对于这部分,我们将选择基于 ACS712 芯片的扩展板。以下是我使用的板子的图片:

你还需要一个包含 CC3000 Wi-Fi 芯片的板子,我们将使用它通过 Android 设备接收命令。对于这个项目,我们将选择 Adafruit 的 CC3000 扩展板。当然,你也可以使用同一品牌的盾牌进行这个项目;代码将完全相同。
为了进行不同的连接,你还需要一个面包板和一些跳线。
以下是在这个项目中使用的组件列表:
-
Arduino Uno 板 (
www.adafruit.com/products/50) -
5V 继电器模块 (
www.pololu.com/product/2480) -
Adafruit CC3000 Wi-Fi 扩展板 (
www.adafruit.com/product/1469)
要将灯或任何其他设备连接到项目,您需要一对电源线:一个公电源插头和一个母电源插头。您还需要一些螺丝端子来制作所需的连接。以下是我为这个项目使用的电缆的图片:

注意
警告:
在这样的项目中使用高电压设备可能会有危险。因此,请确保仔细遵循下一节中的所有说明。当然,您可以在不将项目连接到主电源的情况下制作整个项目;原理完全相同。
在软件方面,您需要最新的 Arduino IDE 版本。您需要位于github.com/adafruit/Adafruit_CC3000_Library的 CC3000 芯片库。
您还需要位于github.com/marcoschwartz/aREST的 aREST 库。
要安装 Arduino 库,只需将library文件夹放入您主Ard uino文件夹内的/libraries文件夹中。
配置硬件
现在是组装项目硬件部分的时候了。让我们从连接 Adafruit CC3000 扩展板开始。首先,将 Arduino Uno 的+5V 引脚连接到面包板上的红色电源轨,将地线引脚连接到蓝色电源轨。
然后,将 CC3000 板的 IRQ 引脚连接到 Arduino 板的3号引脚,VBAT连接到5号引脚,CS连接到10号引脚。之后,您需要将SPI引脚连接到 Arduino 板:MOSI、MISO和CLK分别连接到11、12和13号引脚。最后,注意电源供应:VIN连接到 Arduino 的 5V(红色电源轨),GND连接到GND(蓝色电源轨)。
以下是该项目的示意图,未连接继电器模块:

现在,我们将连接继电器模块。首先,连接电源:继电器的VCC引脚连接到红色电源轨,GND引脚连接到蓝色电源轨。然后,将继电器的信号引脚(通常标记为 SIG)连接到 Arduino 的8号引脚,然后是电流传感器。像继电器一样,首先连接电源:继电器的VCC引脚连接到红色电源轨,GND引脚连接到蓝色电源轨。然后,将传感器的信号引脚(通常标记为 SIG 或 OUT)连接到 Arduino 的模拟引脚A0。
现在,我们将负责将项目连接到您想要控制的设备以及主电源。
注意
在这一步要非常小心,因为它涉及到高电压(110V 或 230V),可能致命。同时,确保在检查其他所有内容时,始终将项目连接到主电源。当所有其他连接完成时,确保您没有触摸任何裸露的电线。还建议您将整个项目放入塑料外壳中。
以下电路图描述了不同电缆如何连接到继电器和电流传感器:

注意,因为我们使用的是交流电压,所以电缆的极性在这里并不重要。
以下是一张说明电缆、继电器和电流传感器之间不同连接的图片:

最后,以下是一个完整项目的图片,其中公电缆连接到主电源,灯泡连接到母插座:

测试继电器
我们将通过测试继电器并开关它来测试项目。这将确保继电器正确连接到您的 Arduino 板,并且电源电缆连接正确(否则,不会有电流流经连接的设备)。再次检查在将项目插入主电源之前每个连接是否正确。
以下是该部分的完整 Arduino 草图:
// Relay pin
const int relay_pin = 8;
void setup() {
pinMode(relay_pin,OUTPUT);
}
void loop() {
// Activate relay
digitalWrite(relay_pin, HIGH);
// Wait for 5 seconds
delay(5000);
// Deactivate relay
digitalWrite(relay_pin, LOW);
// Wait for 5 seconds
delay(5000);
}
我们现在将考虑这个草图的细节。它首先声明继电器连接到的引脚:
const int relay_pin = 8;
然后,在草图的setup()函数中,我们将此引脚声明为输出:
pinMode(relay_pin,OUTPUT);
最后,在草图的loop()函数中,我们将每 5 秒切换引脚从开启状态到关闭状态:
// Activate relay
digitalWrite(relay_pin, HIGH);
// Wait for 5 seconds
delay(5000);
注意,您可以在本书的 GitHub 仓库中找到该部分的完整代码:github.com/marcoschwartz/arduino-android-blueprints。
确保一切连接正确,您有一个设备(如灯泡)连接到我们的项目,并且项目已插入主电源。再次检查在将项目插入主电源之前每个连接是否正确。现在,您可以上传草图到您的 Arduino 板。您应该听到继电器开关的声音,并看到灯泡开关的动作。
编写 Arduino 草图
现在我们确信继电器、电流传感器和电源电缆的连接是正确的,我们将编写一个 Arduino 草图来接受来自 Android 设备的 Wi-Fi 连接。
以下是该部分的完整草图:
// Import required libraries
#include <Adafruit_CC3000.h>
#include <SPI.h>
#include <aREST.h>
// Relay state
const int relay_pin = 8;
// Define measurement variables
float amplitude_current;
float effective_value;
float effective_voltage = 230.; // Set voltage to 230V (Europe) or 110V (US)
float zero_sensor;
// These are the pins for the CC3000 chip if you are using a breakout board
#define ADAFRUIT_CC3000_IRQ 3
#define ADAFRUIT_CC3000_VBAT 5
#define ADAFRUIT_CC3000_CS 10
// Create CC3000 instance
Adafruit_CC3000 cc3000 = Adafruit_CC3000(ADAFRUIT_CC3000_CS, ADAFRUIT_CC3000_IRQ, ADAFRUIT_CC3000_VBAT,
SPI_CLOCK_DIV2);
// Create aREST instance
aREST rest = aREST();
// Your WiFi SSID and password
#define WLAN_SSID "yourWiFiNetworkName"
#define WLAN_PASS "yourPassword"
#define WLAN_SECURITY WLAN_SEC_WPA2
// The port to listen for incoming TCP connections
#define LISTEN_PORT 80
// Server instance
Adafruit_CC3000_Server restServer(LISTEN_PORT);
// Variables to be exposed to the API
int power;
void setup(void)
{
// Start Serial
Serial.begin(115200);
// Init variables and expose them to REST API
rest.variable("power",&power);
// Set relay pin to output
pinMode(relay_pin,OUTPUT);
// Calibrate sensor with null current
zero_sensor = getSensorValue(A0);
// Give name and ID to device
rest.set_id("001");
rest.set_name("smart_lamp");
// Set up CC3000 and get connected to the wireless network.
if (!cc3000.begin())
{
while(1);
}
if (!cc3000.connectToAP(WLAN_SSID, WLAN_PASS, WLAN_SECURITY)) {
while(1);
}
while (!cc3000.checkDHCP())
{
delay(100);
}
// Display connection details
displayConnectionDetails();
// Start server
restServer.begin();
Serial.println(F("Listening for connections..."));
}
void loop() {
// Perform power measurement
float sensor_value = getSensorValue(A0);
// Convert to current
amplitude_current = (float)(sensor_value-zero_sensor)/1024*5/185*1000000;
effective_value = amplitude_current/1.414;
power = (int)(abs(effective_value*effective_voltage/1000));
// Handle REST calls
Adafruit_CC3000_ClientRef client = restServer.available();
rest.handle(client);
}
// Function to display connection details
bool displayConnectionDetails(void)
{
uint32_t ipAddress, netmask, gateway, dhcpserv, dnsserv;
if(!cc3000.getIPAddress(&ipAddress, &netmask, &gateway, &dhcpserv, &dnsserv))
{
Serial.println(F("Unable to retrieve the IP Address!\r\n"));
return false;
}
else
{
Serial.print(F("\nIP Addr: ")); cc3000.printIPdotsRev(ipAddress);
Serial.print(F("\nNetmask: ")); cc3000.printIPdotsRev(netmask);
Serial.print(F("\nGateway: ")); cc3000.printIPdotsRev(gateway);
Serial.print(F("\nDHCPsrv: ")); cc3000.printIPdotsRev(dhcpserv);
Serial.print(F("\nDNSserv: ")); cc3000.printIPdotsRev(dnsserv);
Serial.println();
return true;
}
}
// Get the reading from the current sensor
float getSensorValue(uint8_t pin)
{
uint16_t sensorValue;
float avgSensor = 0;
uint8_t nb_measurements = 100;
for (uint8_t i = 0; i < nb_measurements; i++) {
sensorValue = analogRead(pin);
avgSensor = avgSensor + float(sensorValue);
}
avgSensor = avgSensor/float(nb_measurements);
return avgSensor;
}
现在,让我们更详细地看看 Arduino 草图。它首先导入该项目所需的库:
#include <Adafruit_CC3000.h>
#include <SPI.h>
#include <CC3000_MDNS.h>
#include <aREST.h>
我们还必须定义继电器模块连接到的引脚:
const int relay_pin = 8;
然后,我们必须声明一些变量,这些变量将帮助我们测量和计算设备的功耗:
float amplitude_current;
float effective_value;
float effective_voltage = 230.; // Set voltage to 230V (Europe) or 110V (US)
float zero_sensor;
在这一点上,您还应该更改有效电压的值,使其与您所在国家的电压相匹配。
然后,我们必须定义 CC3000 Wi-Fi 芯片连接到的引脚:
#define ADAFRUIT_CC3000_IRQ 3
#define ADAFRUIT_CC3000_VBAT 5
#define ADAFRUIT_CC3000_CS 10
我们现在可以创建 CC3000 Wi-Fi 芯片的一个实例:
Adafruit_CC3000 cc3000 = Adafruit_CC3000(ADAFRUIT_CC3000_CS, ADAFRUIT_CC3000_IRQ, ADAFRUIT_CC3000_VBAT,
SPI_CLOCK_DIV2);
我们还需要创建 aREST 库的一个实例:
aREST rest = aREST();
您现在需要修改代码以添加您的 Wi-Fi 网络凭据:
#define WLAN_SSID "yourWiFiNetworkName"
#define WLAN_PASS "yourPassword"
#define WLAN_SECURITY WLAN_SEC_WPA2
我们还将定义我们想要通过 Wi-Fi 芯片监听的端口:
#define LISTEN_PORT 80
之后,我们将声明一个在该端口上监听的服务器:
Adafruit_CC3000_Server restServer(LISTEN_PORT);
最后,我们声明一个变量,它将包含设备的功耗,可以通过 HTTP 请求从外部访问(在同一本地 Wi-Fi 网络内):
int power;
在草图的setup()函数中,我们将启动串行连接:
Serial.begin(115200);
我们还将将功耗变量暴露给 aREST API:
rest.variable("power",&power);
我们还将声明继电器引脚为输出:
pinMode(relay_pin,OUTPUT);
然后,我们需要首先从电流传感器进行测量,以获取当没有电流流经连接的设备时电流传感器返回的值。这是通过一个我们不会详细说明的函数完成的:
zero_sensor = getSensorValue(A0);
我们还将为我们的项目分配一个 ID 和名称:
rest.set_id("001");
rest.set_name("smart_lamp");
然后,我们将调用一个函数来显示 Wi-Fi 连接的详细信息,例如 CC3000 芯片的 IP 地址:
displayConnectionDetails();
为了结束setup()函数,我们将启动我们的 Wi-Fi 服务器:
restServer.begin();
Serial.println(F("Listening for connections..."));
现在,在草图的loop()函数中,我们将从连接在模拟引脚 A0 上的传感器读取数据:
float sensor_value = getSensorValue(A0);
一旦我们得到这个值,我们就可以从它计算出电流以及设备的功耗:
amplitude_current = (float)(sensor_value-zero_sensor)/1024*5/185*1000000;
effective_value = amplitude_current/1.414;
power = (int)(abs(effective_value*effective_voltage/1000));
基本上,电流传感器的制造商给出了第一个公式。然后,我们通过将幅值电流除以根号 2 来获取有效电流,大约是 1.414。最后,我们通过将有效电流与有效电压相乘(并除以 1,000 以得到瓦特为单位的结果)来获取有效功率。一旦完成测量,我们使用 aREST 库处理传入的请求:
Adafruit_CC3000_ClientRef client = restServer.available();
rest.handle(client);
注意,您可以在本书的 GitHub 仓库中找到这部分完整的代码:github.com/marcoschwartz/arduino-android-blueprints。
不要忘记将草图修改为包含您自己的 Wi-Fi 网络名称和 Wi-Fi 网络密码。现在,您可以将代码上传到您的 Arduino 板,并打开串行监视器。以下是在一段时间后您应该看到的结果(当然,您的板子的 IP 地址和其他参数可能不同):
IP Addr: 192.168.1.130
Netmask: 255.255.255.0
Gateway: 192.168.1.1
DHCPsrv: 0.0.0.0
DNSserv: 192.168.1.1
Listening for connections...
记下在您的串行监视器中出现的 IP 地址——您现在需要它,并且在稍后编写 Android 应用程序时也需要。现在,我们将通过向项目发送一些命令来测试 Wi-Fi 连接。您可以使用您喜欢的网页浏览器并输入以下内容:
192.168.1.130/digital/8/1
当然,您需要将 IP 地址更改为您自己的板子 IP 地址,正如它在串行监视器中显示的那样。您应该看到继电器立即打开,并且您应该看到以下消息:
{"message": "Pin D8 set to 1", "id": "001", "name": "smart_lamp", "connected": true}
您现在可以再次使用以下命令将其关闭:
192.168.1.130/digital/8/0
现在,我们将尝试读取设备的功耗。您可以通过输入以下内容来完成:
192.168.1.130/power
您应该看到以下回答:
{"power": 0, "id": "001", "name": "smart_lamp", "connected": true}
如果您能看到这个,那么这意味着传感器已正确校准(因为功率为 0),并且功率变量已正确暴露给 aREST API。
我们的 Android 应用程序的线框图
在开始编写任何代码之前,我们对应用程序进行严格的线框设计将帮助我们提供更好的用户体验。以下是我们希望在实现最终代码时遵循的纸面原型:

我们将在 Android Studio 中创建一个名为Arduino Wifi的新项目,最小 SDK 为 15,最大 SDK 为 19(在撰写本文时,这是 Android SDK 的最稳定版本)。这将使我们能够满足市场上超过 80%的 Android 设备。一个带有空白活动的项目就足以开始这个项目。
一旦您设置了项目,我们将继续处理 Android 布局文件,这些文件可以通过导航到app > src > res > layout > activity_main_screen.xml找到。
我们将首先应用一个相对布局,并在该布局中拖放四个按钮以及一个 TextView,其外观大致如下(在此阶段,无需关注布局的美观质量):

我们还将按照以下方式识别每个用户界面项:
-
打开门按钮:
openGateButton -
开启按钮:
switchOnButton -
关闭按钮:
switchOffButton -
检查功率按钮:
checkPowerButton -
功率输出文本视图:
powerOutput
将布局实现到代码中
我们将首先声明一个String TAG对象,该对象引用MainActivity,并将用于日志记录:
public static final String TAG = MainScreen.class.getSimpleName();
然后,我们将声明所有视图变量,并在onCreate方法中将它们分配给layout元素,这意味着onCreate方法将如下所示:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_screen);
//Declare our View Variables and assign them to the layout elements
Button checkPowerButton = (Button) findViewById(R.id.checkPowerButton);
Button openTheGateButton = (Button) findViewById(R.id.openGateButton);
Button switchOnButton = (Button) findViewById(R.id.switchOnButton);
Button switchOffButton = (Button) findViewById(R.id.switchOffButton);
checkPowerButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isNetworkAvailable()) {
checkPowerTask getPowerTask = new checkPowerTask();
getPowerTask.execute();
}
}
});
openTheGateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isNetworkAvailable()) {
SwitchOpenTask switchOpenTask = new SwitchOpenTask();
switchOpenTask.execute();
}
}
});
switchOnButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isNetworkAvailable()) {
SwitchOnTask switchOnTask = new SwitchOnTask();
switchOnTask.execute();
}
}
});
switchOffButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (isNetworkAvailable()) {
SwitchOffTask switchOffTask = new SwitchOffTask();
switchOffTask.execute();
}
}
});
}
如前述代码所示,我们引用了多个 ASync 任务,我们将与我们将使用的 JSON 解析器一起引用,该解析器将用于解析从 Arduino 获取的数据并将其适配到功率输出文本视图。
ASync 任务将帮助我们分别从主用户界面线程运行应用程序任务,从而显著提高用户界面的响应性,从而增强用户体验。
使用以下代码,您需要将yourip部分替换为您自己的 IP 地址,该地址您可以在 Arduino IDE 串行监视器中找到。您可以在Main Activity声明中声明 IP 地址,如下所示:
public static final String URL = "yourip";
然后,我们将声明以下AsyncTasks对象,以启用我们想要实现的不同操作:
private class SwitchOpenTask extends AsyncTask<Object,Void,String> {
@Override
protected String doInBackground(Object... arg0) {
int responseCode = -1;
try {
URL restApiUrl = new URL("http:// " + URL + "mode/8/o");
HttpURLConnection connection = (HttpURLConnection) restApiUrl.openConnection();
connection.connect();
responseCode = connection.getResponseCode();
Log.i(TAG, "Code" + responseCode);
}
catch(MalformedURLException e) {
Log.e(TAG, "Malformed Exception Caught:", e);
}
catch(IOException e) {
Log.e(TAG, "IO Exception Caught:", e);
e.printStackTrace();
}
catch(Exception e){
Log.e(TAG, "Generic Exception Caught:", e);
}
return "Code: " + responseCode;
}
}
private class SwitchOnTask extends AsyncTask<Object,Void,String> {
@Override
protected String doInBackground(Object... arg0) {
int responseCode = -1;
try {
URL restApiUrl = new URL("http://" + URL + "/digital/8/1");
HttpURLConnection connection = (HttpURLConnection) restApiUrl.openConnection();
connection.connect();
responseCode = connection.getResponseCode();
Log.i(TAG, "Code" + responseCode);
}
catch(MalformedURLException e) {
Log.e(TAG, "Malformed Exception Caught:", e);
}
catch(IOException e) {
Log.e(TAG, "IO Exception Caught:", e);
e.printStackTrace();
}
catch(Exception e){
Log.e(TAG, "Generic Exception Caught:", e);
}
return "Code: " + responseCode;
}
}
private class SwitchOffTask extends AsyncTask<Object,Void,String> {
@Override
protected String doInBackground(Object... arg0) {
int responseCode = -1;
try {
URL restApiUrl = new URL("http://" + URL + "/digital/8/0");
HttpURLConnection connection = (HttpURLConnection) restApiUrl.openConnection();
connection.connect();
responseCode = connection.getResponseCode();
Log.i(TAG, "Code" + responseCode);
}
catch(MalformedURLException e) {
Log.e(TAG, "Malformed Exception Caught:", e);
}
catch(IOException e) {
Log.e(TAG, "IO Exception Caught:", e);
e.printStackTrace();
}
catch(Exception e){
Log.e(TAG, "Generic Exception Caught:", e);
}
return "Code: " + responseCode;
}
}
private class checkPowerTask extends AsyncTask<Object,Void,String> {
@Override
protected String doInBackground(Object... arg0) {
int responseCode = -1;
String result = null;
try {
URL restApiUrl = new URL("http://" + URL + "/power");
HttpURLConnection connection = (HttpURLConnection) restApiUrl.openConnection();
connection.connect();
responseCode = connection.getResponseCode();
InputStream is = null;
//http post request
try{
String postQuery = "http://" + URL + "/power";
HttpClient httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost(postQuery);
HttpResponse response = httpclient.execute(httppost);
HttpEntity entity = response.getEntity();
is = entity.getContent();
}catch(Exception e){
Log.e("log_tag", "Error in http connection "+e.toString());
}
//convert response to string
try{
BufferedReader reader = new BufferedReader(new InputStreamReader(is,"UTF-8"),8);
StringBuilder sb = new StringBuilder();
String line = null;
while ((line = reader.readLine()) != null) {
sb.append(line + "\n");
}
is.close();
result=sb.toString();
Log.v(TAG,result);
} catch(Exception e){
Log.e("log_tag", "Error converting result "+e.toString());
}
//parse json data
try {
JSONObject userObject = new JSONObject(result);
final String powerOutputText = userObject.getString("power");
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
TextView powerOutput = (TextView) findViewById(R.id.powerOutput);
powerOutput.setText(powerOutputText + "W");
}
});
} catch(JSONException e){
Log.e(TAG, "JSON Exception Caught:", e);
}
}
catch(MalformedURLException e) {
Log.e(TAG, "Malformed Exception Caught:", e);
}
catch(IOException e) {
Log.e(TAG, "IO Exception Caught:", e);
e.printStackTrace();
}
catch(Exception e){
Log.e(TAG, "Generic Exception Caught:", e);
}
return "Code: " + responseCode;
}
}
我们将在底部添加另一个helper方法,以确保 Wi-Fi 网络连接可用:
private boolean isNetworkAvailable() {
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
boolean isAvailable = false;
if (networkInfo != null && networkInfo.isConnected()) {
isAvailable = true;
}
return isAvailable;
}
在继续之前,我们需要将以下权限添加到我们的 Android Manifest文件中,该文件位于app > src > main > AndroidManifest.xml。
以下权限将允许我们访问 Android 设备的 Wi-Fi 网络功能:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
然后,您可以继续编译应用程序。还重要的是要注意,Wi-Fi 的延迟约为 300 毫秒,并且根据您的 Wi-Fi 网络,该值可能需要相当长的时间才能更新用户界面。
如果您在跟随过程中遇到困难,您也可以通过查看 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中的最终项目来参考。
磨练用户界面和体验
一旦我们成功完成代码并确保用户界面正在更新功率值,并且我们可以打开和关闭灯泡,我们就可以继续改进我们的用户界面。
我们将通过以下主要操作改进用户界面:
-
添加新的应用程序图标
-
放大功率输出文本
-
对齐和样式化按钮
-
在操作栏中更改应用程序名称
添加新的应用程序图标
首先,我们将开始下载图像资源。它可在 GitHub 仓库中找到,也可以在bit.ly/iclauncherchapter4作为公共下载。
您应使用项目树进行导航,然后右键单击app文件夹,如下截图所示:

当您右键单击app时,通过导航到新建 > 图像资源创建一个新的图像资源,如下截图所示:

您将看到一个资产工作室弹出窗口,它允许您选择自己的图像文件,如下截图所示。出于优化目的,我们建议您选择分辨率为 144 像素×144 像素的.png文件。Android Studio 会自动进行所有调整大小和资源创建,以适应不同的屏幕:

一旦您选择了我们提供的ic_launcher图像文件,您将看到一个显示不同尺寸图标的屏幕。点击下一步,您将看到以下屏幕:

此屏幕警告您之前的文件将被覆盖,并再次以多种不同的分辨率向您展示图像启动器文件。点击完成,编译应用程序,在您的物理设备上启动它,您应该在应用程序托盘和应用程序的操作栏中看到一些令人愉快的东西,如下所示:

居中并放大数据输出文本
为了编辑显示传感器数据的文本输出的布局,我们需要打开项目树并导航到布局文件,该文件位于 app > src > main > res > layout > activity_main_screen.xml。
一旦进入此视图,我们建议您使用文本视图修改文本。这将允许您获得更精细的控制,并让您习惯于在程序化编辑 Android 布局文件时使用的不同约定。
当打开 activity_main_screen.xml 文件时,我们将看到按钮和文本视图的不同 XML 代码。在此阶段,注意负责 Power Data Output 文本视图的代码,并添加以下代码:
android:textSize="100sp"
android:textAlignment="center"
负责的 Sensor Data Output 文本视图的整个代码块现在将如下所示:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="100W"
android:textSize="100sp"
android:id="@+id/powerOutput"
android:textAlignment="center"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="78dp"
/>
在此代码块中,我们暂时使用了占位文本 100W,以便我们可以近似地看到它使用 Android 布局设计器时的样子。通过此修改,传感器数据现在足够大,可以显示给用户,并将成为用户体验增强的一部分。
对按钮进行对齐和样式设置
在我们的最后几步中,我们将修改我们的按钮并为文本添加一些颜色。
创建新按钮时将有两个步骤:
-
在
res文件夹中创建一个名为button.xml的新 XMLdrawable文件。 -
然后,我们将连接
drawable资源文件到主 Android 布局文件。
通过在 app > src > main > res 中的 res 文件夹上右键单击来创建 drawable 文件夹。
在 res 文件夹内创建 drawable 文件夹后,我们需要再次右键单击新的 drawable 文件夹,并导航到 新建 > Drawable 资源文件。
将文件命名为 button,输入 shape 作为根元素,然后点击 确定。
在 button.xml 文件中,我们将添加以下代码:
<?xml version="1.0" encoding="utf-8"?>
<shape >
android:shape="rectangle" >
<corners
android:radius="30dp"/>
<solid
android:color="#FFFFFF"/>
<padding
android:left="0dp"
android:top="0dp"
android:right="0dp"
android:bottom="0dp"/>
<size
android:width="120dp"
android:height="60dp"/>
<stroke
android:width="2dp"
android:color="#4A90E2"/>
</shape>
然后,我们转向 activity_main_screen.xml 文件,并在按钮模块中包含以下代码来引用此可绘制资源:
android:background="@drawable/button"
我们将通过在 activity_main_screen.xml 文件中的 Button 和 TextView 模块中添加以下代码行来增加一些特色:
android:textColor="#4A90E2"
#4A90E2 项指的是应用图标中使用的主体颜色的十六进制代码,这样我们就能保持与主用户界面的一致性。
在操作栏中更改应用程序名称
我们都希望将应用程序的名称自定义为我们喜欢的名称,这将是我们的项目中最容易的事情!我们只需转到 strings.xml 文件,其中包含我们项目中的所有常量文本值。它位于 app > src > res > values > string.xml。
然后,您可以将 arduinoWifi 的文本更改为您喜欢的任何名称。在这种情况下,我们将坚持使用 WiFi Lamp Switch:
<string name="app_name">WiFi Lamp Switch</string>
我们的项目现在应该如下所示(在此情况下使用的设备是 Nexus 4):

重要的是要注意,屏幕布局可能会根据不同的设备而有所不同。在这种情况下,你可能需要将你的 Android 布局文件适配到你的特定物理设备。
如何进一步发展
当涉及到进一步修改 Android 应用程序时,选项是无限的,并且有几种实现方式可以改进应用程序,例如实时监控,其中电力数据输出将自动刷新。此外,这些数据可以提供将生成数据存储在云中的用例,这些数据可以被分析,从而允许创建数据的图形解释。这种图形解释可以与一天中的时间相关联,并帮助用户了解何时发生最大的电力消耗。
从编码的角度来看,我们可以重构我们的代码,这意味着我们有效地简化并重用我们的代码。实际上,重构肯定可以通过 JSON 解析器实现,它可以被重构为其自己的类,而我们选择在当前设置中省略它,以便促进学习过程。
关于用户体验,可以引入一个新的EditText字段以及一个提交按钮,以便用户可以手动更改 IP 地址,当从 Arduino 串行监视器发现 IP 地址时将被调用。在此代码中,我们使用连接和 URL 构建器来形成正确的命令。
摘要
我们基于 Arduino 创建了一个智能电源开关的 DIY 版本,并通过 Wi-Fi 由 Android 应用程序控制。我们将所有必需的组件连接到 Arduino 板上,编写了一个 Arduino 草图以通过 Wi-Fi 接受命令,最后创建了一个 Android 应用程序以远程控制开关。
在下一章中,我们将使用另一个 Arduino 板,称为 Arduino Yún,我们将能够插入一个 USB 摄像头。由于这个板也将有 Wi-Fi,我们将使用这个项目来创建一个远程 Wi-Fi 安全摄像头。
第五章:Wi-Fi 远程安全摄像头
在本章中,我们将构建一个 Wi-Fi 远程安全摄像头。摄像头本身将基于 Arduino Yùn 和一个标准的 USB 网络摄像头。Arduino Yùn 是一款功能强大的 Arduino 板,它集成了 Linux 机器和 Wi-Fi 连接。Arduino Yùn 将接收来自摄像头的视频并在本地 Wi-Fi 网络上进行流式传输。
然后,我们将能够从我们的物理 Android 设备访问视频流。这将使我们能够在家里任何地方访问我们的视频流,提供移动灵活性。
从本章,你将学习如何:
-
使用 Arduino Yùn 并连接一个 USB 摄像头到它
-
配置 Yùn 以通过你的本地 Wi-Fi 网络传输视频
-
构建一个 Android 应用程序以从 USB 摄像头获取流
硬件和软件要求
Wi-Fi 远程安全摄像头项目基于 Arduino Yùn 板。Arduino Yùn 是一款功能强大的 Arduino 板,集成了 Wi-Fi,并基于一个非常小的 Linux 发行版 OpenWrt。它还带有一个 USB 端口,以便你可以连接硬盘、摄像头或其他 USB 设备。我们将在这个项目中使用所有这些功能。
以下是在本项目中使用的电路板图片:

你还需要一个 USB 摄像头来与 Yùn 一起进行实时视频流。基本上,你可以获取任何兼容USB 视频类(UVC)的摄像头。对于这个项目,我使用了 Logitech C270 HD 摄像头。
如果你计划将摄像头用于其他应用,例如在 Yùn 上记录静态图片,你还需要一张 microSD 卡来保存数据。最后,你需要一条微型 USB 线来为 Yùn 供电。以下是本章所需的所有硬件组件列表:
-
Arduino Yùn (
www.adafruit.com/products/1498) -
一款 UVC 兼容的 USB 摄像头(
en.wikipedia.org/wiki/List_of_USB_video_class_devices) -
一条微型 USB 线
-
一张 4GB 的 microSD 卡,这是可选的(
www.adafruit.com/products/102)
你需要按照官方指南配置你的 Arduino Yùn,以便它能够连接到你的 Wi-Fi 网络:
arduino.cc/en/Guide/ArduinoYùn
注意,如果你在代理服务器后面,你可能会在配置 Arduino Yùn 时遇到问题。如果是这种情况,尝试禁用代理以查看是否解决问题。
如果你的 Yùn 不是最新的,你可能需要将 OpenWrt(Yùn 的操作系统)更新到最新版本。该过程在指南中有描述,可以在arduino.cc/en/Tutorial/YùnSysupgrade找到。
Wi-Fi 配置完成后,我们将安装处理摄像头和在本地 Wi-Fi 网络上直播视频所需的包。转到终端(如果你使用 Windows,请使用终端软件,如 PuTTY 或 OpenSSH),并输入以下命令:
ssh root@yourYùnName.local
当然,你需要将命令替换为你配置时定义的 Arduino Yùn 的名称。如果你忘记了板子的名称,你需要重置 Yùn 并重新配置。
你将被提示输入你在 Yùn 配置步骤中定义的密码。然后,你会看到一个类似于以下截图的屏幕:

你现在已登录到 Arduino Yùn。你可以输入以下命令来更新可用包的列表:
opkg update
然后输入以下命令来安装直播视频所需的包:
opkg install kmod-video-uvc mjpg-streamer
硬件配置
这个项目的硬件配置非常简单。首先,将格式化好的 microSD 卡插入 Arduino Yùn SD 卡读卡器,如下面的截图所示:

然后,只需将 USB 摄像头连接到 Yùn 的主 USB 端口,如下面的截图所示:

要完成项目的硬件配置,只需通过 micro USB 端口连接到电源即可(实际上,你甚至不需要将其连接到电脑,Arduino Yùn 可以完全独立工作!)。
设置视频流
我们现在将设置 Arduino Yùn,使其持续直播视频。再次使用以下命令登录到你的 Arduino Yùn:
ssh root@yourYùnName
再次,将命令替换为你 Arduino Yùn 的名称。然后输入以下命令:
mjpg_streamer -i "input_uvc.so -d /dev/video0 -r 640x480 -f 25" -o "output_http.so -p 8080 -w /www/webcam" &
这基本上意味着它将以 640 x 480 的分辨率,每秒 25 帧的速度,在8080端口开始直播。
你应该在终端内看到一系列命令被打印出来,这意味着 Yùn 现在正在你的 Wi-Fi 网络上直播视频。现在,打开你喜欢的网页浏览器,输入yourYùnName.local:8080。
这将打开主流媒体界面,你可以选择所需的流媒体类型。要测试访问流本身,请转到http://arduinoYùn.local:8080/stream.html。
注意,此链接仅在你自己的本地 Wi-Fi 网络内有效。你会看到一个来自你的 Arduino Yùn 的实时流,如下面的截图所示:

在 Android 上实现全屏流媒体播放器
在这个项目中,我们将实现一个非常简单的 Android 应用程序,该程序将显示来自我们的 Arduino Yùn 的 MJPEG 流。我们假设您已经在 Android Studio 的首选项中开启了Auto-Import功能。如果没有,请通过前往Auto-Import首选项并选择所有可用选项来激活它。Auto-Import首选项在 Mac 和 Windows 上的位置如下:
-
Mac:Android Studio > Preferences > Editor > Auto-Import
-
Windows:File > Settings > Editor > Auto-Import
在设置好所有必要的设置后,我们将首先创建一个新项目,在New Project设置中选择以下内容:
-
名称:
Android Yùn Security -
最小 SDK:
15 -
项目:
Blank Activity -
活动名称:
StreamActivity
在这个项目中,我们将与三个 Java 类一起工作,并需要创建两个类,即MjpegInputStream和MjpegView。Java 类如下:
-
StreamActivity(在启动新项目时创建的主要活动) -
MjpegInputStream -
MjpegView
要创建一个新类,您需要前往app > src > main > java > com.domainofyourchoice.androidYùnsecurity。
右键单击包名,然后选择New > Java Class,如下面的截图所示:

首先要考虑的是;如果我们不声明网络用户权限,这个应用程序将无法工作。因此,我们前往AndroidManifest.xml文件,并在包名下方添加以下代码行:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
完成后,Android 清单将如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.arduinoandroid.androidYùnsecurity" >
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".StreamActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
然后,我们前往StreamActivity.java文件,在那里我们将开始我们的主要流媒体活动。在这个项目中,我们将使用 ASync 任务来进行网络活动。
我们首先声明String TAG(我们将用它进行日志记录)和MjpegView(它指的是我们已创建的类的实例):
public class StreamActivity extends Activity {
private static final String TAG = "MjpegActivity";
private MjpegView mv;
在onCreate方法中,我们将声明我们的 URL,并声明一系列参数以将视频流设置为全屏。重要的是要将youripaddress替换为您可以从 Arduino Yùn 网络面板轻松找到的 IP 地址:
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//sample public ca
String URL = "http://youripaddress:8080/?action=stream";
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
mv = new MjpegView(this);
setContentView(mv);
new DoRead().execute(URL);
}
我们还需要声明onPause()方法,该方法将在 Android 应用程序关闭时实现,该方法将暂停实时流,以避免使用 Android 设备的电池资源:
public void onPause() {
super.onPause();
mv.stopPlayback();
}
然后,我们将实现DoRead AsyncTask,该任务将执行HttpRequest并与 Arduino Yùn 服务器通信:
public class DoRead extends AsyncTask<String, Void, MjpegInputStream> {
protected MjpegInputStream doInBackground(String... url) {
HttpResponse res = null;
DefaultHttpClient httpclient = new DefaultHttpClient();
Log.d(TAG, "1\. Sending http request");
try {
res = httpclient.execute(new HttpGet(URI.create(url[0])));
Log.d(TAG, "2\. Request finished, status = " + res.getStatusLine().getStatusCode());
if(res.getStatusLine().getStatusCode()==401){
//You must turn off camera User Access Control before this will work
return null;
}
return new MjpegInputStream(res.getEntity().getContent());
} catch (ClientProtocolException e) {
e.printStackTrace();
Log.d(TAG, "Request failed-ClientProtocolException", e);
//Error connecting to camera
} catch (IOException e) {
e.printStackTrace();
Log.d(TAG, "Request failed-IOException", e);
//Error connecting to camera
}
return null;
}
在StreamActivity.Java类中,我们将实现onPostExecute(),它是AsyncTask API 的一部分,将确保视频流播放器在主 UI 线程中显示:
protected void onPostExecute(MjpegInputStream result) {
mv.setSource(result);
mv.setDisplayMode(MjpegView.SIZE_BEST_FIT);
mv.showFps(true);
}
}
}
然后,我们将打开MjpegInputStream.java文件,在那里我们将声明所有必要的代码,以解析从 Arduino Yùn 流到 Android 设备的流数据:
public class MjpegInputStream extends DataInputStream {
private static final String TAG = "MjpegInputStream";
private final byte[] SOI_MARKER = { (byte) 0xFF, (byte) 0xD8 };
private final byte[] EOF_MARKER = { (byte) 0xFF, (byte) 0xD9 };
private final String CONTENT_LENGTH = "Content-Length";
private final static int HEADER_MAX_LENGTH = 100;
private final static int FRAME_MAX_LENGTH = 40000 + HEADER_MAX_LENGTH;
private int mContentLength = -1;
public MjpegInputStream(InputStream in) {
super(new BufferedInputStream(in, FRAME_MAX_LENGTH));
}
private int getEndOfSeqeunce(DataInputStream in, byte[] sequence) throws IOException {
int seqIndex = 0;
byte c;
for(int i=0; i < FRAME_MAX_LENGTH; i++) {
c = (byte) in.readUnsignedByte();
if(c == sequence[seqIndex]) {
seqIndex++;
if(seqIndex == sequence.length) {
return i + 1;
}
} else {
seqIndex = 0;
}
}
return -1;
}
private int getStartOfSequence(DataInputStream in, byte[] sequence) throws IOException {
int end = getEndOfSeqeunce(in, sequence);
return (end < 0) ? (-1) : (end - sequence.length);
}
private int parseContentLength(byte[] headerBytes) throws IOException, NumberFormatException {
ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes);
Properties props = new Properties();
props.load(headerIn);
return Integer.parseInt(props.getProperty(CONTENT_LENGTH));
}
public Bitmap readMjpegFrame() throws IOException {
mark(FRAME_MAX_LENGTH);
int headerLen = getStartOfSequence(this, SOI_MARKER);
reset();
byte[] header = new byte[headerLen];
readFully(header);
try {
mContentLength = parseContentLength(header);
} catch (NumberFormatException nfe) {
nfe.getStackTrace();
Log.d(TAG, "catch NumberFormatException hit", nfe);
mContentLength = getEndOfSeqeunce(this, EOF_MARKER);
}
reset();
byte[] frameData = new byte[mContentLength];
skipBytes(headerLen);
readFully(frameData);
return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData));
}
}
最后但同样重要的是,我们将前往MjpegView.java,在那里我们将声明多个重要方法来整合我们所有的应用程序流程。MjpegView.java类可在git.io/_Mu_Gw找到。
将你版本中的MjpegView.java类内的所有代码替换为在线仓库中的代码,并确保包名和其他类引用与项目中的相匹配。
一旦你确保所有import语句都包含在每个类中,并使用Auto-Import功能,你就可以继续构建应用并在连接到同一 Wi-Fi 网络的物理设备上测试它,该设备连接到 Arduino Yùn。
最终项目应该看起来如下:

如何进一步
在基本的 Android 应用上进行有趣的实现和进一步改进,可以包括在摄像头前检测到运动时能够捕捉快照的功能。这可以通过 Android 的 OpenCV 库实现,该库可在opencv.org/platforms/android.html找到。
此外,用户界面可以改进,包括捕捉特定场景照片的功能。这个项目也可以与稍后将要讨论的移动机器人项目结合,以拥有一个可以通过同一 Android 应用程序控制的实时流移动机器人。修改这种设置的用例是无限的,从远程婴儿监控到医疗监测设备。
摘要
让我们总结一下本章我们做了什么。我们学习了如何将 USB 摄像头连接到 Arduino Yùn,并配置 Arduino 板使其将视频流传输到我们的本地 Wi-Fi 网络。然后我们创建了一个新的 Android 应用程序,在 Android 手机或平板电脑上观看摄像头的视频流。因此,我们创建了一个基于 Arduino 和 Android 的简单 Wi-Fi 安全摄像头。
在下一章中,我们将做一些不同的事情。我们将使用 Android 手机的陀螺仪来控制连接到 Arduino 板上的伺服电机。我们只需倾斜 Android 手机就能控制伺服电机的旋转角度。
第六章:Android 手机传感器
到目前为止,在这本书中,我们已经使用 Android 设备来控制 Arduino 项目,并从连接到 Arduino 板的传感器中获取读数。在本章中,我们将做一些不同的事情:我们将使用手机的传感器来控制 Arduino 板。
我们将把一个伺服电机连接到 Arduino 板上,以便可以从 Android 手机上控制它。伺服电机基本上是一种可以由微控制器精确控制角位置的电机。我们将再次使用 BLE 来接收来自 Android 设备的命令。
在 Android 端,我们将连续测量来自手机陀螺仪传感器的数据,并将这些数据转换为伺服电机有意义的命令。目标是伺服电机持续跟随 Android 设备的移动。
在本章中,您将学习如何:
-
将伺服电机连接到 Arduino 平台
-
编写一个通过 BLE 接收命令的草图
-
编写一个 Android 应用程序,使用 Android 手机的陀螺仪控制伺服电机
硬件和软件要求
您需要为这个项目准备的第一件事是一个 Arduino Uno 板。
然后您需要一个 BLE 模块。我们选择了 Adafruit nRF8001 芯片,因为它附带了一个不错的 Arduino 库,并且已经有现成的 Android 应用程序示例来控制该模块。
对于伺服电机,我们选择了一个简单的 5V 伺服电机模块。您可以使用任何品牌的模块,只要它能用 5V 电压等级控制即可。以下是本项目使用的伺服电机的图片:

最后,您需要一个面包板和一些跳线来制作不同的连接。
这是项目所需的组件列表:
-
Arduino Uno(
www.adafruit.com/product/50) -
Adafruit nRF8001 BLE 扩展板(
www.adafruit.com/product/1697) -
5V 伺服电机(
www.adafruit.com/product/1143)
在软件方面,您需要常用的 Arduino IDE。建议您使用 Arduino IDE 版本 1.5.7 来学习本章内容。
您需要以下库:
-
可以在
github.com/adafruit/Adafruit_nRF8001找到的 nRF8001 板库 -
可以在
github.com/marcoschwartz/aREST找到的 aREST 库
要安装某个库,只需将library文件夹提取到您的Arduino/libra ries文件夹中。
配置硬件
现在我们来为项目进行必要的硬件连接。为了帮助您,以下是项目的原理图:

第一步是将蓝牙模块放置在面包板上。然后,将 Arduino 板上的电源供应连接到面包板:Arduino 板的 5V 连接到红色电源轨,GND连接到蓝色电源轨。
我们将连接 BLE 模块。首先,连接模块的电源:GND连接到蓝色电源轨,VIN连接到红色电源轨。之后,您需要连接负责 SPI 接口的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。然后,将REQ引脚连接到 Arduino 引脚10。最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。如果您需要连接此模块的额外帮助,可以访问制造商指南learn.adafruit.com/getting-started-with-the-nrf8001-bluefruit-le-breakout。
对于伺服电机,将伺服电机的红色电缆连接到红色电源轨,将黑色电缆连接到蓝色电源轨。最后,将剩余的电缆连接到 Arduino 板的 7 号引脚。
以下是为该项目组装的图片:

测试伺服电机
现在,我们将编写一个非常简单的草图来测试伺服电机,并查看 Arduino Servo 库的工作情况。以下是这部分完整的草图:
#include <Servo.h>
// Create servo object
Servo myservo;
// Servo position
int pos = 0;
void setup()
{
// Attaches the servo on pin 7 to the servo object
myservo.attach(7);
}
void loop()
{
// Goes from 0 degrees to 180 degrees
for(pos = 0; pos < 180; pos += 1)
{
myservo.write(pos);
delay(15);
}
// Goes from 180 degrees to 0 degrees
for(pos = 180; pos >= 1; pos -= 1)
{
myservo.write(pos);
delay(15);
}
}
现在让我们看看草图的细节。这首先是通过以下方式包含 Servo 库:
#include <Servo.h>
然后,我们创建一个 Servo 库的实例:
Servo myservo;
我们还将声明一个名为pos的变量,它将包含伺服电机的角位置:
int pos = 0;
在此之后,在草图中的setup()函数中,我们将伺服电机连接到 Arduino 板上的 7 号引脚:
myservo.attach(7);
在此之后,我们将pos变量从0扫描到180,这意味着我们覆盖了伺服电机的所有可能的角位置:
for(pos = 0; pos < 180; pos += 1)
{
myservo.write(pos);
delay(15);
}
在这个测试代码中,我们将使用类似的循环使伺服电机向相反方向移动。
注意
注意,本章的所有代码都可以在本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
现在是时候测试这个 Arduino 草图了。只需将代码上传到 Arduino 板。你应该会看到伺服电机向一个方向完全移动,然后返回到起始位置。之后,这个循环应该会重复。如果一切正常,你可以继续到下一节。
编写 Arduino 草图
现在,我们将编写一个草图来通过 BLE 控制伺服电机。以下是这部分完整的草图:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
#include <Servo.h>
// Lightweight mode
#define LIGHTWEIGHT 1
// Pins
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be pin 2 or 3
#define ADAFRUITBLE_RST 9
// Create servo object
Servo myservo;
// Create aREST instance
aREST rest = aREST();
// Servo position
int pos = 0;
// BLE instance
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
void setup()
{
// Start Serial
Serial.begin(115200);
// Attaches the servo on pin 7 to the servo object
myservo.attach(7);
// Start BLE
BTLEserial.begin();
// Give name and ID to device
rest.set_id("001");
rest.set_name("servo_control");
// Expose function to API
rest.function("servo",servoControl);
}
void loop()
{
// Tell the nRF8001 to do whatever it should be working on.
BTLEserial.pollACI();
// Ask what is our current status
aci_evt_opcode_t status = BTLEserial.getState();
// Handle REST calls
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
}
// Control servo from REST API
int servoControl(String command) {
// Get position from command
int pos = command.toInt();
Serial.println(pos);
myservo.write(pos);
return 1;
}
现在让我们看看这个草图的细节。它首先包含项目所需的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
#include <Servo.h>
我们还将声明我们想要使用 aREST 库的轻量级模式:
#define LIGHTWEIGHT 1
然后,我们将定义蓝牙模块连接到的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
我们还将创建 Servo 库的一个实例:
Servo myservo;
我们还需要创建 aREST 库的一个实例:
aREST rest = aREST();
我们还需要创建 nRF8001 库的一个实例:
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
在草图中的setup()函数中,我们将伺服电机连接到 Arduino 板的 7 号引脚:
myservo.attach(7);
我们还将初始化 BLE 板:
BTLEserial.begin();
然后,我们将为板子提供一个名称和 ID:
rest.set_id("001");
rest.set_name("servo_control");
我们还将将servoControl函数暴露给 aREST API,以便我们可以通过蓝牙访问它。我们将在稍后看到servoControl函数的详细信息:
rest.function("servo",servoControl);
在草图中的loop()函数中,我们将轮询蓝牙芯片以查看是否有设备连接到它:
BTLEserial.pollACI();
我们将把芯片的状态存储到status变量中:
aci_evt_opcode_t status = BTLEserial.getState();
然后,如果状态显示有设备连接到蓝牙芯片,我们将处理任何传入的请求:
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
现在我们来看看我们将用于远程控制伺服电机的servoControl函数的详细情况。它简单地接受一个字符串作为输入,包含我们想要应用在伺服电机上的位置:
int servoControl(String command) {
// Get position from command
int pos = command.toInt();
Serial.println(pos);
myservo.write(pos);
return 1;
}
注意
注意,本章的所有代码都可以在本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
您现在可以将代码上传到 Arduino 板,并进入下一节。
设置 Android 应用程序项目
在这个项目中,我们将设计一个非常简单的 Android 应用程序,该程序将在单行文本视图中显示蓝牙回调,并在另一个文本视图中显示传感器输出。这次,我们还将实现一个刷新按钮,如果需要刷新,它将重新启动蓝牙回调。
项目中更复杂的部分是访问我们可用的硬件传感器,以便向伺服发送命令并根据我们 Android 设备的 x 轴方向旋转轴,该方向由设备中包含的陀螺仪硬件确定。
重要的是要注意,由于不同的硬件设置,传感器读数和数据可能在不同的 Android 设备之间有所不同。然而,您也可以将此项目作为基准来进一步探索。
我们将假设您已经在首选项选项中打开了自动导入功能。如果没有,请通过转到自动导入首选项并选择所有可用选项来激活它。自动导入首选项在 Mac 和 Windows 上的位置如下:
-
在 Mac 上,导航到Android Studio | 首选项 | 编辑器 | 自动导入。
-
在 Windows 上,导航到文件 | 设置 | 编辑器 | 自动导入。
在所有必要的设置就绪后,我们将首先创建一个新的项目,在新建项目设置中选择以下内容:
-
名称:Android 陀螺仪伺服控制
-
最小 SDK:
18 -
项目:
Blank Activity -
活动名称:
MainScreen
为了使这个项目工作,我们首先需要转到 Android 的Manifest文件,该文件位于app > src > main > AndroidManifest.xml。
布局 Android 用户界面和权限
一旦我们打开文件,我们需要添加访问蓝牙和陀螺仪传感器硬件的权限。最终的 Android Manifest.xml文件将如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.arduinoandroid.androidarduinosensserv" >
<uses-permission android:name="android.hardware.sensor.gyroscope"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name=".MainScreen"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
在这个特定的项目中,我们不会强调将用户界面做得非常精致,相反,我们将更多地关注使方向传感器与伺服电机适当地协同工作。
在我们的项目中,我们将导航到主布局文件,可以通过导航到app > src > res > layout > activity_main_screen.xml来访问。
以下代码将实现一个线性布局,包含两个TextView模块和一个按钮。请将您项目中的当前代码替换为以下代码:
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:weightSum="1">
<TextView
android:id="@+id/btView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="80dp"
android:text="bluetooth text"
android:textAppearance="?android:attr/textAppearanceSmall" />
<Button
android:id="@+id/refreshButton"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="60dp"
android:text="Refresh" />
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginTop="250dp"
android:text="Gyro output" />
</LinearLayout>
设置应用程序的内部结构
然后,我们将继续到MainScreen.java文件,该文件位于app > src > main > java > package name > MainScreen.java。
然后,我们将用以下代码替换当前代码,我们将逐步讲解,并启用自动导入;Android Studio 将自动导入我们项目所需的全部语句。
我们首先声明一个扩展Activity的类,并且,我们还需要为 Java 类添加实现SensorEventListener的能力,这包括检测传感器活动所需的主要方法:
public class MainScreen extends Activity implements SensorEventListener {
以下是需要声明的所有变量,以便与 BLE 模块一起工作,日志标签用于日志记录,用户界面元素,处理程序方法,以及蓝牙特性:
// UUIDs for UAT service and associated characteristics.
public static UUID UART_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID TX_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID RX_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
// UUID for the BTLE client characteristic which is necessary for notifications.
public static UUID CLIENT_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
//Getting the name for Log Tags
private final String LOG_TAG = MainScreen.class.getSimpleName();
/**
* Indicates which angle we are currently pointing the phone (and hence servo) in:
* -2: 0-45 degrees
* -1: 45-90 degrees
* 0: 90 degrees
* 1: 90-135 degrees
* 2: 135-180 degrees
* <p/>
* Default is the neutral position, i.e. 0.
*/
int currentPosition = 0;
long lastSensorChangedEventTimestamp = 0;
//Declaring UI Elements
private TextView gyroTextView;
private TextView bluetoothTv;
//Declaring SensorManager variables
private SensorManager sensorManager;
//Sensor Delay Methods
int PERIOD = 1000000000; // read sensor data each second
Handler handler;
boolean canTransmitSensorData = false;
boolean isHandlerLive = false;
private boolean areServicesAccessible = false;
注意
为 Adafruit 蓝牙模块定制的 UART 服务使用以下 UUID,这是您需要知道以使我们的 Android 应用程序与适当的特性进行通信的值。有一个特性用于 TX,另一个用于 RX,类似于 UART 使用两条线发送和接收数据的方式如下:
UART 服务 UUID:6E400001-B5A3-F393-E0A9-E50E24DCCA9E
TX 特性 UUID:6E400002-B5A3-F393-E0A9-E50E24DCCA9E
RX 特性 UUID:6E400003-B5A3-F393-E0A9-E50E24DCCA9E
在我们的项目中,处理所有回调的蓝牙逻辑的完整内容可以在我们的 GitHub 仓库中找到。包含所有蓝牙逻辑的主要 Java 活动位于git.io/XSHnow。
在以下代码段中,我们将声明当活动被创建时会发生什么,并设置所有必要的函数以使应用程序逻辑连接到布局文件。
在onCreate()方法中,我们还将初始化SensorManager类,这将需要我们访问系统的服务。
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main_screen);
handler = new Handler();
// Setup the refresh button
final Button refreshButton = (Button) findViewById(R.id.refreshButton);
refreshButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
restartScan();
}
});
//get the TextView from the layout file
gyroTextView = (TextView) findViewById(R.id.tv);
bluetoothTv = (TextView) findViewById(R.id.btView);
//get a hook to the sensor service
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
}
在onStart()方法中,我们将使用SensorManager类来注册我们将要使用的传感器类型。在这种情况下,我们将使用定向传感器,并设置SENSOR_DELAY_NORMAL,我们稍后需要修改它以确保每次调用之间有足够的延迟。在onStart()方法中,我们还将初始化蓝牙适配器以开始监听设备。
@Override
protected void onStart() {
super.onResume();
/*register the sensor listener to listen to the gyroscope sensor, use the
callbacks defined in this class, and gather the sensor information as quick
as possible*/
sensorManager.registerListener(this,
sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION),
SensorManager.SENSOR_DELAY_NORMAL
);
//handler.post(processSensors);
// Scan for all BTLE devices.
// The first one with the UART service will be chosen--see the code in the scanCallback.
bluetoothAdaper = BluetoothAdapter.getDefaultAdapter();
startScan();
}
当应用关闭时,始终重要的是注销传感器监听器并断开 BLE 连接,以防止电池耗尽和设备内存资源。
//When this Activity isn't visible anymore
@Override
protected void onStop() {
//unregister the sensor listener
sensorManager.unregisterListener(this);
//disconnect and close Bluetooth Connection for better reliability
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
tx = null;
rx = null;
}
super.onStop();
areServicesAccessible = false;
}
以下代码将处理所有需要实现的Sensor方法,以确保每次传感器读取之间有足够的延迟,并向蓝牙启用的 Arduino 发送必要的命令,以便伺服电机根据设备的x轴旋转轴。
//SENSOR METHODS
private final Runnable processSensors = new Runnable() {
@Override
public void run() {
// Do work with the sensor values.
canTransmitSensorData = !canTransmitSensorData;
// The Runnable is posted to run again here:
handler.postDelayed(this, PERIOD);
}
};
@Override
public void onAccuracyChanged(Sensor arg0, int arg1) {
//Do nothing.
}
@Override
public void onSensorChanged(SensorEvent event) {
if ((event.accuracy != SensorManager.SENSOR_STATUS_UNRELIABLE)
&& (event.timestamp - lastSensorChangedEventTimestamp > PERIOD)) {
System.out.println(event.timestamp - lastSensorChangedEventTimestamp);
lastSensorChangedEventTimestamp = event.timestamp;
// Truncate to an integer, since precision loss is really not a serious
// matter here, and it will make it much easier (and cheaper) to compare.
// We will also log the integer values of [2]
int xTilt = (int) event.values[2];
int yTilt = (int) event.values[1];
int zTilt = (int) event.values[0];
gyroTextView.setText("Orientation X (Roll) :" + xTilt + "\n" +
"Orientation Y (Pitch) :" + yTilt + "\n" +
"Orientation Z (Yaw) :" + zTilt);
//Log.i(LOG_TAG, "The XTilt is:" + String.valueOf(xTilt));
if (areServicesAccessible) {
turnServoFinegrained(xTilt);
}
}
}
private void turnServoFinegrained(int xTilt) {
// Default to vertical position
int rotationAngle = 90;
// Turn left
if (xTilt > 0) {
rotationAngle = 90 - xTilt;
}
// Turn right
else {
rotationAngle = 90 + Math.abs(xTilt);
}
String setServoMessage = "/servo?params=" + rotationAngle + " /";
tx.setValue(setServoMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeSensorData("Sent: " + setServoMessage);
} else {
writeSensorData("Couldn't write TX characteristic!");
}
}
以下代码将确保发送到 BLE 模块的命令显示在我们的用户界面布局的蓝牙文本输出中。
private void writeSensorData(final CharSequence text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.e(LOG_TAG, text.toString());
//bluetoothTv = (TextView) findViewById(R.id.btView);
output = text.toString();
bluetoothTv.setText(output);
}
});
}
}
一旦你编写了所有代码,你可以在我们的 GitHub 仓库中轻松地跟随,该仓库位于github.com/marcoschwartz/arduino-android-blueprints/tree/master/chapter6,请确保你有一个运行安卓 4.3 或更高版本并且蓝牙已开启的物理设备。一旦构建了项目,你应该会看到以下截图类似的内容:

如何更进一步
来自安卓应用的定向读取可以在应用中通过实时图表进一步可视化,并且这个项目可以进一步发展并集成到一个远程对象控制应用中,在这个应用中,安卓智能手机的用户可以从特定距离控制连接到伺服电机的对象。
这种动作的简单而实用的应用可以是打开大门或通过陀螺仪控制移动机器人。安卓智能手机还有许多其他传感器可供我们使用,例如加速度计和磁力计,这些传感器可以有效地用于控制连接到 Arduino 微控制器的不同组件。
摘要
在本章中,我们学习了如何利用安卓手机上最重要的传感器之一——陀螺仪传感器,以便能够控制 Arduino 控制的伺服电机。我们通过 Arduino 配备 Adafruit BLE 模块和运行 4.3 或更高版本的安卓操作系统的可能性来实现这种通信和动作。
本章还为本章之后的内容提供了基础步骤,后者将访问安卓设备最重要的硬件之一。
第七章。语音激活 Arduino
在本章中,我们将使用安卓设备的一个新特性来控制 Arduino 系统:语音识别。我们将通过从手机发送语音命令来控制连接到 Arduino 板上的继电器。
这个继电器可以连接到很多东西。例如,它可以连接到电动门锁,这样你只需对着手机说话就可以打开和关闭门。你还可以将继电器连接到灯泡,通过向手机发出语音命令来开关灯泡。
在本章中,你将学习如何:
-
将一个继电器和一个蓝牙模块连接到 Arduino 板,以便可以从 Android 应用程序中控制它
-
使用 Android 语音 API 构建应用程序
-
通过语音控制 Arduino 板上的继电器
硬件和软件要求
对于这个项目,你首先需要一块 Arduino Uno 板。
然后你需要一个 BLE 模块。我们选择了 Adafruit nRF8001 芯片,因为它附带了一个不错的 Arduino 库,并且已经有现成的 Android 应用程序示例来控制该模块。
你还需要一个继电器模块。对于这个项目,我们使用了 Polulu 的 5V 继电器模块,这与我们在前几章中使用的是同一个。这是本章中我们使用的继电器的图片:

最后,为了进行不同的电气连接,你还需要一个面板和一些跳线。
这是你需要为这个项目准备的所有硬件组件列表,以及如何在网络上找到这些组件的链接:
-
Arduino Uno 板(
www.adafruit.com/product/50) -
5V 继电器模块(
www.pololu.com/product/2480) -
Adafruit nRF8001 扩展板(
www.adafruit.com/products/1697)
注意,这些都是我们在前几章中已经使用过的组件。
在软件方面,你需要以下内容:
-
Arduino IDE(
arduino.cc/en/Main/Software) -
Arduino aREST 库(
github.com/marcoschwartz/aREST/) -
用于 BLE 芯片的 nRF8001 Arduino 库(
github.com/adafruit/Adafruit_nRF8001)
要安装一个特定的库,只需将文件夹提取到你的Arduino/libraries文件夹中(如果尚不存在,则创建此文件夹)。
配置硬件
现在我们将构建项目的硬件部分。为了帮助你,以下是无继电器连接的项目原理图:

注意,这些说明与上一章相同。因此,如果您仍然在桌子上构建了相同的配置,您可以直接使用相同的配置。
第一步是将蓝牙模块放置在面包板上。然后,将 Arduino 板上的电源连接到面包板:Arduino 板的 5V 连接到红色电源轨,GND连接到蓝色电源轨。
我们现在将连接 BLE 模块。首先,连接模块的电源:GND连接到蓝色电源轨,VIN连接到红色电源轨。之后,您需要连接负责 SPI 接口的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。然后,将REQ引脚连接到 Arduino 引脚10。最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。
对于继电器模块,将VCC引脚连接到面包板上的红色电源轨,将GND引脚连接到蓝色电源轨。最后,将继电器的SIG引脚连接到 Arduino 板的7号引脚。
下图是组装好的项目的概述图(有关元件之间的精确连接,请参阅前面的说明):

可以看到继电器和 BLE 模块的特写图像如下:

编写 Arduino 草图
我们现在将编写草图以从 Android 设备控制继电器。请注意,这与上一章中的草图相同,所以如果您已经在上一个章节中完成了这部分,您可以跳过它。以下是本部分的完整草图:
// Control Arduino board from BLE
// Libraries
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
// Pins
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // Should be pin 2 or 3
#define ADAFRUITBLE_RST 9
// Relay pin
const int relay_pin = 7;
// Create aREST instance
aREST rest = aREST();
// BLE instance
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
void setup(void)
{
// Start Serial
Serial.begin(115200);
// Start BLE
BTLEserial.begin();
// Give name and ID to device
rest.set_id("001");
rest.set_name("relay_control");
// Init relay pin
pinMode(relay_pin,OUTPUT);
}
void loop() {
// Tell the nRF8001 to do whatever it should be working on.
BTLEserial.pollACI();
// Ask what is our current status
aci_evt_opcode_t status = BTLEserial.getState();
// Handle REST calls
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
}
现在,让我们看看草图的细节。它首先导入 nRF8001 模块和 aREST 库所需的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
然后,我们将定义 BLE 模块连接到的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
我们还需要声明继电器连接到的引脚:
const int relay_pin = 7;
在此之后,我们可以创建一个用于处理通过蓝牙传入的请求的 aREST API 实例:
aREST rest = aREST();
我们还将创建 nRF8001 芯片库的实例:
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
现在,在草图中的setup()函数中,我们将初始化串行通信并打印一条欢迎信息,如下所示:
BTLEserial.begin();
我们还将为设备命名:
rest.set_id("001");
rest.set_name("relay_control");
最后,我们将设置继电器引脚,使其成为输出:
pinMode(relay_pin,OUTPUT);
现在,在草图中的loop()函数中,我们将检查 BLE 芯片的状态:
BTLEserial.pollACI();
aci_evt_opcode_t status = BTLEserial.getState();
然后,如果任何设备连接到芯片,我们将使用 aREST 库处理任何传入的请求:
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
注意,本章的所有代码都可以在本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
现在是时候将草图上传到您的 Arduino 板上了。完成此操作后,您可以继续开发 Android 应用,通过 BLE 草图控制 Arduino 板。
设置 Android 应用
在这个项目中,我们将实现一个利用语音识别 API 的 Android 应用程序,并将输出文本在EditText字段中。在后台,我们还将包括 BLE 服务,以便连接到 BLE 模块并能够向其发送消息。一旦我们设置了 BLE 和语音识别 API,我们就可以通过设置条件将它们连接起来,如果语音被识别为开启,则继电器将开启,而如果被识别为关闭,则继电器将关闭。
我们将假设您已经在首选项中切换了自动导入功能。如果没有,请通过转到自动导入首选项并选择所有可用选项来激活它。自动导入首选项在 Mac 和 Windows 上的位置如下:
-
在 Mac 上,导航到Android Studio > 首选项 > 编辑器 > 自动导入
-
在 Windows 上,导航到文件 > 设置 > 编辑器 > 自动导入
在所有必要的设置就绪后,我们将首先创建一个新的项目,在新建项目设置中选择以下内容:
-
名称:
与 Arduino 对话 -
最小 SDK:
18 -
项目:
空白活动 -
活动名称:
MainScreen -
域名:
arduinoandroid.com
为了使这个项目工作,我们首先需要转到 Android 的Manifest文件,该文件位于app > src > main > java > 包名 > AndroidManifest.xml。
设计 Android 用户界面和权限
一旦我们打开文件,我们需要添加访问蓝牙功能的权限;这将允许我们向 Arduino 传输语音消息。以下两行 XML 需要添加到 Android 的Manifest文件中:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
我们接下来要做的步骤是设置一个非常基本的 Android 布局文件,以便我们能够实现应用程序的功能,并允许用户激活语音识别意图。
在我们的项目中,我们将导航到主布局文件,该文件可以通过app > src > res > layout > activity_speech.xml访问。
通过用以下代码替换当前代码,我们将添加一个包含两个按钮、一个EditText字段和一个TextView字段的相对布局,这将使我们能够看到语音输入的结果:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".SpeechActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Talk to Arduino"
android:id="@+id/talktoArduino"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/recordedTalk"
android:text="What is recorded will be written here"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="139dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceSmall"
android:text="Bluetooth Output"
android:id="@+id/btView"
android:layout_marginTop="76dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refresh"
android:id="@+id/refreshBtn"
android:layout_above="@+id/talktoArduino"
android:layout_alignStart="@+id/talktoArduino"
android:layout_alignEnd="@+id/talktoArduino" />
</RelativeLayout>
编写应用程序的内部代码
然后,我们将转到MainScreen.java文件,该文件位于app > src > main > java > 包名 > MainScreen.java。
然后,我们将用以下代码替换当前代码,我们将逐步讲解,并启用自动导入;Android Studio 将自动导入我们项目所需的所有语句。
注意
您可以自由地通过 GitHub 仓库跟随项目,那里提供了我们书中用户所需的所有源代码。仓库地址为github.com/marcoschwartz/arduino-android-blueprints/tree/master/chapter7/TalktoArduino。
我们将从声明扩展Activity的类开始:
public class SpeechActivity extends Activity {
以下是需要声明的所有变量,以便与 BLE 模块一起工作,用于日志记录的日志标签,用户界面元素,以及用于语音识别请求的蓝牙特征:
private static final int VOICE_RECOGNITION_REQUEST = 1;
//Getting the name for Log Tags
private final String LOG_TAG = SpeechActivity.class.getSimpleName();
//Declare U.I Elements
private Button startTalk;
private Button refresh;
private EditText speechInput;
private TextView btv;
// UUIDs for UAT service and associated characteristics.
public static UUID UART_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID TX_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID RX_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
// UUID for the BTLE client characteristic which is necessary for notifications.
public static UUID CLIENT_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
// BTLE stateta
private BluetoothAdapter adapter;
private BluetoothGatt gatt;
private BluetoothGattCharacteristic tx;
private BluetoothGattCharacteristic rx;
private boolean areServicesAccessible = false;
在OnCreate()方法中,我们将初始化我们之前实现的用户界面布局,并将用户界面元素连接到我们代码中的不同方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_speech);
startTalk = (Button) findViewById(R.id.talktoArduino);
refresh = (Button) findViewById(R.id.refreshBtn);
speechInput = (EditText) findViewById(R.id.recordedTalk);
btv = (TextView) findViewById(R.id.btView);
startTalk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
recordSpeech();
}
});
refresh.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
restartScan();
}
});
}
recordSpeech()方法允许我们启动 Google 语音识别意图,我们可以修改将显示给用户的消息。在这种情况下,我们决定用提示“您现在可以向 Arduino 发送命令”替换默认文本:
public void recordSpeech() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "You can now send a command to the Arduino");
startActivityForResult(intent, VOICE_RECOGNITION_REQUEST);
}
onActivityResult()方法允许应用程序处理已识别的内容,并根据接收到的内容实现方法。在以下方法中,我们将识别出的语音输出到我们之前设置的EditText字段中,并根据输出结果,通过 BLE 发送命令来打开或关闭继电器:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == VOICE_RECOGNITION_REQUEST && resultCode == RESULT_OK) {
ArrayList<String> matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
String userInput = matches.get(0);
TextView textSaid = (TextView) findViewById(R.id.recordedTalk);
textSaid.setText(matches.get(0));
//add an if else loop or case statement
if (userInput.equalsIgnoreCase("switch on")) {
String setOutputMessage = "/digital/7/1 /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeSensorData("Sent: " + setOutputMessage);
} else {
writeSensorData("Couldn't write TX characteristic!");
}
} else if (userInput.equalsIgnoreCase("switch off")) {
String setOutputMessage = "/digital/7/0 /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeSensorData("Sent: " + setOutputMessage);
} else {
writeSensorData("Couldn't write TX characteristic!");
}
}
}
super.onActivityResult(requestCode, resultCode, data);
}
以下代码处理确保蓝牙回调输出被发送到其关联的TextView:
private void writeSensorData(final CharSequence text) {
Log.e(LOG_TAG, text.toString());
btv.setText(text.toString());
}
在这里,我们将处理所有需要实现以连接到 BLE 模块的BluetoothGattCallback类:
// BTLE device scanning bluetoothGattCallback.
// Main BTLE device bluetoothGattCallback where much of the logic occurs.
private BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
// Called whenever the device connection state changes, i.e. from disconnected to connected.
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothGatt.STATE_CONNECTED) {
writeSensorData("Connected!");
// Discover services.
if (!gatt.discoverServices()) {
writeSensorData("Failed to start discovering services!");
}
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
writeSensorData("Disconnected!");
} else {
writeSensorData("Connection state changed. New state: " + newState);
}
}
// Called when services have been discovered on the remote device.
// It seems to be necessary to wait for this discovery to occur before
// manipulating any services or characteristics.
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
writeSensorData("Service discovery completed!");
} else {
writeSensorData("Service discovery failed with status: " + status);
}
// Save reference to each characteristic.
tx = gatt.getService(UART_UUID).getCharacteristic(TX_UUID);
rx = gatt.getService(UART_UUID).getCharacteristic(RX_UUID);
// Setup notifications on RX characteristic changes (i.e. data received).
// First call setCharacteristicNotification to enable notification.
if (!gatt.setCharacteristicNotification(rx, true)) {
writeSensorData("Couldn't set notifications for RX characteristic!");
}
// Next update the RX characteristic's client descriptor to enable notifications.
if (rx.getDescriptor(CLIENT_UUID) != null) {
BluetoothGattDescriptor desc = rx.getDescriptor(CLIENT_UUID);
desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!gatt.writeDescriptor(desc)) {
writeSensorData("Couldn't write RX client descriptor value!");
}
} else {
writeSensorData("Couldn't get RX client descriptor!");
}
areServicesAccessible = true;
}
};
在以下onStart()和onStop()方法中,我们确保开始扫描 BLE 设备,并在关闭应用程序时停止蓝牙扫描,以防止电池耗尽并确保优化前台运行的任务的设备内存资源:
protected void onStart() {
Log.d(LOG_TAG,"onStart has been called");
super.onStart();
// / Scan for all BTLE devices.
// The first one with the UART service will be chosen--see the code in the scanCallback.
adapter = BluetoothAdapter.getDefaultAdapter();
startScan();
}
//When this Activity isn't visible anymore
protected void onStop() {
Log.d(LOG_TAG,"onStop has been called");
//disconnect and close Bluetooth Connection for better reliability
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
tx = null;
rx = null;
}
super.onStop();
}
以下方法处理蓝牙扫描回调的启动、停止和重新启动:
private void startScan() {
if (!adapter.isEnabled()) {
adapter.enable();
}
if (!adapter.isDiscovering()) {
adapter.startDiscovery();
}
writeSensorData("Scanning for devices...");
adapter.startLeScan(scanCallback);
}
private void stopScan() {
if (adapter.isDiscovering()) {
adapter.cancelDiscovery();
}
writeSensorData("Stopping scan");
adapter.stopLeScan(scanCallback);
}
private void restartScan() {
stopScan();
startScan();
}
scanCallback()方法主要关注获取蓝牙设备地址和保持 Android 设备和 BLE 模块之间必要连接的主要逻辑:
/**
* Main callback following an LE device scan
*/
private BluetoothAdapter.LeScanCallback scanCallback = new BluetoothAdapter.LeScanCallback() {
// Called when a device is found.
@Override
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
Log.d(LOG_TAG, bluetoothDevice.getAddress());
writeSensorData("Found device: " + bluetoothDevice.getAddress());
// Check if the device has the UART service.
if (BluetoothUtils.parseUUIDs(bytes).contains(UART_UUID)) {
// Found a device, stop the scan.
adapter.stopLeScan(scanCallback);
writeSensorData("Found UART service!");
// Connect to the device.
// Control flow will now go to the bluetoothGattCallback functions when BTLE events occur.
gatt = bluetoothDevice.connectGatt(getApplicationContext(), false, bluetoothGattCallback);
}
}
};
}
与本书的前几章相比,您会注意到我们将 UUID 解析转移到了utility类中,以便重构代码并使我们的代码更易于阅读。为了创建一个utility类,我们首先需要右键点击我们的包名,创建一个新的包,并将其命名为Bluetooth。
在此之后,我们将右键点击新包,选择新建 > Java 类,并将新类命名为BluetoothUtils。
在这两个步骤之后,我们将替换类中的代码为以下代码:
public class BluetoothUtils {
// Filtering by custom UUID is broken in Android 4.3 and 4.4, see:
// http://stackoverflow.com/questions/18019161/startlescan-with-128-bit-uuids-doesnt-work-on-native-android-ble-implementation?noredirect=1#comment27879874_18019161
// This is a workaround function from the SO thread to manually parse advertisement data.
public static List<UUID> parseUUIDs(final byte[] advertisedData) {
List<UUID> uuids = new ArrayList<UUID>();
int offset = 0;
while (offset < (advertisedData.length - 2)) {
int len = advertisedData[offset++];
if (len == 0)
break;
int type = advertisedData[offset++];
switch (type) {
case 0x02: // Partial list of 16-bit UUIDs
case 0x03: // Complete list of 16-bit UUIDs
while (len > 1) {
int uuid16 = advertisedData[offset++];
uuid16 += (advertisedData[offset++] << 8);
len -= 2;
uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", uuid16)));
}
break;
case 0x06:// Partial list of 128-bit UUIDs
case 0x07:// Complete list of 128-bit UUIDs
// Loop through the advertised 128-bit UUID's.
while (len >= 16) {
try {
// Wrap the advertised bits and order them.
ByteBuffer buffer = ByteBuffer.wrap(advertisedData, offset++, 16).order(ByteOrder.LITTLE_ENDIAN);
long mostSignificantBit = buffer.getLong();
long leastSignificantBit = buffer.getLong();
uuids.add(new UUID(leastSignificantBit,
mostSignificantBit));
} catch (IndexOutOfBoundsException e) {
// Defensive programming.
//Log.e(LOG_TAG, e.toString());
continue;
} finally {
// Move the offset to read the next uuid.
offset += 15;
len -= 16;
}
}
break;
default:
offset += (len - 1);
break;
}
}
return uuids;
}
}
一旦你包含了这段代码,你就可以在你的安卓物理设备上构建并运行这个应用,该设备运行 Android 4.3 或更高版本,并且连接到互联网,因为大多数语音识别服务都是通过互联网工作的。
当你加载应用时,你应该从以下内容开始:

如何更进一步
这个基础项目提供了无限的可能性,你可能会包含其他可识别的命令,以便连接其他组件和传感器,从而增强你语音激活应用的功能。我们希望这个基础项目能够激发你进一步改进你的项目。
摘要
让我们总结一下本章所做的工作。像往常一样,我们将一个 BLE 模块连接到 Arduino 板上,以便它可以通过安卓手机接收命令。我们还连接了一个简单的继电器模块到板上,通过安卓应用程序来控制它。然后我们设计了一个应用,使用安卓语音引擎根据用户对安卓手机说的话来控制继电器。
在下一章中,我们将使用安卓手机的另一个功能来控制 Arduino 项目:NFC。我们将使用 NFC 通过将手机放在 Arduino NFC 保护板前,来控制继电器的状态。
第八章:控制通过 NFC 的 Arduino 板
在本章中,我们将看到将 Arduino 近场通信(NFC)盾牌从 Seeed Studio 集成到具有 NFC 功能的 Android 应用程序的能力,该应用程序使用Android Beam技术将消息从 Android 应用程序发送到 NFC 盾牌天线。NFC 允许两个靠近的设备之间进行即时通信,这使得它成为打开门锁或支付服务的完美技术。
在本章中,我们将制作一个智能家居应用程序。NFC 盾牌将连接到 Arduino Uno 板,以及继电器。因此,我们将能够根据 Android 应用程序发送的消息切换继电器的开关。
这个基线项目将帮助你开发使用 NFC 的有趣项目,并可能扩展此类项目的功能。
以下将是本章的主要收获:
-
将 NFC 盾牌连接到 Arduino 板
-
构建一个与 NFC Arduino 盾牌通信的 Android 应用程序
-
通过 NFC 从 Android 设备打开和关闭继电器
硬件和软件要求
对于这个项目,你首先需要一块 Arduino Uno 板。
然后,你需要一个 NFC 盾牌。市场上有很多 NFC 盾牌,但为了这个项目,我们选择了来自 SeeedStudio 的 NFC 盾牌 V2.0。我们做出这个选择是因为该盾牌有良好的文档,并且因为已经有了一些示例代码可用。
你还需要一个继电器模块。对于这个项目,我们使用了 Polulu 的 5V 继电器模块。
最后,为了进行不同的电气连接,你需要一些跳线。
以下是你将需要用于此项目的所有硬件组件列表,以及在网上找到这些组件的链接:
-
Arduino Uno 板(
www.adafruit.com/product/50) -
5V 继电器模块(
www.pololu.com/product/2480) -
Arduino NFC 盾牌(
www.seeedstudio.com/depot/nfc-shield-v20-p-1370.html)
在软件方面,你当然需要 Arduino IDE。你还需要以下库来使 NFC 芯片工作:
-
首先,下载 PN532 库(
github.com/Seeed-Studio/PN532),并将所有文件夹放入 Arduino 的libraries文件夹中。 -
然后,下载 NDEF 库(
github.com/don/NDEF),并将其放入 Arduino 的libraries文件夹中,并将文件夹重命名为NDEF。
配置硬件
现在,让我们组装项目。第一步是将 NFC 盾牌放在 Arduino Uno 板上,并将 NFC 读取器连接到盾牌。请注意,NFC 可能没有焊接引脚;在这种情况下,您需要自己焊接盾牌上的引脚。要将 NFC 读取器连接到盾牌,只需通过盾牌上的天线连接器连接读取器即可。
现在,让我们连接继电器。只需将继电器模块的VCC引脚连接到 Arduino 板的 5V 引脚,将GND引脚连接到板的GND引脚。最后,将继电器的SIG引脚连接到 Arduino 板的8号引脚。
以下是你应该得到的结果:

测试 NFC 盾牌
在编写通过 NFC 控制继电器的应用程序之前,我们首先确保盾牌功能正常,并且所有库都已正确安装。为此,我们将编写一个简单的 Arduino 草图。以下是这个部分的完整代码:
#include <SPI.h>
#include <PN532_SPI.h>
#include <PN532.h>
#include <NfcAdapter.h>
// NFC instances
PN532_SPI pn532spi(SPI, 10);
NfcAdapter nfc = NfcAdapter(pn532spi);
void setup(void) {
// Start Serial
Serial.begin(9600);
// Start NFC chip
Serial.println("NFC shield started");
nfc.begin();
}
void loop(void) {
// Start scan
Serial.println("\nScan a NFC tag\n");
if (nfc.tagPresent())
{
NfcTag tag = nfc.read();
tag.print();
}
delay(5000);
}
让我们现在看看这个草图的细节。它首先包含所需的库:
#include <SPI.h>
#include <PN532_SPI.h>
#include <PN532.h>
#include <NfcAdapter.h>
然后,我们可以使用这两行代码创建 NFC 适配器的实例:
PN532_SPI pn532spi(SPI, 10);
NfcAdapter nfc = NfcAdapter(pn532spi);
现在,在草图的setup()函数中,我们将初始化串行通信:
Serial.begin(9600);
我们还将启动 NFC 芯片,并在串行监视器上打印一条消息:
nfc.begin();
Serial.println("NFC shield started");
现在,在草图的loop()函数中,我们将检查是否存在 NFC 标签,如果是这样,我们将读取它:
if (nfc.tagPresent())
{
NfcTag tag = nfc.read();
tag.print();
}
delay(5000);
}
注意
注意,本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中可以找到本章的所有代码。
现在,您可以将草图上传到 Arduino 板,并打开串行监视器。您应该看到 NFC 芯片正在初始化,然后它开始检查可用的标签。如果您有一个简单的 NFC 标签,您现在可以测试它了。
例如,您也可以使用来自 SeeedStudio 的简单标签(www.seeedstudio.com/depot/MifareOne-RFID-Tag-1356MHz-p-923.html)。
这些标签实际上比实际的 RFID 技术要简单,但它们足以测试我们的项目。
编写 Arduino 草图
现在,我们将编写将接收来自 Android NFC 应用的命令的代码。此代码的目标是当 NFC 盾牌从 Android 设备接收到给定代码时切换继电器的开或关。由于这部分代码相当长,我们将将其分成几个部分,分别详细说明。
代码首先包含所需的库:
#include "SPI.h"
#include "PN532_SPI.h"
#include "snep.h"
#include "NdefMessage.h"
我们还将定义继电器连接的引脚:
#define RELAY_PIN 8
然后,我们将定义从 Android 应用接收的代码,以切换继电器的开或关:
#define RELAY_ON "oWnHV6uXre"
我们还需要创建 NFC 芯片的实例:
PN532_SPI pn532spi(SPI, 10);
SNEP nfc(pn532spi);
要存储通过 NFC 从 Android 手机来的数据,我们将创建一个 char 缓冲区:
uint8_t ndefBuf[128];
在草图中的 setup() 函数中,我们将开始串行通信:
Serial.begin(9600);
Serial.println("NFC Peer to Peer Light Switch");
我们还将声明继电器引脚为输出:
pinMode(RELAY_PIN, OUTPUT);
现在,在 loop() 函数中,我们将不断检查通过 NFC 从手机来的数据:
Serial.println("Waiting for message from Peer");
int msgSize = nfc.read(ndefBuf, sizeof(ndefBuf));
现在,如果消息的大小不为零,我们将存储它,处理它,然后检查它是否包含我们之前定义的正确密钥。如果是这样,我们将切换继电器的状态。以下代码块正是执行相同的操作:
if (msgSize > 0) {
// Read message
NdefMessage message = NdefMessage(ndefBuf, msgSize);
// Make sure there is at least one NDEF Record
if (message.getRecordCount() > 0) {
NdefRecord record = message.getRecord(0);
Serial.println("Got first record");
// Check the TNF and Record Type
if (record.getTnf() == TNF_MIME_MEDIA && record.getType() == "application/com.arduinoandroid.arduinonfc") {
Serial.println("Type is OK");
// Get the bytes from the payload
int payloadLength = record.getPayloadLength();
byte payload[payloadLength];
record.getPayload(payload);
// Convert the payload to a String
String payloadAsString = "";
for (int c = 0; c < payloadLength; c++) {
payloadAsString += (char)payload[c];
}
// Print out the data on the Serial monitor
Serial.print("Payload is ");Serial.println(payloadAsString);
// Modify the state of the light, based on the tag contents
if (payloadAsString == RELAY_ON) {
digitalWrite(RELAY_PIN, HIGH);
} else {
digitalWrite(RELAY_PIN, LOW);
}
} else {
Serial.print("Expecting TNF 'Mime Media' (0x02) with type 'application/com.arduinoandroid.arduinonfc' but found TNF ");
Serial.print(record.getTnf(), HEX);
Serial.print(" type ");
Serial.println(record.getType());
}
}
}
}
注意,本章的所有代码都可以在书的 GitHub 仓库中找到,网址为 github.com/marcoschwartz/arduino-android-blueprints。
您现在可以将代码上传到 Arduino 板,并继续开发 Android 应用程序。
设置 Android 应用
在这个项目中,我们将实现一个利用 NFC API 和硬件的 Android 应用,允许我们发送 MIME-type 消息来切换继电器的开关。
我们假设您已经在您的首选项中打开了 自动导入 功能。如果没有,请通过转到 自动导入 首选项并选择所有可用选项来激活它。自动导入 首选项在 Mac 和 Windows 上的位置如下:
-
在 Mac 上,导航到 Android Studio > 首选项| 编辑器| 自动导入
-
在 Windows 上,导航到 文件 | 设置 > 编辑器 > 自动导入
在所有必要的设置到位后,我们首先创建一个新的项目,在 New Project 设置中,我们将选择以下内容:
-
名称:
Arduino NFC -
最小 SDK:
18 -
项目:
Blank Activity -
活动名称:MainScreen
-
域名:
arduinoandroid.com
为了使这个项目工作,我们首先需要转到 Android 的 Manifest 文件,该文件位于 app > src > main > AndroidManifest.xml。
布局 Android 用户界面和权限
一旦我们打开文件,我们需要添加权限,以便 Android 应用能够访问 NFC 硬件。我们需要在我们的 AndroidManifest.xml 文件中添加以下两行代码,以便访问用户权限和实际 NFC 硬件:
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="true" />
下一步是设置基本的 Android 布局文件。这将允许我们实现一个由两个按钮(开关)和一个文本视图组成用户界面。
TextView 中的文本将是我们将发送到我们的 NFC 防护罩的消息。第一步是导航到 Android 布局文件,该文件位于 app > src > res > layout > activity_nfc.xml。
一旦我们在这个布局文件中,我们将切换到 Android 布局文件的文本视图,并将当前代码替换为以下代码行:
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".NFCActivity">
<TextView
android:text="NFC Status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/nfcTextStatus"
android:layout_marginTop="83dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Switch On"
android:id="@+id/switchOnBtn"
android:layout_marginTop="59dp"
android:layout_below="@+id/nfcTextStatus"
android:layout_toLeftOf="@+id/nfcTextStatus"
android:layout_toStartOf="@+id/nfcTextStatus" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Switch Off"
android:id="@+id/switchOffBtn"
android:layout_alignTop="@+id/switchOnBtn"
android:layout_toRightOf="@+id/nfcTextStatus"
android:layout_toEndOf="@+id/nfcTextStatus" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="NFC Message to be sent"
android:id="@+id/messageToBeam"
android:layout_below="@+id/switchOnBtn"
android:layout_centerHorizontal="true"
android:layout_marginTop="93dp" />
</RelativeLayout>
到目前为止,我们应该有一个如下所示的东西:

编写应用程序的内部代码
然后,我们将转到 MainScreen.java 文件,该文件位于 app > src > main > java > package name > NFCActivity.java。
我们将逐步在我们的代码中实现项目。请不要担心导入正确的语句,因为如果你开启了 Auto-Import 功能,Android Studio 将会自动导入我们项目所需的所有语句。如果没有,请按照本章 Android 部分的说明操作。
欢迎您通过 GitHub 仓库跟随项目,其中包含了本书读者的所有源代码。本章的仓库地址为 github.com/marcoschwartz/arduino-android-blueprints/tree/master/chapter8/ArduinoNFC。
我们将首先声明用户界面的变量以及启动 NFC 所必需的变量:
//Declaring the User Interface Variables for mStatusText as a TextView
private TextView mStatusText;
private TextView messageToBeam;
private Button switchOn;
private Button switchOff;
//Initializing the NFC Adapater for sending messages
NfcAdapter mNfcAdapter;
private static final int BEAM_BEAMED = 0x1001;
public static final String MIMETYPE = "application/com.arduinoandroid.arduinonfc";
//Keys for Opening and Closing the Relay
String open_key = "oWnHV6uXre";
String close_key = "C19HNuqNU4";
//Getting the name for Log Tags
private final String TAG = NFCActivity.class.getSimpleName();
在 onCreate 方法中,我们将实现多个匿名类,我们将逐步介绍它们。
在第一部分,我们将连接用户界面元素到主要的 Android 代码:
mStatusText = (TextView) findViewById(R.id.nfcTextStatus);
messageToBeam = (TextView) findViewById(R.id.messageToBeam);
switchOn = (Button) findViewById(R.id.switchOnBtn);
switchOff = (Button) findViewById(R.id.switchOffBtn);
然后,在下面的代码中,我们需要将 onClickListeners 设置到我们的按钮上,以便能够将 TextView 部分更改为正确的文本,以便通过我们的 NFC 防护罩发送消息。在这个代码部分使用术语 beam,因为 Android Beam 是 Android 移动操作系统的功能,允许通过 NFC 传输数据。
// Adding OnClick Listeners to the Buttons
switchOn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
messageToBeam.setText(open_key);
}
});
switchOff.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
messageToBeam.setText(close_key);
}
});
为了提升用户体验,我们需要向用户发送消息,告知他们无法使用这个 Android 应用程序,因为他们设备上没有启用 NFC:
// Check for available NFC Adapter
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
if (mNfcAdapter == null) {
mStatusText.setText("NFC is not available on this device.");
}
在 onCreate() 方法中,我们还将实现基本的 NFC 回调函数,以便能够通过 NFC 发送和接收消息:
// Register to create and NDEF message when another device is in range
mNfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
@Override
public NdefMessage createNdefMessage(NfcEvent event) {
//the variable message is from the EditText field
String message = messageToBeam.getText().toString();
String text = (message);
byte[] mime = MIMETYPE.getBytes(Charset.forName("US-ASCII"));
NdefRecord mimeMessage = new NdefRecord(
NdefRecord.TNF_MIME_MEDIA, mime, new byte[0], text
.getBytes());
NdefMessage msg = new NdefMessage(
new NdefRecord[]{
mimeMessage,
NdefRecord
.createApplicationRecord("com.arduinoandroid.arduinonfc")});
return msg;
}
}, this);
// And handle the send status
mNfcAdapter.setOnNdefPushCompleteCallback(
new NfcAdapter.OnNdefPushCompleteCallback() {
@Override
public void onNdefPushComplete(NfcEvent event) {
mHandler.obtainMessage(BEAM_BEAMED).sendToTarget();
}
}, this);
我们还需要实现一个名为 Handler 的方法,该方法将通过 NFC 状态文本视图通知用户消息是否已通过 NFC 发送:
@SuppressLint("HandlerLeak")
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case BEAM_BEAMED:
mStatusText.setText("Your message has been beamed");
break;
}
}
};
为了完整性,我们还将包括必要的读取通过 NFC 发送的 NDEF 消息的方法,并通过不在应用程序中包含多个实例来提高应用程序的性能:
@Override
public void onResume() {
super.onResume();
// Did we receive an NDEF message?
Intent intent = getIntent();
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())) {
try {
Parcelable[] rawMsgs = intent
.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES);
// we created the message, so we know the format
NdefMessage msg = (NdefMessage) rawMsgs[0];
NdefRecord[] records = msg.getRecords();
byte[] firstPayload = records[0].getPayload();
String message = new String(firstPayload);
mStatusText.setText(message);
} catch (Exception e) {
Log.e(TAG, "Error retrieving beam message.", e);
}
}
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
}
一旦包含了所有的方法,你应该能够构建应用程序,并在具有 NFC 功能的 Android 物理设备上运行它,该设备运行 Android 4.3 或更高版本,并且设置中已激活 Android Beam。
你可以通过点击 开关 按钮来开启中继,并将手机至少对准 NFC 防护罩 5 到 10 秒,此时用户界面将缩小。在此之后,你需要再次点击用户界面来发送你的消息。
如何进一步学习
这个项目主要关注使用 NFC 传输消息,并由 Arduino NFC 防护罩读取。
理想的用户体验将是用户只需将手机轻触 NFC 屏蔽器并打开灯光。这可以通过主机卡仿真或进一步修改此基础项目来实现。
摘要
在本章中,我们学习了设置 NFC 功能 Android 应用的基本要素。此应用通过 NFC 屏蔽器和 Android 4.3 及以上版本的 NFC 功能与 Arduino 通信。
在本章中,我们强调了使用 NFC 进行用户参与型项目的机遇。在下一章中,我们将进一步提升这一层次,并使用蓝牙来给用户提供控制并参与与机器人互动的机会。
第九章. 蓝牙低功耗移动机器人
在本章中,我们将使用本书中学到的几乎所有概念,通过 Android 应用程序控制移动机器人。机器人将有两个我们可以控制的电机,还有一个前方的超声波传感器,以便它可以检测障碍物。机器人还将有一个 BLE 芯片,以便它可以接收来自 Android 应用程序的命令。
应用程序将包含以下基本命令,您需要这些命令来控制机器人:
-
向前移动
-
向后移动
-
向左转
-
向右转
-
显示与机器人的连接状态
本章的主要收获如下:
-
基于 Arduino 平台的移动机器人构建
-
将 BLE 模块连接到 Arduino 机器人
-
构建 Android 应用程序以远程控制机器人
硬件和软件要求
让我们先看看这个项目需要什么。
当然,这个项目的基座当然是机器人本身。对于这个项目,我们使用了 DFRobot miniQ 两轮机器人底盘。它包含一个圆形机器人底盘、两个直流电机、两个轮子和一些螺丝和螺栓,以便您可以在其上安装多个 Arduino 板。您基本上可以使用任何等效的机器人底盘,它有两个轮子,配有两个直流电机,并且可以在其上安装 Arduino 兼容板。
为了控制机器人,我们实际上将使用三个不同的 Arduino 板。机器人的“大脑”将是一个简单的 Arduino Uno 板。在其之上,我们将使用 DFRobot 电机盾板来控制机器人的两个直流电机。然后在这两个板子上,我们将放置一个原型板,以便我们可以将不同的模块连接到机器人上。
要远程控制机器人,我们再次使用 BLE。为了给机器人提供 BLE 连接,我们使用了 Adafruit nRF8001 扩展板。
为了让机器人能够检测其前方的东西,我们在项目中添加了一个 URM37 超声波传感器。正如我们将看到的,这个传感器与 Arduino 接口非常简单。
最后,您还需要一些跳线,以便在机器人、传感器和蓝牙模块之间建立不同的连接。
以下是需要为此项目准备的硬件列表,以及这些部件在网上的链接:
-
Arduino Uno 板(
www.dfrobot.com/index.php?route=product/product&search=uno&description=true&product_id=838) -
Arduino 电机盾板(
www.dfrobot.com/index.php?route=product/product&path=35_39&product_id=59) -
Arduino 原型板(
www.dfrobot.com/index.php?route=product/product&product_id=55) -
nRF8001 扩展板(
www.adafruit.com/products/1697) -
超声波传感器安装套件 (
www.dfrobot.com/index.php?route=product/product&product_id=322) -
DFRobot miniQ 底盘 (
www.dfrobot.com/index.php?route=product/product&search=miniq&description=true&product_id=367) -
7.4 V 电池 (
www.dfrobot.com/index.php?route=product/product&product_id=489)
在软件方面,你当然需要 Arduino IDE。你还需要以下内容:
-
用于 nRF8001 芯片的库 (
github.com/adafruit/Adafruit_nRF8001) -
用于向机器人发送命令的 aREST 库 (
github.com/marcoschwartz/aREST)
配置硬件
我们首先将组装机器人本身,然后看看如何连接蓝牙模块和超声波传感器。为了给你一个你最终应该得到的概念,以下是一个完全组装好的机器人的正面视图:

以下图像显示了完全组装好的机器人的背面:

第一步是组装机器人底盘。为此,你可以观看 DFRobot 组装指南 www.youtube.com/watch?v=tKakeyL_8Fg。
然后,你需要将不同的 Arduino 板和屏蔽板连接到机器人上。使用机器人底盘套件中找到的垫圈首先安装 Arduino Uno 板。然后,将 Arduino 电机屏蔽板放在上面。此时,使用螺丝端子连接两个直流电机到电机屏蔽板上。此时应该看起来是这样的:

最后,将原型板安装在电机板上。
我们现在将连接 BLE 模块和超声波传感器到 Arduino 原型板上。以下是一个示意图,显示了 Arduino Uno 板(在我们的案例中是通过原型板完成的)和组件之间的连接:

现在执行以下步骤:
-
首先,我们现在将连接 BLE 模块。
-
将模块放置在原型板上。
-
按照以下方式连接模块的电源:GND 连接到原型板上的 GND 引脚,VIN 连接到原型板上的 +5V。
-
之后,您需要连接负责 SPI 接口的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。
-
然后将REQ引脚连接到 Arduino 引脚10。
-
最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。
-
对于 URM37 模块,将模块的VCC引脚连接到 Arduino +5V,GND连接到GND,PWM引脚连接到 Arduino A3引脚。
注意
要查看 URM37 模块上的引脚顺序,您可以检查 DFRobot 官方文档
www.dfrobot.com/wiki/index.php?title=URM37_V3.2_Ultrasonic_Sensor_(SKU:SEN0001)。以下是与 BLE 模块连接的印刷电路板的特写图像:
![配置硬件]()
-
最后,将 7.4V 电池连接到 Arduino Uno 板的电源插孔。电池只需放在 Arduino Uno 板下面。
测试机器人
现在,我们将编写一个草图来测试机器人的不同功能,首先不使用蓝牙。由于草图相当长,我们将逐部分查看代码。在您继续之前,请确保电池始终插入到机器人中。现在执行以下步骤:
-
草图首先包含我们将用于通过串行命令控制机器人的 aREST 库:
#include <aREST.h> -
现在我们声明电机连接到的引脚:
int speed_motor1 = 6; int speed_motor2 = 5; int direction_motor1 = 7; int direction_motor2 = 4; -
我们还声明超声波传感器连接到的引脚:
int distance_sensor = A3; -
然后,我们创建 aREST 库的一个实例:
aREST rest = aREST(); -
为了存储超声波传感器测量的距离数据,我们声明一个距离变量:
int distance; -
在草图的
setup()函数中,我们首先初始化我们将用于与机器人通信的串行通信:Serial.begin(115200); -
我们还将距离变量暴露给 REST API,这样我们就可以轻松访问它:
rest.variable("distance",&distance); -
为了控制机器人,我们将声明一组函数来执行基本操作:前进、后退、自身转向(左或右)和停止。我们将在稍后看到这些函数的详细信息;现在,我们只需要将它们暴露给 API:
rest.function("forward",forward); rest.function("backward",backward); rest.function("left",left); rest.function("right",right); rest.function("stop",stop); -
我们还为机器人分配一个 ID 和名称:
rest.set_id("001"); rest.set_name("mobile_robot"); -
在草图的
loop()函数中,我们首先测量传感器距离:distance = measure_distance(distance_sensor); -
我们然后使用 aREST 库处理请求:
rest.handle(Serial); -
现在,我们将查看控制电机的函数。它们都是基于控制单个电机的函数,其中我们需要设置电机引脚、速度和电机的方向:
void send_motor_command(int speed_pin, int direction_pin, int pwm, boolean dir) { analogWrite(speed_pin, pwm); // Set PWM control, 0 for stop, and 255 for maximum speed digitalWrite(direction_pin, dir); // Dir set the rotation direction of the motor (true or false means forward or reverse) } -
基于此函数,我们现在可以定义不同的函数来移动机器人,例如
forward:int forward(String command) { send_motor_command(speed_motor1,direction_motor1,100,1); send_motor_command(speed_motor2,direction_motor2,100,1); return 1; } -
我们还定义了一个
backward函数,简单地反转两个电机的方向:int backward(String command) { send_motor_command(speed_motor1,direction_motor1,100,0); send_motor_command(speed_motor2,direction_motor2,100,0); return 1; } -
要使机器人向左转,我们只需使电机以相反的方向旋转:
int left(String command) { send_motor_command(speed_motor1,direction_motor1,75,0); send_motor_command(speed_motor2,direction_motor2,75,1); return 1; } -
我们还有一个停止机器人的函数:
int stop(String command) { send_motor_command(speed_motor1,direction_motor1,0,1); send_motor_command(speed_motor2,direction_motor2,0,1); return 1; }
还有一个使机器人向右转的函数,这里没有详细说明。请注意,本章中使用的所有代码都可以在本书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
现在我们将测试机器人。在你做任何事情之前,确保电池始终插入到机器人中。这将确保电机不会试图从你的计算机 USB 端口获取电力,这可能会损坏它。
还在机器人的底部放置一些小支撑,这样车轮就不会接触地面。这将确保你可以测试机器人的所有命令,而机器人不会离你的计算机太远,因为它仍然通过 USB 电缆连接。
现在,你可以将草图上传到你的 Arduino Uno 板。打开串行监视器并输入以下内容:
/forward
这应该会使机器人的两个车轮朝同一方向转动。你也可以尝试其他命令来移动机器人,以确保它们都能正常工作。然后,通过输入以下内容来测试超声波距离传感器:
/distance
你应该会得到传感器前方的距离(以厘米为单位):
{"distance": 24, "id": "001", "name": "mobile_robot", "connected": true}
尝试通过将手放在传感器前并再次输入命令来改变距离。
编写 Arduino 草图
现在我们已经确认机器人工作正常,我们可以编写最终的草图,该草图将通过蓝牙接收命令。由于草图与测试草图有很多相似之处,我们只将看到与测试草图相比增加了什么。我们首先需要包含更多的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
我们还定义了 BLE 模块连接的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
我们必须创建 BLE 模块的一个实例:
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
在草图中的setup()函数中,我们初始化 BLE 芯片:
BTLEserial.begin();
在loop()函数中,我们检查 BLE 芯片的状态并将其存储在一个变量中:
BTLEserial.pollACI();
aci_evt_opcode_t status = BTLEserial.getState();
如果我们检测到设备连接到芯片,我们将使用 aREST 库处理传入的请求,这将允许我们使用与之前相同的命令来控制机器人:
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
现在,你可以通过确保电池通过电源插头连接到 Arduino Uno 板来上传代码。现在你可以继续开发用于控制机器人的 Android 应用程序。
设置 Android 应用程序
我们将要创建的 Android 应用程序将使我们能够通过 BLE 从物理 Android 设备控制机器人。该应用程序将具有五个基本控制功能,即前进、后退、左转、右转和停止。此外,它还将显示 BLE 连接状态,并且将有一个刷新按钮,允许我们刷新蓝牙回调。
我们将假设您已经在您的首选项中开启了自动导入功能。如果没有,请通过访问自动导入首选项并选择所有可用选项来激活它。自动导入首选项在 Mac 和 Windows 上的位置如下:
-
在 Mac 上,导航到Android Studio > 首选项 > 编辑器 > 自动导入
-
在 Windows 上,导航到文件 > 设置 > 编辑器 > 自动导入
在所有必要的设置就绪后,我们将开始创建一个新的项目,在新建项目设置向导中,我们将选择以下内容:
-
名称:
移动机器人 -
最小 SDK:
18 -
项目:
空白活动 -
活动名称:
RobotControlActivity -
域名:
arduinoandroid.com
布局 Android 用户界面和设置权限
为了使这个项目工作,我们首先需要转到 Android 的Manifest文件,该文件位于app > src > main > AndroidManifest.xml。
由于这个 Android 应用程序使用 BLE 将 Android 物理设备连接到机器人,我们需要将以下权限添加到 Android 的Manifest文件中。这些权限将允许应用程序连接到已发现的配对蓝牙设备:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
下一步,我们将设置一个非常基本的 Android 布局文件,以便我们可以实现应用程序的功能并允许用户激活语音识别意图。
在我们的项目中,我们将导航到主布局文件,该文件可以通过app > src > res > layout > activity_robot_control.xml访问。
Android 用户界面设计中有多种布局格式,在这种情况下,我们将使用一个水平线性布局,其中包含一个垂直线性布局作为子布局。牢记这些概念,我们将用以下代码行替换当前代码:
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connect"
android:id="@+id/connectBtn"
android:layout_gravity="center_horizontal"
/>
<Button
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Forward"
android:id="@+id/fwdBtn"
android:layout_gravity="center_horizontal"
/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="57dp">
<Button
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Left"
android:id="@+id/leftBtn"
android:layout_weight="1"
/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop"
android:id="@+id/stopBtn"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
/>
<Button
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Right"
android:id="@+id/rightBtn"
android:layout_weight="1"
/>
</LinearLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Backward"
android:id="@+id/backwardBtn"
android:layout_gravity="center_horizontal"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Connection Status View"
android:id="@+id/connectionStsView"
android:layout_gravity="center_horizontal"
/>
</LinearLayout>
在这一点上,您应该得到以下截图所示的内容。这是基于 LG Nexus 5:

编写应用程序的内部代码
在这一点上,我们想要开始将我们刚刚设计的 Android 用户界面连接到主要的 Android 代码,我们将通过打开RobotControlActivity.java文件来开始这个过程,该文件位于app > src > main > java > 包名 > RobotControlActivity.java。
我们将首先声明用户界面元素变量以及主要变量,我们可以使用它进行日志记录,如下所示:
//User Interface Elements
Button fwdBtn;
Button leftBtn;
Button rightBtn;
Button backBtn;
Button stopBtn;
Button connectBtn;
TextView connectionSts;
//Logging Variables
private final String LOG_TAG = RobotControlActivity.class.getSimpleName();
我们将为BluetoothCallback变量声明所有必要的变量,其中我们将主要声明与我们的特定 BLE 模块相关的 UUID,然后是蓝牙适配器变量和特性:
// UUIDs for UAT service and associated characteristics.
public static UUID UART_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID TX_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID RX_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
// UUID for the BTLE client characteristic which is necessary for notifications.
public static UUID CLIENT_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
// BTLE states
private BluetoothAdapter adapter;
private BluetoothGatt gatt;
private BluetoothGattCharacteristic tx;
private BluetoothGattCharacteristic rx;
然后,我们将继续到onCreate()方法,并将不同的用户界面元素连接到代码中:
fwdBtn = (Button) findViewById(R.id.fwdBtn);
leftBtn = (Button) findViewById(R.id.leftBtn);
rightBtn = (Button) findViewById(R.id.rightBtn);
backBtn = (Button) findViewById(R.id.backwardBtn);
stopBtn = (Button) findViewById(R.id.stopBtn);
connectBtn = (Button) findViewById(R.id.connectBtn);
connectionSts = (TextView)findViewById(R.id.connectionStsView);
在这个项目中,我们希望在用户点击按钮时向我们的机器人发送特定的 BLE 消息,在这个部分,我们将为之前连接的按钮添加onClickListeners以发送我们需要与机器人接口的消息:
fwdBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/forward /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
leftBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/left /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
rightBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/right /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
backBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/backward /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
stopBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/stop /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
connectBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
restartScan();
}
});
在下一节中,我们需要声明一个新的方法,我们将将其命名为writeConnectionData。其主要作用是将蓝牙回调的状态写入连接状态文本视图:
private void writeConnectionData(final CharSequence text) {
Log.e(LOG_TAG, text.toString());
connectionSts.setText(text.toString());
}
以下代码是所有必要的蓝牙回调,这些回调需要在 Android 物理设备和机器人上的 BLE 模块之间建立连接:
private BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
// Called whenever the device connection state changes, i.e. from disconnected to connected.
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothGatt.STATE_CONNECTED) {
writeConnectionData("Connected!");
// Discover services.
if (!gatt.discoverServices()) {
writeConnectionData("Failed to start discovering services!");
}
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
writeConnectionData("Disconnected!");
} else {
writeConnectionData("Connection state changed. New state: " + newState);
}
}
// Called when services have been discovered on the remote device.
// It seems to be necessary to wait for this discovery to occur before
// manipulating any services or characteristics.
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
writeConnectionData("Service discovery completed!");
} else {
writeConnectionData("Service discovery failed with status: " + status);
}
// Save reference to each characteristic.
tx = gatt.getService(UART_UUID).getCharacteristic(TX_UUID);
rx = gatt.getService(UART_UUID).getCharacteristic(RX_UUID);
// Setup notifications on RX characteristic changes (i.e. data received).
// First call setCharacteristicNotification to enable notification.
if (!gatt.setCharacteristicNotification(rx, true)) {
writeConnectionData("Couldn't set notifications for RX characteristic!");
}
// Next update the RX characteristic's client descriptor to enable notifications.
if (rx.getDescriptor(CLIENT_UUID) != null) {
BluetoothGattDescriptor desc = rx.getDescriptor(CLIENT_UUID);
desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!gatt.writeDescriptor(desc)) {
writeConnectionData("Couldn't write RX client descriptor value!");
}
} else {
writeConnectionData("Couldn't get RX client descriptor!");
}
areServicesAccessible = true;
}
};
安卓应用程序的生命周期使我们能够添加可以在生命周期不同部分激活的方法。以下onStart()和onStop()方法,分别在启动和退出应用程序时被调用,使我们能够节省设备的能量和内存资源:
protected void onStart() {
Log.d(LOG_TAG,"onStart has been called");
super.onStart();
// / Scan for all BTLE devices.
// The first one with the UART service will be chosen--see the code in the scanCallback.
adapter = BluetoothAdapter.getDefaultAdapter();
startScan();
}
//When this Activity isn't visible anymore
protected void onStop() {
Log.d(LOG_TAG,"onStop has been called");
//disconnect and close Bluetooth Connection for better reliability
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
tx = null;
rx = null;
}
super.onStop();
}
为了允许启动、停止和重新启动蓝牙扫描,我们需要声明执行这些特定操作的方法,这正是以下代码的目的:
private void startScan() {
if (!adapter.isEnabled()) {
adapter.enable();
}
if (!adapter.isDiscovering()) {
adapter.startDiscovery();
}
writeConnectionData("Scanning for devices...");
adapter.startLeScan(scanCallback);
}
private void stopScan() {
if (adapter.isDiscovering()) {
adapter.cancelDiscovery();
}
writeConnectionData("Stopping scan");
adapter.stopLeScan(scanCallback);
}
private void restartScan() {
stopScan();
startScan();
}
蓝牙回调最重要的部分是连接到正确的 BLE 设备,以下代码帮助用户实现这一点:
private BluetoothAdapter.LeScanCallback scanCallback = new BluetoothAdapter.LeScanCallback() {
// Called when a device is found.
@Override
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
Log.d(LOG_TAG, bluetoothDevice.getAddress());
writeConnectionData("Found device: " + bluetoothDevice.getAddress());
// Check if the device has the UART service.
if (BluetoothUtils.parseUUIDs(bytes).contains(UART_UUID)) {
// Found a device, stop the scan.
adapter.stopLeScan(scanCallback);
writeConnectionData("Found UART service!");
// Connect to the device.
// Control flow will now go to the bluetoothGattCallback functions when BTLE events occur.
gatt = bluetoothDevice.connectGatt(getApplicationContext(), false, bluetoothGattCallback);
}
}
};
与前几章不同,UUID 解析已被移动到utility类中,以便重构代码并使其更易于阅读。为了创建一个utility类,我们首先需要右键单击我们的包名,创建一个新的名为Bluetooth的包。
之后,我们将右键单击新包,选择新建 > Java 类,并将新类命名为BluetoothUtils。
在前两个步骤之后,我们将用以下代码替换类中的代码:
public class BluetoothUtils {
// Filtering by custom UUID is broken in Android 4.3 and 4.4, see:
// http://stackoverflow.com/questions/18019161/startlescan-with-128-bit-uuids-doesnt-work-on-native-android-ble-implementation?noredirect=1#comment27879874_18019161
// This is a workaround function from the SO thread to manually parse advertisement data.
public static List<UUID> parseUUIDs(final byte[] advertisedData) {
List<UUID> uuids = new ArrayList<UUID>();
int offset = 0;
while (offset < (advertisedData.length - 2)) {
int len = advertisedData[offset++];
if (len == 0)
break;
int type = advertisedData[offset++];
switch (type) {
case 0x02: // Partial list of 16-bit UUIDs
case 0x03: // Complete list of 16-bit UUIDs
while (len > 1) {
int uuid16 = advertisedData[offset++];
uuid16 += (advertisedData[offset++] << 8);
len -= 2;
uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", uuid16)));
}
break;
case 0x06:// Partial list of 128-bit UUIDs
case 0x07:// Complete list of 128-bit UUIDs
// Loop through the advertised 128-bit UUID's.
while (len >= 16) {
try {
// Wrap the advertised bits and order them.
ByteBuffer buffer = ByteBuffer.wrap(advertisedData, offset++, 16).order(ByteOrder.LITTLE_ENDIAN);
long mostSignificantBit = buffer.getLong();
long leastSignificantBit = buffer.getLong();
uuids.add(new UUID(leastSignificantBit,
mostSignificantBit));
} catch (IndexOutOfBoundsException e) {
// Defensive programming.
//Log.e(LOG_TAG, e.toString());
continue;
} finally {
// Move the offset to read the next uuid.
offset += 15;
len -= 16;
}
}
break;
default:
offset += (len - 1);
break;
}
}
return uuids;
}
}
到目前为止,你可以继续构建并运行项目,在一个运行 Android 4.3 且蓝牙已开启的安卓物理设备上。
进一步增强用户界面
一旦我们成功完成了代码,并确保用户界面包含了控制机器人所需的所有基本功能,我们就可以继续改进我们的用户界面。
我们将通过两个主要动作来改进用户界面:
-
添加新的应用图标
-
设计用户界面按钮样式
添加新的应用图标
首先,我们将下载图像资源。它可以在 GitHub 仓库中找到,也可以在bit.ly/mobileroboticon作为公开下载。
你应该导航到项目树,然后右键单击app。
当你右键单击app时,通过新建 > 图像资源创建一个新的图像资源。
然后您将看到一个Asset Studio弹出窗口,它将允许您选择您自己的图像文件。出于优化的目的,我们建议您选择一个 144 像素乘以 144 像素分辨率的.png文件。Android Studio 会自动进行所有调整大小和资源创建,以适应不同屏幕的图形。
一旦您选择了我们提供的ic_launcher图像文件,您将看到一个显示不同尺寸图标的屏幕。点击下一步,您将看到不同尺寸的启动器图标屏幕。
此屏幕警告您将覆盖以前的文件,并再次以多种分辨率显示图像启动器文件。点击完成。然后编译应用程序,在您的物理设备上启动它,您应该在应用程序托盘和应用程序的操作栏中看到一些令人愉快的东西。
用户界面按钮的样式
我们将要采取的最终步骤是修改我们的按钮并给文本添加一些颜色。
在创建新按钮时,需要执行两个步骤:
-
在
res文件夹内创建一个名为button.xml的新 XML 可绘制文件。 -
然后将可绘制资源文件连接到主 Android 布局文件。
通过右键单击res文件夹创建Drawable文件夹,该文件夹位于App > src > main > res。
在res文件夹内创建Drawable文件夹后,我们需要再次右键单击新的drawable文件夹,并导航到新建 > 可绘制资源文件。
将文件命名为buttonshape,将根元素类型设置为shape,然后点击确定。
在button.xml文件中,将当前代码替换为以下内容:
<shape >
<corners
android:radius="30dp"/>
<solid
android:color="#FFFFFF"/>
<padding
android:left="10dp"
android:top="10dp"
android:right="10dp"
android:bottom="10dp"/>
<stroke
android:width="2dp"
android:color="#4A90E2"/>
</shape>
到目前为止,按钮尚未修改,因此我们将转到机器人控制活动布局文件,该文件位于app > main > res > layout > activity_robot_control.xml。
在此文件中,我们还将把buttonshape.xml文件中的更改连接到主布局文件,并为按钮添加margin,以便在按钮之间有足够的间距,以形成一个可展示的布局。
我们将向所有按钮元素添加以下代码,以赋予它们buttonshape样式:
android:background="@drawable/buttonshape"
之后,我们将通过在连接、后退和前进按钮中添加以下代码来添加边距:
android:layout_margin="10dp"
对于左右按钮,由于它们位于不同类型的布局中,我们将添加以下代码:
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
到目前为止,您应该有一个布局,看起来像以下截图中的 Nexus 4,这对用户来说更吸引人,更易于展示:

如何进一步操作
可以通过添加更精细的控制来进一步改进 Android 应用程序,这些控制可以量化您希望机器人向左或向右转的确切角度。我们还可以从超声波传感器中提取数据,并在 Android 应用程序中显示,以获取关于障碍物接近程度的数据。
此外,Android 应用程序将肯定从添加一个连接对话框中受益,该对话框显示用户所有可用的 BLE 设备,用户可以选择连接到机器人的 BLE 芯片。这将增强用户体验,同时提供与机器人更稳定的连接,尤其是在您在一个周围有其他 BLE 发射器的环境中工作时。
最后但同样重要的是,读者还可以进一步修改用户界面和布局,使应用程序更具吸引力和可展示性。我们的主要建议是遵循developer.android.com上可用的设计指南。
摘要
在本章中,我们成功创建了自己的移动机器人,并附带了一个 Android 应用程序,我们可以使用它来控制我们的机器人。
我们通过设置一个 Arduino 启用的机器人和编写配套的 Android 应用程序逐步实现了这一点。它使用运行在 Android 4.3 或更高版本的 Android 物理设备的 BLE 软件和硬件。
在最后一章中,我们将考虑一种更直接的用户交互形式,通过使用 Android、Arduino 和特定传感器来测量我们的脉搏率。
第十章:脉搏率传感器
在本章中,我们将开始探索在健康环境中使用 Arduino 和 Android 的可能性。开始这样冒险最自然的方式是创建一个涉及开源脉搏率传感器的项目。这个传感器将连接到一个配备 BLE 的 Arduino。数据将以有趣的方式在 Android 应用程序中显示,以使体验尽可能无缝。
本章的主要收获如下:
-
使用 Arduino 脉搏率传感器测量心率
-
将 BLE 模块连接到 Arduino 以传输脉搏率数据
-
在 Android 应用程序中可视化这些数据
硬件和软件要求
首先,让我们看看这个项目需要哪些东西。像往常一样,我们将使用 Arduino Uno 板。
你还需要一个心率传感器,这是本章最重要的组件。我们使用了一个与 Arduino 兼容的传感器,简单称为脉搏传感器(pulsesensor.com/)。
以下是我们使用的传感器的图片:

对于无线通信,我们使用了之前章节中使用的 nRF8001 BLE 扩展板。
最后,你需要一个面包板和一些跳线来连接不同部件。
这是你需要为这个项目准备的硬件清单,以及在网上找到这些部件的链接:
-
Arduino Uno 板(
www.adafruit.com/products/50) -
nRF8001 扩展板(
www.adafruit.com/products/1697) -
心率传感器(
pulsesensor.myshopify.com/products/pulse-sensor-amped)
在软件方面,你当然需要 Arduino IDE。你还需要以下内容:
-
nRF8001 芯片的库,可在
github.com/adafruit/Adafruit_nRF8001找到 -
用于向机器人发送命令的 aREST 库,可在
github.com/marcoschwartz/aREST找到
配置我们的硬件
我们现在将通过以下步骤来构建项目:
-
首先,将 BLE 扩展板连接到 Arduino Uno 板。
-
将模块放置在面包板上。
-
连接模块的电源:GND连接到原型板GND,VIN连接到原型板+5V。
-
连接负责 SPI 接口的不同电线:SCK连接到 Arduino 引脚13,MISO连接到 Arduino 引脚12,MOSI连接到 Arduino 引脚11。
-
然后将REQ引脚连接到 Arduino 引脚 10。
-
最后,将RDY引脚连接到 Arduino 引脚2,将RST引脚连接到 Arduino 引脚9。
以下是一个帮助您完成这一部分的示意图:
![配置我们的硬件]()
-
现在,连接脉搏率传感器实际上非常简单。你只需要将红色电线连接到 Arduino +5V 引脚,黑色电线连接到 Arduino GND 引脚,其余引脚连接到 Arduino A0 引脚。
这是一张完全组装好的项目的图片:

如果您想了解更多关于脉搏率传感器的信息,您可以访问官方文档pulsesensor.myshopify.com/pages/code-and-guide。
测试传感器
我们现在将编写一些基本的代码来确保脉搏传感器正确接线且没有损坏。多亏了传感器的创造者的工作,实际上从传感器读数中提取心率是非常容易的。以下草图首先定义了大量的变量,这些变量是计算每分钟心跳次数(BPM)所必需的:
// Sensor and pins variables
int pulsePin = 0;
int blinkPin = 13;
// Pulse rate variable
volatile int BPM;
// Raw signal
volatile int Signal;
// Interval between beats
volatile int IBI = 600; // Default Inter Beats Interval
// Becomes true when the pulse is high
volatile boolean Pulse = false;
// Becomes true when Arduino finds a pulse (QS stands for Quantified Self here)
volatile boolean QS = false;
在草图的setup()函数中,我们简单地启动串行通信并初始化从脉搏传感器读取的读数:
// Start Serial
Serial.begin(115200);
// Sets up to read Pulse Sensor signal every 2mS
interruptSetup();
然后,在草图的loop()函数中,我们不断检查是否找到了心跳,如果是这样,我们就在串行监视器上打印出来:
// If heart beat is found
if (QS == true) {
// Print heart rate
Serial.print("Heart rate: ");
Serial.println(BPM);
// Reset the Quantified Self flag for next time
QS = false;
}
// Wait 20 ms
delay(20);
}
注意
注意,本章中使用的所有代码都可以在书的 GitHub 仓库github.com/marcoschwartz/arduino-android-blueprints中找到。
现在是测试代码的时候了。在将代码上传到你的板子之前,建议你观看pulsesensor.myshopify.com/blogs/news/7406100-getting-started-video上的视频,以便了解如何正确地将传感器放在指尖上。
现在,你可以将代码上传到你的 Arduino 板上,并打开串行监视器。然后将传感器放在你的指尖上。过了一会儿(最初可能会有一些奇怪的读数),你应该在串行监视器上看到你的心率。当你知道值在 60 到 100 BPM 之间时(如果你处于休息状态),你就知道它是正确的。
编写 Arduino 草图
既然我们已经确认传感器工作正常,我们可以为这一章编写最终的 Arduino 草图。这个草图将像之前一样执行 BPM 测量,并且将通过 aREST API 公开 BPM 变量,以便可以通过蓝牙访问测量结果。由于草图与测试草图非常相似,我们在这里只详细说明更改。
草图首先导入所需的库:
#include <SPI.h>
#include "Adafruit_BLE_UART.h"
#include <aREST.h>
我们还定义了 BLE 模块连接的引脚:
#define ADAFRUITBLE_REQ 10
#define ADAFRUITBLE_RDY 2 // This should be an interrupt pin, on Uno thats #2 or #3
#define ADAFRUITBLE_RST 9
然后我们创建了一个 aREST 库和 BLE 模块的实例:
aREST rest = aREST();
// BLE instance
Adafruit_BLE_UART BTLEserial = Adafruit_BLE_UART(ADAFRUITBLE_REQ, ADAFRUITBLE_RDY, ADAFRUITBLE_RST);
我们还需要定义一个变量,该变量将包含 BPM 测量值,并且将暴露给 API:
int bpm = 0;
在setup()函数中,我们需要初始化 BLE 模块:
BTLEserial.begin();
我们还为项目命名并分配 ID:
rest.set_id("1");
rest.set_name("pulse_sensor");
仍然在setup()函数中,我们将 BPM 变量暴露给 aREST API:
rest.variable("bpm",&bpm);
在草图中的loop()函数中,我们将测量的 BPM 分配给暴露给 API 的变量:
bpm = BPM;
然后,像往常一样,我们使用 aREST API 处理 BLE 模块上的传入请求:
// Tell the nRF8001 to do whatever it should be working on.
BTLEserial.pollACI();
// Ask what is our current status
aci_evt_opcode_t status = BTLEserial.getState();
// Handle REST calls
if (status == ACI_EVT_CONNECTED) {
rest.handle(BTLEserial);
}
}
注意,本章的所有代码都可以在本书的 GitHub 仓库中找到,网址为github.com/marcoschwartz/arduino-android-blueprints。您现在可以将代码上传到 Arduino 板,并继续开发 Android 应用程序。
设置 Android 应用程序
我们将要创建的 Android 应用程序将使我们能够在 Android 应用程序中显示由心率传感器测量的数据。此外,它将显示 BLE 连接状态,并且有一个刷新按钮,允许我们刷新蓝牙回调。
我们假设您已经在您的首选项中打开了自动导入功能。如果没有,请通过转到自动导入首选项并选择所有可用选项来激活它。自动导入首选项在 Mac 和 Windows 上的位置如下:
-
在 Mac 上,导航到Android Studio > 首选项 > 编辑器 > 自动导入
-
在 Windows 上,导航到文件 > 设置 > 编辑器 > 自动导入
在所有必要的设置就绪后,我们将开始创建一个新项目,在新建项目设置向导中,我们将选择以下内容:
-
名称:
Pulse Rate Sensor -
最小 SDK:
18 -
项目:
Blank Activity -
活动名称:
PulseActivity -
域名:
arduinoandroid.com
布局 Android 用户界面和设置权限
为了使这个项目工作,我们首先需要转到 Android 的Manifest文件,该文件位于app > src > main > AndroidManifest.xml。
由于这个 Android 应用程序使用 BLE 将 Android 物理设备连接到心率传感器,因此我们需要将以下权限添加到 Android 的Manifest文件中:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
我们接下来要采取的下一步是设置非常基本的 Android 布局文件,以便我们能够实现应用程序功能。
在我们的项目中,我们将导航到主布局文件,该文件可以通过app > src > res > layout > activity_pulse.xml访问。
Android 用户界面设计中有多种布局格式,在这种情况下,我们将实现两个线性布局:一个将设计为充当图表视图的占位符,另一个将支持不同的按钮和文本视图。
将布局文件中当前可用的代码替换为以下代码:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<LinearLayout
android:id="@+id/rest"
android:layout_width="fill_parent"
android:layout_height="250dip"
android:orientation="vertical"
android:weightSum="1">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:id="@+id/pulseValueView"
android:layout_gravity="center_horizontal"
android:textSize="150dp"
android:gravity="center"
android:text="120"/>
</LinearLayout>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Refresh Connection"
android:id="@+id/refreshBtn"
android:layout_gravity="center_horizontal" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Get Pulse Rate"
android:id="@+id/heartRateBtn"
android:layout_gravity="center_horizontal" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Connection Status"
android:id="@+id/connectionStsView"
android:layout_gravity="center_horizontal" />
</LinearLayout>
最终结果将在 IDE 中呈现如下:

文本 120 的目的是作为一个占位文本,以确保在用户界面中有足够的空间来容纳脉搏率读数。在最终实现中,您可以选择删除占位文本,使其为空。
编写应用程序的内部代码
我们需要首先声明所有必要的变量,这些变量用于与蓝牙逻辑、用户界面和日志记录一起工作:
//Logging Variables
private final String LOG_TAG = PulseActivity.class.getSimpleName();
//User Interface Variables
Button getPulseRate;
Button refreshButton;
TextView pulseRateView;
TextView connectionStsView;
//Data Output
private String output;
// UUIDs for UAT service and associated characteristics.
public static UUID UART_UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID TX_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E");
public static UUID RX_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E");
// UUID for the BTLE client characteristic which is necessary for notifications.
public static UUID CLIENT_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
// BTLE stateta
private BluetoothAdapter adapter;
private BluetoothGatt gatt;
private BluetoothGattCharacteristic tx;
private BluetoothGattCharacteristic rx;
private boolean areServicesAccessible = false;
在此之后,我们需要在 onCreate() 方法中将用户界面元素连接到用户界面,并将 onClickListener 类设置到 获取脉搏率 和 刷新 按钮上,这将允许我们请求脉搏率传感器数据并刷新蓝牙连接:
//Connect U.I Elements
getPulseRate = (Button) findViewById(R.id.heartRateBtn);
pulseRateView = (TextView) findViewById(R.id.pulseValueView);
connectionStsView = (TextView) findViewById(R.id.connectionStsView);
refreshButton = (Button) findViewById(R.id.refreshBtn);
getPulseRate.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
String setOutputMessage = "/bpm /";
tx.setValue(setOutputMessage.getBytes(Charset.forName("UTF-8")));
if (gatt.writeCharacteristic(tx)) {
writeConnectionData("Sent: " + setOutputMessage);
} else {
writeConnectionData("Couldn't write TX characteristic!");
}
}
});
refreshButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
restartScan();
}
});
}
由于我们在项目中使用蓝牙,我们需要实现允许我们获取字符数据序列、将它们转换为字符串,并将其最终连接到用户界面以显示数据的方法:
private void writeConnectionData(final CharSequence text) {
Log.e(LOG_TAG, text.toString());
connectionStsView.setText(text.toString());
}
private void writeSensorData(final CharSequence text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.e(LOG_TAG,text.toString());
output=text.toString().trim();
if (output.length() > 0 && output.length() <=3) {
pulseRateView.setText(output);
}
else {
return;
}
}
});
}
以下方法将允许我们执行必要的蓝牙回调,并通过 BLE 模块从 Arduino 脉搏率传感器发送或接收数据:
// BTLE device scanning bluetoothGattCallback.
// Main BTLE device bluetoothGattCallback where much of the logic occurs.
private BluetoothGattCallback bluetoothGattCallback = new BluetoothGattCallback() {
// Called whenever the device connection state changes, i.e. from disconnected to connected.
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
if (newState == BluetoothGatt.STATE_CONNECTED) {
writeConnectionData("Connected!");
// Discover services.
if (!gatt.discoverServices()) {
writeConnectionData("Failed to start discovering services!");
}
} else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
writeConnectionData("Disconnected!");
} else {
writeConnectionData("Connection state changed. New state: " + newState);
}
}
// Called when services have been discovered on the remote device.
// It seems to be necessary to wait for this discovery to occur before
// manipulating any services or characteristics.
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
writeConnectionData("Service discovery completed!");
} else {
writeConnectionData("Service discovery failed with status: " + status);
}
// Save reference to each characteristic.
tx = gatt.getService(UART_UUID).getCharacteristic(TX_UUID);
rx = gatt.getService(UART_UUID).getCharacteristic(RX_UUID);
// Setup notifications on RX characteristic changes (i.e. data received).
// First call setCharacteristicNotification to enable notification.
if (!gatt.setCharacteristicNotification(rx, true)) {
writeConnectionData("Couldn't set notifications for RX characteristic!");
}
// Next update the RX characteristic's client descriptor to enable notifications.
if (rx.getDescriptor(CLIENT_UUID) != null) {
BluetoothGattDescriptor desc = rx.getDescriptor(CLIENT_UUID);
desc.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!gatt.writeDescriptor(desc)) {
writeConnectionData("Couldn't write RX client descriptor value!");
}
} else {
writeConnectionData("Couldn't get RX client descriptor!");
}
areServicesAccessible = true;
}
// Called when a remote characteristic changes (like the RX characteristic).
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
writeSensorData(characteristic.getStringValue(0));
}
};
private BluetoothAdapter.LeScanCallback scanCallback = new BluetoothAdapter.LeScanCallback() {
// Called when a device is found.
@Override
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
Log.d(LOG_TAG, bluetoothDevice.getAddress());
writeConnectionData("Found device: " + bluetoothDevice.getAddress());
// Check if the device has the UART service.
if (BluetoothUtils.parseUUIDs(bytes).contains(UART_UUID)) {
// Found a device, stop the scan.
adapter.stopLeScan(scanCallback);
writeConnectionData("Found UART service!");
// Connect to the device.
// Control flow will now go to the bluetoothGattCallback functions when BTLE events occur.
gatt = bluetoothDevice.connectGatt(getApplicationContext(), false, bluetoothGattCallback);
}
}
};
}
Android 应用程序生命周期允许我们在其不同阶段实现方法,因此在下述代码中,我们将实现两个方法,允许我们在应用程序激活时开始蓝牙扫描,并在用户退出应用程序时停止蓝牙扫描和其他相关活动:
protected void onStart() {
Log.d(LOG_TAG,"onStart has been called");
super.onStart();
// / Scan for all BTLE devices.
// The first one with the UART service will be chosen--see the code in the scanCallback.
adapter = BluetoothAdapter.getDefaultAdapter();
startScan();
}
//When this Activity isn't visible anymore
protected void onStop() {
Log.d(LOG_TAG,"onStop has been called");
//disconnect and close Bluetooth Connection for better reliability
if (gatt != null) {
gatt.disconnect();
gatt.close();
gatt = null;
tx = null;
rx = null;
}
super.onStop();
}
我们还将包括一些方法,这些方法有助于 BLE 扫描回调,并使我们能够重构代码并保持代码整洁:
//BLUETOOTH METHODS
private void startScan() {
if (!adapter.isEnabled()) {
adapter.enable();
}
if (!adapter.isDiscovering()) {
adapter.startDiscovery();
}
writeConnectionData("Scanning for devices...");
adapter.startLeScan(scanCallback);
}
private void stopScan() {
if (adapter.isDiscovering()) {
adapter.cancelDiscovery();
}
writeConnectionData("Stopping scan");
adapter.stopLeScan(scanCallback);
}
private void restartScan() {
stopScan();
startScan();
}
与前几章不同,UUID 解析已被移动到 utility 类中,以便重构代码并使其更易读。为了创建一个 utility 类,我们首先需要右键点击我们的包名,并创建一个新的包,命名为 Bluetooth。
然后,我们将右键点击新包,选择 新建 > Java 类,并将新类命名为 BluetoothUtils。
在这两个步骤之后,我们将用以下代码替换类中的代码:
public class BluetoothUtils {
// Filtering by custom UUID is broken in Android 4.3 and 4.4, see:
// http://stackoverflow.com/questions/18019161/startlescan-with-128-bit-uuids-doesnt-work-on-native-android-ble-implementation?noredirect=1#comment27879874_18019161
// This is a workaround function from the SO thread to manually parse advertisement data.
public static List<UUID> parseUUIDs(final byte[] advertisedData) {
List<UUID> uuids = new ArrayList<UUID>();
int offset = 0;
while (offset < (advertisedData.length - 2)) {
int len = advertisedData[offset++];
if (len == 0)
break;
int type = advertisedData[offset++];
switch (type) {
case 0x02: // Partial list of 16-bit UUIDs
case 0x03: // Complete list of 16-bit UUIDs
while (len > 1) {
int uuid16 = advertisedData[offset++];
uuid16 += (advertisedData[offset++] << 8);
len -= 2;
uuids.add(UUID.fromString(String.format("%08x-0000-1000-8000-00805f9b34fb", uuid16)));
}
break;
case 0x06:// Partial list of 128-bit UUIDs
case 0x07:// Complete list of 128-bit UUIDs
// Loop through the advertised 128-bit UUID's.
while (len >= 16) {
try {
// Wrap the advertised bits and order them.
ByteBuffer buffer = ByteBuffer.wrap(advertisedData, offset++, 16).order(ByteOrder.LITTLE_ENDIAN);
long mostSignificantBit = buffer.getLong();
long leastSignificantBit = buffer.getLong();
uuids.add(new UUID(leastSignificantBit,
mostSignificantBit));
} catch (IndexOutOfBoundsException e) {
// Defensive programming.
//Log.e(LOG_TAG, e.toString());
continue;
} finally {
// Move the offset to read the next uuid.
offset += 15;
len -= 16;
}
}
break;
default:
offset += (len - 1);
break;
}
}
return uuids;
}
}
在这一点上,您可以继续构建并运行项目,在运行 Android 4.3 且蓝牙已开启的 Android 物理设备上。为了获取此项目中的脉搏率,您需要按照之前提到的说明操作,并点击 获取脉搏率 按钮。
如何进一步
我们相信,通过可能包括来自多个在线渠道的其他健康相关传感器,并将相关数据在图表中显示,这个项目可以进一步发展。
Android 图形视图库支持使用多个序列进行多个输入。有关此信息的更多信息,可以在官方文档网站 android-graphview.org/ 获取。
这个应用在 Android 图形视图下的截图可能类似于以下内容:

还可以对用户界面和用户体验进行进一步的改进,其中数据可以通过处理程序或定时器实现实时更新。最后,这种类型的应用中可以包含的最先进集成之一是与 Google Fit SDK 的集成,这是谷歌的专有健康数据平台。另一个选择是使用云存储 API 和本地数据库存储健康数据。
摘要
在本章中,我们将之前章节所学的内容进行了整合,创建了一个基准项目来测量我们的脉搏率。
我们通过创建一个 Android 应用实现了这一点,该应用显示了连接到 Arduino Uno 的脉搏率传感器的数据。这两个设备之间的通信是通过蓝牙进行的。
本书中所介绍的所有基准项目都是为了激励你发挥创意,解决日常挑战。我们已经意识到,将 Arduino 和 Android 结合的可能性是无限的,我们希望你能突破可能的局限。











浙公网安备 33010602011771号