ADK-Arduino-入门指南-全-

ADK Arduino 入门指南(全)

原文:Beginning Android ADK with Arduino

协议:CC BY-NC-SA 4.0

零、前言

这本书解释了如何在 Android 开放附件开发工具包(ADK)的帮助下为与外部硬件通信的 Android 设备建立项目。您将学习如何配置您的开发环境,如何选择硬件并相应地设置电路,以及如何对 Android 应用和硬件进行编程。这本书将教你实现自己想法所需的基础知识。通过几个项目,您将了解 ADK 兼容的硬件板、传感器和致动器的功能,以及如何通过 Android 应用与它们进行交互。

谁应该读这本书

一般来说,任何对移动编程和硬件修补感兴趣的人都会喜欢 ADK 提供的众多可能性。有 Android 编程经验者优先,但不是完全必要。但是,您应该对 Java 编程语言和一般编程基础和算法有一个基本的了解。如果你以前做过电路实验,也会有所帮助。但是,如果您还没有,请不要担心,我将指导您完成电路设置,这样您就不会意外损坏您的硬件。这些项目被设计成建立在彼此的基础上,这样你就可以在这个过程中应用你已经学到的东西。总而言之,你应该享受实验和创新的乐趣。

需要额外资源

除了你的计算机(你需要它来编程)和一个 ADK 兼容的硬件板,你还需要一套软件和硬件组件来完成本书中解释的项目。

硬件组件大多是基本部件,如 led、电线以及其他无源和有源组件。我试图通过选择只需要基本的、负担得起的和容易获得的部件的项目来保持硬件成本最低。在特定项目中需要的地方详细描述了硬件部分。我预先编制了一份你在整本书中需要的所有必要硬件部件的清单,这样你就可以先把所有部件组装起来。没有什么比开始一个项目,却要等一个星期才能收到某些零件更让人恼火的了。

  • 1 × ADK 兼容开发板(有关开发板的具体信息,请参考第一章)
  • 1 ×试验板
  • 试验板电线或普通电子电线
  • 1 × LED,工作电压为 5V
  • 1 ×红外发射器或红外 LED
  • 1 × IR 检测器或 IR 光电晶体管
  • 1 ×按钮或开关
  • 1 ×电位计
  • 1 ×压电蜂鸣器
  • 1 ×照片性别歧视
  • 1×4.7kω热敏电阻
  • 1 × NPN 晶体管(BC547B)
  • 1 ×伺服,工作电压为 3V 或 5V
  • 1 × DC 电机,工作电压为 3V 或 5V(如果> 5V,则需要外部电池)
  • 1 ×卷家用铝箔
  • 1 ×胶带
  • 电阻(220ω、10k 和 1Mω,或一整套)

所有的软件组件都是免费的,其中一些甚至是开源的。必要的软件列在第一章中,我提供了一个分步安装指南来正确设置您的工作环境。当您以后处理项目代码时,您可以在阅读本书的同时自己键入源代码示例,或者您可以直接从 Apress 网站下载源代码,以使用本书中的代码片段作为参考。你可以在[www.apress.com](http://www.apress.com)找到代码。

轮廓一览

这本书分为十章。在介绍了开发环境和 ADK 兼容板的设置后,您将立即投入到第一个项目中,熟悉设置 ADK 实验的过程。在很大程度上,以下章节相互借鉴,将教会你从制作简单的 LED 闪烁到设计利用不同传感器的高级报警系统,以及使用 Android 设备的功能。下面是章节的快速总结:

  • 第一章:简介:简介给你一个关于 ADK 的概述,并展示一些支持 ADK 的硬件板。它还可以帮助你建立你的开发环境,这样你就可以跟踪每一章中的项目。
  • 第二章 : Android 和 Arduino:相互了解:本章将指导您编写必要的软件,在您的 ADK 板和 Android 设备之间建立连接。您还将学习在两台设备之间实现通信的基础知识。
  • 第三章:输出 : ADK 开发板为输出目的提供不同的手段。本章中的项目将向您展示如何通过控制 LED 的状态,在数字和模拟环境中利用这些输出特性。你将使用你的 Android 设备来打开和关闭 LED,你将能够控制它的强度。
  • 第四章:输入:ADK 板的输入能力使你能够从传感器读取数据或测量施加到其输入引脚的电压变化。本章的项目将向您展示如何在数字和模拟环境中读取和处理接收到的输入值,方法是用一个按钮改变数字输入引脚的引脚状态,以及用一个电位计改变模拟输入读数。按下一个按钮,你的 Android 设备将振动给用户反馈,你将看到一个进度条模拟输入读数的变化。
  • 第五章:声音:本章将向您展示如何使用压电蜂鸣器作为多用途组件,不仅可以产生声音,还可以感知附近的声音。您将使用您的 Android 设备来选择生成声音的频率,并且您将构建一个敲击传感器,在每次敲击时改变应用的背景。
  • 第六章:光强度感应:识别周围环境光线的变化对很多应用都很有用。本章的项目将向你展示如何使用光敏电阻来感知这些照明变化。您的 Android 设备将评估这些变化,点亮或调暗屏幕,以适应当前的照明水平。
  • 第七章 : 感温:电子设备经常要在极端条件下工作。因此,记录当前温度有时是必要的。本章将向您展示如何在热敏电阻的帮助下感应和计算当前温度。当前温度值将使用 Android 系统的 2D 图形功能绘制到您的 Android 设备的屏幕上。
  • 第八章:触觉:触摸界面已经成为日常生活的一部分。当你有一个触摸界面来控制它时,每个项目都感觉更好。本章将向您展示如何构建自己的低成本触摸传感器。您将使用您的 Android 设备和触摸传感器来构建一个简单的游戏节目蜂鸣器,当被激活时,它会发出嗡嗡的声音并振动。
  • 第九章:让东西动起来:机器人可能是爱好电子产品中最令人兴奋的实验品。由于机器人需要一些运动方式,你需要了解可以帮助你的执行器。本章将向你展示如何控制伺服和 DC 汽车,使你未来的项目以任何方式前进。您将使用您的 Android 设备的加速度传感器,通过沿 x 轴和 y 轴倾斜您的设备来控制您的致动器。
  • 第十章:警报系统:在最后一章,你将利用你之前学到的知识来建造你自己的警报系统。在两个项目中,你将了解两种不同的触发警报的方式,一种是通过倾斜开关,另一种是通过自建的红外光栅,你还将了解你的 Android 设备如何增强警报系统。您还将学习如何通过 SMS 发送文本消息,并使用设备的摄像头拍摄可能的入侵者。

一、简介

2011 年 5 月,谷歌举行了年度开发者大会 Google IO,向大约 5000 名与会者展示了其最新技术。除了对谷歌 API 或核心搜索技术等已经众所周知的技术进行改进,谷歌还将重点放在了两大主题上:Chrome 和 Android。一如既往,Android 平台的最新进展得到了展示和讨论,但谷歌稍后在 Android keynote 上宣布的内容有点令人惊讶:谷歌的第一个 Android 设备与外部硬件通信的标准。Android 开放附件标准和附件开发工具包(ADK)将是与硬件通信和为 Android 设备构建外部附件的关键。为了鼓励开发,谷歌向感兴趣的与会者分发了 ADK 硬件包,并展示了一些 ADK 项目的例子,如将数据传输到连接的安卓设备的跑步机和可以用安卓设备控制的巨大倾斜迷宫。活动结束后不久,第一个 DIY 项目浮出水面,这已经显示了 ADK 的巨大潜力。

由于我不能参加这次活动,我没有机会得到其中的一个工具包;当时,谷歌 ADK 版只有一家经销商,而这家经销商并没有为如此大的需求做好准备。这并没有阻止我自己建立一个替代方案,并体验 Android 开发这一新领域的乐趣。随着时间的推移,越来越多的分销商生产了最初的谷歌 ADK 板的衍生产品,在大多数情况下,这些产品更便宜,而且只提供让你一起开始黑客项目的基础。

您可能只想一头扎进去,但是首先您应该了解 ADK 的具体情况,并设置您的开发环境。你不会在不知道怎么做或者没有合适的工具的情况下盖房子,是吗?

什么是 ADK?

附件开发套件(ADK)基本上是一个微控制器开发板,它遵循 Google 创建的简单开放附件标准协议作为参考实现。尽管这可能是任何符合 ADK 兼容规范的主板,但大多数主板都基于 Arduino 设计,这是 2005 年创建的开放式硬件平台。这些板是基于 Arduino Mega2560 和 Circuits@Home USB Host Shield 实现的支持 USB 的微控制器板。然而,还有其他已知的 ADK 兼容板设计,如基于 PIC 的板,甚至普通 USB 主机芯片板,如 FTDI 的 VNCII。Google 决定在 Arduino Mega2560 设计的基础上构建其参考套件,并以开源方式提供软件和硬件资源。这是一个聪明的举动,因为 Arduino 社区在过去的几年里发展迅速,使得设计师、爱好者和普通人可以很容易地将他们的想法变成现实。随着 Android 和 Arduino 爱好者群体的不断增长,ADK 有了一个很好的开始。

为了与硬件板通信,支持 Android 的设备需要满足某些标准。随着 Android Honeycomb 版本 3.1 和 backported 版本 2.3.4 的推出,引入了必要的软件 API。然而,这些设备还必须配备合适的 USB 驱动程序。该驱动程序支持通用 USB 功能,但特别支持所谓的附件模式。附件模式允许没有 USB 主机功能的 Android 设备与外部硬件通信,外部硬件反过来充当 USB 主机部分。

开放附件标准的规范规定,USB 主机必须为 USB 总线提供电源,并且可以枚举连接的设备。根据 USB 2.0 规范,外部设备必须在 5V 下提供 500mA 用于 Android 设备的充电目的。

ADK 还为开发板提供固件,固件以一组源代码文件、库和一个演示套件草图的形式出现,演示套件草图是 Arduino 术语,指项目或源代码文件。固件关心 USB 总线的枚举,并寻找与附件模式兼容的连接设备。

谷歌还为 Android 设备提供了一个示例应用,可以轻松访问和演示参考板及其传感器和执行器的功能。如果您使用的衍生板没有相同种类的传感器,您仍然可以使用示例应用,但您可能希望将代码剥离到通信的基本部分。

当你在 ADK 建立一个硬件项目时,你是在打造一个所谓的安卓配件。您的硬件项目是 Android 设备的附件,例如,键盘是 PC 的附件,不同之处在于您的附件为整个系统提供动力。配件需要支持已经提到的设备电源,并且它们必须遵守 Android 配件协议。该协议规定附件遵循四个基本步骤来建立与 Android 设备的通信:

  1. 附件处于等待状态,并试图检测任何连接的设备。
  2. 附件检查设备的附件模式支持。
  3. 如有必要,附件会尝试将设备设置为附件模式。
  4. 如果设备支持 Android 配件协议,则配件会建立通信。

如果你想了解更多关于 ADK 和开放附件标准的信息,请查看位于[developer.android.com/guide/topics/usb/adk.html](http://developer.android.com/guide/topics/usb/adk.html)的 Android 开发者页面。

硬件开发板

本节将概述目前市场上各种兼容 ADK 的开发板。请注意,我不能保证这个列表的完整性,因为社区发展的速度如此之快,以至于新的委员会可能会随时出现。在撰写本文时,我将集中讨论最受欢迎的主板。

谷歌 ADK

谷歌 ADK 是在 2011 年 5 月的谷歌 IO 上展示的参考套件,它是第一个遵循开放附件标准的主板。该套件带有 ADK 基板和演示屏蔽,如图图 1-1 所示。

images

图 1-1。谷歌 ADK 板和演示盾

基板(图 1-2 )包含 DC 电源连接器、连接手机或平板电脑的 USB 连接器(A 型插座)以及连接电脑用于编程和调试的微型 USB 连接器(微型 B 型插座)。它的顶部安装了 Atmel 的 ATmega2560 AVR 芯片,针对 C 编译代码进行了优化,这使得它非常快速且易于编程,而不是必须用汇编语言编程的可比微控制器。ATmega2560 有一个 256 千字节的内部闪存和一个 8 位 CPU,工作频率为 16MHz。它提供 8KB 的 SRAM 和 4KB 的 EEPROM。ATmega 芯片的 IO 端口控制 16 个模拟引脚,提供 10 位输入分辨率,支持 1,024 个不同值的模数转换。默认情况下,它们的测量范围是从地到 5V。该芯片有 54 个数字引脚,其中 14 个是 PWM(脉宽调制)使能的,例如,允许 led 变暗或控制伺服系统。板的中间是一个复位按钮,用于复位板上的程序执行。该板的工作电压为 5V。虽然您可以通过 USB 电缆为电路板供电,但如果您打算控制伺服系统或驱动电机,则应该考虑使用电源适配器。

images

图 1-2。近距离观察谷歌 ADK 董事会

Demo Shield 是一个附加板,包含各种不同的传感器和执行器。 Shield 是 Arduino 术语,指可以放在 Arduino 基板上的扩展板。这种连接是通过可堆叠的引脚接头实现的。基板的 IO 引脚大多委托给屏蔽层的引脚,因此可以重复使用。然而,某些屏蔽可能会占用引脚来操作它们的传感器。演示屏蔽罩本身预焊有插头,因此没有额外的屏蔽罩可以堆叠在上面。这并不令人惊讶,因为 shield 使用大多数引脚来让基板与其所有传感器进行通信。由于屏蔽隐藏了基板的重置按钮,它本身包含一个按钮,因此您仍然可以使用重置功能。然而,最重要的部分是传感器和致动器,而且数量很多。

  • 一个模拟操纵杆
  • 三个按钮
  • 三个 RGB LEDs
  • 起温度传感器作用的晶体管
  • 具有用于光感测的集成光电二极管的 IC
  • 安卓标志形式的电容式触摸区域
  • 两个带螺丝端子的继电器,可切换 24V 至 1A 的外部电路
  • 三个伺服连接器

谷歌 ADK 最初是由一家日本公司为谷歌 IO 生产的。可以在[www.rt-net.jp/shop/index.php?main_page=product_info&cPath=3_4&products_id=1](http://www.rt-net.jp/shop/index.php?main_page=product_info&cPath=3_4&products_id=1)订购。价格约为 400 美元(不包括销售税),是最贵的主板之一。

Arduino ADK

Arduino ADK ( 图 1-3 )是 Arduino 系列制造商自己生产的 ADK 兼容基板。它也基于 ATmega2560,与 Google 参考板仅略有不同。

images

图 1-3。 Arduino ADK 板

Arduino ADK 板还有一个 DC 电源连接器和一个 USB 连接器(A 型插座),用于连接 Android 设备。然而,编程和调试连接器与标准 USB 连接器不同(B 型插座)。重置按钮位于电路板的远端,ATmega 芯片位于电路板的中间。IO 引脚布局与 Google board 中的完全相同,并且具有相同的模拟和数字引脚特性。然而,Arduino ADK 有两个 ICSP 6 针接头,用于微芯片的在线串行编程(ICSP)。Arduino ADK 和谷歌 ADK 共享相同的引脚布局和外形,与演示盾和其他基于 Arduino 的盾兼容。

Arduino ADK 在意大利制造,可以直接从 Arduino 网站[store.arduino.cc/ww/index.php?main_page=product_info&cPath=11_12&products_id=144](http://store.arduino.cc/ww/index.php?main_page=product_info&cPath=11_12&products_id=144)订购,也可以从遍布全球的经销商[arduino.cc/en/Main/Buy](http://arduino.cc/en/Main/Buy)处订购。

它的价格约为 90 美元(不包括可能的运输成本和税收),对于普通爱好者和硬件黑客来说,它比谷歌 ADK 更实惠。

IOIO

IOIO(发音为 yo-yo)板(图 1-4 )是一种基于 PIC 微控制器的开发板,由 Sparkfun Electronics 在开放附件标准公布之前开发。

images

图 1-4。火花乐趣 IOIO 板

IOIO 板设计用于所有版本为 1.5 及以上的 Android 设备。最初的固件设计旨在与 Android Debug Bridge (ADB)配合使用,后者通常在 Android 应用的开发过程中用于调试过程和文件系统操作。在开放附件标准公布后,IOIO 被更新为新的固件,以支持开放附件协议和作为后备的 ADB 协议,从而仍然支持较旧的设备。在撰写本书时,固件仍处于测试阶段。因为你需要通过 PIC 编程器更新板的固件,以使板 ADK 兼容,它可能不是一个没有经验的修补程序的完美选择。

该板的硬件特性如下。IOIO 的外形尺寸约为常规 ADK 兼容板的四分之一,这使它成为目前最小的板之一。然而,它几乎跟上了它的老大哥的众多 IO 引脚。总共 48 个 IO 引脚中有许多都有几种工作模式,这可能会使引脚分配有点混乱。

在 48 个 IO 引脚中,所有引脚都可以用作通用输入输出端口。此外,其中 16 个引脚可用作模拟输入,3 对引脚可用于 I C 通信,1 个引脚可用作外设输入,28 个引脚可用于外设输入和输出。通常,这些引脚只能承受 3.3V 电压,但 22 个引脚能够承受 5V 输入和输出。I C 引脚提供快速简单的双线接口,与传感器板等外部集成电路通信。

除 IO 引脚外,该板还提供 3 个 Vin 引脚为板供电。在电路板的底部,您可以焊接一个额外的 JST 连接器来连接一个 LiPo 电池作为电源。应提供 5V 至 15V 的工作电压。此外,它有 3 个引脚用于 3.3V 输出,3 个引脚用于 5V 输出,9 个引脚用于接地。

该板上唯一的连接器是所需的 USB (A 型插座)连接器。这是因为不需要对硬件编程,不像其他 ADK 兼容板,硬件部分需要 C 编译代码。IOIO 提供了实现所有需求的固件。你只需要通过使用高级 API 来编写 Android 部分,以便于 pin 访问。

电路板上一个有趣的组件是一个小型微调电位计,可以限制 Android 设备的充电电流,以便在电路板处于电池模式时不会消耗太多电力。IOIO 有一个 PIC 微控制器芯片,而不是大多数其他板使用的 AVR 芯片。PIC24FJ256-DA206 芯片工作频率为 32MHz,具有 256KB 可编程存储器和 96KB RAM。

IOIO 由 Sparkfun Electronics 开发,可通过 Sparkfun 网站[www.sparkfun.com/products/10748](http://www.sparkfun.com/products/10748)或其经销商订购。

运费和税之前的价格约为 50 美元,这是最便宜的主板之一,但对初学者来说不是最友好的。

seeed uino ADK 主板

seedeuino ADK 板(图 1-5 )也源自 ATmega 板,看起来与标准的 Arduino ADK 板非常相似,但乍一看,它有一些不错的额外功能。

images

图 1-5。 Seeeduino ADK 董事会(图片由 Seeedstudio 提供)

它有 56 个数字 IO 引脚,其中 14 个支持 PWM,16 个模拟输入引脚和 1 个 ICSP 接头。板上的连接器与最初谷歌设计中的连接器类型相同。它有一个 DC 电源连接器、一个 USB 连接器(A 型插座)和一个微型 USB 连接器(微型 B 型插座)。

与大多数其他类似 Atmega 的板的最大区别是,Seeeduino ADK 板已经随 MicroBridge 固件一起发运,因此它可以在操作系统版本 2.3.4 及以上的 Android 设备上以 ADK 模式工作,在操作系统版本 2.3.4 之前的设备上以 ADB 模式工作,这与 IOIO 非常相似。

Seeeduino ADK 板由 Seeedstudio 开发,可在该公司网站[www.seeedstudio.com/depot/seeeduino-adk-main-board-p-846.html](http://www.seeedstudio.com/depot/seeeduino-adk-main-board-p-846.html)订购,也可从其经销商处订购。

它的售价为 79 美元(不含运费和税),这使它成为一款非常实惠但功能强大的主板。

更多 ADK 的可能性

在你看到了最常见的支持 ADK 的主板后,你可能会想知道是否只有这些。虽然开放附件标准只有大约一年的历史,但已经可用的板的数量是令人难以置信的,在这个年轻但快速发展的开源硬件领域中还有许多。使用开放附件标准进行开发还有很多其他的可能性。一些代表纯粹的 DIY(自己动手)方法,而另一些则是在 ADK 问世之前就已经使用的主板的扩展。

一种早期的方法是将 ADK 港移植到通用的 Arduino Uno 或 Duemilanove。你唯一需要的是一个额外的 USB 主机屏蔽来连接 Android 设备。我是早期的 DIY 黑客中的一员,也是朝着这个方向发展的。当时,它是最初的谷歌参考板的唯一负担得起的替代品。如今,我不会推荐它;已经有完美的一体化主板,不需要额外的屏蔽、黑客攻击或剥离代码。如果你仍然想使用你的普通 Arduino,有很多商店出售 USB 主机保护罩,你可以使用:

  • [www.circuitsathome.com/products-page/arduino-shields/usb-host-shield-2-0-for-arduino/](http://www.circuitsathome.com/products-page/arduino-shields/usb-host-shield-2-0-for-arduino/)
  • [www.sparkfun.com/products/9947](http://www.sparkfun.com/products/9947)
  • [www.dfrobot.com/index.php?route=product/product&filter_name=usb%20host&product_id=498](http://www.dfrobot.com/index.php?route=product/product&filter_name=usb%20host&product_id=498)
  • [emartee.com/product/42089/Arduino%20ADK%20Shield%20For%20Android](http://emartee.com/product/42089/Arduino%20ADK%20Shield%20For%20Android)

您可能已经了解了与运行低于 2.3.4 版本操作系统的 Android 设备进行通信的可能性,一些主板提供了这种可能性。如果你也想在你的项目中支持这一点,你应该看看使用亚行建立通信的微桥项目。请在[code.google.com/p/microbridge/](http://code.google.com/p/microbridge/)查看项目页面了解更多详情。

一些一体式主板还捆绑成套件,让您可以摆弄一堆传感器。这些套件通常提供一些与谷歌演示盾功能相同的传感器。

Arduino 商店出售一套 ADK 传感器套件,包括一个 Arduino ADK 巨型板和一个巨型传感器屏蔽。传感器护罩有 22 个 3 针连接器,可轻松连接传感器模块,无需担心布线和设置。更多信息请访问[store.arduino.cc/eu/index.php?main_page=product_info&cPath=2&products_id=140](http://store.arduino.cc/eu/index.php?main_page=product_info&cPath=2&products_id=140)

Seeedstudio 也有一个套件,称为格罗夫 ADK 仪表板套件。像 Arduino 套件一样,它也提供了一个简单的即插即用机制,可以立即启动,并且它具有一系列适用于各种用途的传感器。在[www.seeedstudio.com/depot/grove-adk-dash-kit-p-929.html](http://www.seeedstudio.com/depot/grove-adk-dash-kit-p-929.html)有售。

如果你仍然想要一个基于谷歌原始设计的套件,但进口日本原始设计不是一个选项,你也可以考虑下面的德国克隆,这几乎是一个精确的克隆,略有改进,提供了一个镀金的触摸区域,具有更好的导电性和阻碍氧化。它也比原来的更实惠,而且根据你住的地方,运费可能会更低。查看[www.troido.de/de/shoplsmallgbuy-android-stufflsmallg/product/view/1/1](http://www.troido.de/de/shoplsmallgbuy-android-stufflsmallg/product/view/1/1)了解更多信息。

你应该使用哪种板?

现在,您已经了解了支持开放附件标准的各种电路板,您可能想知道哪种电路板最适合您自己的项目。这总是一个难题,没有唯一的答案。你应该提前计划好你的项目,分析哪种板最合适。

如果你是硬件开发和 ADK 的初学者,你应该坚持使用最常用的板。在撰写本文时,这将是谷歌 ADK 董事会,它被分发给数百名参加谷歌 IO 2011 的开发者。如果你不是幸运地收到这些主板中的一个,并且你的预算相当紧张——这是通常的情况——考虑标准的 Arduino ADK 主板。到目前为止,我所见过的大多数黑客和创客项目都使用这两种板,如果你有需要,它们周围有一个巨大的社区可以帮助你。

表 1-1 给出了正在讨论的电路板的概况。

支持的 Android 设备

随着越来越多支持 Android 的平板电脑的推出,在 Android Honeycomb 版本中,开放附件标准作为 Android API 的一部分被引入。为了不仅支持蜂窝设备,Google 决定将必要的类移植到 2.3.4 版本,使它们也可以用于手机。较新的功能作为 Google API 附加库被反向移植。这个库基本上是一个 JAR 文件,必须包含在 Android 项目的构建路径中。

第一批获得必要版本更新并支持开放配件模式的候选产品是摩托罗拉 Xoom 和谷歌 Nexus S。其他设备很快就会跟进,这很快导致了众所周知的片段问题。通常,当涉及不同版本的操作系统时,片段通常是一个问题,但现在的问题是,即使设备有必要的操作系统版本 2.3.4 或 3.1,开放附件模式仍有可能无法在设备上工作。怎么会这样呢?问题是仅仅更新系统软件是不够的。设备的 USB 驱动程序必须与开放附件模式兼容。很多开发者更新自己的设备甚至 rooted 安装了 Cyanogen mod 这样的自制 Mod 最终运行 2.3.4 版本,却发现设备厂商的 USB 驱动不兼容。

然而,有很多设备已经过测试,据说可以在开放附件模式下完美工作,其中一些是官方的,另一些是由 DIY mods 驱动的。以下是一些已经过社区验证可与 ADK 配合使用的设备列表:

  • 谷歌 Nexus S
  • 谷歌 Nexus One
  • 摩托罗拉 Xoom
  • 钢图标 A100
  • 钢图标 A500
  • LG Optimus Pad
  • ASUS Eee 垫转座垫 TF101
  • 三星 Galaxy Tab 10.1
  • 三星 Galaxy S
  • 三星 Galaxy Ace

就个人而言,我建议使用 Nexus S 这样的谷歌开发者设备,因为这些设备对最新的 API 和功能提供了最好的支持。

设置开发环境

你对 ADK 的历史和技术细节了如指掌,但是在你的想法变成现实之前,你需要建立你的工作环境。您需要为您的 Android 设备和硬件板编写软件,让双方能够相互通信,并控制执行器或读取传感器值。编程是在两个集成开发环境(ide)的帮助下完成的。要编写 Android 应用,Google 建议使用 Eclipse IDE。Eclipse IDE 是最常见的 Java 开发 IDE,拥有最大的社区之一、各种插件和出色的支持。由于硬件板基于 Arduino 设计,您将使用 Arduino IDE 对它们进行编程,以编写所谓的草图,这些草图将被上传到板上。为了让这些 ide 正常工作,您还需要 Java 开发工具包(JDK),它比普通的 Java 运行时环境(JRE)具有更多的功能,并且您的系统可能已经安装了它。您还需要 Android SDK 来编写您的 Android 应用。

这个分步指南将帮助您建立必要的开发环境。请严格按照您选择的操作系统的步骤操作。如果您遇到任何问题,也可以参考软件网站上的官方安装指南。

Java 开发工具包

你首先需要的是 JDK。转到[www.oracle.com/technetwork/java/javase/downloads/index.html](http://www.oracle.com/technetwork/java/javase/downloads/index.html)并点击 JDK 下载按钮(图 1-6 )。

images

图 1-6。 JDK 下载页面

接受许可协议并为您的操作系统选择文件(图 1-7 )。x86 文件适用于 32 位操作系统,x64 文件必须安装在 64 位系统上,因此请确保选择正确的文件。

images

图 1-7。 JDK 平台下载量

您可能会注意到,Mac OS 上没有 JDK 文件。这些并没有在甲骨文网站上发布,因为苹果提供了自己版本的 JDK。JDK 应该预装在您的 Mac OS X 系统上。您可以通过在终端窗口中键入java -version来验证这一点。您应该会在终端窗口中看到您当前安装的 Java 版本。

在 Windows 上安装

下载完可执行文件后,打开它并按照指导您完成安装过程的说明进行操作。之后,您应该将 JDK 路径设置为您的 Windows PATH变量。path 变量用于方便地从系统中的任何地方运行可执行文件。否则,您将不得不总是键入完整的路径,以便从命令行执行某些东西,比如C:\Program Files\Java\jdk1.7.0\bin\java

Eclipse 也依赖于要设置的JAVA_HOME变量。要设置系统环境变量,您必须执行以下操作。

  • Right-click My Computer and select Properties as shown in Figure 1-8.images

    图 1-8。打开系统属性对话框

  • On the system Properties dialog select the Advanced tab. Near the bottom you should see the Environment Variables button (Figure 1-9), which opens the variables dialog to set User and System variables.images

    图 1-9。打开环境变量对话框

  • Click New in the System Variables area and insert the following:

    变量名:JAVA_HOME

    变量值:C:\Program Files\Java\jdk1.7.0

  • 点击OK

  • 另外选择PATH变量进行编辑(图 1-10 ,点击Edit。在其他值前面插入%JAVA_HOME%/bin;

  • 点击OK

  • Now your JDK is set up and ready for work.images

    图 1-10。设置环境变量

在 Linux 上安装

为您的系统(32 位/ 64 位)下载tar.gz文件,并将该文件移动到您想要安装 JDK 的位置。通常 JDK 安装在/usr/java/中,但是请记住,您需要 root 权限才能安装到该目录中。

打开包装并安装 JDK,包括:

# tar zxvf jdk-7-linux-i586.tar.gz

JDK 安装在当前目录下的/jdk1.7.0目录中。

为了让您的系统知道您在哪里安装了 JDK,并且能够从系统中的任何地方运行它,您必须设置必要的环境变量。为此,创建一个简短的 shell 脚本放在/etc/profile.d目录中是一个好主意。

在该目录中创建一个名为java_env.sh的脚本,并将以下内容添加到脚本中:

`#!/bin/bash

JAVA_HOME=/usr/java/jdk1.7.0

PATH=\(JAVA_HOME/bin:\)PATH

export PATH JAVA_HOME
export CLASSPATH=.`

最后要做的事情是在新创建的脚本上设置权限,以便系统在用户登录时执行它。

# chmod 755 java_env.sh

重新登录后,将设置环境变量。

在 Mac OS X 上安装

如前所述,Mac 版 JDK 不是通过 Oracle 下载网站发布的。JDK 预装在 Mac OS X 上,但也可以通过 Apple Store 下载。如果您的环境变量JAVA_HOMEPATH尚未设置,您可以参考 Linux 安装中使用的相应步骤,因为 Mac OS X 也是基于 Unix 的。您可以在终端窗口中使用以下命令检查变量是否已设置:

`# echo $JAVA_HOME And similar for the PATH variable:

echo $PATH`

Android SDK

为了能够编写 Android 应用,你需要 Android 软件开发工具包,它提供了 Google 目前支持的所有 Android 版本的所有库和工具。

你可以在[developer.android.com/sdk/index.html](http://developer.android.com/sdk/index.html)从 Android 开发者页面(图 1-11 )下载 SDK。

images

图 1-11。 Android SDK 下载页面

该网站提供了适用于 Windows、Linux 和 Mac 的 SDK 的压缩文档。还有另一个 Windows 版本,它是一个可执行文件,应该会引导您完成安装过程。由于所有平台的初始设置都是相同的,因此没有特定于操作系统的步骤。

下载完 SDK 档案后,将其移动到您选择的位置并解压缩。您将看到add-onsplatforms目录都是空的。只有tools目录包含几个二进制文件。现在该目录中的重要文件是android脚本。它启动 SDK 和 AVD 管理器(图 1-12 )。

images

图 1-12。 SDK 经理

SDK 管理器是 SDK 的核心。它管理已安装的 Android 版本,并更新新版本和附加包。使用 SDK 和 AVD 管理器,你还可以设置模拟器来测试 Android 应用。

在第一次启动时,它会连接到谷歌服务器检查版本更新,然后会提示您安装 SDK 包(图 1-13 )。这是一个好主意,只需点击接受所有并安装安装所有的 SDK 版本和额外的软件包。这可能需要很长时间,取决于您的互联网连接。如果您只想安装绝对必要的软件包,只需接受以下软件包,拒绝所有其他软件包:

  • Android SDK 平台-工具
  • Android SDK 的文档
  • Android 2.3.3、API 10 和所有更新版本的 SDK 平台
  • SDK API 10 和所有更新版本的示例
  • 谷歌公司的谷歌 API,Android API 10 和所有更新的 API
  • 谷歌 USB 驱动程序包
  • Android 兼容性包

images

图 1-13。 SDK 包安装

您可以通过单击“已安装和可用的软件包”来管理 SDK 安装,从而随时卸载和安装软件包。当 SDK 管理器下载完所有必要的包后,你会看到你的 SDK 目录已经增长,并且有了一些新的文件夹。我后面会讲到其中的一些,所以现在没有必要去理解。SDK 现在已经可以开发了。

Eclipse IDE

Eclipse 集成开发环境是软件开发人员最常用的 ide 之一。它有一个巨大的支持社区和一些针对各种开发场景的最强大的插件。您将需要 Eclipse 来开发您的 Android 应用。编写 Android 应用的编程语言是 Java。尽管您正在编写 Java 代码,但最终它将被编译成 Android 特有的dex代码,并打包成一个以文件结尾.apk的归档文件。这个存档文件将被放到您的 Android 设备上进行安装。

要下载 Eclipse,请前往[www.eclipse.org/downloads/](http://www.eclipse.org/downloads/)。在分发列表的右上角选择您的操作系统(图 1-14 )。

images

图 1-14。月食下载网站

您应该选择 Eclipse 经典版,因为它没有针对其他目的进行预配置。单击您的系统类型(32 位/64 位)的下载按钮。现在选择下载的默认镜像页面或选择离你最近的镜像(图 1-15 )。

images

图 1-15。下载镜像选择

根据您的操作系统,下载的文件可能是ziptar.gz存档文件。您不需要安装 Eclipse,因为它已经打包好可以运行了。将归档文件移动到您希望放置 Eclipse 的目录中,并提取它。Eclipse IDE 现在可以进行普通的 Java 开发了。然而,您需要安装一个额外的插件并进行一些配置,以便为 Android 开发准备 Eclipse。

你首先需要的是谷歌的 Android 开发工具(ADT)插件。ADT 插件用一套强大的 Android 开发工具增强了 Eclipse。除了能够设置特定于 Android 的项目,您还将受益于用于用户界面(UI)开发、资源管理、调试和监控视图以及构建和分析工具的专用编辑器。关于 ADT 插件的细节可以在[developer.android.com/sdk/eclipse-adt.html](http://developer.android.com/sdk/eclipse-adt.html)找到。ADT 插件通过其更新机制从 Eclipse 中安装。

打开 Eclipse,点击Help。选择Install New Software ( 图 1-16 )。

images

图 1-16。 Eclipse 插件安装

点击右上角的Add。在存储库对话框中,您必须输入更新站点的 URL 和一个识别名称(图 1-17 )。名称可以是任何东西,但建议使用描述性的东西,如 ADT 插件。在 URL 字段中输入[dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/)。如果您在建立 SSL 连接时遇到困难,您也可以使用 http 作为协议,但是它不太安全。

images

图 1-17。添加插件网站

将列出更新站点可用软件。选中Developer Tools处的复选框,点击Next ( 图 1-18 )。

images

图 1-18。插件包选择

将会显示一个安装摘要,您可以点击Next。最后一步是接受许可协议并点击Finish ( 图 1-19 )。

images

图 1-19。许可协议

如果弹出一个对话框说真实性或有效性没有保证,点击OK。插件现在已经安装好了,Eclipse 将提示您要么重启 Eclipse,要么只应用更改。重启 Eclipse 总是一个好主意,这样在加载插件时就不会出现副作用。在您能够设置 Android 项目之前,您需要在 Eclipse 中设置一些首选项。在顶栏中选择WindowPreferences(图 1-20 )。

images

图 1-20。配置首选项

在左侧列表中选择Android。如果你第一次点击它,会弹出一个对话框,要求你发送使用统计数据到谷歌服务器。您不需要允许这样做,但是您必须做出选择并点击Proceed关闭对话框。现在您必须配置您的 Android SDK 位置。点击Browse…,选择放置 SDK 安装的目录,如图图 1-21 所示。

images

图 1-21。设置 Android SDK 路径

应用更改并点击OK。您已经完成了设置,现在准备开发 Android 应用。

Arduino IDE

与 Eclipse 相比,Arduino 集成开发环境是一个非常小的 IDE。您将使用它为基于 Arduino 的微控制器板编写代码。Arduino IDE 本身是基于 Java 的,但是您将使用 c 编写代码。IDE 使用 avr-gcc 编译代码。Arduino 的编写软件是一个所谓的草图。IDE ( 图 1-22 )本身包含一个带有语法高亮显示的代码编辑器,一个用于调试和信息目的的控制台,它通过串行连接(USB)连接到你的硬件板。IDE 附带了许多方便的库,用于各种 IO 操作。Arduino 社区有一个庞大的、不断增长的社区,由设计师、爱好者和开发人员组成,他们也制作各种各样的库和草图。

images

图 1-22。Arduino IDE

根据您的操作系统,您可以从位于[arduino.cc/en/Main/Software](http://arduino.cc/en/Main/Software)的 Arduino 网站(图 1-23 )下载 IDE 作为档案文件或磁盘映像。

images

图 1-23。 Arduino 下载网站

在 Windows 和 Linux 上安装 Arduino IDE

如果你使用的是 Windows 或者 Linux,你必须下载一个存档文件,你可以把它放在任何你喜欢的目录下。之后,解压存档文件。不需要安装,IDE 已经准备就绪。

在 Mac OS X 上安装 Arduino IDE

如果你使用的是 Mac OS X 系统,你必须下载一个.dmg文件,这是一个磁盘镜像。如果系统没有自动装载它,请双击该文件。安装后,您会看到一个名为Arduino.app的文件。将该文件移动到主目录中的应用文件夹中。IDE 现在已经安装完毕,可以开始编码了。

安装硬件驱动程序

外部硬件几乎总是需要所谓的驱动程序。驱动程序是一个软件,它让你的操作系统知道硬件。驱动程序使操作系统能够与外部设备通信。因为在编程阶段你有两个硬件设备要与之通信(ADK 板和 Android 设备),所以你也必须为这些设备提供驱动程序。

在 Windows 上安装硬件驱动程序

尽管 ADK 板基于 ATmega 2560,其驱动程序已经与 Arduino IDE 一起部署,但 Windows 用户应该下载一个包含 ADK 型板驱动程序定义的档案文件。为此,请访问位于[arduino.cc/en/Main/ArduinoBoardADK](http://arduino.cc/en/Main/ArduinoBoardADK)的 Arduino ADK 详细页面。

您将在页面底部或以下网址找到存档文件:[arduino.cc/en/uploads/Main/Arduino_ADK.zip](http://arduino.cc/en/uploads/Main/Arduino_ADK.zip)

归档文件只包含一个.inf文件。将你的 ADK 板连接到你的电脑上,你会被提示提供一个驱动程序或者让系统搜索一个。选择手动安装一个,指向提到的.inf文件。系统将安装驱动程序,并且系统知道您的 ADK 板。

Android 设备还需要一个驱动程序,以便您能够部署和调试您编写的应用。在大多数情况下,您的 Android SDK 安装提供的通用驱动程序就足够了。当您在 SDK 管理器中安装额外的 SDK 包时,您选择了一个名为Google USB Driver Package, revision x的包。当您第一次连接您的 Android 设备时,系统会提示您手动选择一个驱动程序或让系统搜索一个。再次选择手动分配驱动程序,并在% SDK_HOME%\extras\google\usb_driver的 SDK 安装目录中选择通用驱动程序。

images 注意在某些情况下你可能需要特定厂商的 USB 驱动。你可以在制造商主页上找到它们。例如,三星将其驱动程序与同步工具 Kies 打包在一起。

在 Linux 上安装硬件驱动程序

Linux 系统应该自动检测你的硬件板和你的 Android 设备。当您稍后将代码上传到 ADK 板时,您可以在 Arduino IDE 中选择 ATmega2560 设置。如果在 Linux 机器上设置开发板有任何问题,请参考[www.arduino.cc/playground/Learning/Linux](http://www.arduino.cc/playground/Learning/Linux)的安装指南。

在 Mac OS X 上安装硬件驱动程序

在 Mac 上,同样的情况也适用于 Linux 机器。应该可以自动检测设备。但是,如果你在使用 ADK 板时遇到了问题,安装你之前下载的磁盘镜像文件附带的 FTDI 驱动程序可能会有所帮助。双击名为FTDIUSBSerialDriver_xx_xx_xx_xx_xx_xx.mpkg的文件即可。遵循安装说明并重新启动系统。如果还是有问题,可以参考http://arduino.cc/en/Guide/MacOSX的安装指南。

ADK 参考包

为了给你的想法一个起点,谷歌提供了一个 ADK 参考包。该软件包包含用于制造的开发板的原始设计、固件、ADK 开发板的演示草图以及与 ADK 开发板通信的示例 Android 应用。可以在[dl-ssl.google.com/android/adk/adk_release_0512.zip](https://dl-ssl.google.com/android/adk/adk_release_0512.zip)下载。你应该下载参考资料包,因为它将是你的第一个项目和整本书的例子的基础。

images ADK 参考包的版本可能会不时更新。你可以在开发者页面的[developer.android.com/guide/topics/usb/adk.html](http://developer.android.com/guide/topics/usb/adk.html)ADK 组件部分找到当前链接。

Fritzing(可选软件)

虽然 Fritzing 是一个完全可选的开源软件组件,但并不一定需要,我想给你一个简短的概述,介绍它如何在以后的项目中帮助你。Fritzing 是一个强大的原型制作工具,可以让您以不同的形式可视化您的电路和硬件设计。它旨在支持业余爱好者、设计师和制造商的项目文档、可视化和制造。

使用 Fritzing,您可以创建如图 1-24 所示的试验板原理图。

images

图 1-24。油炸面包板示意图

您可以将您的设计抽象为电路原理图,如图图 1-25 所示。

images

图 1-25。烧结电路原理图

您甚至可以将您的设计转化为 PCB 设计(图 1-26 ),以便日后生产。

images

图 1-26。烧结 PCB 原理图

该工具有各种各样的部件,从最常见的,如电阻和常见的 IC(集成电路),到 Sparkfun 目录中的几个部件,再到用户自己生成的部件。这个社区正在 Arduino 用户中成长,它越来越成为一个伪标准。虽然它可能不如 CadSoft 的 EAGLE 强大,它是电气工程的行业标准工具,但它仍然拥有业余爱好者或制造商需要的一切,而且完全免费。你会看到本书所有项目中使用的 Fritzing 原理图,以帮助可视化项目设置。如果你想更深入地了解它,你可以点击[fritzing.org](http://fritzing.org)阅读更多关于 Fritzing 的内容。

准备,集合,开始

恭喜你!您已经完成了为 Android 开放附件开发套件设置开发环境的繁琐任务。你也了解了一些关于仍然年轻的 ADK 的历史,以及哪些硬件存在于野外。接下来的章节将带领你创建一个 ADK 项目。接下来,我将通过设置不同种类的实验和项目来描述您需要了解的关于 ADK 板和可能的外部元件的基础知识。每个项目教给你一个具体的细节,你可能需要在后续的项目。这些例子大部分是建立在彼此的基础上的;最后,您将利用目前所学的知识建立更复杂的项目。这些项目将基于最常见的 ADK 兼容板,Arduino ADK 板。

现在你已经准备好了,让我们开始你的第一个项目。

二、Android 和 Arduino:相互了解

既然你已经了解了 Android 和 Arduino 平台的基础知识,是时候让他们互相了解了。本章将指导你完成你的第一个 Android 开放附件开发包(ADK)项目。您将了解开放附件协议的细节以及它在双方是如何实现的。您将编写您将在本书中用于所有后续示例的代码基础。为了了解代码示例的生命周期和结构,您将从为这两个平台编写有史以来最受欢迎的“Hello World”开始。

您将从为 Arduino 和 Android 创建两个非常基本的项目开始,学习如何在这两个平台上设置项目。一步一步,你将实现必要的功能,让 Android 设备和 ADK 板相互识别时,他们连接。最后,您将实现实际的通信,从 Android 设备向 ADK 板发送文本消息,反之亦然。

你好 Arduino

首先,打开你安装的 Arduino IDE。如果您以前没有使用过 Arduino IDE,请不要害怕。它有一个非常清晰和基本的结构,它提供了足够的功能让你为 Arduino 平台进行适当的开发。

你会看到一个很大的文本可编辑区域,在这里你可以编写你的代码(图 2-1 )。代码编辑器提供了语法突出显示,但不幸的是,没有代码补全,这可能会使针对外部库的开发更加困难,因为您必须直接在库代码中查找函数定义。

images

图 2-1。 Arduino IDE 代码编辑器

编辑器的顶部是操作栏。操作栏可让您快速访问编译等功能,以及NewOpenSaveUpload等文件操作,并启动串行监视器进行调试或直接向连接的电路板发送命令。在顶部的系统栏中(如图图 2-2 所示),你可以选择更细粒度的操作,比如选择合适的板卡、通信端口、导入外部库等等。

images

图 2-2。 Arduino IDE 系统栏和操作栏

编辑器底部的状态栏显示你的编译和上传进度以及警告或编译错误(见图 2-3 )。关于 Arduino IDE 更详细的解释,你可以参考 Arduino 网站上的原始文档。

images

图 2-3。 Arduino IDE 状态字段

Arduino 草图有两个重要的方法。第一个是setup方法,它只在代码执行开始时运行一次。这是你初始化程序的地方。第二种是loop法。该方法无限循环运行,直到板复位。这是实现程序逻辑的地方。正如你在图 2-4 中看到的,Arduino 草图的生命周期相当简单。

images

图 2-4。草图生命周期

您也可以在同一个草图中定义自己的方法,或者链接其他源文件或库。要链接一段代码,可以使用 C 开发中已知的#include指令。包括放置在草图的最顶部。全局变量也必须在你的 include 指令下面的草图的顶部定义。由于 Arduino ADK 和大多数其他 ADK 板有 256 千字节的内存限制,你必须记住写干净的代码,这意味着没有代码重复,你必须坚持尽可能最小的数据类型,以便不耗尽内存。幸运的是,编译器总是向您显示编译后代码的确切大小,并在超出限制时发出警告。

现在是时候为 Arduino 编写你的“Hello World”程序了。在 Arduino IDE 中,选择操作栏中的New。在新打开的编辑器窗口中,输入如清单 2-1 所示的代码。

清单 2-1。 Arduino Hello World 草图

`//includes would be placed here

//constant definition

define ARRAY_SIZE 12

//global variable definition
char hello[ARRAY_SIZE] = {'h','e','l','l','o',' ','w','o','r','l','d','!'};

void setup() {
//set baud rate for serial communication
Serial.begin(115200);
}

void loop() {
//print characters from array to serial monitor
for(int x = 0; x < ARRAY_SIZE; x++) {
Serial.print(hello[x]);
delay(250);
}
Serial.println();
delay(250);
}`

我们来谈谈代码是做什么的。首先,您为数组的大小定义了一个常数。然后定义了一个 char 数组,其大小为 12。因为您的 char 数组中有 12 个字符的空间,所以您可以将“hello world!”投入其中。

在设置方法中,为串行通信准备电路板。串行对象提供了简化串行通信的方法。要开始通信,您可以使用参数 115200 调用 begin 方法。这就是所谓的波特率,定义了每秒可以传输多少位。串行通信中的接收方必须配置相同的波特率,以正确读取传输数据。

如前所述,循环方法会无休止地运行,直到电路板复位。在 loop 方法中,在 for 循环的帮助下打印 char 数组的每个字符,for 循环遍历所有元素。使用 Serial 类的 print 方法将元素打印到串行监视器上。您还会看到在每个字符打印输出之间调用了一个 delay 方法来降低输出速度,以获得更好的可读性。延迟参数以毫秒为单位。

当您输入所有内容后,单击操作栏中的 Verify,查看您的代码是否可以编译而不出现错误。如果一切正常,你可以连接你的 ADK 板到你的电脑。您需要告诉 IDE 您将 ADK 板连接到了哪个端口,以及它实际上是哪种类型的板,这样 IDE 才能以正确的方式将程序代码传输到板上。

点击工具image板,选择 Arduino Mega 2560(参见图 2-5 )。

images

图 2-5。 Arduino IDE 板选择

现在转到工具image串行端口并选择你的板连接的端口(图 2-6 )。

images

图 2-6。 Arduino IDE 串口选择

完成所有配置后,点击操作栏中的上传。你的代码将被编译并转移到 ADK 董事会。完成后,状态字段将显示“上传完成”。代码现在由您的 ADK 板处理并开始执行。单击 IDE 操作栏中的串行监视器,打开串行监视器并查看打印输出。要正确查看传输的数据,请确保将波特率正确设置为代码中的 115200。这是在串行监视器的右下角完成的。你现在应该看到“你好,世界!”在串行监视器窗口中反复打印,如图图 2-7 所示。

images

图 2-7。 Arduino IDE 串行监视器

恭喜你!你已经写好了你的第一张 Arduino 草图,并且已经做好了一切准备来开始真正的 ADK 部分。

images 注意在撰写本文时,Arduino IDE 的新版本正在发布。1.0 版在内部库和 IDE 用户界面中引入了许多变化。一些与 ADK 相关的库可能还不完全兼容。如果您在编译示例时遇到任何问题,您可能希望确保使用 IDE 的旧版本。本书中的示例是用 Arduino IDE 修订版 0022 编写和编译的。

你好安卓

如果你以前没有使用过 Android,那么最好先熟悉一下这个平台的基础知识,了解一下它的关键组件。由于这本身就可以写满另一本书,我强烈建议你看看 Android 开发者页面,通读基础知识和基本原理,这样当我提到活动广播接收者时,你就知道我在说什么了。下面简单介绍一下我刚才提到的基本组件。更多详情请参考 Android 开发指南。

  • Activity:Activity 是处理用户交互的组件。它的主要目的是以内容视图元素的形式提供一个用户界面,例如,用于文本显示或文本输入。活动用于可视化程序流。它们可以彼此交互,系统以堆栈结构维护它们。这种堆栈结构在系统需要从一个到另一个导航时特别有用。
  • 服务:服务是长时间运行的过程,不需要用户交互。它们不会被误认为是线程,因为它们被绑定到同一个应用进程。服务还可以用于向其他应用公开功能。
  • broadcast receiver:broadcast receiver 是一个接收和处理系统或其他应用发送的广播的组件。当实现时,它可以在某些情况下做出反应,例如电池电量低,或者甚至可以用来启动应用。
  • content provider:content provider 用于在多个应用之间共享数据。例如,它提供了访问数据库中数据的方法。Android 系统中一个流行的 ContentProvider 是 ContentProvider for your contacts,它可以被很多应用使用。

你可以在[developer.android.com/guide/index.html](http://developer.android.com/guide/index.html)找到 Android 开发指南和其他几篇文章。

要编写 Android 的“Hello World”等价物,您必须打开 Eclipse IDE。一旦启动,选择File image New image Other(见图 2-8 )。

images

图 2-8。 Eclipse IDE 新项目创建

一个新的对话框将会弹出,里面有很多项目类型可供选择。您需要选择Android Project,点击Next,如图图 2-9 所示。

images

图 2-9。 Eclipse IDE 项目类型选择

在下一个对话框中,您可以配置项目设置。输入项目的名称,如HelloWorld。然后选择一个构建目标。目标取决于您使用的设备。如果您使用 Android 版本 2.3.4 或更高版本的设备,则选择Google APIs platform 2.3.3 API level 10。如果您使用的是 Android 或更高版本的设备,请选择Google APIs platform 3.1 API level 12。(参见图 2-10 。)

images

图 2-10。 Eclipse IDE Android 项目向导(上半部分)

接下来,定义您的应用名称。也可以输入HelloWorld。现在选择你的包名。包名将是 Android Market 中的唯一标识符,通常反映您的公司或组织的域名。对于这个例子,你可以输入helloworld.adk。检查显示Create Activity的复选标记,并键入HelloWorldActivity。最小 SDK 版本字段描述了应用兼容的 API 级别。因为您将需要 USB API 特性,如果您使用 2.3.4 设备,您应该为 API 级别 10 选择10,否则为 API 级别 12 选择12。其余的设置可以保持不变。(参见图 2-11 。)

images

图 2-11。 Eclipse IDE Android 项目向导(下部分)

点击Finish,等待 Eclipse 建立您的第一个 Android ADK 项目。你新创建的项目应该看起来像图 2-12 。

images

图 2-12。hello world 项目的 Eclipse IDE 包浏览器视图

如果您看一下左边的 Package Explorer 区域,您会看到 Eclipse 用几个文件夹和文件创建了一个新项目。让我们看看他们都是什么意思。

首先你会看到的是src文件夹,它包含了你将要编写的所有 Java 源代码。您已经可以看到 Eclipse 已经构建了您配置的包结构,并将给定的HelloWorldActivity.java文件放入其中。在查看那个项目之前,您将进一步探索生成的项目。

除了src文件夹,还有一个文件夹叫gen。如果项目中的资源发生了变化,则每次构建项目时都会自动生成该文件夹及其内容。在那个文件夹里有一个叫做R.java的文件。这是一个文件,其中所有的资源都被索引并映射到整数。这些静态数字用于在代码中以一种简单的方式访问您的资源。你不应该接触gen文件夹,因为 Android 构建过程直接管理R.java文件,你手动做的每一个改变都会在下一次构建中被覆盖。

接下来你看到的是将在你的构建路径中使用的引用库。您应该看到Google APIs,旁边是您配置的目标版本。如果您展开节点,您会看到项目引用了一个android.jar,它包含您选择的 Android 版本的系统类文件;一个usb.jar,它包含您与 ADK 设备进行 USB 通信所需的 USB 特定类;以及一个maps.jar,它用于 Google Maps 集成。

下一个文件夹叫做assets,它通常包含你必须自己管理的额外资源。您必须通过它们在应用中的相对路径来引用它们,因为构建系统不会将引用放入R.java文件中。

第二个资源文件夹是一个名为res的托管文件夹。放入该文件夹的每个资源都会在R.java文件中获得一个自动生成的 id,以便于访问。你已经可以在res文件夹中看到几个子文件夹。drawable文件夹有一个带有屏幕密度标识符的后缀。如果在这些文件夹中为不同的屏幕密度提供不同的图像,系统将在执行应用时选择正确的文件。还有一个文件夹叫做layout。该文件夹包含定义应用 UI 的 xml 布局文件。一个布局文件可以有多个视图元素,这些元素构成了您的用户界面。您看到的最后一个文件夹是values文件夹。在values文件夹中,你可以为静态资源定义 xml 文件,比如字符串、维度、数组等等。

除了所有这些文件夹,你还会看到一些独立的文件:AndroidManifest.xmldefault.propertiesproguard.cfg。您唯一感兴趣的文件是AndroidManifest.xml,它是您的应用的中央注册表。关于活动、服务、权限、强制设备特性和许多其他特性的细节必须在这个文件中定义。

生成的项目不仅仅是一个骨架。它已经是一个成熟的应用,尽管非常初级。它只显示“Hello World,HelloWorldActivity”。让我们来看看这个应用的结构和生命周期。每个 Android 应用的起点是AndroidManifest.xml。打开它,看看它的结构(清单 2-2 )。

清单 2-2。 AndroidManifest.xml

`

`

您可以看到一个application标签,它的属性定义了应用的iconlabel。这两个资源都用 xml 资源语法@resource-type/id引用。用@string/app_name引用的应用名称将取自res/values文件夹中strings.xml文件中定义的字符串。相同的资源查找语法适用于 drawables 和您将在任何 xml 文件中看到的所有其他资源。

在应用节点中,您定义了所有的活动、服务、内容提供者和广播接收者。到目前为止,清单中只声明了一项活动。看看那个。name属性必须是活动的类名。因为该类在应用默认包中,所以它前面有一个点。如果您将类移动到另一个包中,请确保使用正确的包结构更新名称,否则系统将无法再找到它。

活动节点中重要的是intent-filterintent-filter定义了系统如何启动活动以及如何触发活动。

action标签中,您可以看到该活动被标记为应用的主活动,并且将在用户启动应用时首先启动。

category标签指定了启动器类别,这意味着应用链接将放入设备上的应用概览菜单中,将启动您的活动。

在与application标签相同的层次级别上,您可以看到uses-sdk标签,它定义了 Android API 级别版本,并且是必需的。在该层级中,您还可以定义用户在安装过程中必须授予的权限,例如访问附件。

您已经知道应用的起点在哪里。现在让我们看看你的主要活动实际上是做什么的。打开HelloWorldActivity.java文件并查看其内容(清单 2-3 )。

清单 2-3。HelloWorldActivity.java

`package helloworld.adk;

import android.app.Activity;
import android.os.Bundle;

public class HelloWorldActivity extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}`

如你所见,这个文件很小。该类必须扩展Activity类,它是 Android 系统的一部分。因为活动负责提供用户界面,所以您必须提供一些视图来显示给用户。视图通常在活动创建时加载。Android 平台提供了活动生命周期的挂钩,这样您就可以创建资源、运行应用逻辑,并在活动的相应阶段进行清理。一个 Android 活动的生命周期比一个简单的 Arduino 草图要复杂一些,如图 2-13 所示。

images

图 2-13。安卓活动生命周期(谷歌公司图片财产来源:[developer.android.com/guide/topics/fundamentals/activities.html](http://developer.android.com/guide/topics/fundamentals/activities.html) )

你可以看到setContentView方法是以布局资源为参数调用的。该方法采用layout/main.xml中的布局定义,并将其所有视图呈现在设备屏幕上。如果你打开这个main.xml文件,你可以看到它只定义了两个视图元素(见清单 2-4 )。

清单 2-4。布局文件 main.xml

`
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"


`

LinearLayout是一个可以容纳其他视图或容器的容器视图。layout_widthlayout_height属性被设置为在设备的整个屏幕上延伸。orientation属性指定所包含的元素应该垂直对齐。目前包含在LinearLayout中的唯一元素是TextView元素。从它的属性可以看出,它应该填充屏幕的宽度,但应该只与它自己的内容一样高。TextView的文本由来自strings.xml文件的@string/hello引用解析。如果您在 Eclipse 中从 xml 编辑器切换到图形布局编辑器,您应该已经在虚拟设备屏幕上看到文本“Hello World, HelloWorldActivity!”。现在够了。让我们在真实设备上看看这个应用。

将您的 Android 设备连接到您的计算机,右键单击该项目,选择Run As,然后选择Android Application。您的应用应该被打包成一个apk文件,并被推送到设备上进行安装。如果一切正常,您应该看到应用在您的设备上启动。例如,如果系统由于缺少驱动程序而无法识别你的设备,它将使用默认的 Android 虚拟设备(AVD)启动一个模拟器。当应用启动后,你应该会看到类似图 2-14 的内容。

images

图 2-14。运行在安卓设备上的 HelloWorld 应用

相识

恭喜你!您已经编写了您的第一个 Arduino 应用和第一个 Android 应用。现在,您对设置项目和为两个平台编写代码已经稍微熟悉了一些,让我们看看这两个设备如何在开放附件协议的帮助下相互识别。

为 Arduino 扩展 Hello World

您先前下载的 ADK 参考包包含两个库,您将需要它们来建立 USB 通信。一个是USB_Host_Shield库的修改版本,最初是由 Oleg Mazurov 为 Circuits@Home 创建的。该库最初设计用于 Arduino USB 主机保护。由于 ADK 兼容板上的 USB 芯片相当于 USB host shield,因此只对库进行了一些小的修改。第二个库是AndroidAccessory库,负责实现开放附件协议。将在ADK_release_xxxx\firmware\arduino_libs\找到的两个库文件夹复制到arduino-xxxx\libraries的 Arduino IDE 安装的库文件夹中。修改你的 Arduino HelloWorld草图,如清单 2-5 所示。

清单 2-5。 Hello World Sketch 扩展识别安卓设备

`#include <Max3421e.h>

include <Usb.h>

#include <AndroidAccessory.h>

define ARRAY_SIZE 12

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial");

char hello[ARRAY_SIZE] = {'h','e','l','l','o',' ',
'w','o','r','l','d','!'};

void setup() {
Serial.begin(115200);
acc.powerOn();
}

void loop() {
if (acc.isConnected()) {
for(int x = 0; x < ARRAY_SIZE; x++) {
Serial.print(hello[x]);
delay(250);
}
Serial.println();
delay(250);
}
}
`

如您所见,准备开放式配件沟通的草图只需三处改动。这里你要做的第一件事是初始化一个AndroidAccessory对象,这个实现了开放附件协议,这样你就不用担心了。你用一些描述性的字符串来初始化它,这些字符串定义了你创建的附件。这些字段是不言自明的。最重要的参数是ManufacturerModelVersion。它们将在 Android 应用中用于验证您是否与正确的 ADK 板通信。此外,如果没有安装安卓应用,安卓系统使用 URI 参数来寻找合适的安卓应用。这可以是一个链接到 Android 市场或产品页面。

setup例程中,您用powerOn方法将对象设置为活动状态。loop例程在每个循环中检查是否有东西连接到附件,然后才执行其中的代码。在这个方便的方法中,实现了实际的连接协议。

只用了三行代码,ADK 板就能识别支持附件模式的连接的 Android 设备。就这么简单。如果你把代码上传到你的 ADK 板上,你会看到“你好,世界!”只有当您将 Android 设备连接到 ADK 板时,才会打印到串行监视器。您将在串行监视器上看到如下内容:

`Device addressed... Requesting device descriptor.
found possible device. switching to serial mode
device supports protcol 1

Device addressed... Requesting device descriptor.
found android accessory device
config desc
interface desc
interface desc
5
7`

第一段告诉你,一般来说,一个设备已经被识别,并且这个设备支持开放的 Android 附件协议。在第二段中,再次读取设备描述符,以查看 Android 设备是否将自己标识为处于附件模式的设备。每个 USB 设备都有这样一个描述符,用于向连接系统标识自己。您可以看到,该设备被正确识别为配件模式兼容。现在正在读取配置和接口描述符,您将看到的最后两个数字是要使用的通信的输入和输出端点。如果一切顺利,你会看到“你好,世界!”是一个字符一个字符打印出来的。如果你想了解更多关于AndroidAccessory库的内部,以及它是如何实现开放附件协议的,可以看看[developer.android.com/guide/topics/usb/adk.html](http://developer.android.com/guide/topics/usb/adk.html)的“实现 Android 附件协议”一节。

为 Android 扩展 Hello World

要为开放附件协议准备 Android 应用,您还不需要编写任何代码。你要做的第一件事是在AndroidManifest.xml中做一些改变。你必须声明你正在使用 USB 功能。由于您希望通过 USB 进行通信,因此需要在 Android 版本低于 3.1 的设备的清单中声明 USB 库的使用。将下列行添加到AndroidManifest.xml:

`

`

这个 USB 库被反向移植到 Android 版本 2.3.4,并被命名为 com.android.future.usb。Android 版本 3.1 的类被放在名为 android.hardware.usb 的包中。如果你想支持广泛的设备,你应该使用 com . Android . future . USB 包,因为它对两个版本都兼容。在蜂窝设备上,com.android.future.usb 包中的类只是包装类,它们委托给 android.hardware.usb 包中的类。

接下来需要声明的是另一个意图过滤器,当 Android 设备连接到附件时,它会启动应用。在 HelloWorldActivity 的 activity 节点中添加以下代码行:

<activity android:name=".HelloWorldActivity" android:label="@string/app_name" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" /> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/accessory_filter" /> </activity>

如您所见,这里添加了第二个意图过滤器,它对 USB_ACCESSORY_ATTACHED 动作做出反应。当您将 Android 设备连接到 ADK 兼容板时,这将触发 HelloWorldActivity。还添加了一个新元素。元数据标签引用额外的资源,这些资源可被提供给意图过滤器以进一步细化过滤机制。这里引用的 accessory_filter.xml 定义了一个更细粒度的过滤标准,只匹配您的附件,不匹配其他附件。在/res 文件夹中创建一个名为 xml 的文件夹。在创建的文件夹中添加一个名为 accessory_filter.xml 的新文件。现在将清单 2-6 中的内容添加到 xml 文件中。

清单 2-6。定义附件过滤的元文件

<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory manufacturer="Manufacturer" model="Model" version="Version" /> </resources>

请注意,您在这里指定的值必须与初始化AndroidAccessory对象时 Arduino 草图中的值相同。如果这些值与板传输的值匹配,过滤器将触发活动开始。将您的 Android 设备连接到您的 PC,并上传更改后的应用。现在把你的设备连接到 ADK 板上。您可能会看到弹出一个对话框,询问您是否希望总是将您的应用与已识别的意图相关联。您可以确认这一点,然后,您将看到应用已经启动。如果您发现连接设备后没有任何反应,请检查您的过滤器是否与 Arduino 草图中定义的值相匹配。另一个错误来源是,您的主板无法提供足够的功率来为 Android 设备正常供电。由于这是开放附件标准的要求,如有必要,请确保使用外部电源为电路板供电。

我们谈谈吧

现在,您已经确保了两台设备能够相互识别,但您希望它们能够真正相互通话。通信是在一个相当简单的自定义协议中完成的。消息通过字节流发送和接收。在 Android 应用中,这是通过读写一个特殊文件的输入输出流来完成的。在 Arduino 端,AndroidAccessory类提供了读写消息的方法。对于通信协议应该是什么样子没有限制。在示例demokit应用中,Google 将消息定义为 3 字节长的字节数组。(参见图 2-15 )。第一个字节是命令类型。它定义了传输哪种类型的消息。在demokit应用中使用的命令是伺服系统、发光二极管、温度传感器和许多其他设备的命令类型。第二个字节是该命令的实际目标。谷歌演示盾有多个发光二极管和伺服连接器,并解决适当的,目标字节使用。第三个字节是应该发送到该目标或从该目标接收的值。一般来说,当您自己实现这些消息时,您可以选择您想要的任何消息结构,但是我建议您坚持使用示例,因为您可能会在整个 Web 上找到基于相同消息结构的教程和示例。请记住,您只能传输字节,因此您必须相应地转换更大的数据类型。在大多数例子中,你也会遵循这个惯例。

images

图 2-15。Google 在 demokit 应用中定义的默认消息协议

然而,对于第一个例子,你必须稍微改变一下规则,定义一个自定义协议来传输文本信息(见图 2-16 )。传输的数据也是字节数组,但形式略有不同。第一个字节将定义命令类型,第二个字节将定义目标,第三个字节定义文本消息的长度(不超过 252 个字节),最后剩余的字节定义实际的文本消息。

images

图 2-16。发送和接收短信的自定义短信协议

处理 Arduino 命令

Arduino 草图中的通信实现非常简单。如清单 2-7 中的所示扩展草图。

清单 2-7。hello world Sketch 中的通信实现

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

**#define ARRAY_SIZE 25

define COMMAND_TEXT 0xF

define TARGET_DEFAULT 0xF**

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial");

char hello[ARRAY_SIZE] = {'H','e','l','l','o',' ',
'W','o','r','l','d',' ', 'f', 'r', 'o', 'm', ' ',
'A', 'r', 'd', 'u', 'i', 'n', 'o', '!'};

byte rcvmsg[255];
byte sntmsg[3 + ARRAY_SIZE];

void setup() {
Serial.begin(115200);
acc.powerOn();
}

**void loop() {
if (acc.isConnected()) {
//read the sent text message into the byte array
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_TEXT) {
if (rcvmsg[1] == TARGET_DEFAULT){
//get the textLength from the checksum byte
byte textLength = rcvmsg[2];
int textEndIndex = 3 + textLength;
//print each character to the serial output
for(int x = 3; x < textEndIndex; x++) {
Serial.print((char)rcvmsg[x]);
delay(250);
}
Serial.println();
delay(250);
}
}
}

sntmsg[0] = COMMAND_TEXT;
sntmsg[1] = TARGET_DEFAULT;
sntmsg[2] = ARRAY_SIZE;
for(int x = 0; x < ARRAY_SIZE; x++) {
sntmsg[3 + x] = hello[x]; }
acc.write(sntmsg, 3 + ARRAY_SIZE);
delay(250);
}
}**`

让我们看看这里有什么新内容。因为您想要向 Android 设备发送更具体的文本,所以您需要更改文本消息及其大小常量。

#define ARRAY_SIZE 25 char hello[ARRAY_SIZE] = {'H','e','l','l','o',' ', 'W','o','r','l','d',' ', 'f', 'r', 'o', 'm', ' ', 'A', 'r', 'd', 'u', 'i', 'n', 'o', '!'};

接下来您需要做的是为接收的消息声明一个字节数组,为要发送的消息声明一个字节数组。

byte rcvmsg[255]; byte sntmsg[3 + ARRAY_SIZE];

请注意,要发送的字节数组的大小等于消息本身加上命令类型、目标和校验和的附加字节。命令类型字节和目标字节也可以定义为常量。

`#define COMMAND_TEXT 0xF

define TARGET_DEFAULT 0xF`

在循环方法中,您将处理消息的接收和发送。首先看一下信息是如何被接收的:

if (acc.isConnected()) { //read the sent text message into the byte array int len = acc.read(rcvmsg, sizeof(rcvmsg), 1); if (len > 0) { if (rcvmsg[0] == COMMAND_TEXT) { if (rcvmsg[1] == TARGET_DEFAULT){ //get the textLength from the checksum byte byte textLength = rcvmsg[2]; int textEndIndex = 3 + textLength; //print each character to the serial output for(int x = 3; x < textEndIndex; x++) { Serial.print((char)rcvmsg[x]); delay(250); } Serial.println(); delay(250); } } } … }

AndroidAccessory 对象的 read 方法读取 inputstream 并将其内容复制到提供的字节数组中。作为参数,read 方法采用应该填充的字节数组、该字节数组的长度以及一个阈值,以防传输未被确认。之后,执行检查以查看是否传输了正确的命令和目标类型;只有这样,才能确定传输消息的长度。从 for 循环中的字节数组读取实际的文本消息,并逐字符打印到串行输出。收到一条消息后,另一条消息被发送到 Android 设备,如下所示:

if (acc.isConnected()) { … sntmsg[0] = COMMAND_TEXT; sntmsg[1] = TARGET_DEFAULT; sntmsg[2] = ARRAY_SIZE; for(int x = 0; x < ARRAY_SIZE; x++) { sntmsg[3 + x] = hello[x]; } acc.write(sntmsg, 3 + ARRAY_SIZE); delay(250); }

同样,要发送的字节数组是根据自定义协议构建的。第一个字节设置为命令类型常量,第二个字节设置为目标常量,第三个字节设置为作为校验和的实际文本消息的大小。现在,程序遍历 hello char 数组,用文本消息填充字节数组。当字节数组设置完毕后,调用 AndroidAccessory 对象的 write 方法,通过 outputstream 将数据传输到 Android 设备。write 方法有两个参数:要传输的字节数组和传输的字节大小。

正如你所看到的,Arduino 草图非常简单,AndroidAccessory 对象为你做了所有的脏工作。现在让我们来看看 Android 通信部分。

处理 Android 命令

在 Android 中实现通信部分比在 Arduino 端需要更多的工作。扩展HelloWorldActivity类,如清单 2-8 中的所示。此后,您将了解每个代码片段的作用。

清单 2-8。【HelloWorldActivity.java ??(进口和变量)】

`package helloworld.adk;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log; import android.widget.TextView;

import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;

public class HelloWorldActivity extends Activity {

private static final String TAG = HelloWorldActivity.class.getSimpleName();

private PendingIntent mPermissionIntent;
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
private boolean mPermissionRequestPending;

private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;

private static final byte COMMAND_TEXT = 0xF;
private static final byte TARGET_DEFAULT = 0xF;

private TextView textView;

…`

首先让我们来看看你需要做的变量声明。

private static final String TAG = HelloWorldActivity.class.getSimpleName();

constant 标签是当前类的标识符,在 Android 中仅用于记录目的。如果您在设备或模拟器运行时查看 Eclipse 中的 logcat 视图,您会看到记录的消息与一个标记相关联,这简化了读取日志输出。(参见图 2-17 。)您可以为此定义任何字符串,但是出于调试目的使用应用名甚至类名是个好主意。

images

图 2-17。月食日志视图

private static final byte COMMAND_TEXT = 0xF; private static final byte TARGET_DEFAULT = 0xF;

COMMAND_TEXT 和 TARGET_DEFAULT 与 Arduino 草图中使用的常量相同。它们构成了数据协议的前两个字节。

private PendingIntent mPermissionIntent; private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION"; private boolean mPermissionRequestPending;

与外部设备建立连接必须得到用户的许可。当用户授予连接到您的 ADK 板的权限时,PendingIntent 将广播 ACTION_USB_PERMISSION,并带有一个反映用户是确认还是拒绝访问的标志。布尔变量 mPermissionRequestPending 仅用于在用户交互仍未完成时不再显示权限对话框。

private UsbManager mUsbManager; private UsbAccessory mAccessory; private ParcelFileDescriptor mFileDescriptor; private FileInputStream mInputStream; private FileOutputStream mOutputStream;

UsbManager 是一项系统服务,用于管理与设备 USB 端口的所有交互。它用于列举连接的设备,并请求和检查连接到附件的许可。UsbManager 还负责打开与外部设备的连接。USB 存储器是连接附件的参考。ParcelFileDescriptor 是在与附件建立连接时获得的。它用于访问附件的输入和输出流。

private TextView textView;

唯一用户可见的 UI 元素是 textView,它应该显示从 ADK 板传输的消息。

这就是你需要声明的所有变量。现在我们将看看活动的生命周期方法(清单 2-9 )。

清单 2-9。【HelloWorldActivity.java ??(生命周期方法)】

`/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUsbManager = UsbManager.getInstance(this);
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent( ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
registerReceiver(mUsbReceiver, filter);

setContentView(R.layout.main);
textView = (TextView) findViewById(R.id.textView);
}

/** Called when the activity is resumed from its paused state and immediately after onCreate(). */
@Override
public void onResume() {
super.onResume();

if (mInputStream != null && mOutputStream != null) {
return;
}

UsbAccessory[] accessories = mUsbManager.getAccessoryList();
UsbAccessory accessory = (accessories == null ? null : accessories[0]);
if (accessory != null) {
if (mUsbManager.hasPermission(accessory)) {
openAccessory(accessory);
} else {
synchronized (mUsbReceiver) {
if (!mPermissionRequestPending) {
mUsbManager.requestPermission(accessory, mPermissionIntent);
mPermissionRequestPending = true;
}
}
}
} else {
Log.d(TAG, "mAccessory is null");
}
}

/** Called when the activity is paused by the system. */
@Override
public void onPause() { super.onPause();
closeAccessory();
}

/** Called when the activity is no longer needed prior to being
removed from the activity stack. */
@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mUsbReceiver);
}`

每个活动类的第一个生命周期回调方法是 onCreate 方法。这个方法通常是你进行基本初始化的地方。不过,要小心。onCreate 方法只在系统创建活动时调用一次。该活动将继续存在,直到系统需要释放内存并终止它,或者如果您显式调用该活动的 finish 方法来告诉系统不再需要该活动。

`/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUsbManager = UsbManager.getInstance(this);
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
registerReceiver(mUsbReceiver, filter);

setContentView(R.layout.main);
textView = (TextView) findViewById(R.id.textView);
}`

在生命周期回调方法中需要做的最重要的事情是将调用委托给被扩展的父类;否则,将发生异常。委托调用 super . oncreate(savedInstanceState)允许父 activity 类完成它的所有初始化逻辑,然后 HelloWorldActivity 可以继续它自己的初始化。接下来,获取一个对 USB 系统服务的引用,以便您稍后可以调用它的方法。现在定义了一个带有 ACTION_USB_PERMISSION 参数的 PendingIntent。当您请求用户允许连接到 USB 设备时,您将需要它。您在这里看到的意图过滤器与广播接收器一起使用,以确保应用只侦听特定的广播。过滤器定义它对您在开始时定义为常量的 ACTION_USB_PERMISSION 动作和 ACTION_USB_ACCESSORY_DETACHED 动作起作用,用于 ADK 附件断开时。registerReceiver 方法在系统中向所描述的意图过滤器注册广播接收器。因此,当系统发送广播时,广播接收器将得到通知,并可以采取相关措施。

在 onCreate 方法中,您需要做的最后一件事是设置您的 UI 元素,以便用户可以实际看到正在发生的事情。您已经了解到 Android 中的 UI 布局大部分是在 xml 文件中定义的。再次使用 setContentView 方法加载布局。代码中的最后一行用于从布局中获取对视图元素的引用,以便可以在代码中对其进行管理。findViewById 方法获取一个视图标识符,并返回该引用的通用视图元素。这就是为什么需要对视图元素的正确实现进行强制转换的原因。为了能够从布局 xml 文件中引用视图,这些视图需要定义一个标识符。在 res/layout/打开 main.xml 文件,并将 id 属性添加到 TextView。

<TextView android:id="@+id/textView" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="@string/hello" />

这里可以看到动态资源生成的新语法。语法@+id/textView 意味着该视图元素应该分配有 id textView。id 前面的加号意味着,如果这个 id 在 R.java 文件中不存在,那么应该在那里创建一个新的引用。

您已经完成了活动的创建阶段。现在你进入下一个重要的生命周期阶段,onResume 生命周期钩子。每次活动从暂停状态返回时,都会调用 onResume 方法。当您离开活动开始新的活动或返回设备的主屏幕时,您的活动将被设置为暂停状态,而不是被终止。系统这样做是为了节省时间和内存分配,以防稍后再次显示该活动。在暂停状态下,活动对用户不再可见。如果应该再次显示该活动,则该活动仅从其暂停状态返回,并设置为恢复状态,而不是再次完全初始化。当这种情况发生时,onResume 生命周期方法被调用。onResume 方法不应该负责进行主要的初始化。在这种情况下,它应该检查您是否仍然能够与附件通信。这正是你在这里做的。

`@Override
public void onResume() {
super.onResume();

if (mInputStream != null && mOutputStream != null) {
return;
}

UsbAccessory[] accessories = mUsbManager.getAccessoryList();
UsbAccessory accessory = (accessories == null ? null : accessories[0]);
if (accessory != null) {
if (mUsbManager.hasPermission(accessory)) {
openAccessory(accessory);
} else {
synchronized (mUsbReceiver) {
if (!mPermissionRequestPending) {
mUsbManager.requestPermission(accessory, mPermissionIntent);
mPermissionRequestPending = true;
}
}
}
} else {
Log.d(TAG, "mAccessory is null");
}
}`

如果输入和输出流仍然是活动的,那么你可以进行通信,并且可以从 onResume 方法中提前返回。否则,您必须从 UsbManager 获取附件的参考。如果您已经拥有与设备通信的用户权限,您可以打开并重新分配输入和输出流。这部分是在一个名为 openAccessory 的方法中实现的,稍后会详细介绍。这里我要讲的最后两个生命周期方法是 onPause 方法和 onDestroy 方法。

`@Override
public void onPause() {
super.onPause();
closeAccessory();
}

@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mUsbReceiver);
}`

onResume 方法的反面是 onPause 方法。您已经了解了活动的暂停状态,因为您在 onResume 方法中打开了到附件的连接,所以您应该注意在 onPause 方法中关闭连接以释放内存。

如果调用生命周期方法 onDestroy,您的活动将被终止,并且不再出现在应用活动堆栈中。这个生命周期阶段可以描述为 onCreate 方法的对立面。尽管您在 onCreate 阶段完成了所有的初始化工作,但是您将在 onDestroy 阶段完成反初始化和清理工作。因为应用只有这一个活动,所以当 onDestroy 在活动上被调用时,应用也会被系统杀死。您在创建时注册了一个广播接收器,以收听特定于配件的事件。由于应用不再存在,你应该在这里注销这个广播接收器。为此,使用广播接收器作为参数调用 unregisterReceiver 方法。

生命周期方法到此结束。到目前为止,这相当容易。通信部分的实现一开始看起来有点棘手,但是不要担心。我会引导你完成它(见清单 2-10 )。

清单 2-10。【HelloWorldActivity.java (建立附件连接)

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { openAccessory(accessory); } else { Log.d(TAG, "permission denied for accessory " + accessory); } mPermissionRequestPending = false; } } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (accessory != null && accessory.equals(mAccessory)) { `closeAccessory();
}
}
}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}`

这里首先看到的是 BroadcastReceiver 的实现。现在您知道您需要在各自的生命周期方法中注册和注销广播接收器,但是现在让我们看看应该如何实现广播接收器。

`private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
openAccessory(accessory);
} else {
Log.d(TAG, "permission denied for accessory " + accessory);
}
mPermissionRequestPending = false;
}
} else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
closeAccessory();
}
}
}
};`

广播接收器是作为 broadcast receiver 类型的匿名内部类实现的。您必须覆盖的唯一方法是 onReceive 方法,如果该广播接收器已注册并与提供的 intent-filter 匹配,则系统会调用该方法。记住,在意图过滤器中定义了两个动作。您必须检查在调用广播接收器时发生了什么操作。如果您收到描述许可请求已被回应的操作,您必须检查用户是否被授予与您的配件通信的许可。如果是这样,您可以打开附件的通信通道。可能已经触发广播接收器的第二个动作是附件已经从 Android 设备分离的通知。在这种情况下,你需要清理并关闭你的沟通渠道。如您所见,BroadcastReceiver 调用 openAccessory 方法和 closeAccessory 方法来打开和关闭附件的通信通道。接下来我们来看看那些方法。

private void openAccessory(UsbAccessory accessory) { mFileDescriptor = mUsbManager.openAccessory(accessory); if (mFileDescriptor != null) { mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); Thread thread = new Thread(null, commRunnable, TAG); thread.start(); Log.d(TAG, "accessory opened"); } else { Log.d(TAG, "accessory open fail"); } }

在 openAccessory 方法中,您委托 USB 服务方法(也称为 openAccessory)来获取附件的 FileDescriptor。FileDescriptor 管理您将用来与设备通信的输入和输出流。一旦流被分配,你也将启动一个单独的线程,该线程将实际接收和发送消息,但稍后会详细介绍。

如果您还没有权限连接到您的附件,并且您没有处于用户权限的挂起状态,您必须通过调用 USB 服务上的 request permission 方法来请求您的附件的权限。requestPermission 方法有两个参数,您请求权限的附件和待定意向。这个挂起的意图是您在 onCreate 方法中定义的 mPermissionIntent,它负责在用户授予或拒绝与附件通信的权限时发送带有 ACTION_USB_PERMISSION 的广播。您可能还记得,您还在 onCreate 方法中注册了一个广播接收器,该方法为完全相同的操作提供了一个 intent-filter。一旦广播被发送,广播接收器将对其做出反应。

closeAccessory 方法负责关闭附件的所有剩余的打开连接。

private void closeAccessory() { try { if (mFileDescriptor != null) { mFileDescriptor.close(); } } catch (IOException e) { } finally { mFileDescriptor = null; mAccessory = null; } }

它所做的只是关闭附件的文件描述符。系统将处理与其流相关联的所有底层 OS 资源。

当您最终打开与附件的连接时,您可以来回发送和接收数据。清单 2-11 显示了实际的通信实现。

清单 2-11。【HelloWorldActivity.java (通信实施)

`Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[255];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
break;
}

switch (buffer[0]) {
case COMMAND_TEXT:

final StringBuilder textBuilder = new StringBuilder();
int textLength = buffer[2];
int textEndIndex = 3 + textLength;
for (int x = 3; x < textEndIndex; x++) {
textBuilder.append((char) buffer[x]);
}

runOnUiThread(new Runnable() {

@Override
public void run() {
textView.setText(textBuilder.toString());
}
}); sendText(COMMAND_TEXT, TARGET_DEFAULT, "Hello World from Android!");
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}

}
}
};
public void sendText(byte command, byte target, String text) {
int textLength = text.length();
byte[] buffer = new byte[3 + textLength];
if (textLength <= 252) {
buffer[0] = command;
buffer[1] = target;
buffer[2] = (byte) textLength;
byte[] textInBytes = text.getBytes();
for (int x = 0; x < textLength; x++) {
buffer[3 + x] = textInBytes[x];
}
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}
}`

一旦建立了与附件的连接,就可以开始实际发送和接收消息了。您可能还记得,openAccessory 方法中启动了一个单独的线程,负责消息处理。

Thread thread = new Thread(null, commRunnable, TAG); thread.start();

传递给线程的 Runnable 对象也是您必须实现的匿名内部类。只要您拥有来自附件的活动 inputstream,就会执行它的 run 方法。

`Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[255];

while (ret >= 0) {
try { ret = mInputStream.read(buffer);
} catch (IOException e) {
break;
}

switch (buffer[0]) {
case COMMAND_TEXT:
final StringBuilder textBuilder = new StringBuilder();
int textLength = buffer[2];
int textEndIndex = 3 + textLength;
for (int x = 3; x < textEndIndex; x++) {
textBuilder.append((char) buffer[x]);
}
runOnUiThread(new Runnable() {

@Override
public void run() {
textView.setText(textBuilder.toString());
}
});
sendText(COMMAND_TEXT, TARGET_DEFAULT, "Hello World from Android!");
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};`

在每个迭代步骤中,inputstream 的内容被读入一个字节数组。如果第一个字节表示您收到了 COMMAND_TEXT 类型的消息,那么将使用 StringBuilder 从发送的剩余字节中构建消息。

既然您已经完成了消息,那么您需要向用户显示它。记住你仍然在一个单独的线程中。系统只允许 UI 更新发生在 UI 线程上。要更新 TextView UI 元素的文本,可以使用方便的方法 runOnUiThread,该方法在系统 UI 线程上执行给定的 Runnable 对象。

这就是消息处理的接收部分。收到一条消息后,另一条消息会立即发送回公告板。为此,您将编写自己的名为 sendText 的方法,该方法采用前两个标识符字节和实际消息来构建您的消息数据结构,您可以通过 outputstream 将其发送到附件。

public void sendText(byte command, byte target, String text) { int textLength = text.length(); byte[] buffer = new byte[3 + textLength]; if (textLength <= 252) { buffer[0] = command; buffer[1] = target; buffer[2] = (byte) textLength; byte[] textInBytes = text.getBytes(); for (int x = 0; x < textLength; x++) { buffer[3 + x] = textInBytes[x]; } if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } } }

恭喜你!您已经完成了通信双方的实现。现在,将 Arduino 草图上传到您的 ADK 板上,并将 Android 应用部署到您的设备上。如果你把你的 Android 设备连接到你的 ADK 板,你应该看到你的 Android 应用自动启动,它将打印 ADK 板发送的信息。如果您在开发板连接到 PC 时打开串行监视器,您可以看到来自 Android 设备的信息。每个设备显示对方的信息,如图图 2-18 和图 2-19 所示。

images

图 2-18。Android 设备上的 HelloWorld 应用接收消息

images

图 2-19。接收消息的 Arduino 应用的串行监视器输出

总结

您已经了解了 Android 设备和 Arduino 配件在连接时如何相互识别。您了解了在 Arduino 端和通信的 Android 端实现开放附件协议的必要条件。Arduino 端的实现相当简单,因为大部分工作已经在 AndroidAccessory Arduino 库的帮助下完成了。你已经看到,软件的挑战在于 Android 设备的编码,因为有更多的工作要做。本章向您展示了如何使用自定义的数据结构跨两个平台传输文本消息。接下来的章节将基于你已经学到的知识,使你能够用 ADK 板读取传感器值或控制执行器。您在此完成的项目将是本书中后续示例的基础。

三、输出

源自原始 Arduino 设计的 ADK 板有几个引脚和连接器。这些引脚大多数是数字引脚。这种 ADK 板上的数字引脚可以配置为输入或输出。本章描述了如何将数字引脚配置为输出引脚。

在这种特殊情况下,输出意味着什么?这意味着当设置为输出模式时,引脚将发射功率。源自 Arduino 的 ADK 板上的数字引脚可以发射高达 5V 的电压。它们可以用在数字环境中,可以有两种状态,HIGHLOW。将输出引脚设置为HIGH意味着它将发出 5V。如果设置为LOW,则发出 0V,因此没有电压。一些输出引脚也可用于模拟环境。这意味着它们能够发出从 0V 到 5V 的输出值范围。

以下两个项目将在实际应用中解释这两种用例。

项目 1:切换 LED

这是您将使用附加硬件部件的许多项目中的第一个。您将利用 ADK 板的数字引脚作为输出端口来为发光二极管(LED)供电,并编写一个 Android 应用来打开和关闭 LED。

零件

您将在本项目中使用以下硬件(如图 3-1 所示):

  • ADK 董事会
  • 试验板
  • 工作电压为 5V 的 LED
  • S7-1200 可编程控制器
  • 一些电线

images

图 3-1。项目 1 零件(ADK 板、试验板、电阻器、LED、电线)

LED

一个发光二极管 (LED)是一个充当光源的小半导体(见图 3-2 )。你家里几乎所有的电子设备上都可以找到 led。大多数情况下,它们被用作状态指示器。led 被设计成非常节能和可靠。这就是为什么他们也找到了进入艺术装置、汽车前灯和普通家庭照明解决方案的方法,这里仅举几个例子。

images

图 3-2。5 毫米红色 LED

有许多类型的发光二极管。它们在尺寸、色谱和工作电压上有所不同。led 具有方向性,这意味着如何在电路中连接它们至关重要。普通 led 有一个阳极(正极连接器)和一个阴极(负极连接器)。你必须将能量源的正极连接到阳极,负极连接到阴极。如果你把它反过来连接,你会永久损坏它。在普通 LED 上,您可以通过几种方式区分连接器。您可能会注意到,LED 的引脚长度不同。长腿是阳极(正极连接器),短腿是阴极(负极连接器)。如果您有一个透明的 LED 透镜,您可能会看到两个 LED 连接器在其嵌入端具有不同的形式。看起来像半个箭头的小一点的就是所谓的。接线柱是阳极连接器的嵌入端。阴极预埋件称为。一些发光二极管在其透镜的一侧也有一个扁平点。这一面标志着阴极。你可以看到,我们已经做了很多工作来区分两种连接器,这样你就不会因为连接错误而意外损坏 LED。

在这个项目中,您将使用 ADK 板的一个数字输出端口,当它被设置为HIGH时在 5V 下工作,当它被设置为LOW时在 0V 下工作。你应该使用同样在 5V 电压下工作的 LED,这样它的寿命会更长。您也可以使用 3.3V 的较低额定 LED,但较高的电压水平会更快地磨损 LED。LED 通常在 20mA 到 30mA 的电流下工作,您应该限制流动的电流,以便 LED 不会被更高的电流损坏。为了限制电流,你使用一个电阻。如果没有这样的限流电阻,就不应该使用 led。

电阻器

电阻器是用来限制电路中电流的电子元件。电阻是施加在电阻上的电压与流过电阻的电流成正比的比值。这个比例是由欧姆定律定义的。欧姆定律是电气工程中最重要的公式之一。你经常需要它来决定在电路中使用哪个电阻来限制电流,这样你就不会烧坏你的元件。该公式的定义如下:

V = R × I

如你所见,电压是电阻和电流的乘积。电压用伏特测量,单位符号为 v。电流用安培测量,单位符号为 a。电阻用欧姆测量,单位符号为希腊字母ω。在一个简单的例子中,公式可以这样应用:

5V = 250Ω × 0.02A

标准的 3 毫米和 5 毫米 led 在 20mA 至 30mA 的电流限制下工作。当数字输出端口设置为HIGH时,您希望将电流限制在 30mA 左右,并提供 5V 电压。如果你应用欧姆定律并重新排列,你就可以计算出所需电阻的电阻值。

images

电阻有标准化的范围,你找不到像 166ω这样的特定值。您应该始终使用下一个可用的较高电阻值,而不要使用较低值,因为您不想因过载而永久损坏您的组件。下一个更高的电阻值是 220ω电阻。

你已经学会了如何确定在这个项目中你需要的电阻值。现在,我们来看看常见的电阻种类,以及如何通过观察来识别它们的值。

电阻有多种形式和尺寸,但除了小型表面贴装器件(SMD),最常用的电阻是碳化合物电阻和薄膜电阻,如图图 3-3 所示。

images

图 3-3。碳化合物电阻器(下),薄膜电阻器(上)

碳化合物电阻器由碳和其他化合物组成,因此得名。电阻值取决于混合物中的碳含量。碳化合物电阻器通常比其他电阻器更耐用,因为它们可以更好地处理高脉冲,而不会对其电阻值产生长期影响。缺点是它们不是最精确的电阻。

薄膜电阻器具有由金属薄膜覆盖的绝缘陶瓷棒或基底。金属涂层的厚度决定了电阻器的电阻特性。薄膜电阻器不如碳化合物电阻器坚固,因为它们容易受到高脉冲和过载的影响,这会损害它们的电阻能力。这些电阻的优点是比碳化合物电阻更精确。

上述标准应在生产电路设计中考虑,但不适用于我们的简单项目。

两种类型的电阻器表面都涂有彩色条纹。这些条带有助于识别电阻器的电阻值。碳化合物电阻器具有 4 段颜色编码,而薄膜电阻器具有 5 段颜色编码。

表 3-1 给你一个颜色编码的概述。

images

您可能想知道应该从电阻的哪一端读取色带。如果你仔细观察,你会发现一个波段与其他波段的距离稍大。这是公差带。图 3-4 显示了一个 4 频段碳化合物 220ω电阻,公差为+- 5%。第一个频段为红色(2),第二个频段为红色(2),乘法器频段为棕色(10),即 22×10ω= 220ω。公差带为金色(+- 5%)。

images

图 3-4。220ω+-5%碳化合物电阻器

试验板

一个试验板,也称为原型板,是一种不需要焊接的原型板。它通常是一块有穿孔的塑料。这些孔的标准间距通常为 0.1 英寸(2.54 毫米)。

images

图 3-5。试验板/原型板

嵌入电路板的是以特殊布局排列的导电触点。这些板允许即插即用机制,以便您可以专注于电路设置,而不是将所有东西焊接在一起。这样,如果你在设置中犯了错误,你可以快速调整电路。电路板有多种形式和尺寸,但基本布局基本相同。顶部和底部导轨上的触点主要用于连接电源的正极和负极端口。电路板中间的区域是实际的原型制作区域。嵌入试验板的连接布局如图图 3-6 所示。

images

图 3-6。试验板触点布局

ADK 董事会

在第一章中,您了解了 ADK 板的规格。Arduino 衍生的 ADK 板有几个数字输入和输出引脚。您将使用其中一个引脚作为输出端口来开关 LED。输出端口可以提供高达 5V 的电压。您将使用在图 3-7 中看到的数字输出引脚 2,并且您将在数字环境(HIGH / LOW)中设置输出值。

images

图 3-7。数字输出引脚 2

电线

你需要一些电线将试验板上的电阻和 LED 连接到 ADK 板上。对于原型制作和试验板工作,有特殊的试验板或跳线。使用这些电线的好处是你不必自己剥开它们,它们有不同的长度,并且有公母接头。

images

图 3-8。左起:电子线、跳线(公对公)、跳线(母对公)

如果你不想买现成的电线,你也可以使用电子或贝尔线。您必须剥去这些电线上的电线末端,露出大约 3/16 英寸至 5/16 英寸(5 毫米至 8 毫米)的电线,以便与嵌入试验板的触点良好接触。你可以用小刀小心地切开电线绝缘体,然后把它剥掉,但我强烈推荐使用电缆剥线钳,这是一种更安全、更容易操作的工具。你只需抓住电线,施加一些软压力,将隔离器从电线上剥离。(参见图 3-9 。)

images

图 3-9。剥线器

设置

你需要把电阻串联到发光二极管上。ADK 板的数字输出引脚 2 将连接到您的电阻器,电阻器连接到 LED 的阳极,ADK 板的地(GND)将连接到 LED 的阴极(负极引线)。如图 3-10 所示连接所有部件。

images

图 3-10。项目 1 设置

软件

硬件设置已经完成,是时候编写控制 LED 的代码了。您将编写一个 Arduino 草图,它接收切换命令,并根据 Android 应用发送的命令切换 LED。Android 应用将由一个控制开关状态的切换按钮组成。

Arduino 草图

以第二章中写的 Arduino 草图作为这个草图的基础。您已经在其中实现了特定于 ADK 的部分,您只需通过为 LED 切换场景定义另一个数据协议来更改通信部分。创建一个新的草图,并输入清单 3-1 中所示的代码。之后,我会解释发生了什么变化。

清单 3-1。项目一:Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_LED 0x2
#define TARGET_PIN_2 0x2
#define VALUE_ON 0x1
#define VALUE_OFF 0x0

#define PIN 2

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte rcvmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
pinMode(PIN, OUTPUT);
}

void loop() {
if (acc.isConnected()) {
//read the received data into the byte array
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_LED) {
if (rcvmsg[1] == TARGET_PIN_2){
//get the switch state
byte value = rcvmsg[2];
//set output pin to according state
if(value == VALUE_ON) {
digitalWrite(PIN, HIGH); } else if(value == VALUE_OFF) {
digitalWrite(PIN, LOW);
}
}
}
}
}
}`

你可能注意到的第一件事是来自第二章的短信专用代码被删除了。在这个项目中你不需要发送文本,所以代码已经被修改以支持 3 字节数据协议,这在第二章中也提到过。为了评估从 Android 设备接收的数据,您必须定义一个命令字节、一个目标字节和一个值字节常量。定义的数据协议字节常量COMMAND_LED、TARGET_PIN_2、VALUE_ON,VALUE_OFF的含义应该是不言自明的。您还定义了一个PIN常量,它反映了应该被控制的管脚。

除了已知的必须在setup方法中进行的附件初始化之外,您还需要配置想要使用的数字引脚的模式。因为您希望引脚作为输出工作,所以需要用pinMode(PIN, OUTPUT)设置引脚模式。

loop方法中,您检查已建立的连接并读取传入的数据。然后计算第三个字节的值。如果您接收到一个0x1字节,您将引脚设置为HIGH以输出 5V,如果您接收到一个0x0字节,您将引脚设置为LOW以使其输出为 0V。为此,您将使用digitalWrite方法。它的参数是要设置的引脚和它应该切换到的状态,HIGHLOW

Arduino 部分到此为止。让我们继续 Android 软件部分。

Android 应用

对于 Android 部分,你也将建立在你在第二章中从你的 Android 应用中学到的原则之上。您还必须调整数据协议并引入一个新的 UI 元素,一个 ToggleButton,它允许用户打开和关闭 LED。让我们看看清单 3-2 中的类,以及您必须做出的更改。

清单 3-2。项目一:ProjectOneActivity.java

`package project.one.adk;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor; import android.util.Log;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ToggleButton;

import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;

public class ProjectOneActivity extends Activity {

private static final String TAG = ProjectOneActivity.class.getSimpleName();

private PendingIntent mPermissionIntent;
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
private boolean mPermissionRequestPending;

private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;

private static final byte COMMAND_LED = 0x2;
private static final byte TARGET_PIN_2 = 0x2;
private static final byte VALUE_ON = 0x1;
private static final byte VALUE_OFF = 0x0;

private ToggleButton ledToggleButton;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUsbManager = UsbManager.getInstance(this);
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
registerReceiver(mUsbReceiver, filter);

setContentView(R.layout.main);
ledToggleButton = (ToggleButton) findViewById(R.id.led_toggle_button);
ledToggleButton.setOnCheckedChangeListener(toggleButtonCheckedListener);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override public void onResume() {
    super.onResume();

if (mInputStream != null && mOutputStream != null) {
return;
}

UsbAccessory[] accessories = mUsbManager.getAccessoryList();
UsbAccessory accessory = (accessories == null ? null : accessories[0]);
if (accessory != null) {
if (mUsbManager.hasPermission(accessory)) {
openAccessory(accessory);
} else {
synchronized (mUsbReceiver) {
if (!mPermissionRequestPending) {
mUsbManager.requestPermission(accessory, mPermissionIntent);
mPermissionRequestPending = true;
}
}
}
} else {
Log.d(TAG, "mAccessory is null");
}
}

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

OnCheckedChangeListener toggleButtonCheckedListener = new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.led_toggle_button) {

new AsyncTask<Boolean, Void, Void>() {

@Override
protected Void doInBackground(Boolean... params) { sendLedSwitchCommand(TARGET_PIN_2, params[0]);
return null;
}
}.execute(isChecked);
}
}
};

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
openAccessory(accessory);
} else {
Log.d(TAG, "permission denied for accessory " + accessory);
}
mPermissionRequestPending = false;
}
} else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
closeAccessory();
}
}
}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null; mAccessory = null;
}
}

public void sendLedSwitchCommand(byte target, boolean isSwitchedOn) {
byte[] buffer = new byte[3];
buffer[0] = COMMAND_LED;
buffer[1] = target;
if (isSwitchedOn) {
buffer[2] = VALUE_ON;
} else {
buffer[2] = VALUE_OFF;
}
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}
}`

如果您像这里一样更改了活动名和包名,请确保您也更改了 AndroidManifest.xml 条目以反映这种重命名。

清单 3-3。项目 1: AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android" **package="project.one.adk"** android:versionCode="1" android:versionName="1.0"> … <activity **android:name=".ProjectOneActivity"** android:label="@string/app_name" android:screenOrientation="portrait"> … </manifest>

数据协议常量已从 4 字节协议更改为 3 字节协议。正如您在 Arduino 草图中所做的那样,您还为 LED 切换数据消息定义了常数。

private static final byte COMMAND_LED = 0x2; private static final byte TARGET_PIN_2 = 0x2; private static final byte VALUE_ON = 0x1; private static final byte VALUE_OFF = 0x0;

这个项目的范围是开关 LED,所以你不需要显示任何文本。因此,TextView UI 元素已被 ToggleButton 替换。

`private ToggleButton ledToggleButton;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
ledToggleButton = (ToggleButton) findViewById(R.id.led_toggle_button);
ledToggleButton.setOnCheckedChangeListener(toggleButtonCheckedListener);
}`

您可以看到一个OnCheckedChangeListener被分配给了ledToggleButton,它实现了一个回调方法,每次按钮被按下时都会触发这个回调方法。ToggleButtonButton的一个特殊的有状态实现,这意味着它知道自己是否被选中。OnCheckedChangeListener的实现是在匿名内部类中完成的。唯一需要实现的方法是onCheckedChange方法,它有两个参数:触发事件的按钮和指示按钮新状态的布尔标志。

`OnCheckedChangeListener toggleButtonCheckedListener = new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (buttonView.getId() == R.id.led_toggle_button) {
new AsyncTask<Boolean, Void, Void>() {

@Override
protected Void doInBackground(Boolean... params) {
sendLedSwitchCommand(TARGET_PIN_2, params[0]);
return null;
}
}.execute(isChecked);
}
}
};`

如果您有多个按钮使用同一个监听器,您应该总是验证是否按下了正确的按钮。这里就是这么做的。在这个特定的项目中,您不需要检查这一点,因为您只使用了一个按钮,但是如果您计划将侦听器用于更多的组件,这是一个很好的实践。在确认正确的按钮触发了事件后,您可以开始向 ADK 板发送命令来切换 LED。sendText方法已经被移除,因为在这个项目中不需要它。实现了一种新方法,将 3 字节数据消息发送到名为sendLedSwitchCommand的板。它有两个参数,LED 连接的 ADK 板的目标引脚和它应该切换到的状态。

你可能想知道这个AsyncTask是怎么回事。事件回调方法在 UI 线程上执行。如果您只是将消息发送到那里,您也可以在 UI 线程中执行 outputstream 操作。这通常是可行的,但这是一种不好的做法。较长的操作可能会阻塞用户界面,这对用户来说是非常令人沮丧的。为了避免这些情况,你可以做几件事:打开另一个Thread,利用 Android 的Handler机制,或者像在这种情况下一样,使用一个AsyncTask来实现并发。解释每种方法的优缺点超出了本书的范围,但是你可以在http://developer.android.com/guide/topics/fundamentals/processes-and-threads.html的 Android 开发指南中读到很多。AsyncTask基本上做的是在后台打开另一个线程来处理你的操作,同时你的 UI 线程运行来服务用户。这里只实现了doInBackground方法,因为这是您所需要的。此外,AsyncTask具有运行在 UI 线程上的回调方法,以可视化后台操作的进度或在操作完成时更新 UI 元素。

sendLedSwitchCommand方法看起来类似于您已经知道的sendText方法,但是它实现了 3 字节数据协议。

public void sendLedSwitchCommand(byte target, boolean isSwitchedOn) { byte[] buffer = new byte[3]; buffer[0] = COMMAND_LED; buffer[1] = target; if (isSwitchedOn) { buffer[2] = VALUE_ON; } else { buffer[2] = VALUE_OFF; } if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }

代码改动到此为止。您记得应该向用户显示一个ToggleButton,所以您也需要在布局文件main.xml中做一些更改(清单 3-4 )。

清单 3-4。项目一:main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> **<ToggleButton android:id="@+id/led_toggle_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textOn="@string/led_on" android:textOff="@string/led_off" />** </LinearLayout>

如您所见,TextView元素被ToggleButton元素所取代。它被定义为只和它自己的内容一样宽一样高;勾选或取消勾选时显示的文本在strings.xml文件中被引用。如果你在 Eclipse 中切换到图形布局编辑器,你已经可以在你的布局中间看到这个按钮了(图 3-11 )。

images

图 3-11。项目 1:main . XML 的 Eclipse 图形化布局

这就是这个项目的 Android 部分要做的全部工作。项目 1 现在已经完成,可以测试了。上传两个设备的应用,将它们连接在一起,你应该会得到类似于你在图 3-12 中看到的东西。

images

图 3-12。项目 1:最终结果

项目 2:调暗 LED 灯

在本项目中,您将了解 ADK 板上数字 IO 引脚的另一个特性,即脉宽调制。你将学习什么是脉宽调制,以及如何用它来调暗 LED。您将编写一个 Android 应用,在滑块的帮助下控制调光过程。

零件

这个项目的部件与项目 1 中的完全相同。你不需要任何新的硬件部件。

  • ADK 董事会
  • 面包板
  • 工作电压为 5V 的 LED
  • S7-1200 可编程控制器
  • 一些电线

这些部件已经在项目 1 中解释过了,但你将使用 ADK 板的一个新功能,我将在下面解释。

ADK 董事会

ADK 板的一些数字 IO 引脚有一个称为 PWM 的附加功能。PWM 代表脉宽调制。具有该特性的管脚在 Arduino 衍生的 ADK 板上有标记,如图图 3-13 所示。

images

图 3-13。Arduino 板上的 PWM 标记

PWM 可以被描述为数字输出的快速高-低来回切换。切换时,数字引脚产生方波信号(参见图 3-14 )。

images

图 3-14。占空比为 50%的脉宽调制示例

信号处于开启状态的时间与其处于关闭状态的时间相比称为占空比。信号处于 on 状态的时间称为脉冲宽度。因此,在图 3-14 中,占空比为 50%。

引脚的快速状态变化直接影响模拟特性,即引脚提供的电压。占空比为 100%时,该引脚产生约 5V 的模拟值。Arduino 衍生板将 256 个值映射到 0V 和 5V 之间的范围。因此,值 127 将导致引脚产生占空比为 50%的方波,产生约 2.5V 的电压。

为了控制 Arduino 草图中管脚的脉冲宽度,使用了analogWrite方法,其参数是要使用的数字管脚,值在 0 到 256 之间。

设置

电路设置与项目 1 完全相同,参见图 3-10 以供参考。

软件

两个平台的大部分代码都可以保持原样。您将只更改较小的细节来传输更宽的脉冲宽度值范围,并且您将在 Android 代码中引入一个新的 UI 元素SeekBar,用于选择 PWM 值。

Arduino 草图

支持 PWM 输出的更改后的 Arduino 代码可以在清单 3-5 中看到。

清单 3-5。项目二:Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

**#define COMMAND_LED 0x2

define TARGET_PIN_2 0x2

define PIN 2**

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte rcvmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
pinMode(PIN, OUTPUT);
}

void loop() {
if (acc.isConnected()) {
//read the received data into the byte array
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_LED) {
if (rcvmsg[1] == TARGET_PIN_2){
//get the analog value
byte value = rcvmsg[2];
//set output pin to according analog value
analogWrite(PIN, value);

}
}
}
}
}`

您可以看到,LED 状态的常量(VALUE_ON / VALUE_OFF)已被删除,因为您现在使用的是模拟值,而不是数字状态。Android 应用传输的字节值被读取并直接输入到analogWrite方法中。如果数字引脚支持 PWM,这种方法会触发数字引脚产生具有特定占空比的方波。作为参数,它采用要使用的引脚和一个 0 到 255 的字节值,该值映射到 0V 到 5V 范围内的模拟值。

Android 应用

来自项目 1 的 Android 应用也可以用作这个项目的基础。在这个项目中,你不需要改变很多东西。您将为您的应用引入一个新的 UI 元素:SeekBar。在查看了ProjectTwoActivity的完整代码清单后,我将解释已更改的部分,这些部分使您能够传输 ADK 板稍后用于 PWM 的值范围。因为代码的大部分没有改变,并且在前面的清单中有描述,所以我将用三个点()隐藏它们的实现部分,以便只关注重要的部分(参见清单 3-6 )。然而,和往常一样,完整的代码参考可以在www.apress.com找到。

清单 3-6。项目二:ProjectTwoActivity.java

`package project.two.adk;

import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import android.widget.SeekBar;
import android.widget.SeekBar.OnSeekBarChangeListener;
import android.widget.TextView;

import com.android.future.usb.UsbAccessory;
import com.android.future.usb.UsbManager;

**public class ProjectTwoActivity extends Activity {

private static final String TAG = ProjectTwoActivity.class.getSimpleName();**

private PendingIntent mPermissionIntent;
private static final String ACTION_USB_PERMISSION = "com.android.example.USB_PERMISSION";
private boolean mPermissionRequestPending;

private UsbManager mUsbManager;
private UsbAccessory mAccessory;
private ParcelFileDescriptor mFileDescriptor;
private FileInputStream mInputStream;
private FileOutputStream mOutputStream;

private static final byte COMMAND_LED = 0x2;
private static final byte TARGET_PIN_2 = 0x2;

**private TextView ledIntensityTextView;
private SeekBar ledIntensitySeekBar;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
ledIntensityTextView = (TextView) findViewById(R.id.led_intensity_text_view);
ledIntensitySeekBar = (SeekBar) findViewById(R.id.led_intensity_seek_bar);
ledIntensitySeekBar.setOnSeekBarChangeListener(ledIntensityChangeListener);
ledIntensityTextView.setText("LED intensity: " + ledIntensitySeekBar.getProgress());
}**

@Override
public void onResume() {
super.onResume();

}

@Override
public void onPause() {
super.onPause();

}

@Override
public void onDestroy() {
super.onDestroy();

}

**OnSeekBarChangeListener ledIntensityChangeListener = new OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
ledIntensityTextView.setText("LED intensity: " + ledIntensitySeekBar.getProgress());
new AsyncTask<Byte, Void, Void>() {

@Override
protected Void doInBackground(Byte... params) {
sendLedIntensityCommand(TARGET_PIN_2, params[0]);
return null;
}
}.execute((byte) progress);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// not implemented
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// not implemented
}
};**

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

};

private void openAccessory(UsbAccessory accessory) {

}

private void closeAccessory() {

}

public void sendLedIntensityCommand(byte target, byte value) {
byte[] buffer = new byte[3];
buffer[0] = COMMAND_LED;
buffer[1] = target;
buffer[2] = value;
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}

}`

你可以看到,这里也删除了 LED 开关状态的字节常量。在这个项目中,向用户显示了两个 UI 元素。第一个是一个TextView,它应该显示当前传输到 ADK 板的选定值。第二个元素是一个SeekBar,它是一个滑块控件,让用户可以轻松地在预定义的范围内选择一个值。

`private TextView ledIntensityTextView;
private SeekBar ledIntensitySeekBar;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
ledIntensityTextView = (TextView) findViewById(R.id.led_intensity_text_view);
ledIntensitySeekBar = (SeekBar) findViewById(R.id.led_intensity_seek_bar);
ledIntensitySeekBar.setOnSeekBarChangeListener(ledIntensityChangeListener);
ledIntensityTextView.setText("LED intensity: " + ledIntensitySeekBar.getProgress());
}`

像所有其他的View元素一样,SeekBar可以注册一组广泛的监听器,当某些事件发生时,这些监听器会得到通知。一个专用于SeekBar的监听器是在onCreate方法中注册的OnSeekBarChangeListener。如果滑块收到第一个触摸手势,如果滑块改变其值,如果触摸被释放,它会得到通知。您只关心SeekBar的变化状态,因此实现如下:

`OnSeekBarChangeListener ledIntensityChangeListener = new OnSeekBarChangeListener() {

@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
ledIntensityTextView.setText("LED intensity: " + ledIntensitySeekBar.getProgress());
new AsyncTask<Byte, Void, Void>() {

@Override
protected Void doInBackground(Byte... params) {
sendLedIntensityCommand(TARGET_PIN_2, params[0]);
return null;
}
}.execute((byte) progress);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// not implemented
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// not implemented
}
};`

当调用onProgressChanged方法时,它从系统接收三个参数。第一个是触发事件的实际的SeekBar元素,第二个是SeekBar的当前进度,第三个是一个布尔标志,指示进度的改变是由用户滑过SeekBar造成的,还是进度是通过编程设置的。实现非常简单。在TextView的帮助下,你向用户显示数值的变化,然后你将数值传送到 ADK 板。注意progress的数据类型是 byte。稍后你会看到SeekBar的范围被配置为从 0 到 255。然而,数据类型字节的范围是从-128 到 127。实际情况是,进度值被转换成一个字节,如果该值大于 127,它就变成负数。这与位算术和所谓的符号位有关。这不应该是你现在关心的问题,因为在 Arduino 端,当一个可能的负字节值被提供给analogWrite方法时,它将被转换回原来的表示。请注意,一般来说,这种强制转换是不安全的,尽管在这个例子中它是有效的。

您已经了解到 IO 操作应该在 UI 独立的线程中进行,因此您将再次使用AsyncTask来实现这一目的。实际的通信逻辑封装在sendLedIntensityCommand方法中。

public void sendLedIntensityCommand(byte target, byte value) { byte[] buffer = new byte[3]; buffer[0] = COMMAND_LED; buffer[1] = target; buffer[2] = value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }

实现几乎等于来自项目 1 的sendLedSwitchCommand。您将传输SeekBar的当前值,其范围从 0 到 255,而不是只传输两种可能的状态。

这就是项目 2 的所有代码实现。您仍然需要更改 main.xml 文件,以便向用户实际显示TextViewSeekBar。新的 main.xml 文件看起来像清单 3-7 。

清单 3-7。项目 2–main . XML

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> <TextView android:id="@+id/led_intensity_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="LED intensity: 0"/> **<SeekBar android:id="@+id/led_intensity_seek_bar" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="255" />** </LinearLayout>

您已经了解了TextView元素的属性,所以让我们看看SeekBar有什么特别之处。除了已经知道的idlayout_widthlayout_height等属性之外,你看到一个叫做max的属性。该属性定义了SeekBar可以达到的最大值。初始值 0 是默认值,您不必自己定义。所以在布局中,您已经定义了从 0 到 255 的范围。如果您切换到图形布局编辑器,您已经可以看到该用户界面的预览(图 3-15 )。

images

图 3-15。项目 2:main . XML 的 Eclipse 图形化布局

项目 2 现已完成,准备测试。将应用上传到您的设备并启动它们。你完成的项目应该看起来像图 3-16 。

images

图 3-16。项目二:最终结果

总结

在这一章中,你学习了什么是 ADK 板上的输出引脚以及它的功能。在本章的第一个项目中,您看到了如何在数字环境中使用输出引脚,当输出切换到HIGHLOW时,通过提供 5V 或 0V 来开关简单的 LED。第二个项目引入了数字输出的 PWM 或脉宽调制模式,其中一个输出引脚可以发出 0V 至 5V 范围内的输出电压。为了让用户控制传输到 ADK 板的值,您使用了两个不同的 Android UI 元素:ToggleButton在数字环境中打开和关闭 LED,而SeekBar在模拟环境中从一系列值中选择来调暗 LED。

四、输入

在 ADK 板的环境中,输入是板上的引脚和连接器,通过它们可以接收数据或测量值。虽然通用 USB 型连接器从技术上来说也是输入,但本章将只关注可用于测量或检测数字状态变化的输入。这种意义上的输入是 ADK 董事会的引脚。

ADK 板上的大多数引脚都可以用作输入引脚。请记住,数字引脚可以配置为输出和输入。默认情况下,数字引脚配置为输入引脚。您可以使用pinMode方法将它们设置为输入模式,但您不一定需要这样做。

此外,ADK 板有专用的模拟输入引脚。使用模拟输入引脚,您可以测量这些引脚上施加电压的变化。测得的模拟电压被映射为数字表示,您可以在代码中进行处理。

以下两个项目描述了两种输入引脚类型及其使用情况。

项目 3:读取按钮的状态

在这个项目中,您将学习如何使用 ADK 板上的数字输入引脚来检测按钮或开关的状态。对于额外的硬件,你需要一个按钮或一个开关和一个电阻。你可以在这个项目中使用按钮或开关,因为它们的工作方式基本相同。这两个元件都可以用来闭合或断开电路。您将编写一个 Arduino 草图,它读取按钮的当前状态,并将状态更改传输到 Android 应用。接收状态变化的 Android 应用将在TextView中传播该变化,并且每当按下按钮时,您的 Android 设备的振动器将被触发振动。

零件

到现在为止,你已经知道了这个项目的大部分内容。不过,我将解释按钮或开关的原理、所谓上拉电阻的使用以及配置为输入引脚的数字引脚的使用。在该项目中,您将需要以下硬件(如图 4-1 所示):

  • ADK 董事会
  • 试验板
  • 按钮或开关
  • 10kΩ上拉电阻
  • 一些电线

images

图 4-1。项目 3 部分(ADK 板、试验板、电阻、按钮、电线)

按钮或开关

按钮开关是用于控制电路状态的元件。电路可以是闭合的,这意味着电源有回路,也可以是断开的,这意味着电路的回路被阻断或没有连接到电路。为了实现从开路到闭路的转换,需要使用按钮或开关。在ON状态下,按钮或开关本身没有电压降,也没有限流特性。在其OFF状态,按钮或开关理想地没有电压限制和无穷大的电阻值。在一个简单的电路图中,一个闭合电路看起来像图 4-2 中的所示。

images

图 4-2。闭路

如你所见,功率可以通过电路的元件流向回路。如果您将一个开关或按钮连接到该电路,您可以控制该电路是断开还是闭合。通过按下按钮或将开关切换到ON位置,您可以闭合电路,这样电力就可以流过电路。如果您松开按钮或将开关切换回其OFF位置,您将断开电路,从而使其保持打开状态。按钮或开关的电路图符号在电路中显示为开路部分。在图 4-3 的电路图中可以看到开关的符号。

images

图 4-3。带开关的电路

按钮和开关有多种类型和尺寸。典型的按钮可以是按钮,您需要按住它来闭合电路,松开它来打开电路,或者它们可以是拨动按钮,在被按下后保持其当前状态。开关也有几种形状和应用类型,但最常见的是众所周知的定义两种状态的ON / OFF开关,以及可以在多种状态之间切换的拨动开关(见图 4-4 )。

images

图 4-4。按钮和开关

上拉电阻

你已经用一个电阻来限制电路中的电流。在这个项目中,您将使用一个电阻和一个按钮或开关将输入引脚拉至LOW (0V)或HIGH (5V)。这可以通过特殊的电路设置来实现。

在某些情况下,您可能希望输入引脚处于定义的状态。因此,例如,当一个数字引脚被配置为输入,并且没有元件与之相连时,您仍然会测量到电压波动。这些波动是外部信号或其他电干扰的结果。引脚上测得的电压将介于 0V 和 5V 之间,这将导致引脚状态的数字读数连续变化(LOW / HIGH)。为了消除这些干扰,您需要将该输入引脚上的电压拉高。在这种用例中,电阻器被称为上拉电阻器

上拉电阻必须放置在电路内的电压源和输入引脚之间。按钮或开关位于输入引脚和地之间。该设置的简单示意图如图 4-5 所示。

images

图 4-5。上拉电阻电路

这里发生的事情的一个简单解释是,如果开关或按钮没有按下,输入只连接到 Vcc (5V),线被拉高,输入被设置为HIGH。当按下开关或按钮且输入连接到 Vcc 和 GND (0V)时,电流在 10kΩ电阻处的电阻大于开关或按钮处的电阻,后者的电阻非常低(通常远低于 1ω)。在这种情况下,输入被设置为LOW,因为到 GND 的连接强于到 Vcc 的连接。

还需要高阻值电阻来限制电路中的总电流。如果你按下开关或按钮,你直接连接 Vcc 到 GND。如果没有高阻值电阻,会让太多的电流直接流向 GND,从而导致短路。高电流会导致热量积聚,在大多数情况下,会永久性地损坏您的部件。

ADK 董事会

您已经使用了配置为输出引脚的 ADK 板的数字引脚。在这个项目中,您将使用处于输入模式的引脚。通过使用数字引脚作为输入引脚,您可以测量数字信号:数字HIGH表示输入引脚上大约 5V 的电压,而数字LOW接近 0V。您已经了解到,上拉电阻可用于稳定输入引脚,通过将引脚稳定上拉至 5V 电源电压,使其不受干扰影响。ADK 电路板的一个特点是,嵌入式 ATmega 芯片集成了可以通过代码激活的上拉电阻。要激活集成上拉电阻,只需将引脚设置为输入模式,并将其设置为HIGH

pinMode(pin, INPUT); // set digital pin to input mode digitalWrite(pin, HIGH); // turn on pullup resistor for pin

不过,我不建议在这个项目中使用这种技术,这样您可以直接了解上拉电阻的基本原理。如果您手头没有高值电阻,您仍然可以如上所示更改该项目的代码来激活内部上拉电阻。请注意,如果在代码中之前输入引脚被用作输出引脚,那么您只需使用pinMode方法来定义输入引脚。默认情况下,所有数字引脚都被配置为输入,因此,如果该引脚始终仅用作输入,则不必显式设置pinMode

设置

您刚刚了解到需要将想要使用的数字输入引脚连接到上拉电阻电路。在图 4-6 中可以看到,ADK 板的+5V Vcc 引脚必须连接到 10kΩ上拉电阻的一个引线上。另一根引线连接到数字输入引脚 2。数字引脚 2 也连接到开关或按钮的一个引线。相反的引线接地。就这么简单。通过这种设置,当按钮或开关未按下时,将输入引脚拉至 5V,使数字输入引脚测量数字HIGH。如果现在按下按钮或开关,数字输入引脚被拉至 GND,导致输入测量数字LOW

images

图 4-6。项目 3 设置

软件

如本章开头的项目描述所述,您将编写一个 Arduino 草图,持续监控数字输入引脚的状态。每当 pin 码的状态从HIGH变为LOW,或者相反,你就会向连接的 Android 设备发送一条消息。Android 应用将监听传入的状态变化,并在一个TextView中显示当前状态。此外,只要按下按钮,Android 设备的振动器就会被激活。

Arduino 草图

和以前一样,Arduino sketch 实现非常简单。看看清单 4-1 中的,稍后我会解释细节。

清单 4-1。项目三:Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_BUTTON 0x1
#define TARGET_BUTTON 0x1
#define VALUE_ON 0x1
#define VALUE_OFF 0x0
#define INPUT_PIN 2

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte sntmsg[3];
int lastButtonState;
int currentButtonState;

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_BUTTON;
sntmsg[1] = TARGET_BUTTON;
}

void loop() {
if (acc.isConnected()) {
currentButtonState = digitalRead(INPUT_PIN);
if(currentButtonState != lastButtonState) {
if(currentButtonState == LOW) {
sntmsg[2] = VALUE_ON;
} else {
sntmsg[2] = VALUE_OFF;
}
acc.write(sntmsg, 3);
lastButtonState = currentButtonState;
}
delay(100);
}
}`

这里要做的第一件事是为按钮状态消息定义一些新的消息字节。

`#define COMMAND_BUTTON 0x1

define TARGET_BUTTON 0x1

define VALUE_ON 0x1

define VALUE_OFF 0x0

define INPUT_PIN 2`

因为消息的前两个字节不会改变,所以您已经可以在您的setup方法中设置它们了。

sntmsg[0] = COMMAND_BUTTON; sntmsg[1] = TARGET_BUTTON;

注意,没有必要在setup方法中调用pinMode方法,因为默认情况下数字引脚是输入引脚。

第一种新方法是digitalRead方法,它测量输入引脚上施加的电压,并将其转换为两种可能的数字状态:HIGHLOW。提供给该方法的唯一参数是 pin,应该读取它。

currentButtonState = digitalRead(INPUT_PIN);

接下来,您会看到当前状态与之前的状态进行了比较,因此只有在状态发生变化时,才会向 Android 设备发送消息。

if(currentButtonState != lastButtonState) { if(currentButtonState == LOW) { sntmsg[2] = VALUE_ON; } else { sntmsg[2] = VALUE_OFF; } acc.write(sntmsg, 3); lastButtonState = currentButtonState; }

现在让我们来看看 Android 应用。

Android 应用

这个项目的 Android 应用没有引入新的 UI 元素。在已知的TextView的帮助下,您将看到按钮或开关的状态变化。但是,您将学习如何调用系统服务来处理某些系统或硬件功能。对于这个项目,Android 设备的振动器服务将负责控制设备中的振动器电机。首先,看看清单 4-2 中的代码。我将在后面解释新的功能。同样,没有改变的已知代码部分被缩短了,这样您就可以专注于重要的部分。

清单 4-2。项目三:ProjectThreeActivity.java

`package project.three.adk;

import …; public class ProjectThreeActivity extends Activity {

private static final byte COMMAND_BUTTON = 0x1;
private static final byte TARGET_BUTTON = 0x1;
private static final byte VALUE_ON = 0x1;
private static final byte VALUE_OFF = 0x0;

private static final String BUTTON_PRESSED_TEXT = "The Button is pressed!";
private static final String BUTTON_NOT_PRESSED_TEXT = "The Button is not pressed!";

private TextView buttonStateTextView;

private Vibrator vibrator;
private boolean isVibrating;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
buttonStateTextView = (TextView) findViewById(R.id.button_state_text_view);

vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));
}

@Override
public void onResume() {
super.onResume();

}

@Override
public void onPause() {
super.onPause();
closeAccessory();
stopVibrate();
}

@Override
public void onDestroy() {
super.onDestroy();
unregisterReceiver(mUsbReceiver);
}

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {

}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
final byte[] buffer = new byte[3];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
break;
}

switch (buffer[0]) {
case COMMAND_BUTTON:

if(buffer[1] == TARGET_BUTTON) {
if(buffer[2] == VALUE_ON) {
startVibrate();
} else if(buffer[2] == VALUE_OFF){
stopVibrate();
}
runOnUiThread(new Runnable() {

@Override
public void run() { buttonStateTextView.setText(buffer[2] == VALUE_ON ?
BUTTON_PRESSED_TEXT : BUTTON_NOT_PRESSED_TEXT);
}
});
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);

break;
}
}
}
};

public void startVibrate() {
if(vibrator != null && !isVibrating) {
isVibrating = true;
vibrator.vibrate(new long[]{0, 1000, 250}, 0);
}
}

public void stopVibrate() {
if(vibrator != null && isVibrating) {
isVibrating = false;
vibrator.cancel();
}
}
}`

看看这个项目增加了哪些变量:

`private static final byte COMMAND_BUTTON = 0x1;
private static final byte TARGET_BUTTON = 0x1;
private static final byte VALUE_ON = 0x1;
private static final byte VALUE_OFF = 0x0;

private static final String BUTTON_PRESSED_TEXT = "The Button is pressed!";
private static final String BUTTON_NOT_PRESSED_TEXT = "The Button is not pressed!";

private TextView buttonStateTextView;

private Vibrator vibrator;
private boolean isVibrating;`

您应该已经认识到稍后验证发送的消息所需的协议字节。然后你会看到两个String常量,如果按钮或开关的状态改变了,它们用来更新TextView的文本。最后两个变量用于引用系统振动器服务,并检查振动器是否已被激活。

onCreate方法中,您请求设备振动器的系统服务:

vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));

getSystemService方法返回 Android 设备系统服务的句柄。这个方法可以从Context类的每个子类中调用,或者直接从Context引用中调用。所以你可以从一个Activity或者一个Service以及一个Application子类中访问系统服务。Context类还定义了访问系统服务的常量。

在第二章中,您已经了解了从您的HelloWorld应用接收数据消息的实现细节。一个单独的线程检查传入的数据并处理消息。根据接收到的按钮状态值,调用startVibratestopVibrate方法。startVibrate方法检查您是否仍然拥有系统服务的有效句柄,以及振动器是否已经停止振动。然后,它设置布尔标志来描述振动器被激活,并定义要立即开始的振动模式。

public void startVibrate() { if(vibrator != null && !isVibrating) { isVibrating = true; vibrator.vibrate(new long[]{0, 1000, 250}, 0); } }

系统服务的方法有两个参数。第一个是数据类型为 long 的数组。它包含三个值:振动开始前的等待时间、振动时间和关闭振动时间。vibrate方法的第二个参数定义了模式中应该重复的索引。传入值 0 意味着从头开始一遍又一遍。如果不想重复这个模式,只需传入一个值-1。值的时间单位是毫秒。所以这个模式所做的就是立即启动,振动一秒钟,关闭 250 毫秒,然后重新开始。

如果你的应用暂停了,你应该确保不要留下不必要的资源分配,所以如果发生这种情况,一定要停止振动器。这就是为什么在onPause生命周期方法中调用stopVibrate方法的原因。实现很简单。

public void stopVibrate() { if(vibrator != null && isVibrating) { isVibrating = false; vibrator.cancel(); } }

首先检查你是否仍然有一个有效的服务参考,振动器是否还在振动。然后重置布尔标志并取消振动。

现在,将 Arduino 草图上传到您的 ADK 板上,并将 Android 应用部署到您的设备上。如果你做的一切都正确,你的项目应该看起来像图 4-7 中的所示,并且你的 Android 设备应该在你每次按下连接到你的 ADK 板的按钮或开关时振动并改变它的TextView

images

图 4-7 。项目 3:最终结果

项目 4:用电位计调节模拟输入

模拟输入测量用于识别模拟输入引脚上施加电压的变化。许多传感器和部件通过改变它们的输出电压来表示值的变化。这个项目将教你如何使用电路板的模拟输入引脚,以及如何将模拟输入映射到你可以在代码中使用的数字值。为了改变模拟输入,你将使用一种叫做电位计的新元件。您将更改模拟值,该值将被转换为数字值,并传输到 Android 应用。在 Android 应用中,您将使用一个ProgressBar UI 元素来可视化接收到的值的变化。

零件

对于这个项目,您只需要一个电位计和一些电线作为附加硬件组件(如图图 4-8 所示):

  • ADK 董事会
  • 试验板
  • 电位计
  • 一些电线

images

图 4-8 。项目 4 部分(ADK 板、试验板、电位器、电线)

ADK 董事会

这是您第一次不用 ADK 板的数字 IO 引脚。相反,您将使用电路板上的模拟输入引脚。顾名思义,它们只能用作输入。这些引脚的特殊之处在于它们可以测量模拟值,即施加电压的变化。ADK 板能够将这些测量值转换成数字值。这个过程被称为模数转换。这是由一个叫做 ADC 的内部组件完成的,ADC 是一个模数转换器。在 ADK 板的情况下,这意味着从 0V 到 5V 的值被映射到从 0 到 1023 的数字值,因此它能够可视化 10 位范围内的值的变化。模拟输入引脚位于电路板上数字引脚的另一侧,通常标有模拟输入和引脚编号前缀 a,因此模拟引脚 5 应标为 A5。你可以在图 4-9 中看到那些针脚。

images

图 4-9 。模拟输入引脚

电位计

电位计是一种可变电阻器。它有三根导线可以连接到电路上。它有两种功能,取决于你如何连接它。如果您只是将一个外部端子和一个中间端子连接到您的电路,它只是一个简单的可变电阻器,如图图 4-10 所示。

images

图 4-10 。电位器作为可变电阻

如果你还连接了第三根引线,它就充当了所谓的分压器。分压器(也称为分压器)是一种特殊的电路设置,顾名思义,它能够将电路中的电压分成电路组件之间的不同电压电平。典型的分压电路由两个串联电阻或一个电位计组成。在图 4-11 中可以看到电路可视化。

images

图 4-11 。带电位计的分压器(左),带两个串联电阻的分压器(右)

Vin 是施加在两个串联电阻上的电压,Vout 是第二个电阻(R2)上的电压。确定输出电压的公式如下:

images

让我们看一个例子。考虑这样一个使用案例,您有一个 9V 电池,但您的一个电子元件只能在 5V 电压下工作。您已经确定了 Vin(9V)和 Vout(5V)。唯一缺少的是电阻值,这是你需要的。

让我们尝试使用一个 27k 电阻来测量 R2。现在唯一缺少的是 R1。将这些值输入公式,结果如下:

images

重新排列公式,以便确定缺失的变量 R1。

images

由于您找不到这样一个特定的电阻值,因此可以采用下一个更高的值,即 22k。对于 R1 的那个值,你将得到 4.96V,这非常接近于目标 5V。

如果你扭动电位器,你基本上改变了它的内阻比例,也就是说如果左边端子和中间端子之间的电阻减小,右边端子和中间端子之间的电阻增大,反之亦然。因此,如果你将这个原理应用于分压公式,这意味着如果 R1 值增加,R2 值就会减少,反之亦然。因此,当电位计内的电阻比例发生变化时,会导致 Vout 发生变化。电位计有几种形状和电阻范围。最常见的类型是微调器,其通过使用螺丝刀或类似的装配物体来调整,以及旋转电位计,其具有轴或旋钮来调整电阻值(如图图 4-12 所示)。在这个项目中,我使用微调类型,因为它通常比旋转电位器便宜一点。

images

图 4-12 。电位器:微调(左),旋转电位器(右)

设置

这个项目的设置很简单。只需将+5V 引脚连接到电位计的一个外部引线,并将 GND 引脚连接到相反的外部引线。将模拟引脚 A0 连接到电位计的中间引线,就大功告成了。你的设置应该看起来像图 4-13 。如果调整电位计,模拟引脚上的测量值将会改变。

images

图 4-13 。项目 4 设置

软件

Arduino 草图负责读取模拟引脚的 ADC 值。传输的 10 位值将由 Android 应用接收,值的变化将显示在TextViewProgressBar UI 元素中。您还将学习传输大值的转换技术。

Arduino 草图

看看清单 4-3 中完整的 Arduino 草图。之后我会讨论新的内容。

清单 4-3。项目 4: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_ANALOG 0x3
#define TARGET_PIN 0x0
#define INPUT_PIN A0

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte sntmsg[6];
int analogPinReading;

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_ANALOG;
sntmsg[1] = TARGET_PIN;
}

void loop() {
if (acc.isConnected()) {
analogPinReading = analogRead(INPUT_PIN);
sntmsg[2] = (byte) (analogPinReading >> 24);
sntmsg[3] = (byte) (analogPinReading >> 16);
sntmsg[4] = (byte) (analogPinReading >> 8);
sntmsg[5] = (byte) analogPinReading;
acc.write(sntmsg, 6);
delay(100);
}
}`

第一个可以看到的新方法是analogRead方法。它将模拟电压值转换为 10 位数字值。因为它是一个 10 位的值,所以太大而不能存储在字节变量中。这就是为什么你必须把它存储在一个整型变量中。

**analogPinReading = analogRead(INPUT_PIN);**

问题是你只能传输字节,所以你必须把整数值转换并拆分成几个字节。作为一种数据类型,整数的大小有 4 个字节那么大,这就是为什么你必须把整数转换成 4 个单字节,以便以后传输。为了转换该值,这里使用了一种称为移位的技术。移位意味着值以二进制表示进行处理,二进制表示由单个位组成,并且您将所有位向某个方向移位。

为了更好地理解什么是移位,请看一个例子。假设您想要传输值 300。正如您已经知道的,这个值是一个整数。该值的二进制表示如下:

00000000 00000000 00000001 00101100 = 300

正确的数学表达式更短,不需要你写所有的前导零。只是前缀是 0b。

0b100101100 = 300

如果将该值简单地转换为一个字节,则只有最后八位将构成字节值。在这种情况下,您最终得到的值是 44。

00101100 = 44

那只是整个价值的一部分。要转换其余的位,您需要首先将它们放到适当的位置。这就是使用移位的地方。您可以使用运算符<>,向两个方向移动位,将它们移动到右侧。在这种情况下,您需要右移,所以您使用>>操作符。在将该值转换为新的字节之前,需要将它向右移动八次。因为您需要将它移位几次来构造所有四个字节,所以完整的语法应该是这样的:

(byte) (300 >> 24) (byte) (300 >> 16) (byte) (300 >> 8) (byte) 300

在其新的二进制表示中,上述值如下所示:

00000000 00000000 00000001 00101100

可以看到,移出的位被简单地忽略了。现在,您可以传输所有四个数据字节,并在另一端将它们重新转换回初始整数。

Android 应用

在 Android 应用中,接收到的四字节值将被转换回整数值,测量值的变化将通过显示当前值的TextView可视化。第二个可视指示器是ProgressBar UI 元素。它看起来与已经推出的SeekBar相似,但是这里用户没有与工具条交互的可能性。看看清单 4-4 中的代码。稍后我会解释细节。

清单 4-4。项目四:ProjectFourActivity.java

`package project.four.adk;

import …;

public class ProjectFourActivity extends Activity {

private static final byte COMMAND_ANALOG = 0x3;
private static final byte TARGET_PIN = 0x0;

private TextView adcValueTextView;
private ProgressBar adcValueProgressBar;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
adcValueTextView = (TextView) findViewById(R.id.adc_value_text_view);
adcValueProgressBar = (ProgressBar) findViewById(R.id.adc_value_bar);
}

@Override
public void onResume() {
super.onResume();

}

@Override
public void onPause() {
super.onPause();

}

@Override
public void onDestroy() {
super.onDestroy();

}

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {

}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[6];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_ANALOG:

if (buffer[1] == TARGET_PIN) {
final int adcValue = ((buffer[2] & 0xFF) << 24)
+ ((buffer[3] & 0xFF) << 16)
+ ((buffer[4] & 0xFF) << 8)
+ (buffer[5] & 0xFF);
runOnUiThread(new Runnable() {

@Override
public void run() {
adcValueProgressBar.setProgress(adcValue);
adcValueTextView.setText(getString(R.string.adc_value_text,
adcValue));
}
});
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};
}`

正如您所看到的,这个代码片段中的新变量与 Arduino 草图中的消息定义字节相同,还有我在开始时描述的两个 UI 元素。

`private static final byte COMMAND_ANALOG = 0x3;
private static final byte TARGET_PIN = 0x0;

private TextView adcValueTextView;
private ProgressBar adcValueProgressBar;`

看看需要在清单 4-5 所示的main.xml布局文件中进行的 UI 元素定义。除了这两个元素通常的布局属性之外,您还必须定义ProgressBarmax值属性,以便可以在从 0 到 1023 的正确范围内进行图形可视化。

你可以看到还有第二个重要的属性。属性告诉系统以某种风格呈现 UI 元素的外观。如果省略该属性,ProgressBar将以默认样式呈现,这是一个加载类型的旋转轮。这不是你想要的,所以你可以用另一个样式覆盖它。这种特殊样式查找的语法看起来有点奇怪。前缀?android:意味着这个特殊的资源不能在当前项目的 res 文件夹中找到,但是可以在 Android 系统资源中找到。

清单 4-5。项目 4: main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> <TextView android:id="@+id/adc_value_text_view" android:layout_width="wrap_content" android:layout_height="wrap_content"/> **<ProgressBar android:id="@+id/adc_value_bar" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="1023" style="?android:attr/progressBarStyleHorizontal"/>** </LinearLayout>

在项目 3 中,您对接收到的输入感兴趣,因此接收数据的逻辑基本保持不变。一个单独的线程负责读取 inputstream 并处理接收到的消息。您可以看到,通过使用移位技术,接收到的消息的最后四个字节被再次转换为一个整数值—只是这一次,移位发生在另一个方向。

`final int adcValue = ((buffer[2] & 0xFF) << 24)

  • ((buffer[3] & 0xFF) << 16)
  • ((buffer[4] & 0xFF) << 8)
  • (buffer[5] & 0xFF);`

您还可以看到,字节值在进行位移之前已经改变。这种操作称为按位 AND。通过应用值 0xFF,可以消除处理负数和正数时可能出现的符号位错误。

如果您考虑前面的示例,并假设测得的值为 300,那么四个接收到的字节在没有移位的情况下将具有以下值:

00000000 = 0 00000000 = 0 00000001 = 1 00101100 = 44

要重建原始的整数值,你需要像上面那样左移字节值。

00000000 << 24 = 00000000 00000000 00000000 00000000 = 0 00000000 << 16 = 00000000 00000000 00000000 00000000 = 0 00000001 << 8 = 00000000 00000000 00000001 00000000 = 256 00101100 = 00000000 00000000 00000000 00101100 = 44

现在,如果您将接收到的字节值相加,您将再次得到原始的整数值。

0 + 0 + 256 + 44 = 300

最后要做的是将价值可视化给用户。使用 helper 方法runOnUiThread,两个 UI 元素都被更新。TextView相应地获取其文本设置,ProgressBar设置其新的进度值。

上传 Arduino sketch 和 Android 应用,查看调整电位计后数值如何变化。最终结果如图 4-14 所示

images

图 4-14。项目 4:最终结果

总结

本章展示了如何从 ADK 板的输入引脚读取数值。您使用输入配置中的数字引脚来读取HIGHLOW的数字输入。按钮或开关用于在这两种状态之间切换,每当按钮被按下或开关关闭时,Android 应用就会通过振动来表达当前状态。您还了解了通过将 ADK 板模拟输入引脚上的模拟电压读数转换为 0 到 1023 范围内的数字表达式来测量数值范围的第二种可能性。一个 Android 应用用一个新的 UI 元素ProgressBar将当前阅读可视化。您通过应用不同的样式更改了 UI 元素的外观。在此过程中,您了解了分压器和上拉电阻的原理,并了解到移位可以作为一种数据转换方式。

五、声音

ADK 板本身不能产生或检测声音。幸运的是,有一个组件可以帮助完成这两项任务:压电蜂鸣器。

声音的定义是什么?一般来说,声音是一组可以通过固体、液体和气体传播的压力波。压电蜂鸣器通过不同频率的振动在空气中传播声音。这些波的不同频率组成了你能听到的不同声音。人类能够听到 20Hz 到 20,000Hz 范围内的频率。频率的单位是赫兹。它定义了每秒的周期数。所以人耳每秒探测到的声波越多,感知到的声音就越高。如果你曾经站在一个大的音频音箱附近,你可能会看到扬声器的薄膜在振动。这实质上是扬声器产生不同频率的压力波。

在接下来的两个项目中,你将学习如何使用压电蜂鸣器发声,以及如何探测附近的声音。第一个项目将为您提供一种为自己的项目生成声音的方法,以便您可以构建音频警报系统、通知设备或简单的乐器。第二个项目将向你展示一种检测近距离声音甚至振动的方法。例如,这些功能用于爆震传感器项目或测量可能伤害敏感商品的振动。

项目 5:用压电蜂鸣器发声

这个项目将向你展示如何使用压电蜂鸣器来产生声音。它将解释逆压电效应的原理。您将使用您的 Android 设备来选择一个音符的频率值,该值将被传输到您的 ADK 板,以通过压电蜂鸣器产生声音。

零件

对于这个项目,你需要一个新的组件:压电组件,也就是压电蜂鸣器。除此之外,您只需要以下组件(如图图 5-1 所示):

  • ADK 董事会
  • 面包板
  • 压电蜂鸣器
  • 一些电线

images

图 5-1。项目 5 部分(ADK 板、试验板、电线、压电蜂鸣器)

ADK 董事会

您将使用支持脉宽调制(PWM)的 ADK 板的一个数字引脚。您已经使用了数字引脚的 PWM 功能来调暗 LED。再次使用 PWM 特性来产生方波,稍后将应用于压电蜂鸣器。方波特性的变化将导致压电蜂鸣器产生不同的振荡频率,从而产生不同的声音。

压电蜂鸣器

压电蜂鸣器是一种可以利用压电效应和逆压电效应的压电元件。这意味着它可以感知和产生声音。典型的压电蜂鸣器由放置在金属板上的陶瓷片组成。陶瓷晶片包含对振荡敏感的压电晶体。

压电效应描述了压力等机械力导致压电元件上产生电荷。压力波让陶瓷晶片膨胀和收缩。它与金属板一起引起振动,由此产生的压电晶体变形产生可测量的电荷。(参见图 5-2 。)在第二个项目中,压电效应被用于感应其附近的振动。

images

图 5-2。压电效应(压电元件的膨胀和收缩)

逆压电效应描述了当施加电势时产生机械力(例如压力波)的压电元件的效应。在电势的刺激下,压电元件再次收缩和膨胀,由此产生的振动产生声波,该声波甚至可以被共振的中空壳体放大。产生的不同声波取决于振荡的频率。这种效果将在本章的第一个项目中演示,以生成不同频率的声音。

最常见的压电蜂鸣器装在塑料外壳中,但你也可以找到陶瓷压电蜂鸣器板(图 5-3 )。

images

图 5-3。压电蜂鸣器

压电蜂鸣器用于家用电器、工业机器,甚至音乐设备。你可能在火警系统、无障碍系统中听到过它们,或者当你的洗衣机或烘干机试图告诉你它们的工作完成了。有时你会看到它们作为拾音器连接在原声吉他上,将共鸣吉他琴体的振动转换成电信号。

设置

这个项目的设置非常简单(见图 5-4 )。你只需要将压电蜂鸣器的一个连接到 GND,另一个连接到你的 ADK 板的数字引脚 2。请记住,一些压电蜂鸣器可能有一定的极性。通常它们被相应地标记或者它们已经连接了相应的电线。在这种情况下,将负极线连接到 GND,正极线连接到数字引脚 2。

images

图 5-4。项目 5 设置

软件

对于这个项目,您将编写一个 Android 应用,让用户通过Spinner UI 元素选择一个注释,这是一个类似下拉列表的东西,您可能从 Web 上了解到。音符将被映射到其代表频率,其值将被传输到 ADK 板。在 Arduino 端,您利用 Arduino tone方法,它是 Arduino IDE 的一部分,在连接的压电蜂鸣器上生成相应的声音。

Arduino 草图

这个项目的 Arduino 草图与项目 2 中使用的非常相似。只是这一次,您将使用tone方法,而不是使用analogWrite方法直接写入输出引脚,这种方法会生成必要的波形来产生所需的声音。在内部,它利用寻址的数字 PWM 引脚的能力来产生波形。看看完整的清单 5-1 。我将在后面解释tone方法的作用。

清单 5-1。项目 5: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

define COMMAND_ANALOG 0x3

define TARGET_PIN_2 0x2

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version", "URI",
"Serial");

byte rcvmsg[6];

void setup() {
Serial.begin(19200);
pinMode(TARGET_PIN_2, OUTPUT);
acc.powerOn();
}

void loop() {
if (acc.isConnected()) {
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_ANALOG) {
if (rcvmsg[1] == TARGET_PIN_2){
int output = ((rcvmsg[2] & 0xFF) << 24)
+ ((rcvmsg[3] & 0xFF) << 16)
+ ((rcvmsg[4] & 0xFF) << 8)
+ (rcvmsg[5] & 0xFF);
//set the frequency for the desired tone in Hz
tone(TARGET_PIN_2, output);
}
}
}
}
}`

Arduino IDE 提供了一个名为tone的重载特殊方法来生成方波,它可以用来通过扬声器或压电蜂鸣器产生声音。在其第一个变体中,tone方法接受两个参数,蜂鸣器连接的数字 PWM 引脚和以 Hz 为单位的频率。

tone(pin, frequency);

它的第二个变体甚至接受第三个参数,您可以指定音调的持续时间,单位为毫秒。

tone(pin, frequency, duration);

在内部,tone方法实现使用analogWrite方法利用 ADK 板的 PWM 功能来产生波形。正如你所看到的,这个例子中使用了双参数的tone方法来产生一个稳定连续的音调。在将接收到的频率值馈送到音调方法之前,通过使用移位技术对其进行转换。

Android 应用

对于 Android 部分,您将使用一个名为Spinner的类似下拉列表的 UI 元素,让用户选择一个将被映射到其相应频率的音符。您将学习如何初始化类似列表的 UI 元素,以及如何使用它们。在我解释细节之前,请看一下完整的清单 5-2 。

清单 5-2。项目五:ProjectFiveActivity.java

`package project.five.adk;

import …;

public class ProjectFiveActivity extends Activity {

private static final byte COMMAND_ANALOG = 0x3;
private static final byte TARGET_PIN_2 = 0x2;

private Spinner notesSpinner;
private ArrayAdapter adapter;
private int[] notes = {/C3/ 131, /D3/ 147, /E3/ 165,
/F3/ 175, /G3/ 196, /A3/ 220, /B3/ 247};

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
notesSpinner = (Spinner) findViewById(R.id.spinner);
notesSpinner.setOnItemSelectedListener(onItemSelectedListener);
adapter = ArrayAdapter.createFromResource(this, R.array.notes,
android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
notesSpinner.setAdapter(adapter);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

    }

/** Called when the activity is paused by the system. /
@Override
public void onPause() {
super.onPause();
closeAccessory();
} /
*

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

OnItemSelectedListener onItemSelectedListener = new OnItemSelectedListener() {

@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long id) {
new AsyncTask<Integer, Void, Void>() {

@Override
protected Void doInBackground(Integer... params) {
sendAnalogValueCommand(TARGET_PIN_2, notes[params[0]]);
return null;
}
}.execute(position);
}

@Override
public void onNothingSelected(AdapterView<?> arg0) {
// not implemented
}
};

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {

}

private void closeAccessory() {

}

public void sendAnalogValueCommand(byte target, int value) {
byte[] buffer = new byte[6];
buffer[0] = COMMAND_ANALOG;
buffer[1] = target;
buffer[2] = (byte) (value >> 24); buffer[3] = (byte) (value >> 16);
buffer[4] = (byte) (value >> 8);
buffer[5] = value;
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}
}`

让我们先来看看新的变量。

private Spinner notesSpinner; private ArrayAdapter<CharSequence> adapter; private int[] notes = {/*C3*/ 131, /*D3*/ 147, /*E3*/ 165, /*F3*/ 175, /*G3*/ 196, /*A3*/ 220, /*B3*/ 247};

您将使用一个名为Spinner的 UI 元素为用户提供选择注释的可能性。Spinner是一个列表元素,非常类似于下拉列表。它是一个 input 元素,单击时会展开一个列表。列表中的元素是可以选择的可能输入值。类似列表的 UI 元素用适配器管理它们的内容。这些适配器负责用内容填充列表,并在以后访问它。您在这里看到的ArrayAdapter就是这样一个适配器,可以保存内容元素的类型化数组。这里的最后一件事是一个映射数组,它将所选的音符映射到它以后的频率表示。这些值非常接近相应音符的频率,单位为赫兹(Hz)。

在你给变量分配新的视图元素之前,你必须在你的布局main.xml文件中定义它(见清单 5-3 )。

清单 5-3。项目 5: main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center"> **<Spinner android:id="@+id/spinner"** **android:layout_width="fill_parent"** **android:layout_height="wrap_content"** **android:prompt="@string/notes_prompt"/>** </LinearLayout>

Spinner有一个新的属性叫做prompt,定义了显示Spinner的列表内容时的提示。您可以在该属性中引用的strings.xml文件中定义一个简短的描述性标签。

<string name="notes_prompt">Choose a note</string>

现在您可以在onCreate方法中正确初始化视图元素了。

`/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
notesSpinner = (Spinner) findViewById(R.id.spinner);
notesSpinner.setOnItemSelectedListener(onItemSelectedListener);
adapter = ArrayAdapter.createFromResource(this, R.array.notes, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
notesSpinner.setAdapter(adapter);
}`

如果选择了新值,为了得到通知并做出反应,您必须在Spinner上设置一个监听器。在这种情况下,您将使用一个OnItemSelectedListener,稍后您将实现它。负责内容管理的ArrayAdapter可以通过一个名为createFromResource的静态方法轻松初始化。顾名思义,它从资源定义中构造内容。这个定义是在 strings.xml 文件中进行的。您只需要定义一个字符串项数组,如下所示。

<string-array name="notes"> <item>C3</item> <item>D3</item> <item>E3</item> <item>F3</item> <item>G3</item> <item>A3</item> <item>B3</item> </string-array>

必须给它一个name属性,以便以后可以引用它。初始化方法调用需要三个参数。第一个是上下文对象。这里您可以使用当前活动本身,因为它扩展了上下文类。第二个参数是内容定义的资源 id。这里您将使用之前定义的 notes 数组。最后一个参数是下拉框布局本身的资源 id。您可以使用一个定制的布局,或者通过使用标识符android.R.layout.simple_spinner_item使用默认的系统微调项目布局。

ArrayAdapter.createFromResource(this, R.array.notes, android.R.layout.simple_spinner_item);

您还应该设置列表中单个内容项的外观。这也是通过使用布局 id 调用setDropDownViewResource方法来完成的。同样,您可以在这里使用系统默认值。

adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);

最后,您可以将已配置的适配器与Spinner相关联。

notesSpinner.setAdapter(adapter);

初始步骤已经完成,是时候实现负责处理值已被选择的情况的监听器了。

`OnItemSelectedListener onItemSelectedListener = new OnItemSelectedListener() {

@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
long id) {
new AsyncTask<Integer, Void, Void>() {

@Override
protected Void doInBackground(Integer... params) {
sendAnalogValueCommand(TARGET_PIN_2, notes[params[0]]);
return null;
}
}.execute(position);
}

@Override
public void onNothingSelected(AdapterView<?> arg0) {
// not implemented
}
};`

当实现OnItemSelectedListener时,您将不得不处理两个方法。一个是onNothingSelected方法,在这种情况下不感兴趣;另一个是onItemSelected方法,当用户做出选择时被触发。当它被系统调用时,它提供四个参数:带有底层适配器的AdapterView、被选择的视图元素、被选择的项目在列表中的位置以及列表项目的 id。现在您已经知道选择了哪个项目,您可以将音符映射到它的实际频率,并将值发送到 ADK 板。这是在一个AsyncTask中完成的,这样 IO 操作就不会发生在 UI 线程上。

`new AsyncTask<Integer, Void, Void>() {

@Override
protected Void doInBackground(Integer... params) {
sendAnalogValueCommand(TARGET_PIN_2, notes[params[0]]);
return null;
}
}.execute(position);`

在将频率整数值作为四字节数据包传输之前,必须在 sendAnalogValueCommand 方法中对其进行位移。

一切都准备好了,你可以开始了(图 5-5 )。部署 Android 应用和 Arduino 草图,并聆听压电蜂鸣器的声音。你甚至可以扩展这个项目,用压电蜂鸣器演奏旋律。关于如何做到这一点的教程可以在 Arduino 主页的教程区找到,网址是[www.arduino.cc/en/Tutorial/PlayMelody](http://www.arduino.cc/en/Tutorial/PlayMelody).

images

图 5-5。项目 5:最终结果

项目 6:用压电蜂鸣器感知声音

本章的第二个项目将向你展示压电效应的原理。您将使用压电蜂鸣器构建一个爆震传感器,当压电元件振荡时,它会产生电荷。您将编写一个 Android 应用,在该应用中,每次检测到敲门声时,背景都会发生变化。一个简单的ProgressBar UI 元素将显示已检测到的当前 ADC 值。

零件

这个项目唯一需要的额外部件是一个高阻值电阻。您将使用一个 1Mω的下拉电阻。其他组件已经在之前的项目中使用过(见图 5-6 ):

  • ADK 董事会
  • 试验板
  • 1Mω下拉电阻
  • 压电蜂鸣器
  • 一些电线

images

图 5-6。项目 6 部分(ADK 板、试验板、电线、1Mω电阻器、压电蜂鸣器)

ADK 董事会

由于需要测量压电蜂鸣器振荡时的电压变化,因此需要使用 ADK 板上的一个模拟输入引脚。模拟输入将被转换成数字值(ADC),稍后可以在您的 Android 应用中进行处理。

压电蜂鸣器

正如已经提到的,你将在这个项目中利用压电蜂鸣器的压电效应。爆震或突然的压力波以某种方式影响压电元件,使其振荡。振荡频率对压电元件上产生的电荷有影响。所以振荡的频率与产生的电荷成正比。

下拉电阻

在前一章中,您使用上拉电阻将数字输入引脚稳定地拉至状态HIGH (+5V),以避免电路处于空闲状态时产生静态噪声。当按下连接按钮,电路连接到GND (0V)时,电阻最小的路径通向GND,输入引脚设置为 0V。

由于现在需要测量模拟引脚上施加的电压,将输入引脚上拉至 5V 毫无意义。您无法正确测量压电蜂鸣器引起的电压变化,因为输入引脚会持续在 5V 左右浮动。为了继续避免空闲状态下产生的静态噪声,同时能够测量电压变化,您可以将输入引脚拉低至GND (0V),并在压电元件产生负载时测量电压。该用例的简单电路原理图如图 5-7 中的所示。

images

图 5-7。压电蜂鸣器输入测量下拉电阻电路

设置

该项目的设置(如图图 5-8 所示)仅与之前略有不同。你只需要将高阻值电阻并联到压电蜂鸣器上。压电蜂鸣器的正极引线连接到电阻器的一端和 ADK 板的模拟输入引脚 A0。负极引线连接到电阻器和 GND 的另一端。

images

图 5-8。项目 6 设置

软件

您将编写一个读取模拟输入引脚 A0 的 Arduino 草图。如果压电蜂鸣器振荡,并在该引脚上测量到电压,相应的值将被转换为数字值,并可以传输到 Android 设备。Android 应用将通过ProgressBar UI 元素可视化传输的值,如果达到某个阈值,容器视图元素的背景颜色将变为随机颜色。所以每次敲击最终都会产生一个新的背景色。

Arduino 草图

这个项目的 Arduino 草图与项目 4 中的基本相同。您将测量引脚 A0 上的模拟输入,并将转换后的 ADC 值(范围为 0 至 1023)传输至连接的 Android 设备。参见完整的清单 5-4 。

清单 5-4。项目 6: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

define COMMAND_ANALOG 0x3

define INPUT_PIN_0 0x0

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI", "Serial");

byte sntmsg[6];

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_ANALOG;
sntmsg[1] = INPUT_PIN_0;
}

void loop() {
if (acc.isConnected()) {
int currentValue = analogRead(INPUT_PIN_0);
sntmsg[2] = (byte) (currentValue >> 24);
sntmsg[3] = (byte) (currentValue >> 16);
sntmsg[4] = (byte) (currentValue >> 8);
sntmsg[5] = (byte) currentValue;
acc.write(sntmsg, 6);
delay(100);
}
}`

同样,您可以看到,在通过预定义的消息协议将模数转换后的整数值传输到 Android 设备之前,您必须使用移位技术将它们编码为字节。

Android 应用

Android 应用对接收到的消息进行解码,并将接收到的字节转换回测得的整数值。如果达到阈值 100,LinearLayout视图容器将随机改变它的背景颜色。作为第二个可视化元素,您将为LinearLayout添加一个ProgressBar,这样如果用户在压电蜂鸣器附近敲门,就可以看到测量中的尖峰。

清单 5-5。项目六:ProjectSixActivity.java

`package project.six.adk;

import …;

public class ProjectSixActivity extends Activity {

private static final byte COMMAND_ANALOG = 0x3;
private static final byte TARGET_PIN = 0x0;

private LinearLayout linearLayout;
private TextView adcValueTextView;
private ProgressBar adcValueProgressBar; private Random random;
private final int THRESHOLD = 100;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
adcValueTextView = (TextView) findViewById(R.id.adc_value_text_view);
adcValueProgressBar = (ProgressBar) findViewById(R.id.adc_value_bar);

random = new Random(System.currentTimeMillis());
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

    }

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
}; private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[6];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_ANALOG:

if (buffer[1] == TARGET_PIN) {
final int adcValue = ((buffer[2] & 0xFF) << 24)

  • ((buffer[3] & 0xFF) << 16)
  • ((buffer[4] & 0xFF) << 8)
  • (buffer[5] & 0xFF);
    runOnUiThread(new Runnable() { @Override
    public void run() {
    adcValueProgressBar.setProgress(adcValue);
    adcValueTextView.setText(getString(R.string.adc_value_text,
    adcValue));
    if(adcValue >= THRESHOLD) {
    linearLayout.setBackgroundColor(Color.rgb(
    random.nextInt(256), random.nextInt(256),
    random.nextInt(256)));
    }
    }
    });
    }
    break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};
}`

这里对你来说唯一新的东西是Random类。Random类提供了为各种数字数据类型返回伪随机数的方法。特别是nextInt方法有一个重载的方法签名,它接受一个上限整数n,因此它只返回从 0 到n的值。在从 ADK 爆震传感器接收到的值被重新转换成整数后,它将对照阈值进行检查。如果该值超过阈值,则调用random对象的nextInt方法来生成三个随机整数。这些数字用于产生 RGB 颜色(红、绿、蓝),其中每个整数定义相应色谱的强度以形成新的颜色。屏幕的linearLayout视图容器用新的颜色更新,这样它的背景颜色在每次敲门时都会改变。

如果您已经完成了 Arduino 草图和 Android 应用的编写,请将它们部署到设备上并查看您的最终结果。它应该看起来像图 5-9 。

images

图 5-9。项目 6:最终结果

总结

在本章中,您学习了压电效应和反向压电效应的原理,从而能够感知并产生声音。你影响了压电蜂鸣器产生声音的振荡频率。您还使用压电蜂鸣器来检测由蜂鸣器附近的压力波或振动引起的压电元件的振荡。在这个过程中,您学习了 Arduino tone方法以及如何使用 Android Spinner UI 元素。您再次利用 ADK 板的模拟功能读取模拟值并将其转换为数字值,以感测您附近的声音或振动。你可以在自己的进一步项目中使用所有这些知识,例如,给出听觉反馈或感知振动。

六、光强感知

在这一章中,你将学会如何感知你周围环境中的光线强度。为了做到这一点,你将需要另一个新的组件,称为光敏电阻或光敏电阻(LDR)。我将在“部件”一节中解释这个组件的工作原理但是首先你需要理解光本身的描述。

那么光到底是什么?在我们的日常生活中,它无处不在。我们星球的完整生态系统依赖于光。它是所有生命的源泉,然而我们大多数人从未真正费心去理解光到底是什么。我不是一个物理学家,也不声称对它的物理原理提供了最好的解释,但我想至少提供一个关于光是什么的简要描述,让你对本章项目的目标有所了解。

物理上描述为电磁辐射。辐射是高能波或粒子穿过介质的术语。在这种情况下,光是能量波的任何波长。人眼只能看到一定范围的波长。它可以对波长为 390 纳米到 750 纳米的光做出响应。当检测到特定波长和频率的光时,会感觉到不同的光色。表 6-1 给出了人眼能看到的光的色谱的概述。

images

人眼看不到的光的一个很好的例子是电视遥控器上的小红外 LED。红外光谱在 700 纳米到 1000 纳米的范围内。LED 的光波长通常在 980 纳米左右,因此超过了人眼可见的光谱。LED 以取决于制造商的模式与电视的接收器单元进行通信。由于太阳光覆盖的波长范围很广,红外光也是其中一部分,因此通常会干扰通信。为了避免这个问题,电视制造商使用了在阳光中找不到的特定频率的红外光。

红外光的波长高于可见光,但也有一种光的波长低于可见光,称为紫外光。紫外光,简称紫外光,范围在 10 纳米到 400 纳米之间。紫外线本质上是电磁辐射,它可以引起化学反应,甚至可以破坏生物系统。这种效应的一个很好的例子是,当你长时间暴露在大量的紫外线下而没有任何保护性乳液时,你通常会晒伤。这个危险的波长低于 300 纳米。随着波长的减小,每个光子的能量增加。该波长光子的高能量在分子水平上对物质和有机体产生影响。由于能够引起化学反应,紫外光经常用于检测某些物质。一些物质通过发光起反应。这种效应通常用于犯罪调查,以检测假币、伪造护照,甚至体液。

项目 7:用光敏电阻感应光强度

这一章的项目应该为你提供一种方法来轻松地感知你周围的光线变化。您将在 ADK 板的模拟输入引脚上测量光敏电阻的光照强度引起的电压变化。由此产生的转换后的数字值将被发送到 Android 设备,以根据周围的光线条件调整 Android 设备的屏幕亮度。大多数 Android 设备已经内置了这样的传感器来实现这一点,但这个项目应该可以帮助你了解你的设备是如何操作的,以及你如何自己影响它的光线设置。

零件

这个项目的新部件是光敏电阻。其余部分对您来说并不陌生(参见图 6-1 ):

  • ADK 董事会
  • 面包板
  • 光敏电阻
  • 10kω电阻
  • 一些电线

images

图 6-1。项目 7 部分(ADK 板、试验板、电线、光敏电阻、10k 电阻)

ADK 董事会

又到了使用 ADK 板的模拟输入引脚来测量电压变化的时候了。这个项目的电路设置将最终建立一个与光敏电阻连接的分压器。在模拟输入引脚上测量电压变化时,会用数字 ADC 值表示。稍后,您将使用数字值对相对环境照明进行假设。

光敏电阻

一个光敏电阻是一个电阻,当它暴露在光线下时,电阻会减小(见图 6-2 )。这种行为是由所谓的光电效应造成的。

images

图 6-2。光敏电阻

诸如光敏电阻的半导体的电子可以具有不同的状态。这些状态由能带描述。能带由价带、电子束缚在单个原子上、带隙,没有电子态存在,以及导带,电子可以自由移动。如果电子从吸收的光的光子中获得足够的能量,它们就会被从它们的原子上撞下来,从价带移动到导带,在那里它们可以自由移动。这个过程对光敏电阻的电阻值有直接影响。这就是光电效应的原理,如图图 6-3 所示。

images

图 6-3。光电效应

光敏电阻通常用于需要感应照明变化的项目。例如,夜灯是光敏电阻的完美用例。当环境光线非常暗时,你需要打开夜灯,这样人们在晚上就能更好地辨别方向,而不必打开主灯。白天,当照明条件好得多的时候,你会想关掉夜灯以节约能源。这里可以使用光敏电阻将照明变化传播到微控制器,微控制器可以依次打开或关闭小夜灯。

另一种情况是使用光敏电阻和其他环境传感器来建立一个气象站,以监测全天的天气变化。例如,你可以判断天气是多云还是晴朗。如果你将这种气象站与 Android 设备结合使用,你可以保存你的数据,甚至将它发送到远程位置。如你所见,可能性是无限的。

电阻器

需要额外的电阻器来创建分压器电路。你在第四章中学习了分压电路的原理。当光敏电阻暴露于光下时,需要分压器来测量电压变化。如果光敏电阻的阻值变化,电路的输出电压也会变化。如果您只是将光敏电阻单独连接到模拟输入引脚,您将不会测量到引脚上的电压变化,因为暴露在光线下只会改变光敏电阻的电阻特性,因此只会影响通过的电流。如果太多的电流通过,你也可能最终损坏你的 ADK 板,因为未使用的能量将在大量热量积累中表现出来。

设置

如上所述,您需要为这个项目构建一个分压器电路。为此,您需要将光敏电阻的一根引线连接到+5V,另一根引线连接到附加电阻和模拟输入引脚 A0。电阻器的一条引线连接到光敏电阻和模拟输入引脚 A0,另一条引线连接到 GND。项目设置见图 6-4 。

images

图 6-4。项目 7 设置

软件

您将编写一个 Arduino 草图,在模拟输入引脚 A0 获取模拟读数,并将其转换为 10 位数字值。该值被映射到 0 到 100 之间的较低值,并发送到 Android 设备。Android 应用将根据接收到的值计算新的屏幕亮度。

Arduino 草图

同样,您将读取一个模拟引脚,只是这次您不会利用位移位技术来传输 ADC 值。您将首先使用 utility map方法来转换您的测量值,稍后会详细介绍。首先看看完整的清单 6-1 。

清单 6-1。项目 7: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_LIGHT_INTENSITY 0x5
#define INPUT_PIN_0 0x0

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte sntmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_LIGHT_INTENSITY;
sntmsg[1] = INPUT_PIN_0;
}

void loop() {
if (acc.isConnected()) {
int currentValue = analogRead(INPUT_PIN_0);
sntmsg[2] = map(currentValue, 0, 1023, 0, 100);
acc.write(sntmsg, 3);
delay(100);
}
}`

如您所见,新的命令字节和所用的模拟输入引脚是在开始时定义的。

`#define COMMAND_LIGHT_INTENSITY 0x5

define INPUT_PIN_0 0x0`

在这个项目中,您只需要一个三字节的消息,因为您不需要对测得的 ADC 值进行位移。您不需要对该值进行比特移位,因为您将在传输消息之前使用map方法。map方法的作用是将一个范围的值转换成另一个范围的值。您将把 ADC 值(范围为 0 到 1023)映射到 0 到 100 的范围。例如,ADC 值 511 将被转换为值 50。转换时,测量值不会大于 100,100 小到可以放入一个字节。构建完整的三字节消息后,您可以简单地将其传输到 Android 设备。

int currentValue = analogRead(INPUT_PIN_0); sntmsg[2] = map(currentValue, 0, 1023, 0, 100); acc.write(sntmsg, 3); delay(100);

Arduino 部分到此为止。让我们看看在 Android 端有什么要做的。

Android 应用

同样,Android 应用负责接收来自 ADK 板的消息。当 Android 应用收到该值时,它会计算屏幕亮度的新强度。之后,设置新的屏幕亮度。清单 6-2 只强调了重要的部分;代码很短,你现在应该知道了。

清单 6-2。项目 7:ProjectSevenActivity.java

`package project.seven.adk;

import …;

public class ProjectSevenActivity extends Activity {

private static final byte COMMAND_LIGHT_INTENSITY = 0x5;
private static final byte TARGET_PIN = 0x0;

private TextView lightIntensityTextView;
private LayoutParams windowLayoutParams;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
lightIntensityTextView = (TextView) findViewById(R.id.light_intensity_text_view);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    /
    @Override
    public void onResume() {
    super.onResume();

    } /
    * Called when the activity is paused by the system. */
    @Override
    public void onPause() {
    super.onPause();
    closeAccessory();
    }

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}

Runnable commRunnable = new Runnable() {`

`**@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[3];**
while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_LIGHT_INTENSITY:
if (buffer[1] == TARGET_PIN) {
final byte lightIntensityValue = buffer[2];
runOnUiThread(new Runnable() {

@Override
public void run() {
lightIntensityTextView.setText(
getString(R.string.light_intensity_value,
lightIntensityValue));
windowLayoutParams = getWindow().getAttributes();
windowLayoutParams.screenBrightness =
lightIntensityValue / 100.0f;
getWindow().setAttributes(windowLayoutParams);
}
});
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};
}`

首先,您必须定义相同的命令字节和 pin 字节,以便稍后匹配接收到的消息。

`private static final byte COMMAND_LIGHT_INTENSITY = 0x5;
private static final byte TARGET_PIN = 0x0;

private TextView lightIntensityTextView;
private LayoutParams windowLayoutParams;`

您在这里声明了唯一的 UI 元素,一个以任意单位向用户显示当前照明水平的TextView。您还可以看到一个LayoutParams对象的声明。LayoutParams定义父视图应该如何布局视图。WindowManager.LayoutParams类还定义了一个名为screenBrightness的字段,该字段指示电流Window在设置时覆盖用户的首选照明设置。

类型Runnable的内部类实现了上面描述的屏幕亮度调整逻辑。从 ADK 板收到值后,更新TextView UI 元素,向用户提供文本反馈。

lightIntensityTextView.setText(getString(R.string.light_intensity_value, lightIntensityValue));

为了调整屏幕的亮度,你首先要获得一个对当前WindowLayoutParams对象的引用。

windowLayoutParams = getWindow().getAttributes();

顾名思义,LayoutParams类的screenBrightness属性定义了屏幕的亮度。它的值是数字数据类型Float。该值的范围是从 0.0 到 1.0。因为您接收到一个介于 0 和 100 之间的值,所以您必须将该值除以 100.0f 才能达到要求的范围。

windowLayoutParams.screenBrightness = lightIntensityValue / 100.0f;

当你设置完亮度值后,你就可以更新当前Window对象的LayoutParams

getWindow().setAttributes(windowLayoutParams);

现在是时候看看 Android 设备如何响应您构建的光传感器了。在相应的设备上部署这两个应用并找出答案。如果一切顺利,你的最终结果应该看起来像图 6-5 。

images

图 6-5。项目 7:最终结果

奖励:用 Android 测量照度(勒克斯)

有时,像这个项目中所做的那样,仅仅使用相对值是不够的。测量光强度的更科学的方法是测量给定区域的照度。照度的单位是勒克斯;它的符号是 lx。

许多 Android 设备都内置了光线传感器,可以根据周围的环境照明来调整屏幕亮度。这些传感器返回以勒克斯(lx)为单位的测量值。要请求这些值,首先必须获得对SensorManager类的引用,该类充当设备传感器的一种注册表。之后,您可以通过使用光传感器Sensor.TYPE_LIGHT的传感器类型常量调用SensorManager上的getDefaultSensor方法来获得对光传感器本身的引用。

SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); Sensor lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT);

你现在想要的是当当前照度值改变时得到通知。为了实现这一点,您在sensorManager处注册了一个SensorEventListener,并将相应的传感器与这个监听器相关联。

sensorManager.registerListener(lightSensorEventListener, lightSensor, SensorManager.SENSOR_DELAY_NORMAL);

lightSensorEventListener的实现如下:

`SensorEventListener lightSensorEventListener = new SensorEventListener(){

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// nothing to implement here
}

@Override
public void onSensorChanged(SensorEvent sensorEvent) {
if(sensorEvent.sensor.getType() == Sensor.TYPE_LIGHT){
Log.i("Light in lx", sensorEvent.values[0]);
}
}};`

您只需要实现onSensorChanged方法,因为这是您感兴趣的事件。系统传递给该方法的SensorEvent对象包含一个值数组。根据您正在读取的传感器类型,您会在该数组中获得不同的值。传感器类型光的值位于该阵列的索引 0 处,它以勒克斯为单位反映当前的环境光。

你也可以把光敏电阻的测量值转换成勒克斯。然而,如果光敏电阻是非线性的(大多数光敏电阻都是非线性的),这需要更深入地理解数据手册和对数函数的使用。由于这涉及太多的细节,我不会在这里覆盖它。然而,如果你用你选择的搜索引擎搜索“计算勒克斯光敏电阻”,你可以在网上找到详细的信息和教程。

如果你不是数学的狂热爱好者,你也可以使用简单实用的方法。您可以尝试照明条件,并将从您的 Android 设备的光传感器接收到的结果与项目 7 中使用光敏电阻进行测量的结果进行比较。然后,您可以将 lux 值映射到相对值,并定义自己的查找表以供将来参考。请注意,这更多的是一个近似值,而不是精确的计算。

总结

本章向你展示了光电效应的原理,以及如何在光敏电阻的帮助下,利用它来测量光强的变化。为此,您应用了分压器电路布局。您还学习了如何在 Arduino 平台上将一个范围的值映射到另一个范围的值,并根据周围的光线强度更改了 Android 设备屏幕的亮度。作为一个小奖励,您看到了如何在 Android 设备的内置光传感器上请求当前的环境照度(以勒克斯为单位)。

七、温度感应

温度传感器广泛用于许多家用设备和工业机械中。它们的目的是测量附近的当前温度。它们通常用于预防目的,例如防止敏感部件过热,或者仅仅是监测温度的变化。

有几种非常常见的低成本元件可以用来测量环境温度。一种这样的元件叫做热敏电阻。它是一个可变的温度相关电阻,必须通过分压电路(见第六章)进行设置,以测量电路电压的变化。其他类型是小型集成电路(IC),如谷歌 ADK 演示盾上的 LM35,或通常可以直接连接到微控制器而无需特殊电路设置的传感器。

本章将向您展示如何使用热敏电阻,因为它是测量温度的最便宜、最普遍的元件。您将了解如何在元件数据手册和一些公式的帮助下计算温度。您将编写一个 Android 应用,通过使用定制的视图组件在设备屏幕上直接绘制形状和文本来可视化温度的变化。

项目 8:用热敏电阻感应温度

项目 8 将指导您完成构建温度传感器的过程。您将使用热敏电阻来计算与其电阻值相对应的温度。为此,您必须设置一个分压器电路,并将其连接到 ADK 板的模拟输入引脚。您将测量电压的变化,并应用一些公式来计算温度。您将了解如何借助器件数据手册和施泰因哈特-哈特方程计算温度。之后,您将把确定的值传输到 Android 设备。Android 应用将通过在屏幕上绘制温度计和文本值来可视化测量的温度。

零件

除了前面提到的热敏电阻之外,您还需要一个 10k 的电阻、ADK 板、试验板和一些电线。在本项目描述中,我将使用 4.7k 热敏电阻。4.7k 电阻值是 25 摄氏度时的电阻。选择哪个电阻值并不重要,但热敏电阻的系数是负还是正,以及数据手册提供的规格值都很重要(稍后将详细介绍)。本项目所需的零件如图 7-1 所示:

  • ADK 董事会
  • 面包板
  • 4.7kω热敏电阻
  • 10kω电阻
  • 一些电线

images

图 7-1。项目 8 部分(ADK 板、试验板、电线、4.7k 热敏电阻、10k 电阻)

热敏电阻

热敏电阻是一个可变电阻,其电阻值取决于环境温度。它的名字是由热敏电阻两个字组成。热敏电阻没有方向性,也就是说,它与普通电阻一样,与电路的连接方式无关。它们可以具有负的或正的系数,这意味着当它们具有负的系数时,它们对应于温度的电阻增加,而当它们具有正的系数时,电阻减小。与光敏电阻一样,它们也依赖于能带理论,详见第六章光敏电阻一节。温度变化对热敏电阻的电子有直接影响,促使它们进入导电带,导致电导率和电阻发生变化。热敏电阻有不同的形状,但最常见的是带引线的盘形热敏电阻,它类似于典型的陶瓷电容器。(参见图 7-2 。)

images

图 7-2。热敏电阻

选择热敏电阻时,最重要的事情是先看看它的数据手册。数据手册需要包含温度计算的一些重要细节。有些数据手册包含查找表,每个电阻值都映射到一个温度值。虽然您可以使用这样的表,但是将它转换到您的代码中是一项繁琐的任务。

更好的方法是用施泰因哈特-哈特方程计算当前温度。下面的摘要将向你展示计算温度的必要方程。不要害怕这里的数学。一旦你知道你要把哪些值放进方程,这就相当容易了。

施泰因哈特-哈特方程

施泰因哈特-哈特方程描述了一种模型,其中半导体的电阻取决于当前温度 T 。该公式如下所示:

images

为了应用这个公式,你需要三个系数——ab、c——此外,还有热敏电阻的当前电阻值 R 。如果您的热敏电阻数据手册包含这些值,您可以很好地使用它们,但大多数数据手册只提供所谓的 Bβ系数。幸运的是,对于特定的温度 T. ,施泰因哈特-哈特方程还有另一种表示,它与这个 B 参数和一对温度 ?? 和电阻 R0 一起工作

images

这个方程中不同的参数只是代表了 abc

a = (1 / ??) - (1 / B)×ln(R0)

b = 1 / B

c = 0

R0指定为 T 0 处的电阻,通常为 298.15 开尔文,等于 25 摄氏度。下面是 B 参数方程的简化公式:

r = r∞e〖??〗b/t〖??〗

R∞ 描述了趋于无穷大的电阻,可通过下式计算:

R∞ = R 0 × e -B/??

现在,您可以计算所有必要的值,您可以重新排列之前的公式,以最终计算温度。

images

这些等式将在稍后的 Arduino 草图中应用,因此您稍后会再次遇到它们。

设置

当热敏电阻的电阻变化时,你必须设置一个分压电路来测量电压的变化。分压器的组成取决于您使用的热敏电阻的类型。如果您使用负系数热敏电阻(NTC ),您的基本电路设置如图 7-3 所示。

images

图 7-3。 NTC 热敏电阻分压器

如果你使用正系数热敏电阻(PTC),你需要一个如图图 7-4 所示的电路。

images

图 7-4。 PTC 热敏电阻分压器

在这个项目中,温度上升时,模拟输入引脚上测得的电压会增加,温度下降时,测得的电压会降低。因此,请确保根据您使用的热敏电阻构建您的分压器电路,如上所示。图 7-5 显示了 NTC 热敏电阻的项目设置。

images

图 7-5。项目 8 设置

软件

这个项目的 Arduino 草图将使用 Arduino 平台的一些数学函数。您将使用自己编写的方法来表达公式,以计算当前温度。温度值将随后传输到 Android 设备。Android 应用将演示如何在 Android 设备的屏幕上绘制简单的形状和文本,以可视化测量的温度。

Arduino 草图

您将首次在 Arduino 草图中编写自己的自定义方法。自定义方法必须在强制设置和循环方法之外编写。它们可以有返回类型和输入参数。

此外,您将使用 Arduino 平台的一些数学函数。您将需要logexp函数来应用施泰因哈特-哈特方程计算温度。计算出的温度值需要进行比特移位,以便正确传输到 Android 设备。看一下完整的清单 7-1;我描述一下上市后的细节。

清单 7-1。项目 8: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_TEMPERATURE 0x4
#define INPUT_PIN_0 0x0
//-----
//change those values according to your thermistor's datasheet
long r0 = 4700;
long beta = 3980;
//-----

double ?? = 298.15;
long additional_resistor = 10000;
float v_in = 5.0;
double r_inf;
double currentThermistorResistance;

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte sntmsg[6];

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_TEMPERATURE;
sntmsg[1] = INPUT_PIN_0;
r_inf = r0 * (exp((-beta) / ??));
}

void loop() {
if (acc.isConnected()) {
int currentADCValue = analogRead(INPUT_PIN_0); float voltageMeasured = getCurrentVoltage(currentADCValue);
double currentThermistorResistance = getCurrentThermistorResistance(voltageMeasured);
double currentTemperatureInDegrees =
getCurrentTemperatureInDegrees(currentThermistorResistance);

// multiply the float value by 10 to retain one value behind the decimal point before
// converting to an integer for better value transmission
int convertedValue = currentTemperatureInDegrees * 10;

sntmsg[2] = (byte) (convertedValue >> 24);
sntmsg[3] = (byte) (convertedValue >> 16);
sntmsg[4] = (byte) (convertedValue >> 8);
sntmsg[5] = (byte) convertedValue;
acc.write(sntmsg, 6);
delay(100);
}
}

// "reverse ADC calculation"
float getCurrentVoltage(int currentADCValue) {
return v_in * currentADCValue / 1024;
}

// rearranged voltage divider formula for thermistor resistance calculation
double getCurrentThermistorResistance(float voltageMeasured) {
return ((v_in * additional_resistor) - (voltageMeasured * additional_resistor)) /
voltageMeasured;
}

//Steinhart-Hart B equation for temperature calculation
double getCurrentTemperatureInDegrees(double currentThermistorResistance) {
return (beta / log(currentThermistorResistance / r_inf)) - 273.15;
}`

让我们看看草图顶部定义的变量。您在这里看到的第一个变量是数据协议的定义。为了确认传输了温度数据,选择了字节常数COMMAND_TEMPERATURE 0x4。用于测量的模拟输入引脚被定义为INPUT_PIN_0 0x0

现在已经定义了特定于数据表的值:

long r0 = 4700; long beta = 3980;

我在这个项目中使用了一个 4.7kω的热敏电阻,这意味着热敏电阻在 25 摄氏度时的电阻( R0 )为 4.7kω。这就是为什么r0被定义为 4700。在我的例子中,热敏电阻的数据表只定义了 B 值,即 3980。查看热敏电阻数据表,必要时调整这些值。

接下来,您将看到用于计算目的的常量值的一些定义:

double ?? = 298.15; long additional_resistor = 10000; float v_in = 5.0;

你需要 25 摄氏度时的开尔文温度( ?? )来计算 R∞ 。此外,还需要分压器电路中的第二个电阻值(10k)和输入电压来计算热敏电阻的电流电阻。

计算当前温度时,施泰因哈特-哈特方程的 B 参数变量中需要最后两个变量。

现在让我们看看程序流中发生了什么。在设置方法中,您将计算 R∞ 的值,因为它只需要在开始时计算一次。

r_inf = r0 * (exp((-beta) / ??));

循环方法中的重复步骤可描述如下:

  1. 读取当前 ADC 值。
  2. 根据 ADC 值计算引脚上的实际电压。
  3. 计算当前热敏电阻的电阻。
  4. 计算当前温度。
  5. 将温度转换为整数以便于传输。
  6. 传输数据。

现在让我们来看看单个步骤的详细描述。

analogRead方法返回当前读取的 ADC 值。您将使用它来计算施加于模拟输入引脚的实际电压。为此,您可以使用自己编写的自定义方法:

float getCurrentVoltage(int currentADCValue) { return v_in * currentADCValue / 1024; }

getCurrentVoltage方法将currentADCValue作为输入参数,并将计算出的电压作为float返回。由于 Arduino 平台将 0V 到 5V 的电压范围映射为 1024 个值,所以你只需将currentADCValue乘以 5.0V,再除以 1024,就可以计算出当前的电压。

现在你已经有了测量的电压,你可以用自己编写的方法getCurrentThermistorResistance计算热敏电阻的实际电阻。

double getCurrentThermistorResistance(float voltageMeasured) { return ((v_in * additional_resistor) - (voltageMeasured * additional_resistor)) / voltageMeasured; }

getCurrentThermistorResistance方法将测得的电压作为输入参数,计算电阻,并将其作为double返回。

最后可以进行最重要的计算。你用自己写的方法getCurrentTemperatureInDegrees来计算温度。

double getCurrentTemperatureInDegrees(double currentThermistorResistance) { return (beta / log(currentThermistorResistance / r_inf)) - 273.15; }

该方法将当前热敏电阻的电阻作为输入参数。它使用施泰因哈特-哈特方程的 B 参数变量来计算当前温度,单位为开尔文。要把它转换成摄氏度,你必须减去 273.15。该方法以摄氏度为单位返回当前温度作为double。这里使用的 Arduino log函数是上面公式中使用的自然对数函数 ln。

在将数据传输到 Android 设备之前,剩下的最后一步是转换温度值,以便于传输。例如,您可能已经计算出 22.52 摄氏度的双精度值。因为您只传输字节,所以您必须将值转换成非浮点数。小数点后有一个数字的精度就足够了,因此转换就像将该值乘以 10 一样简单,从而得到 225。

int convertedValue = currentTemperatureInDegrees * 10;

在乘法过程中,小数点向右移动一位。由于乘法运算也会将值转换为非浮点数,因此小数点后的数字用于在删除前一个数字之前向上或向下舍入。因此,值 22.52 将变成 225,值 22.56 将变成 226。

现在您有了一个整数值,您需要再次使用移位技术将它转换成一个四字节数组。

sntmsg[2] = (byte) (convertedValue >> 24); sntmsg[3] = (byte) (convertedValue >> 16); sntmsg[4] = (byte) (convertedValue >> 8); sntmsg[5] = (byte) convertedValue; acc.write(sntmsg, 6);

Arduino 部分到此为止,让我们来看看 Android 应用。

Android 应用

正如您已经知道的,Android 应用的第一步是建立与 ADK 板的通信,读取传输的数据并将其转换回原来的整数值。完成后,你可以通过在设备屏幕上绘制 2D 图形来可视化当前温度。这个应用将向你展示如何使用一些 2D 图形类和方法在屏幕的画布上绘制简单的形状。清单 7-2 显示了当前项目活动的一个片段,重点是新的和重要的部分。

清单 7-2。项目八:ProjectEightActivity.java

`package project.eight.adk;

import …;

public class ProjectEightActivity extends Activity {

private static final byte COMMAND_TEMPERATURE = 0x4;
private static final byte TARGET_PIN = 0x0;

private TemperatureView temperatureView;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);

setContentView(R.layout.main);
temperatureView = (TemperatureView) findViewById(R.id.temperature_view);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

    }

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened"); } else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[6];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_TEMPERATURE:
if (buffer[1] == TARGET_PIN) {
final float temperatureValue = (((buffer[2] & 0xFF) << 24)
+ ((buffer[3] & 0xFF) << 16)
+ ((buffer[4] & 0xFF) << 8)
+ (buffer[5] & 0xFF))
/ 10;
runOnUiThread(new Runnable() {

@Override
public void run() {
temperatureView.setCurrentTemperature(temperatureValue);
}
});
}
break;

default: Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};
}`

首先看看变量的定义。前两个消息字节必须与 Arduino 草图中定义的字节相匹配,因此您可以按如下方式定义它们:

private static final byte COMMAND_TEMPERATURE = 0x4; private static final byte TARGET_PIN = 0x0;

然后你可以看到另一个类型为TemperatureView的变量。

private TemperatureView temperatureView;

TemperatureView顾名思义,是扩展安卓系统View类的自编自定义View。我们很快就会看到这个类,但是首先让我们继续 activity 类的剩余代码。

在读取接收到的消息后,您必须将字节数组转换回它原来的整数值。您只需反转 Arduino 部分中完成的位移来获得整数值。此外,您需要将整数除以 10,以获得您最初计算的浮点值。

`final float temperatureValue = (((buffer[2] & 0xFF) << 24)

  • ((buffer[3] & 0xFF) << 16)
  • ((buffer[4] & 0xFF) << 8)
  • (buffer[5] & 0xFF))
    / 10;`

收到的值 225 现在将被转换为 22.5。

最后要做的事情是将值传递给TemperatureView,这样您就可以在它的画布上绘制温度可视化。

`runOnUiThread(new Runnable() {

@Override
public void run() {
temperatureView.setCurrentTemperature(temperatureValue);
}
});`

请记住,您应该只在 UI 线程上更新 UI 元素。您必须在runOnUIThread方法中设置TemperatureView的温度值,因为它会在以后重新绘制时使自己失效。

2D 绘图是在TemperatureView类中实现的,所以先看看完整的清单 7-3 。

清单 7-3。项目八:TemperatureView.java

`package project.eight.adk;

import android.content.Context;
import android.content.res.TypedArray; import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

public class TemperatureView extends View {
private float currentTemperature;
private Paint textPaint = new Paint();
private Paint thermometerPaint = new Paint();
private RectF thermometerOval = new RectF();
private RectF thermometerRect = new RectF();

private int availableWidth;
private int availableHeight;

private final float deviceDensity;

private int ovalLeftBorder;
private int ovalTopBorder;
private int ovalRightBorder;
private int ovalBottomBorder;

private int rectLeftBorder;
private int rectTopBorder;
private int rectRightBorder;
private int rectBottomBorder;

public TemperatureView(Context context, AttributeSet attrs) {
super(context, attrs);
textPaint.setColor(Color.BLACK);
thermometerPaint.setColor(Color.RED);
deviceDensity = getResources().getDisplayMetrics().density;
TypedArray attributeArray = context.obtainStyledAttributes(attrs,
R.styleable.temperature_view_attributes);
int textSize = attributeArray.getInt(
R.styleable.temperature_view_attributes_textSize, 18);
textSize = (int) (textSize * deviceDensity + 0.5f);
textPaint.setTextSize(textSize);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
availableWidth = getMeasuredWidth();
availableHeight = getMeasuredHeight();

ovalLeftBorder = (availableWidth / 2) - (availableWidth / 10);
ovalTopBorder = availableHeight - (availableHeight / 10) - (availableWidth / 5);
ovalRightBorder = (availableWidth / 2) + (availableWidth / 10);
ovalBottomBorder = availableHeight - (availableHeight / 10); //setup oval with its position centered horizontally and at the bottom of the screen
thermometerOval.set(ovalLeftBorder, ovalTopBorder, ovalRightBorder, ovalBottomBorder);

rectLeftBorder = (availableWidth / 2) - (availableWidth / 15);
rectRightBorder = (availableWidth / 2) + (availableWidth / 15);
rectBottomBorder = ovalBottomBorder - ((ovalBottomBorder - ovalTopBorder) / 2);
}

public void setCurrentTemperature(float currentTemperature) {
this.currentTemperature = currentTemperature;
//only draw a thermometer in the range of -50 to 50 degrees celsius
float thermometerRectTop = currentTemperature + 50;
if(thermometerRectTop < 0) {
thermometerRectTop = 0;
} else if(thermometerRectTop > 100){
thermometerRectTop = 100;
}
rectTopBorder = (int) (rectBottomBorder - (thermometerRectTop *
(availableHeight / 140)));
//update rect borders
thermometerRect.set(rectLeftBorder, rectTopBorder, rectRightBorder, rectBottomBorder);
invalidate();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//draw shapes
canvas.drawOval(thermometerOval, thermometerPaint);
canvas.drawRect(thermometerRect, thermometerPaint);
//draw text in the upper left corner
canvas.drawText(getContext().getString(
R.string.temperature_value, currentTemperature),
availableWidth / 10, availableHeight / 10, textPaint);
}
}`

先看一下变量。如前所述,currentTemperature变量将由包含TemperatureView的活动设置。

private float currentTemperature;

接下来你可以看到两个Paint参考。一个Paint对象定义了颜色、大小、笔划宽度等等。当你在绘制图形或文本时,你可以为相应的方法调用提供一个Paint对象来优化绘制结果。您将使用两个 Paint 对象,一个用于文本可视化,另一个用于稍后将绘制的形状。

private Paint textPaint = new Paint(); private Paint thermometerPaint = new Paint();

RectF对象可以理解为用于定义形状边界的边界框。

private RectF thermometerOval = new RectF(); private RectF thermometerRect = new RectF();

你将画一个温度计,所以你将画两个不同的形状:一个椭圆形的底部和一个矩形的温度条(见图 7-6 )。

images

图 7-6。 2D 形状创建温度计(椭圆形+长方形=温度计)

接下来的两个变量将包含View的宽度和高度。它们被用来计算你将画温度计的位置。

private int availableWidth; private int availableHeight;

为了能够根据设备的屏幕属性调整文本大小,您必须确定您的屏幕密度(稍后会详细介绍)。

private final float deviceDensity;

您将绘制的描绘温度计的 2D 图形具有定义的边界。这些边界需要动态计算,以适应任何屏幕尺寸,因此您也可以将它们保存在全局变量中。

`private int ovalLeftBorder;
private int ovalTopBorder;
private int ovalRightBorder;
private int ovalBottomBorder;

private int rectLeftBorder;
private int rectTopBorder;
private int rectRightBorder;
private int rectBottomBorder;`

变量就是这样。现在我们将看看方法的实现,从TemperatureView的构造函数开始。

public TemperatureView(Context context, AttributeSet attrs) { super(context, attrs); textPaint.setColor(Color.BLACK); thermometerPaint.setColor(Color.RED); deviceDensity = getResources().getDisplayMetrics().density; TypedArray attributeArray = context.obtainStyledAttributes(attrs, R.styleable.temperature_view_attributes); int textSize = attributeArray.getInt( R.styleable.temperature_view_attributes_textSize, 18); textSize = (int) (textSize * deviceDensity + 0.5f); textPaint.setTextSize(textSize); }

如果您想将自定义的View嵌入到一个 XML 布局文件中,您需要实现一个构造函数,它不仅接受一个Context对象,还接受一个AttributeSet。一旦View充气,这些将由系统设置。AttributeSet包含您可以进行的 XML 定义,比如宽度和高度,甚至自定义属性。您还需要调用父View的构造函数来正确设置属性。该构造函数也用于设置Paint对象。这只需要一次,所以你可以在这里设置颜色和文本大小。

当定义文本大小时,你必须考虑到设备有不同的屏幕属性。它们可以有从小到特大的不同尺寸,每种尺寸也可以有不同的密度,从低密度到超高密度。尺寸描述了以屏幕对角线测量的实际物理尺寸。密度描述了定义的物理区域中的像素数量,通常表示为每英寸点数(dpi)。如果您要为文本定义一个固定的像素大小,它将在具有相同大小的设备之间显示非常不同。在低密度的设备上,它可以呈现得非常大,而相同大小的其他设备会呈现得非常小,因为它们具有更高的密度。

images 注意要了解更多关于屏幕尺寸和密度的信息,请访问位于[developer.android.com/guide/practices/screens_support.html](http://developer.android.com/guide/practices/screens_support.html)的 Android 开发者指南。

要解决这个问题,你需要做几件事。首先,您必须确定设备的密度,以计算您需要设置的实际像素大小,以便文本在不同设备上看起来一致。

deviceDensity = getResources().getDisplayMetrics().density;

现在你有了密度,你只需要文本的相对大小来计算实际的像素大小。当编写自己的View元素时,您也可以为该视图定义自定义属性。在这个例子中,您将为TemperatureView定义属性textSize。为了做到这一点,您必须创建一个新文件,定义所有的定制属性TemperatureView可以有。在res/values中创建一个名为attributes.xml的 XML 文件。文件的名字没有限制,你可以选择随便叫;只要确保它以.xml结尾。在这个 XML 文件中,你必须定义如清单 7-4 所示的属性。

清单 7-4。项目 8: attributes.xml

<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="temperature_view_attributes"> <attr name="textSize" format="integer"/> </declare-styleable> </resources>

接下来,您需要将TemperatureView添加到布局中,并设置它的textSize属性。如果您在 XML 布局文件中使用您自己的定制视图,您必须用它们的完全限定类名来定义它们,即它们的包名加上它们的类名。这个项目的main.xml布局文件看起来像清单 7-5 中的。

清单 7-5。项目 8: main.xml

<?xml version="1.0" encoding="utf-8"?> <project.eight.adk.TemperatureView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:temperatureview="http://schemas.android.com/apk/res/project.eight.adk" android:id="@+id/custom_view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FFFFFF" temperatureview:textSize=”18”> </project.eight.adk.TemperatureView

因为您不仅添加了系统属性,还添加了自己的属性,所以除了标准的系统名称空间之外,您还必须定义自己的名称空间。

xmlns:temperatureview="http://schemas.android.com/apk/res/project.eight.adk"

要定义您自己的名称空间,您需要指定一个名称,就像这里用temperatureview所做的那样,并添加模式位置。您之前添加的定制属性的模式位置是[schemas.android.com/apk/res/project.eight.adk](http://schemas.android.com/apk/res/project.eight.adk)。模式位置的最后一个重要部分反映了您的包结构。一旦添加了模式,就可以通过添加名称空间名称作为前缀来定义定制属性textSize

temperatureview:textSize=”18”>

现在您已经成功配置了自定义属性textSize。让我们看看如何在初始化TemperatureView时访问它的值。

TypedArray attributeArray = context.obtainStyledAttributes(attrs, R.styleable.temperature_view_attributes); int textSize = attributeArray.getInt(R.styleable.temperature_view_attributes_textSize, 18);

首先,您必须获得对一个TypedArray对象的引用,该对象包含给定的可样式化属性集的所有属性。为此,您调用当前上下文对象上的obtainStyledAttributes方法。这个方法有两个参数,当前视图的AttributeSet和您感兴趣的 styleable 属性集。在返回的TypedArray中你会找到你的textSize属性。要访问它,您可以在TypedArray上调用类型特定的 getter 方法,并提供您感兴趣的属性名称,如果找不到该属性,还可以提供一个默认值。

最后,您有了定义好的文本大小,可以用来计算设备密度所需的实际像素大小。

textSize = (int) (textSize * deviceDensity + 0.5f);

对于TemperatureView的构造函数就是这样。接下来是onMeasure方法。

`@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
availableWidth = getMeasuredWidth();
availableHeight = getMeasuredHeight();

ovalLeftBorder = (availableWidth / 2) - (availableWidth / 10);
ovalTopBorder = availableHeight - (availableHeight / 10) - (availableWidth / 5);
ovalRightBorder = (availableWidth / 2) + (availableWidth / 10);
ovalBottomBorder = availableHeight - (availableHeight / 10);
//setup oval with its position centered horizontally and at the bottom of the screen
thermometerOval.set(ovalLeftBorder, ovalTopBorder, ovalRightBorder, ovalBottomBorder);

rectLeftBorder = (availableWidth / 2) - (availableWidth / 15);
rectRightBorder = (availableWidth / 2) + (availableWidth / 15);
rectBottomBorder = ovalBottomBorder - ((ovalBottomBorder - ovalTopBorder) / 2);
}`

onMeasure方法继承自View系统类。系统调用它来计算显示View所需的尺寸。它在这里被覆盖以获得View的当前宽度和高度,以便以后可以以适当的比例绘制形状。注意,同样调用父类的onMeasure方法是很重要的,否则系统将抛出一个IllegalStateException。一旦你有了宽度和高度,你就可以定义椭圆形的边界框了,因为它以后不会改变。您还可以计算矩形四个边框中的三个。唯一依赖于当前温度的边界是顶部边界,因此将在以后进行计算。边界的计算定义了在每个设备上看起来成比例的形状。

为了更新测量的温度值以便可视化,您编写了一个名为setCurrentTemperature的 setter 方法,它将当前温度作为一个参数。setCurrentTemperature方法不仅仅是一个简单的变量设置器。它还用于更新温度计栏矩形的边界框,并使视图无效,以便重新绘制视图。

public void setCurrentTemperature(float currentTemperature) { this.currentTemperature = currentTemperature; //only draw a thermometer in the range of -50 to 50 degrees celsius float thermometerRectTop = currentTemperature + 50; if(thermometerRectTop < 0) { thermometerRectTop = 0; } else if(thermometerRectTop > 100){ thermometerRectTop = 100; } rectTopBorder = (int) (rectBottomBorder - (thermometerRectTop * (availableHeight / 140))); //update rect borders thermometerRect.set(rectLeftBorder, rectTopBorder, rectRightBorder, rectBottomBorder); invalidate(); }

更新矩形的边界后,你需要使TemperatureView无效。从TemperatureView的超类View继承而来的invalidate方法告诉系统这个特定的视图元素是无效的,需要重新绘制。

最后一种方法是负责 2D 图形绘制的实际方法。每次需要更新时,在一个View上调用onDraw方法。你可以像在setCurrentTemperature方法中一样,通过调用invalidate方法告诉系统它需要重画。让我们来看看它的实现。

`@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//draw shapes
canvas.drawOval(thermometerOval, thermometerPaint);
canvas.drawRect(thermometerRect, thermometerPaint);

//draw text in the upper left corner
canvas.drawText(getContext().getString(
R.string.temperature_value, currentTemperature),
availableWidth / 10, availableHeight / 10, textPaint);
}`

当系统调用onDraw方法时,它提供一个与View关联的Canvas对象。Canvas对象用于在其表面上绘图。draw 方法调用的顺序很重要。你可以把它想象成现实生活中的画布,你可以在上面一层一层地画。在这里你可以看到,首先一个椭圆形及其预定义的RectFPaint对象被绘制。接下来,绘制象征温度计条的矩形。最后通过定义要绘制的文本来绘制文本可视化,其坐标和原点是左上角及其关联的Paint对象。编码部分到此为止。

算了这么多,终于到了看你自建温度计是否管用的时候了。部署您的应用,如果您用指尖加热热敏电阻,应该会看到温度升高。最终结果应该看起来像图 7-7 。

images

图 7-7。项目 8:最终结果

总结

在这一章中,你学会了如何制作自己的温度计。您还学习了热敏电阻的基本知识,以及如何借助施泰因哈特-哈特方程计算环境温度。出于可视化的目的,您编写了自己的自定义 UI 元素。您还使用 2D 图形绘制了一个虚拟温度计以及当前测量温度的文本表示。

八、触觉

触摸用户界面已经越来越成为我们日常生活的一部分。我们在自动售货机、家用电器、手机和电脑上看到它们。触摸界面让日常活动看起来更有未来感和时尚感。当你在看老科幻电影时,你会注意到,即使在那时,触摸也是想象未来用户输入的首选方式。如今,孩子们是在这种技术的陪伴下成长的。

有许多不同类型的触摸界面技术,每种技术都有其优点和缺点。三种最普遍的技术是电阻触摸感应、电容触摸感应和红外(IR)触摸感应。

电阻式触摸感应主要由两个电阻层系统实现,当按压时,它们在某一点相互接触。一层负责检测 x 轴上的位置,另一层负责 y 轴上的位置。电阻式触摸屏已经存在了相当长的一段时间,在早期的商务智能手机中达到了顶峰。然而,基于电阻式触摸原理的新型智能手机仍在不断问世。电阻式触摸的优势在于,你不仅可以用手指作为输入工具,还可以用任何物体作为输入工具。缺点是,你必须对触摸界面的表面施加压力,随着时间的推移,这可能会损坏系统或磨损系统。

电容式触摸是另一种触摸感应方式,被更新的智能手机等现代设备所采用。其原理依赖于人体的电容特性。电容式触摸表面形成电场,该电场在被触摸时被人体扭曲,并被测量为电容的变化。电容式触摸系统的优势在于,您不必触摸表面就能感受到触摸。当系统没有足够高的绝缘时,当您靠近传感器时,可能会影响传感器。然而,触摸屏有玻璃绝缘,需要你直接触摸。电容式触摸系统不需要力量来感知输入。缺点是不是每个物体都可以与电容式触摸系统交互。你可能已经注意到,当你戴上普通的冬季手套时,你无法控制你的智能手机。那是因为你没有导电性,手指上包裹了太多绝缘材料。除了手指之外,您只能使用特殊的触笔或等效物来控制电容式触摸系统。

你可能听说过的最后一个系统是红外(IR)触摸系统。这种触摸系统主要用于户外亭或大型多点触摸桌,你可能听说过。红外系统的工作原理是由红外发光二极管发出的红外光投射到屏幕或玻璃表面的边缘。红外光束在屏幕内以一定的模式反射,当物体放在屏幕表面时,这种模式会被破坏。IR LEDs 被定位成覆盖 x 轴和 y 轴,以便可以确定放置在屏幕上的对象的正确位置。红外系统的优势在于,每个物体都可以用来与系统进行交互,因为该系统对电导率或电容等属性没有要求。一个缺点是,便宜的系统会受到阳光直射的影响,阳光中含有红外光谱。然而,大多数工业或消费系统都有适当的滤波机制来避免这些干扰。

项目 9: DIY 电容式触摸游戏展示蜂鸣器

这个项目将释放你自己动手(DIY)的精神,使你能够建立自己的定制电容式触摸传感器。电容式触摸传感器是迄今为止最容易和最便宜的为自己制作的,这就是为什么你将在本章的项目中使用它。您将使用铝箔制作一个自定义传感器,它将成为项目电路的一部分。您将使用 ADK 板的一个数字输入引脚来感知用户何时触摸传感器或影响其电场。触摸信息将被传播到一个 Android 应用,通过振动和播放一个简单的声音文件,让你的 Android 设备成为一个游戏节目蜂鸣器。

零件

如您所知,您不会在本章的项目中使用预建的传感器。这一次,您将使用任何家庭中都能找到的零件来制作自己的传感器。为了制作一个可以连接到电路的电容式触摸传感器,你需要胶带、铝箔和一根电线。以下是该项目的完整零件清单(如图图 8-1 ):

  • ADK 董事会
  • 试验板
  • 铝箔
  • 胶带
  • 10kω电阻
  • 一些电线

images

图 8-1。项目 9 件(ADK 板、试验板、电线、铝箔、胶带、10k 电阻)

铝箔

铝箔是铝压制成的薄片(图 8-2 )。家用床单通常具有大约 0.2 毫米的厚度。在一些地区,它仍然被错误地称为锡箔,因为锡箔是铝箔的前身。铝箔具有导电的特性,因此可以用作电路的一部分。你也可以在本章的项目中使用一根简单的线,但是箔片提供了一个更大的触摸目标区域,并且可以按照你喜欢的方式形成。不过,如果想将铝箔集成到电路中,它有一个缺点。用普通的焊锡将电线焊接到箔片上是不可能的。铝箔有一层薄的氧化层,防止焊锡与铝形成化合物。然而,有一些方法可以在焊接时减缓铝的氧化,迫使化合物与焊锡结合。这些方法是繁琐的,大多数时候你会得到一张损坏的铝箔,所以你不会在这个项目中这样做。相反,你会建立一个松散的铝箔连接。

images

图 8-2。铝箔

胶带

如前所述,您需要在连接到项目电路的电线和一片铝箔之间建立松散连接。为了将电线紧紧地固定在铝箔上,您将使用胶带(图 8-3 )。你可以使用任何类型的胶带,比如管道胶带,所以只要用你喜欢的胶带就可以了。

images

图 8-3。胶带

设置

你要做的第一件事是构建电容式触摸传感器。你先把一小块铝箔切成一定的形状。保持它相当小,以获得最佳结果;手掌大小的四分之一应该足够了。(参见图 8-4 。)

images

图 8-4。电线、铝箔片、胶带

接下来,在箔片上放一根电线,并用一条胶带粘上。你要确保电线接触到金属箔并且牢牢地固定在上面。这种方法的一种替代方法是使用鳄鱼夹连接到可以夹在箔片上的金属丝上。如果您的连接有问题,您可以使用这些作为替代。但是你可能想先用胶带试试。(参见图 8-5 。)

images

图 8-5。电容式触摸传感器

现在你的传感器已经准备好了,你只需要把它连接到电路上。电路设置非常简单。您只需通过一个高阻值电阻将一个配置为输出的数字引脚连接到一个数字输入引脚。使用大约 10k 的电阻值。由于电路中没有消耗大量电流的实际用电设备,所以需要电阻,这样只有非常小的电流流过电路。触摸传感器就像一根分叉的电线一样连接到电路上。你可以在图 8-6 中看到完整的设置。

images

图 8-6。项目 9 设置

那么这个电容传感器实际上是如何工作的呢?如您所见,您通过一个电阻将一个输出引脚连接到一个输入引脚,以读取输出引脚的当前状态。当输出引脚向电路供电时,输入引脚需要一段时间才能达到相同的电平。这是因为输入引脚具有电容特性,这意味着它在一定程度上充放电。触摸传感器是电路的一部分,当通电时,它会在其周围产生一个电场。当用户现在触摸传感器或者甚至非常接近传感器时,人体的水分子会干扰电场。这种干扰会导致输入引脚的电容发生变化,输入引脚需要更长时间才能达到与输出引脚相同的电压水平。我们将利用这个时间差来确定触摸是否发生。

软件

该项目的软件部分将向您展示如何使用CapSense Arduino 库,通过您新构建的电容式触摸传感器来感知触摸。当达到某个阈值时,您将识别触摸事件,并将该信息传播到 Android 设备。如果发生触摸,运行的 Android 应用将播放蜂鸣器声音并振动,就像游戏节目蜂鸣器一样。

Arduino 草图

幸运的是,您不必自己实现容性检测逻辑。有一个额外的 Arduino 库,名为CapSense,它可以为您完成这项工作。CapSense由 Paul Badger 撰写,旨在鼓励使用 DIY 电容式触摸界面。你可以在[www.arduino.cc/playground/Main/CapSense](http://www.arduino.cc/playground/main/capsense)下载它,并将其复制到 Arduino IDE 安装的 libraries 文件夹中。

CapSense的作用是通过反复改变相连输出引脚的状态来监控数字输入引脚的状态变化行为。原理如下。数字输出引脚被设置为数字状态HIGH,,这意味着它向电路施加 5V 电压。连接的数字输入引脚需要一定的时间才能达到相同的状态。经过一段时间后,测量输入引脚是否已经达到与输出引脚相同的状态。如果如预期的那样,则没有发生触摸。之后,再次将输出引脚设置为LOW (0V),并重复该过程。如果用户现在触摸附着的铝箔,电场会变形,导致电容值增加。这减缓了输入引脚上的电压积累过程。如果输出引脚现在再次将其状态变为HIGH,则输入引脚需要更长时间才能变为相同状态。现在对状态变化进行测量,输入引脚没有达到预期的新状态。这是发生触摸的指示器。

先看看完整的清单 8-1 。我将在清单之后更详细地介绍CapSense库。

清单 8-1。项目 9: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#include <CapSense.h>

#define COMMAND_TOUCH_SENSOR 0x6
#define SENSOR_ID 0x0;
#define THRESHOLD 50

CapSense touchSensor = CapSense(4,6);

AndroidAccessory acc("Manufacturer",
"Model",
"Description",
"Version",
"URI",
"Serial");

byte sntmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
//disables auto calibration
touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);
sntmsg[0] = COMMAND_TOUCH_SENSOR;
sntmsg[1] = SENSOR_ID;
} void loop() {
if (acc.isConnected()) {
//takes 30 measurements to reduce false readings and disturbances
long value = touchSensor.capSense(30);
if(value > THRESHOLD) {
sntmsg[2] = 0x1;
}
else {
sntmsg[2] = 0x0;
}
acc.write(sntmsg, 3);
delay(100);
}
}`

除了附件通信所需的库之外,您还需要包含带有以下指令的CapSense库:

#include <CapSense.h>

由于电容式触摸按钮是一种特殊的按钮,我对数据消息使用了新的命令字节0x6。您可以使用与常规按钮相同的命令字节,即0x1,但是您必须在代码中进一步区分这两种类型。这里定义的第二个字节是触摸传感器的 id:

`#define COMMAND_TOUCH_SENSOR 0x6

define SENSOR_ID 0x0;`

另一个常量定义是阈值。需要指定触摸发生的时间。由于电干扰会使测量值失真,您需要为测量值定义一个阈值或变化量,以确定什么是触摸,什么是简单的电噪声。

#define THRESHOLD 50

对于这个项目,我选择了值 50,因为对于这个电路设置,CapSense测量返回的值范围相当小。如果您使用一些Serial.println()方法调用来监控测量值,以查看触摸电容式传感器时该值如何变化,会有所帮助。如果您在设置中使用另一个电阻,或者您发现 50 不是您的设置的最佳阈值,那么您可以简单地调整THRESHOLD值。

你在草图中看到的下一个东西是CapSense对象的定义。CapSense类的构造函数将两个整数值作为输入参数。第一个定义了数字输出引脚,它在数字状态HIGHLOW之间交替。第二个参数定义数字输入引脚,该引脚被拉至与输出引脚相同的电流状态。

CapSense touchSensor = CapSense(4,6);

看完变量定义之后,让我们来看看设置方法。除了通常的初始化步骤,您现在已经知道了,还有对CapSense对象的第一个方法调用。

touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);

该方法关闭了感测程序的自动校准,否则在测量期间可能会发生自动校准。

循环法实现触摸检测。首先,调用capSense方法,其中必须提供样本数量的参数。30 个样本的值似乎足够了。该方法以任意单位返回值。如果返回的检测值超过您之前定义的阈值,则检测到触摸,并在返回消息中设置相应的字节。

long value = touchSensor.capSense(30); if(value > THRESHOLD) { sntmsg[2] = 0x1; } else { sntmsg[2] = 0x0; }

最后要做的是将当前数据消息发送到连接的 Android 设备。

Android 应用

Android 应用使用了一些你已经知道的功能,比如使用振动器服务。这个应用的一个新功能是音频播放。当接收到触摸传感器数据消息时,代码评估数据以确定触摸按钮是否被按下。如果它被按下,背景颜色会变成红色,一个TextView会显示哪个触摸按钮被按下,以防您添加其他按钮。与此同时,该设备的振动器打开,并播放蜂鸣器声音,以获得最终游戏节目蜂鸣器般的感觉。清单 8-2 中的这个项目的 Android 代码显示了应用逻辑。

清单 8-2。项目 9:ProjectNineActivity.java

`package project.nine.adk;

import …;

public class ProjectNineActivity extends Activity {

private static final byte COMMAND_TOUCH_SENSOR = 0x6;
private static final byte SENSOR_ID = 0x0;

private LinearLayout linearLayout;
private TextView buzzerIdentifierTextView;

private Vibrator vibrator;
private boolean isVibrating;

private SoundPool soundPool;
private boolean isSoundPlaying;
private int soundId;

private float streamVolumeMax;

/** Called when the activity is first created. */
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
buzzerIdentifierTextView = (TextView) findViewById(R.id.buzzer_identifier);

vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));

soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
soundId = soundPool.load(this, R.raw.buzzer, 1);

AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
streamVolumeMax = mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

    }

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
stopVibrate();
stopSound();
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    releaseSoundPool();
    }

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
}
};

private void openAccessory(UsbAccessory accessory) {
mFileDescriptor = mUsbManager.openAccessory(accessory);
if (mFileDescriptor != null) {
mAccessory = accessory;
FileDescriptor fd = mFileDescriptor.getFileDescriptor();
mInputStream = new FileInputStream(fd);
mOutputStream = new FileOutputStream(fd);
Thread thread = new Thread(null, commRunnable, TAG);
thread.start();
Log.d(TAG, "accessory opened");
} else {
Log.d(TAG, "accessory open fail");
}
}

private void closeAccessory() {
try {
if (mFileDescriptor != null) {
mFileDescriptor.close();
}
} catch (IOException e) {
} finally {
mFileDescriptor = null;
mAccessory = null;
}
}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[3];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_TOUCH_SENSOR:

if (buffer[1] == SENSOR_ID) {
final byte buzzerId = buffer[1]; final boolean buzzerIsPressed = buffer[2] == 0x1;
runOnUiThread(new Runnable() {

@Override
public void run() {
if(buzzerIsPressed) {
linearLayout.setBackgroundColor(Color.RED);
buzzerIdentifierTextView.setText(getString(
R.string.touch_button_identifier, buzzerId));
startVibrate();
playSound();
} else {
linearLayout.setBackgroundColor(Color.WHITE);
buzzerIdentifierTextView.setText("");
stopVibrate();
stopSound();
}
}
});
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};

private void startVibrate() {
if(vibrator != null && !isVibrating) {
isVibrating = true;
vibrator.vibrate(new long[]{0, 1000, 250}, 0);
}
}

private void stopVibrate() {
if(vibrator != null && isVibrating) {
isVibrating = false;
vibrator.cancel();
}
}

private void playSound() {
if(!isSoundPlaying) {
soundPool.play(soundId, streamVolumeMax, streamVolumeMax, 1, 0, 1.0F);
isSoundPlaying = true;
}
} private void stopSound() {
if(isSoundPlaying) {
soundPool.stop(soundId);
isSoundPlaying = false;
}
}

private void releaseSoundPool() {
if(soundPool != null) {
stopSound();
soundPool.release();
soundPool = null;
}
}
}`

首先,像往常一样,让我们看看变量的定义。

`private static final byte COMMAND_TOUCH_SENSOR = 0x6;
private static final byte SENSOR_ID = 0x0;

private LinearLayout linearLayout;
private TextView buzzerIdentifierTextView;

private Vibrator vibrator;
private boolean isVibrating;

private SoundPool soundPool;
private boolean isSoundPlaying;
private int soundId;

private float streamVolumeMax;`

数据消息字节与 Arduino 草图中的相同。LinearLayout是容器视图,它稍后会填充整个屏幕。它用于通过将背景颜色更改为红色来指示触摸按钮被按下。TextView显示当前按钮的标识符。接下来的两个变量负责保存对 Android 系统的Vibrator服务的引用,并负责确定振动器当前是否在振动。最后一个变量负责媒体回放。Android 有几种媒体播放的可能性。一种简单的低延迟播放短声音片段的方法是使用SoundPool类,它甚至能够一次播放多个流。SoundPool对象在初始化后负责加载和播放声音。在本例中,您将需要一个布尔标志,即isSoundPlaying标志,这样,如果蜂鸣器已经在播放,您就不会再次触发它。一旦声音文件被加载,soundId将保存一个对声音文件的引用。最后一个变量用于设置稍后播放声音时的音量。

接下来是进行必要初始化的onCreate方法。除了视图初始化之外,您可以看到这里分配了一个对系统振动器服务的引用。之后,初始化SoundPoolSoundPool类的构造函数有三个参数。第一个是SoundPool可以同时播放的流的数量。第二个定义了哪种流将与SoundPool相关联。您可以为音乐、系统声音、通知等分配流。最后一个参数指定源质量,目前没有影响。文档说明您现在应该使用 0 作为缺省值。一旦它被初始化,你必须将声音载入SoundPool。为此,您必须调用load方法,它的参数是一个Context对象、要加载的声音的资源 id 和一个优先级 id。load方法返回一个引用 id,稍后您将使用它来回放预加载的声音。将您的声音文件放在res/raw/buzzer.mp3下的res文件夹中。

images 注意您可以在 Android 系统上使用多种音频文件编码类型。完整的列表可以在[developer.android.com/guide/appendix/media-formats.html](http://developer.android.com/guide/appendix/media-formats.html)的开发者页面中找到。

这里要做的最后一件事是确定您所使用的流类型的最大可能音量。稍后,当您播放声音时,您可以定义音量级别。因为你想要一个相当响的蜂鸣器,所以最好把音量调到最大。

`setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
buzzerIdentifierTextView = (TextView) findViewById(R.id.buzzer_identifier);

vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));

soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
soundId = soundPool.load(this, R.raw.buzzer, 1);

AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
streamVolumeMax = mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);`

和往常一样,在接收消息时,分配给接收工作线程的Runnable对象实现评估逻辑,并最终触发类似蜂鸣器的行为。

`switch (buffer[0]) {
case COMMAND_TOUCH_SENSOR:
if (buffer[1] == SENSOR_ID) {
final byte buzzerId = buffer[1];
final boolean buzzerIsPressed = buffer[2] == 0x1;
runOnUiThread(new Runnable() {

@Override
public void run() {
if(buzzerIsPressed) {
linearLayout.setBackgroundColor(Color.RED);
buzzerIdentifierTextView.setText(getString(
R.string.touch_button_identifier, buzzerId));
startVibrate();
playSound();
} else {
linearLayout.setBackgroundColor(Color.WHITE);
buzzerIdentifierTextView.setText("");
stopVibrate();
stopSound();
}
} });
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}`

您可以看到LinearLayout的背景颜色根据按钮的状态而改变,并且TextView也相应地更新。startVibratestopVibrate方法在第四章的项目 3 中已经熟悉。

`private void startVibrate() {
if(vibrator != null && !isVibrating) {
isVibrating = true;
vibrator.vibrate(new long[]{0, 1000, 250}, 0);
}
}

private void stopVibrate() {
if(vibrator != null && isVibrating) {
isVibrating = false;
vibrator.cancel();
}
}`

startVibratestopVibrate方法只是在开始振动或取消当前振动之前检查振动器是否已经在振动。

根据触摸按钮的状态,开始或停止蜂鸣声播放。这里可以看到方法的实现:

`private void playSound() {
if(!isSoundPlaying) {
soundPool.play(soundId, streamVolumeMax, streamVolumeMax, 1, 0, 1.0F);
isSoundPlaying = true;
}
}

private void stopSound() {
if(isSoundPlaying) {
soundPool.stop(soundId);
isSoundPlaying = false;
}
}`

要播放声音,你必须调用SoundPool对象上的play方法。它的参数是soundId,这是您之前在加载声音文件时检索到的,左右声道的音量定义,声音优先级,循环模式,以及当前声音的回放速率。要停止声音,你只需调用SoundPool对象上的stop方法,并提供相应的soundId。您不需要在您的AndroidManifest.xml中为SoundPool的工作定义任何额外的权限。

当应用关闭时,你也应该清理一下。要释放SoundPool分配的资源,只需调用release方法。

private void releaseSoundPool() { if(soundPool != null) { stopSound(); soundPool.release(); soundPool = null; } }

由于屏幕布局与上一个项目有所不同,你应该看看这个项目的main.xml布局文件,如清单 8-3 所示。

清单 8-3。项目 9: main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> <TextView android:id="@+id/buzzer_identifier" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000"/> </LinearLayout>

您可以看到该布局只定义了一个嵌入到LinearLayout容器中的TextView

这就是本章项目编码的全部内容。如果你愿意,你可以用额外的蜂鸣器来扩展这个项目,这样你就可以在和你的朋友和家人玩游戏时使用你自己定制的蜂鸣器。部署您的应用,并对项目进行测试。你的最终结果应该看起来像图 8-7 。

images

图 8-7。项目 9:最终结果

额外的实际例子:ADK 纸钢琴

您已经看到,构建一个简单的 DIY 电容式触摸传感器既不困难也不昂贵。你可以很容易地想象,这种技术在爱好社区中被大量使用来构建具有漂亮和酷的交互用户界面的项目。为了让你自己的创意源源不断,我想借此机会向你展示我的一个项目,这是我为 2011 年柏林谷歌开发者日做的,只是可以实现的一个例子。

2011 年 7 月,谷歌宣布公开呼吁谷歌开发者日。谷歌开发者日是谷歌在美国以外最大的开发者大会。它在全球几个大城市举办。2011 年的比赛地点是阿根廷、澳大利亚、巴西、捷克共和国、德国、以色列、日本和俄罗斯。这次公开征集让开发者有机会向大约 2000 名开发者展示他们的技能和项目。两个挑战是公开呼吁的一部分:HTML5 挑战和 ADK 挑战。参与者首先必须回答一些关于他们挑战的相应技术的基本问题。当他们成功回答这些问题时,他们就有资格参加第二轮挑战。现在,我不知道 HTML5 挑战赛的流程到底是如何运作的,但 ADK 挑战赛的第二轮要求拿出一个合理的项目计划。项目计划应该将 ADK 技术与 Android 设备结合起来,创造一些有趣的东西,如机器人、乐器,甚至是解决日常问题的设备。我的项目计划是用纸做一架带有电容式触摸键的钢琴,ADK 纸钢琴。当用户触摸一个键时,连接的 ADK 板应该会识别它,并在连接的 Android 设备的帮助下播放该键的相应音符。

我从一个只有四个电容式触摸键的小型原型开始。我想看看我用铝箔制成的电容式触摸传感器,当我用纸的顶层和底层绝缘时,它是否会有足够的响应。我使用了CapSense库来识别触摸时不同的键,并使用了SoundPool类来回放每个键对应的音符。设计示意图如图图 8-8 所示。

images

图 8-8。钢琴键构造示意图

原型运行得非常好,我的项目计划被认为是十大提交项目之一,所以我在谷歌开发者日的展览区获得了一席之地。谷歌提供了谷歌 ADK 董事会和演示盾牌,以实现该项目的活动。

在 2011 年 11 月 19 日之前,我有大约三个月的时间来完成这个项目。让我告诉你,三个月听起来像很多时间,但当你在全职工作的同时写一本书时,你往往没有多少时间留给这样的项目。尽管如此,我还是及时完成了这个项目,最后一切都很顺利。然而,建造过程本身就充满了挑战。

正如我对原型所做的那样,我决定用覆有纸的铝箔条来制作电容式触摸钢琴键。每个钥匙下面都有自己的铝箔条。我必须确保这些条带覆盖了钥匙的大部分区域,而不会碰到附近的其他条带。

images

图 8-9。钢琴键布局表

总共需要对 61 个按键进行切割并粘贴在纸键下方的繁琐工作(参见图 8-10 )。).

images

图 8-10。完成琴键布局

如果你还记得的话,谷歌 ADK 板是基于 Arduino Mega 设计的,只有 54 个数字引脚。光凭黑板,我无法识别目标的 61 个键。这就是为什么我建立了自己的扩展板,能够提供更多的输入。电路板上有所谓的 8 位输入移位寄存器。这些 IC 提供八个输入通道,只需使用 ADK 板的三个引脚即可读取。有了 8 个这样的 IC,我只用了大约一半的 ADK 数字引脚就能有 64 路输入。(参见图 8-11 。)

images

图 8-11。自定义输入移位寄存器板

由于我不能将输入连接直接焊接到铝箔条上,我使用鳄鱼夹来建立连接(图 8-12 )。

images

图 8-12。成品纸钢琴建造

构建阶段已经完成,我必须编写软件来识别触摸事件并播放相应的音符。我不能再使用CapSense库了,因为我现在使用 ADK 板的数字引脚来寻址输入移位寄存器。所以我实现了自己的类似于CapSense库的例程,它也依赖于输入到达某个状态的时间。对于 Android 部分,我还使用了SoundPool类,当相应的键被触摸时,播放预先载入的 MP3 音乐。当前纸币的波形也显示在设备的屏幕上。

在截止日期前完成项目(如图 8-13 所示)后,我不得不一路穿过柏林将钢琴运送到场地(图 8-14 )。ADK 项目的展览区受到了热烈的欢迎,ADK 纸钢琴给人留下了深刻的印象。它甚至被当地报纸的一篇报道选中。在活动中展示的 ADK 项目引起了社区的极大兴趣,并很好地概述了可以用 ADK 做些什么,并且希望激发一些人自己尝试一下。

images

图 8-13。柏林 2011 年谷歌开发者日的 ADK 纸钢琴

images

图 8-14。【2011 年柏林谷歌开发者日在柏林国际商会举行

images 你可以在[marioboehmer.blogspot.com/2011/11/adk-paper-piano-at-google-developer-day.html](http://marioboehmer.blogspot.com/2011/11/adk-paper-piano-at-google-developer-day.html)的我的博客里找到更多关于这个项目和活动的信息。

你也应该看看这个网站,它给出了在谷歌开发者日之旅的不同地点展示的所有 ADK 项目的概述。

总结

自从你开始写这本书以来,你第一次用家用物品制作了自己的传感器。您已经了解了构建自己的电容式触摸传感器有多简单。对于 Arduino 部分,您学习了如何使用 Arduino CapSense库来感测触摸事件。通过结合一些以前学到的 Android 功能,比如使用Vibrator服务和通过SoundPool类添加回放声音的能力,您创建了自己的游戏节目蜂鸣器。您还了解到在一个更大的项目中使用 DIY 电容式触摸传感器的个人实例。

九、让东西动起来

业余电子爱好最有趣的方面之一可能是制造机器人或让你的项目移动。根据使用情况,有许多方法可以实现一般的移动。推动项目的一种常见方式是通过马达。马达被称为致动器,因为它们作用于某物,而不是像传感器那样感知某物。不同种类的马达提供不同程度的运动自由度和动力。三种最常见的电机是 DC 电机、伺服电机和步进电机(见图 9-1 )。

DC 汽车是靠直流电(DC)运行的电动机,主要用于遥控车等玩具。它们提供轴的连续旋转,轴可以连接到齿轮以实现不同的动力传输。它们没有位置反馈,这意味着你不能确定电机转动了多少度。

例如,伺服系统通常用于机器人移动手臂或腿的关节。它们的旋转大多被限制在一定的度数范围内。大多数伺服系统不提供连续旋转,仅支持 180 度范围内的运动。然而,有特殊的伺服能够旋转 360 度,甚至有黑客的限制伺服压制他们的 180 度限制。伺服系统具有位置反馈,这使得通过发送特定信号将它们设置到某个位置成为可能。

步进电机主要用于扫描仪或打印机中的精密机器运动。它们提供带有精确位置反馈的全旋转。当齿轮或传送带连接到步进电机上时,它们可以移动到准确的位置。

由于步进电机项目不像 DC 电机项目和伺服项目那样受欢迎,所以在本章中我将只描述后者。不过,如果你想试试步进电机,你可以在 Arduino 网站的[arduino.cc/hu/Tutorial/StepperUnipolar](http://arduino.cc/hu/Tutorial/StepperUnipolar)[arduino.cc/en/Tutorial/MotorKnob](http://arduino.cc/en/Tutorial/MotorKnob)找到一些教程。

images

图 9-1。电机(DC 电机、伺服电机、步进电机)

项目 10:控制伺服系统

伺服系统非常适合控制有限的运动。为了控制一个伺服系统,你需要通过你的 ADK 板的数字引脚发送不同的波形给伺服系统。为了定义您的伺服系统应该向哪个方向移动,您将编写一个利用您设备的加速度传感器的 Android 应用。因此,当你沿着 x 轴的某个方向倾斜你的设备时,你的伺服系统也会反映相对运动。

设备的加速度计实际上做的是测量施加到设备上的加速度。加速度是相对于一组轴的速度变化率。重力影响测得的加速度。当 Android 设备放在桌子上时,不会测量到加速度。当沿设备的一个轴倾斜设备时,沿该轴的加速度发生变化。(参见图 9-2 )。

images

图 9-2。设备轴概述(谷歌公司的图像财产,在知识共享 2.5 下,developer . Android . com/reference/Android/hardware/sensor event . html)

零件

幸运的是,伺服系统不需要复杂的电路或额外的部件。它们只有一条数据线接收波形脉冲,以将伺服设置在正确的位置。所以除了一个伺服系统,你只需要你的 ADK 板和一些电线(如图 9-3 所示)。

  • 【ADK 板】
  • servo
  • Some wires

images

图 9-3。项目 10 部分(ADK 板、电线、伺服)

ADK 董事会

在之前的项目中,您必须生成波形,您将使用 ADK 板的一个数字引脚。ADK 板将向伺服系统发送电脉冲。这些脉冲的宽度负责将伺服设置到特定的位置。为了产生必要的波形,您可以通过在定义的时间周期内将数字引脚设置为交替的HIGH / LOW信号来自行实现逻辑,也可以使用 Arduino Servo库,稍后将详细介绍。

伺服

如前所述,伺服机构是一种马达,在很大程度上只能在预定范围内转动其轴。业余爱好伺服的范围通常是 180 度。当内部齿轮到达预定位置时,通过阻止内部齿轮机械地实现限制。你可以在网上找到黑客来摆脱这种阻碍,但你必须打开你的伺服来打破这种阻碍,然后做一些焊接。获得更多旋转自由度的另一种可能性是使用特殊的 360 度伺服系统。这些往往有点贵,这就是为什么黑客的低预算 180 度伺服似乎是一个很好的选择,对一些人来说。无论如何,在大多数情况下,你不会需要一个完整的旋转伺服大多数项目。在这些情况下,你最好使用 DC 电机或步进电机。

那么伺服系统是如何工作的呢?基本上,伺服系统只是一个电动机,它将其动力传递给一组齿轮,这些齿轮可以在预定义的角度范围内转动轴。旋转轴的范围受到机械限制。集成电路与电位计相结合,接收发送到伺服机构的波形脉冲,并确定伺服机构必须设置的角度。通常的业余爱好伺服系统用频率为 50Hz 的脉冲操作,这描述了大约 20 毫秒的信号周期。信号设置为HIGH的时间量指定了伺服角度。根据伺服行业标准,信号必须设置为HIGH1 ms,然后在剩余时间内设置为LOW,以将伺服移动到其最左边的位置。要将伺服移动到最右边的位置,必须将信号设置为HIGH2 ms,然后在剩余时间内设置为LOW。(参见图 9-4 。)注意,这些信号时间是缺省值,并且实际上它们可能因伺服机构而异,因此对于左侧位置,这些值可能更低,而对于右侧位置,这些值可能更高。在某些情况下,你甚至不需要坚持 20 毫秒的时间,所以即使这样也可以定义为更短。一般来说,您应该首先坚持使用默认值,只有在遇到问题时才更改这些值。

images

图 9-4。伺服控制信号波形(上:最左位置,下:最右位置)

对于不同的使用情况,伺服系统有许多不同的外形规格(图 9-5 )。在业余爱好电子产品中,你会发现模型飞机或机器人的小型伺服系统。这些伺服系统可以区分大小和速度,但它们通常很小,很容易安装。

images

图 9-5。不同的伺服外形尺寸

大多数伺服系统都有不同的驱动轴附件,所以你可以用它们作为机器人的关节或控制模型飞机或轮船的方向舵。(参见图 9-6 )。

images

图 9-6。伺服驱动轴附件

因为你不需要一个特殊的电路来控制一个伺服系统,如果你有一个能够产生所需波形的微控制器,连接一个伺服系统是非常简单的。一个伺服系统有三根电线与之相连。通常它们以某种方式着色。红线是 Vin,它连接到+5V。但是,您应该阅读数据手册,看看您的伺服系统是否有不同的输入电压额定值。黑线必须接地(GND)。最后一根线是数据线,通常为橙色、黄色或白色。这是连接微控制器数字输出引脚的地方。它用于将脉冲传输到伺服系统,伺服系统进而移动到所需的位置。

设置

由于你不必为这个项目建立一个特殊的电路,你可以直接连接你的伺服到你的 ADK 板。如前所述,将红线连接到+5V,黑线连接到 GND,橙线、黄线或白线连接到数字引脚 2。伺服系统通常带有母连接器,所以你要么直接将电线连接到连接器,要么在中间使用公对公连接器。(参见图 9-7 )。

images

图 9-7。项目 10 设置

软件

为了控制伺服系统,您将编写一个 Android 应用,请求更新其加速度计传感器在 x 轴上的当前倾斜度。当你向左或向右倾斜你的设备时,你将把产生的方向更新发送到 ADK 板。Arduino 草图接收新的倾斜值,并向伺服系统发送相应的定位脉冲。

Arduino 草图

Arduino 草图负责接收加速度计数据,并将伺服设置到正确的位置。你有两种可能做那件事。您可以实现自己的方法来创建想要发送到伺服系统的波形,也可以使用 Arduino IDE 附带的Servo库。就个人而言,我更喜欢使用库,因为它更精确,但是我将在这里向您展示这两种方法。您可以决定哪种解决方案更符合您的需求。

手动波形生成

第一种方法是自己实现波形生成。先看看完整的清单 9-1 。

清单 9-1。项目 10: Arduino 草图(自定义波形实现)

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_SERVO 0x7
#define SERVO_ID_1 0x1 #define SERVO_ID_1_PIN 2

int highSignalTime;
float microSecondsPerDegree;
// default boundaries, change them for your specific servo
int leftBoundaryInMicroSeconds = 1000;
int rightBoundaryInMicroSeconds = 2000;

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial");

byte rcvmsg[6];

void setup() {
Serial.begin(19200);
pinMode(SERVO_ID_1_PIN, OUTPUT);
acc.powerOn();
microSecondsPerDegree = (rightBoundaryInMicroSeconds – leftBoundaryInMicrosSeconds) / 180.0;
}

void loop() {
if (acc.isConnected()) {
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_SERVO) {
if(rcvmsg[1] == SERVO_ID_1) {
int posInDegrees = ((rcvmsg[2] & 0xFF) << 24)
+ ((rcvmsg[3] & 0xFF) << 16)
+ ((rcvmsg[4] & 0xFF) << 8)
+ (rcvmsg[5] & 0xFF);
posInDegrees = map(posInDegrees, -100, 100, 0, 180);
moveServo(SERVO_ID_1_PIN, posInDegrees);
}
}
}
}
}

void moveServo(int servoPulsePin, int pos){
// calculate time for high signal
highSignalTime = leftBoundaryInMicroSeconds + (pos * microSecondsPerDegree);
// set Servo to HIGH
digitalWrite(servoPulsePin, HIGH);
// wait for calculated amount of microseconds
delayMicroseconds(highSignalTime);
// set Servo to LOW
digitalWrite(servoPulsePin, LOW);
// delay to complete waveform
delayMicroseconds(20000 – highSignalTime);
}`

像往常一样,让我们从草图的变量开始。

`#define COMMAND_SERVO 0x7

define SERVO_ID_1 0x1

define SERVO_ID_1_PIN 2`

您可以看到为伺服控制消息定义了一个新的命令类型常量。第二个常量是伺服的 ID,如果你想在你的 ADK 板上安装多个伺服,应该控制这个 ID。第三个常量是连接伺服系统的 ADK 板上的相应引脚。接下来,您有一些变量来指定以后的波形。

int highSignalTime; float microSecondsPerDegree; int leftBoundaryInMicroSeconds = 1000; int rightBoundaryInMicroSeconds = 2000;

变量highSignalTime稍后被计算,它描述了信号被设置为HIGH的时间量(以微秒计)。正如你所记得的,将信号设置到HIGH大约 1ms 意味着将伺服向左转,将信号设置到HIGH大约 2ms 导致伺服向右转。microSecondsPerDegree变量用于转换目的,顾名思义,它描述了每度需要添加到HIGH信号时间中的微秒数。最后两个变量是以微秒为单位的伺服边界。如前所述,约 1ms 的HIGH信号应导致伺服系统向左完全移动,2ms 应导致向右完全移动。然而,在实践中,情况通常并非如此。如果你用这些默认值工作,你可能会看到你的伺服不会把它的全部潜力。您应该试验边界值来调整代码以适应您的伺服,因为每个伺服都是不同的。我甚至不得不将我的一个伺服系统的默认值改为从 600 到 2100 的边界,这意味着如果我施加一个HIGH信号 0.6 毫秒,伺服系统将完全移动到左边,如果我施加一个HIGH信号大约 2.1 毫秒,它将完全移动到右边。正如你所看到的,你可以使用默认值,但是如果你遇到问题,你应该尝试伺服系统的边界。

setup方法中,您必须将伺服信号引脚的pinMode设置为输出。

pinMode(SERVO_ID_1_PIN, OUTPUT);

您还应该在这里计算每度的微秒数,以便您可以在以后的定位计算中使用该值。

microSecondsPerDegree = (rightBoundaryInMicroSeconds - leftBoundaryInMicroSeconds) / 180.0;

计算很简单。由于您的伺服很可能只有 180 度的范围,您只需要将右边界值和左边界值之差除以 180。

接下来是loop方法。在从 Android 应用中读取接收到的数据消息后,您需要使用移位技术对传输的值进行解码。

`int posInDegrees = ((rcvmsg[2] & 0xFF) << 24)

  • ((rcvmsg[3] & 0xFF) << 16)
  • ((rcvmsg[4] & 0xFF) << 8)
  • (rcvmsg[5] & 0xFF);`

您将在后面编写的 Android 应用将从-100 到 100 的范围内为左侧位置和右侧位置传输值。因为您需要为伺服位置提供相应的度数,所以您需要首先使用 map 函数。

posInDegrees = map(posInDegrees, -100, 100, 0, 180);

位置值现在可以与相应伺服系统的信号引脚一起提供给自定义的moveServo方法。

moveServo(SERVO_ID_1_PIN, posInDegrees);

moveServo方法的实现描述了控制伺服所需波形的构造。

void moveServo(int servoPulsePin, int pos){ // calculate time for high signal highSignalTime = leftBoundaryInMicroSeconds + (pos * microSecondsPerDegree); // set Servo to HIGH digitalWrite(servoPulsePin, HIGH); // wait for calculated amount of microseconds delayMicroseconds(highSignalTime); // set Servo to LOW digitalWrite(servoPulsePin, LOW); // delay to complete waveform delayMicroseconds(20000 - highSignalTime); }

让我们通过一个例子来解决这个问题。假设你得到了一个 90 度的理想位置。要确定HIGH信号的时间,你可以用 90 乘以microSecondsPerDegree并加上左边界值。如果您使用默认边界值,则您的计算如下所示:

highSignalTime = 1000 + (90 * 5.55556);

这导致HIGH信号时间约为 1500 微秒,所以应该是伺服的中间位置。你现在要做的是将伺服系统的信号引脚设置为数字HIGH等待计算好的时间,然后再次将其设置为LOW。现在可以计算一个 20ms 信号周期的剩余延迟,以完成一个完整的脉冲。仅此而已。

用伺服库产生波形

如您所见,实现波形生成并不特别困难,但是如果您使用 Arduino IDE 附带的Servo库,它会变得更加容易。清单 9-2 显示了使用Servo库重写的草图。

清单 9-2。项目 10: Arduino 草图(使用伺服库)

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#include <Servo.h>

define COMMAND_SERVO 0x7

define SERVO_ID_1 0x1

define SERVO_ID_1_PIN 2

Servo servo;

AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");

byte rcvmsg[6];

void setup() {
Serial.begin(19200);
servo.attach(SERVO_ID_1_PIN);
acc.powerOn();
}

void loop() {
if (acc.isConnected()) {
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_SERVO) {
if(rcvmsg[1] == SERVO_ID_1) {
int posInDegrees = ((rcvmsg[2] & 0xFF) << 24)

  • ((rcvmsg[3] & 0xFF) << 16)
  • ((rcvmsg[4] & 0xFF) << 8)
  • (rcvmsg[5] & 0xFF);
    posInDegrees = map(posInDegrees, -100, 100, 0, 180);
    servo.write(posInDegrees);
    // give the servo time to reach its position
    delay(20);
    }
    }
    }
    }
    }`

乍一看,您可以看到代码变得短了很多。您将不再需要任何计算或自定义方法。要使用Servo库,你首先必须将它包含在你的草图中。

#include <Servo.h>

通过将它包含在你的草图中,你可以用一个Servo物体来为你做所有繁重的工作。

Servo servo;

要初始化Servo对象,你必须调用attach方法,并提供连接伺服信号线的数字引脚。

servo.attach(SERVO_ID_1_PIN);

为了实际控制伺服系统,你只需要调用它的write方法以及你想要的位置值(以度为单位)。

servo.write(posInDegrees); delay(20);

注意,你必须在这里调用delay方法,给伺服一些时间到达它的位置,并完成一个完整的脉冲。如果你不提供延迟,伺服会抖动,因为位置更新会太快。波形生成是在后台的Servo库中处理的,所以你再也不用担心了。简单多了,不是吗?

Arduino 部分到此为止。请记住,在实现 Arduino 草图时,您可以选择最适合您需求的方法。

Android 应用

大多数 Android 设备都有识别其在三维空间中的方向的方法。通常,他们通过向加速度计传感器和磁场传感器请求传感器更新来实现这一点。对于 Android 部分,您还将从加速度计请求方向更新,这将直接关系到稍后的伺服运动。清单 9-3 显示了活动的实现,我将在后面详细讨论。

清单 9-3。项目 10:ProjectTenActivity.java

`package project.ten.adk;

import …;

public class ProjectTenActivity extends Activity {

private static final byte COMMAND_SERVO = 0x7;
private static final byte SERVO_ID_1 = 0x1;

private TextView servoDirectionTextView;

private SensorManager sensorManager;
private Sensor accelerometer;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
servoDirectionTextView = (TextView) findViewById(R.id.x_axis_tilt_text_view);

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume(); sensorManager.registerListener(sensorEventListener, accelerometer,
    SensorManager.SENSOR_DELAY_GAME);

}

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
sensorManager.unregisterListener(sensorEventListener);
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

private final SensorEventListener sensorEventListener = new SensorEventListener() {

int x_acceleration;

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// not implemented
}

@Override
public void onSensorChanged(SensorEvent event) {
x_acceleration = (int)(-event.values[0] * 10);
moveServoCommand(SERVO_ID_1, x_acceleration);
runOnUiThread(new Runnable() {

@Override
public void run() {
servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration));
}
});
}
};

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {

}

private void closeAccessory() {

}

public void moveServoCommand(byte target, int value) {
byte[] buffer = new byte[6];
buffer[0] = COMMAND_SERVO;
buffer[1] = target;
buffer[2] = (byte) (value >> 24);
buffer[3] = (byte) (value >> 16);
buffer[4] = (byte) (value >> 8);
buffer[5] = (byte) value;
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}
}`

正如 Arduino 草图中所做的那样,您首先必须为您打算稍后发送的控制消息定义相同的命令和目标 id。

private static final byte COMMAND_SERVO = 0x7; private static final byte SERVO_ID_1 = 0x1;

您将使用的唯一可视组件是一个简单的TextView元素,用于调试目的,并为您提供倾斜设备时 x 轴方向值如何变化的概述。

private TextView servoDirectionTextView;

为了从你的 Android 设备的任何传感器请求更新,你首先需要获得对SensorManagerSensor本身的引用。从某些传感器获取数据还有其他方法,但SensorManager是大多数传感器的通用注册表。

private SensorManager sensorManager; private Sensor accelerometer;

onCreate()方法中完成内容视图元素的常规设置后,通过调用上下文方法getSystemService并提供常量SENSOR_SERVICE作为参数,您获得了前面提到的对SensorManager的引用。此方法提供对所有类型的系统服务的访问,例如连接服务、音频服务等等。

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);

随着SensorManager准备就绪,您可以访问 Android 设备的传感器。您特别想要加速度传感器,所以当您在SensorManager上调用getDefaultSensor方法时,您必须将它指定为一个参数。

accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

Android 系统提供了一种注册传感器变化的机制,因此您不必担心这一点。因为你不能直接从Sensor物体上获得传感器读数,你必须为那些SensorEvent注册。

onResume()方法中,你调用SensorManager对象上的registerListener方法,并传递三个参数。第一个是SensorEventListener,它实现了对传感器变化做出反应的方法。(我一会儿会谈到这一点。)第二个参数是实际的Sensor参考,应该听听。最后一个参数是更新速率。

SensorManager类中定义了不同的速率常数。我对SENSOR_DELAY_GAME常数有最好的体验。它的更新速度非常快。正常延迟在伺服运动中引起了一些滞后行为。我也不推荐使用SENSOR_DELAY_FASTEST常数,因为这通常会使伺服抖动很大;更新来得太快了。

sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);

因为您在onResume()方法中注册了监听器,所以您还应该确保应用释放了资源,并相应地取消了onPause()方法中的监听过程。

sensorManager.unregisterListener(sensorEventListener);

现在让我们来看看监听器本身,因为它负责处理传感器更新。

`private final SensorEventListener sensorEventListener = new SensorEventListener() {

int x_acceleration;

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// not implemented
}

@Override
public void onSensorChanged(SensorEvent event) {
x_acceleration = (int)(-event.values[0] * 10);
moveServoCommand(SERVO_ID_1, x_acceleration);
runOnUiThread(new Runnable() {

@Override
public void run() {
servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration));
}
});
}
};`

当你实现一个SensorEventListener时,你将不得不写两个方法,即onAccuracyChanged方法和onSensorChanged方法。在这个项目中,第一个不是你真正感兴趣的;你现在不关心准确性。然而,第二个函数提供了一个名为SensorEvent的参数,它由系统提供并保存您感兴趣的传感器值。加速度计的SensorEvent值包含三个值,x 轴上的加速度、y 轴上的加速度和 z 轴上的加速度。你只对 x 轴上的加速度感兴趣,所以你只需要担心第一个值。返回值的范围从 10.0(完全向左倾斜)到-10.0(完全向右倾斜)。对于稍后看到这些值的用户来说,这似乎有点违背直觉。这就是这个示例项目中的值被否定的原因。为了更容易地传输这些值,最好将这些值乘以 10,这样以后就可以传输整数而不是浮点数。通过这样做,可传输的数字将在从-100 到 100 的范围内。

x_acceleration = (int)(-event.values[0] * 10);

现在您已经有了 x 轴上的加速度数据,您可以更新TextView元素来给出视觉反馈。

`runOnUiThread(new Runnable() {

@Override
public void run() {
servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration));
}
});`

最后要做的事情是向 ADK 板发送控制消息。为此,您将使用自定义方法moveServoCommand,提供要控制的伺服 ID 和实际加速度数据。

moveServoCommand(SERVO_ID_1, x_acceleration);

该方法的实现非常简单。您只需设置基本的数据结构,将整数加速度值位移到四个单字节,并通过输出流将完整的消息发送到 ADK 板。

public void moveServoCommand(byte target, int value) { byte[] buffer = new byte[6]; buffer[0] = COMMAND_SERVO; buffer[1] = target; buffer[2] = (byte) (value >> 24); buffer[3] = (byte) (value >> 16); buffer[4] = (byte) (value >> 8); buffer[5] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }

Android 部分到此为止,最终结果如图图 9-8 所示。当你准备好了,部署这两个应用,看看每当你倾斜你的 Android 设备时,你的伺服如何转动。

images

图 9-8。项目 10:最终结果

项目 11:控制 DC 汽车公司

下一个项目将向您展示如何控制另一种电机,即所谓的 DC 电机。正如我在本章开始时已经解释过的,DC 马达提供连续的旋转,不像伺服马达那样受到人为的限制。您将再次使用设备的加速度计来控制 DC 电机,只是这次您将处理设备 y 轴上的加速度变化。因此,当你向前倾斜设备时,电机将开始旋转。你也可以在这样做的时候控制它的速度。注意,我没有涉及旋转方向的改变,这需要更多的硬件。在开始之前,你需要了解控制 DC 发动机所需的部件以及发动机是如何运转的。

零件

你不需要很多零件就能让马达运转起来。事实上,你将要建造的电路的唯一新元件是一个所谓的 NPN 晶体管。我一会儿会解释它的目的。

images 注意如果您使用基于 Arduino 设计的 ADK 板,并且碰巧使用工作电压高于 5V 的 DC 电机,或者如果其功耗超过 40mA,您可能需要额外的外部电源,因为板的输出引脚被限制在这些值。

零件清单如下所示(图 9-9 ):

  • 网页净化器板
  • breadboard
  • Transistor NPN [BC 547 b]
  • DC motor
  • Partial wire
  • Optional: 1-10kω resistor, external battery

images

图 9-9。项目 11 个零件(ADK 板、试验板、电线、NPN 晶体管、DC 电机)

ADK 董事会

稍后,您将使用输出引脚的脉宽调制(PWM)功能来产生影响电机速度的不同电压。

DC 汽车

DC 汽车靠直流电运行,因此得名。有两种类型的 DC 电机:有刷 DC 电机和无刷 DC 电机。

有刷 DC 电机的工作原理是固定磁铁干扰电磁场。驱动轴上安装的线圈在其电枢周围产生电磁场,不断被周围的固定磁铁吸引和排斥,导致驱动轴旋转。有刷 DC 电机通常比无刷 DC 电机便宜,这就是为什么它们更广泛地应用于爱好电子产品。

无刷 DC 电机的制造与有刷 DC 电机正好相反。它们的驱动轴上安装有固定的磁铁,当电磁场作用于它们时,磁铁开始运动。它们的不同之处还在于电机控制器将直流电(DC)转换为交流电(AC)。无刷 DC 电机比他们的同类产品贵一点。不同的 DC 电机外形如图 9-10 所示。

images

图 9-10。不同外形的 DC 汽车

大多数爱好 DC 汽车有一个双线连接,Vin 和 GND。要改变旋转的方向,通常只需改变连接的极性。当你想在飞行中改变马达的旋转方向时,你需要一个比你在这里将要建立的电路更复杂的电路。出于这些目的,你需要一个特殊的电路设置,称为 H 桥或一个特殊的电机驱动器集成电路。关于这些的进一步细节,只需搜索网页,在那里你会找到大量的教程和信息。为了避免使项目复杂化,我将只坚持一个马达方向。电机的速度会受到所施加的电压水平的影响。你提供的越多,它通常会变得越快,但是注意不要提供超过它能处理的。快速浏览一下电机的数据表,你会对工作电压范围有一个大致的了解。

DC 发动机通常与齿轮和变速器一起使用,这样它们的扭矩就可以传递到齿轮上,从而转动例如车轮或其他机械结构。(参见图 9-11 。)

images

图 9-11。带齿轮附件的 DC 电机

请注意,大多数 DC 汽车不附带预先连接的电线,所以你可能必须先焊接电线。

NPN 晶体管(BC547B)

晶体管是能够在电路中开关和放大功率的半导体。它们通常有三个连接器,称为基极、集电极和发射极(见图 9-12 )。

images

图 9-12。晶体管(平面朝向:发射极、基极、集电极)

晶体管能够通过向另一对施加较小的电压或电流来影响一对连接之间的电流。例如,当电流从集电极流向发射极时,可以将一小部分电流施加于流经发射极的基极,以控制更高的电流。所以基本上晶体管可以用作电路中电源的开关或放大器。

有几种不同类型的晶体管用于不同的任务。在这个项目中,你需要一个 NPN 晶体管。一个 NPN 晶体管在基极连接器被拉高的情况下工作,这意味着当高电压或电流施加到基极时,它被设置为“on”。所以当基极上的电压增加时,从集电极到发射极的连接会让更多的电流通过。NPN 晶体管的电气符号如图图 9-13 所示。

images

图 9-13。NPN 晶体管的电气符号

与 NPN 晶体管相反的是 PNP 晶体管,其工作方式完全相反。要在集电极和发射极之间切换电流,需要将基极拉低。因此,随着电压的降低,更多的电流通过集电极-发射极连接。PNP 晶体管的电气符号如图 9-14 所示。

images

图 9-14。PNP 晶体管的电气符号

稍后,您将使用 NPN 晶体管来控制应用于电机的电源,以便控制其速度。

设置

本章第二个项目的连接设置也相当简单(图 9-15 )。根据您使用的 DC 电机,您将电机的一根电线连接到+3.3V 或 5V,这由电机的额定电压决定。如果你碰巧使用一个额定电压更高的电机,你可能需要连接一个外部电池。在这种情况下,你必须确保连接电池和 ADK 板到一个共同的地面(GND)。电机的第二根线需要连接到晶体管的集电极。晶体管的发射极连接到 GND,晶体管的基极连接到数字引脚 2。如果你在晶体管上找不到正确的连接,只要把晶体管平的一面朝向你就行了。右边的引脚是集电极,中间的引脚是基极,左边的引脚是发射极。

images

图 9-15。项目 11 设置

NPN 晶体管已被添加到电路中,以防您的电机需要比 ADK 板所能提供的更多的功率。因此,如果你遇到一个非常慢的电机运行或根本没有运动,你可以很容易地将外部电池连接到电路上。如果这样做,还应确保在数字引脚 2 上增加一个 1kω至 10k 范围内的高阻值电阻,以保护 ADK 板免受来自高功率电路的功率异常影响。如果你需要一个外部电池,你的电路看起来会像图 9-16 。

images

图 9-16。使用外部电池的项目 11 设置

你应该先尝试使用第一个电路设置,但如果你的电机需要更多的电力,很容易切换到第二个电路设置。

软件

这个小项目的软件部分相当简单。您将再次使用 Android 设备的加速度计来获取设备倾斜时倾斜值的变化,只是这次您感兴趣的是 y 轴而不是 x 轴的倾斜值。y 轴倾斜描述的是设备屏幕朝上时的倾斜运动,其顶部边缘被向下推,底部边缘被向上拉。这就好像你要向前推一架飞机的油门杆。倾斜值将被传输到 ADK 板,运行的 Arduino 草图将把接收到的值映射到 0 到 255 之间的一个值,该值将只在 0 到 100 的范围内,因为您对另一个倾斜方向不感兴趣,以输入到analogWrite方法中。这导致数字引脚 2 以 PWM 模式工作,该引脚的输出电压将相应改变。请记住,晶体管的基极连接器连接到引脚 2,因此随着电压的变化,您将影响为电机提供的总功率,速度将根据您的倾斜而变化。

Arduino 草图

Arduino 草图会比你写的控制伺服系统的草图短一点(如清单 9-4 所示)。Arduino IDE 没有官方的 DC 汽车库,但是对于这个例子,你不需要库,因为代码非常简单。在网站上有社区编写的自定义库,用于使用 H 桥控制旋转方向的情况,但这超出了本例的范围。您只需使用analogWrite方法,根据您将从连接的 Android 设备接收到的倾斜值,更改数字引脚 2 上的电压输出。

清单 9-4。项目 11: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define COMMAND_DC_MOTOR 0x8
#define DC_MOTOR_ID_1 0x1
#define DC_MOTOR_ID_1_PIN 2

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial");

byte rcvmsg[3];

void setup() {
Serial.begin(19200);
pinMode(DC_MOTOR_ID_1_PIN, OUTPUT);
acc.powerOn();
}

void loop() {
if (acc.isConnected()) {
int len = acc.read(rcvmsg, sizeof(rcvmsg), 1);
if (len > 0) {
if (rcvmsg[0] == COMMAND_DC_MOTOR) {
if(rcvmsg[1] == DC_MOTOR_ID_1) {
int motorSpeed = rcvmsg[2] & 0xFF;
motorSpeed = map(motorSpeed, 0, 100, 0, 255);
analogWrite(DC_MOTOR_ID_1_PIN, motorSpeed);
}
}
}
}
}`

这里要做的第一件事是像往常一样更改命令和目标字节。

`#define COMMAND_DC_MOTOR 0x8

define DC_MOTOR_ID_1 0x1

define DC_MOTOR_ID_1_PIN 2`

接下来,将数字引脚 2 配置为输出引脚。

pinMode(DC_MOTOR_ID_1_PIN, OUTPUT);

一旦你从连接的 Android 设备收到有效信息,你必须将原始倾斜值从byte转换为int

int motorSpeed = rcvmsg[2] & 0xFF;

因为analogWrite方法处理 0 到 255 之间的值,而不是 0 到 100 之间的值,所以在将它们提供给analogWrite方法之前,必须先将它们映射到适当的范围。

motorSpeed = map(motorSpeed, 0, 100, 0, 255);

最后,您可以通过提供目标引脚 2 和电机速度的转换值来调用analogWrite方法。

analogWrite(DC_MOTOR_ID_1_PIN, motorSpeed);

这一小段代码是用来控制一个简单的 DC 马达的单向速度的。

Android 应用

Android 应用与之前的伺服项目几乎相同。唯一需要改变的是消息字节和当SensorEventListener接收到SensorEvent时得到的值。让我们看看需要做些什么(清单 9-5 )。

清单 9-5。项目 11:ProjectElevenActivity.java

`package project.eleven.adk;

import …;

public class ProjectElevenActivity extends Activity {

private static final byte COMMAND_DC_MOTOR = 0x8;
private static final byte DC_MOTOR_ID_1 = 0x1;

private TextView motorSpeedTextView;

private SensorManager sensorManager;
private Sensor accelerometer;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

setContentView(R.layout.main);
motorSpeedTextView = (TextView) findViewById(R.id.y_axis_tilt_text_view);

sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
} /**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);

}

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
sensorManager.unregisterListener(sensorEventListener);
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(mUsbReceiver);
    }

private final SensorEventListener sensorEventListener = new SensorEventListener() {

int y_acceleration;

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// not implemented
}

@Override
public void onSensorChanged(SensorEvent event) {
y_acceleration = (int)(-event.values[1] * 10);
if(y_acceleration < 0) {
y_acceleration = 0;
} else if(y_acceleration > 100) {
y_acceleration = 100;
}
moveMotorCommand(DC_MOTOR_ID_1, y_acceleration);
runOnUiThread(new Runnable() { @Override
public void run() {
motorSpeedTextView.setText(getString( R.string.y_axis_tilt_text_placeholder, y_acceleration));
}
});
}
};

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {

}
};

private void openAccessory(UsbAccessory accessory) {

}

private void closeAccessory() {

}

public void moveMotorCommand(byte target, int value) {
byte[] buffer = new byte[3];
buffer[0] = COMMAND_DC_MOTOR;
buffer[1] = target;
buffer[2] = (byte) value;
if (mOutputStream != null) {
try {
mOutputStream.write(buffer);
} catch (IOException e) {
Log.e(TAG, "write failed", e);
}
}
}
}`

此处命令和目标消息字节已被更改,以匹配 Arduino 草图中的字节。

private static final byte COMMAND_DC_MOTOR = 0x8; private static final byte DC_MOTOR_ID_1 = 0x1;

在伺服示例中,您将使用一个简单的TextView元素为用户提供视觉反馈,该元素显示设备沿 y 轴的当前倾斜值。

private TextView motorSpeedTextView;

实际上,这里唯一有趣的新部分是SensorEventListener的实现。

private final SensorEventListener sensorEventListener = new SensorEventListener() { `int y_acceleration;

@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// not implemented
}

@Override
public void onSensorChanged(SensorEvent event) {
y_acceleration = (int)(-event.values[1] * 10);
if(y_acceleration < 0) {
y_acceleration = 0;
} else if(y_acceleration > 100) {
y_acceleration = 100;
}
moveMotorCommand(DC_MOTOR_ID_1, y_acceleration);
runOnUiThread(new Runnable() {

@Override
public void run() {
motorSpeedTextView.setText(getString( R.string.y_axis_tilt_text_placeholder, y_acceleration));
}
});
}
};`

同样,您不需要实现onAccuracyChanged方法,因为您只想知道当前的倾斜值,而不是它的准确性。在onSensorChanged方法中,您可以看到您访问了事件值的第二个元素。您可能还记得,SensorEvent为这种传感器类型提供了三个值:x 轴、y 轴和 z 轴的值。因为需要 y 轴上的值变化,所以必须访问第二个元素。

y_acceleration = (int)(-event.values[1] * 10);

正如在伺服示例中所做的那样,您需要调整该值,以获得更好的用户可读性,并便于以后的传输。通常,如果您将设备向前倾斜,您会收到-10.0 到 0.0 之间的值。为了避免混淆用户,您将首先否定该值,这样向前倾斜的增加将显示一个增加的数字,而不是减少的数字。为了便于传输,只需将该值乘以 10,并将其转换为整数数据类型,就像前面的项目一样。

当向后倾斜时,您仍然可以接收到传感器值,您不希望稍后将这些值传输到 ADK 板,因此只需定义边界并调整接收到的值。

if(y_acceleration < 0) { y_acceleration = 0; } else if(y_acceleration > 100) { y_acceleration = 100; }

现在你已经有了最终的倾斜值,你可以更新TextView并将数据信息发送到 ADK 板。传输是通过一种叫做moveMotorCommand的独立方法完成的。

public void moveMotorCommand(byte target, int value) { byte[] buffer = new byte[3]; buffer[0] = COMMAND_DC_MOTOR; buffer[1] = target; buffer[2] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }

Android 代码这次没有太大的麻烦,因为您只需要调整上一个示例中的一些代码行。然而,你已经完成了机器人的部分,现在你可以看到你的 DC 汽车在运行了(图 9-17 )。部署 Arduino sketch 和 Android 应用,当您向前倾斜设备时,可以看到您的电机在旋转。

images

图 9-17。项目 11:最终结果

总结

这一章给了你一个简单的概述,让你的项目以任何方式前进。这一概述远未完成。有很多方法可以将运动带入游戏,但使用伺服或 DC 马达是最常见的方法,它给你进一步的实验提供了一个完美的开始。您学习了这些致动器如何操作,以及如何使用 ADK 板驱动它们。您还学习了如何处理 Android 设备的加速度传感器,以及如何使用它来读取其三个轴中任何一个轴的当前加速度或倾斜值。最后,您使用设备的加速度传感器来控制执行器。

十、报警系统

现在你已经对你的 ADK 板和你已经使用过的不同的传感器和组件感到满意了,是时候来点更大的了。在最后一章中,您将结合前几章中使用的一些组件来构建两个版本的报警系统。您将了解新组件—倾斜开关和由红外 LED 和红外探测器组成的红外挡光板—在现实世界中广泛应用。在两个独立的项目中,您将学习如何将这些组件集成到一个小型报警系统中,以便它们触发警报。在硬件方面,警报将通过闪烁的红色 LED 和产生高音的压电蜂鸣器来表达。您的 Android 设备会将警报事件保存在一个日志文件中,并且根据当前的项目,它还会发送一条通知短信,或者给入侵者拍照并保存到您的文件系统中。

项目 12:带倾斜开关的短信报警系统

在这个项目中,你将使用一个所谓的倾斜开关来触发警报,如果开关倾斜到其关闭位置。触发器将使红色 LED 发出脉冲,压电蜂鸣器发出警报声。此外,Android 设备将收到警报通知,并在其文件系统中写入日志文件。如果您的 Android 设备具有电话功能,它还会向指定的电话号码发送短信,显示报警时间和报警消息。

零件

你的第一个报警系统需要几个部件。您将使用一个红色 LED 来发出警报发生的视觉信号。对于听觉反馈,您将使用一个压电蜂鸣器产生报警声。如果倾斜到关闭位置,倾斜开关将触发警报。要重置报警系统,以便它可以报告下一个报警,您将使用一个简单的按钮。以下是您的报警系统所需部件的完整列表(参见图 10-1 ):

  • ADK 董事会
  • 试验板
  • S7-1200 可编程控制器
  • 2 X10 kω电阻
  • 一些电线
  • 纽扣
  • 倾斜开关
  • 工作电压为 5V 的 LED
  • 压电蜂鸣器

images

图 10-1。项目 12 部分(ADK 板、试验板、电线、电阻器、按钮、倾斜开关、压电蜂鸣器、红色 LED)

ADK 董事会

本项目将使用 ADK 板,提供两种输入方式,即倾斜开关和按钮,以及两种输出方式,即 LED 和压电蜂鸣器。由于您没有任何模拟输入要求,您将只使用 ADK 板的数字引脚。您将使用数字输入功能来感应按钮的按下或倾斜开关的闭合。数字输出功能,尤其是 PWM 功能,将用于脉冲 LED,并通过压电蜂鸣器产生声音。

LED

正如您在第三章中所学,您可以通过使用 ADK 板的 PWM 功能来调暗 LED。当警报发生时,LED 将用于给出视觉反馈。这将通过让 LED 脉冲来实现,这意味着它将在其最亮的最大水平和最暗的最小水平之间持续变暗,直到警报被重置。

压电蜂鸣器

第五章向您展示了当向压电蜂鸣器的压电元件供电时,您可以产生声音,这被描述为反向压电效应。压电元件的振荡产生压力波,该压力波被人耳感知为声音。振荡是通过使用 ADK 板的 PWM 功能来调制输出到压电蜂鸣器的电压来实现的。

按钮

在第四章中,您学习了如何将按钮或开关用作项目的输入。原理很简单。当按钮或开关被按下或闭合时,电路闭合。

倾斜开关

倾斜开关与普通开关非常相似。不同的是,用户没有真正的开关来按下或翻转以闭合连接的电路。最常见的倾斜开关通常以部件内的连接器引线彼此分离的方式构造。此外,在组件内还有一个由导电材料制成的球。(参见图 10-2 )。

images

图 10-2。打开倾斜开关(内部视图)

当开关以某种方式倾斜时,导电球移动并接触开路连接。球关闭了连接,电流可以从一端流到另一端。由于球受重力影响,你只需倾斜倾斜开关,使连接点指向地面。(参见图 10-3 )。

images

图 10-3。关闭倾斜开关(内部视图)

就这么简单。当你摇动倾斜开关时,你可以听到球在组件内移动。

倾斜开关也被称为水银开关,因为在过去,大多数倾斜开关内部都有一个水银滴来关闭连接。水银的优点是它不像其他材料那样容易反弹,所以它不会受到振动的太大影响。然而,缺点是水银毒性很强,所以如果倾斜开关损坏,就会对环境造成危险。如今,其他导电材料通常用于倾斜开关。

倾斜开关在许多实际应用中使用。汽车工业广泛使用它们。汽车的行李箱灯只是一个例子:如果你把行李箱打开到一个特定的角度,灯就会打开。另一个例子是经典的弹球机,如果用户过度倾斜机器以获得优势,它会进行记录。你也可以很容易地想象一个报警系统的用例,比如当门把手被按下时进行感应。

一些倾斜开关可能带有焊接引脚,您不能直接将其插入试验板,因此您可能需要在这些引脚上焊接一些电线,以便稍后将倾斜开关连接到电路。(参见图 10-4 )。有时倾斜开关可以有两个以上的连接器,以便您可以将它们连接到多个电路。你只需要确保你总是连接正确的引脚对。查看数据手册或构建一个简单电路来测试哪些连接相互关联。

images

图 10-4。带焊接连接器引脚的倾斜开关和库存倾斜开关

设置

设置将通过一次连接一个组件来完成,这样当您启动项目时,您就不会弄混并最终损坏某些东西。我将首先展示每个独立的电路设置,然后展示包含所有元件的完整电路设置。

LED 电路

先说 LED。众所周知,led 通常以大约 20mA 到 30mA 的电流工作。为了将电流限制在至少 20mA 的值,您需要在 LED 上连接一个电阻。ADK 板的输出引脚提供 5V 电压,因此您需要应用欧姆定律来计算电阻值。

r = 5v/0.02a
r = 250ω

最接近的普通电阻是 220ω电阻。有了这个电阻,你最终会得到一个大约 23mA 的电流,这很好。现在将电阻的一端连接到数字引脚 2,另一端连接到 LED 的阳极(长引线)。发光二极管的阴极接地(GND)。LED 电路设置如图图 10-5 所示。

images

图 10-5。项目 12: LED 电路设置

压电蜂鸣器电路

接下来,您将压电蜂鸣器连接到 ADK 板。这非常简单,因为你不需要额外的组件。如果你碰巧有一个压电蜂鸣器,确保按照标记正确连接正负引线。否则,只需将一根引线连接到数字引脚 3,另一根引线接地(GND)。压电蜂鸣器电路设置如图 10-6 所示。

images

图 10-6。项目 12:压电蜂鸣器电路设置

按钮电路

您可能还记得第四章中的内容,使用按钮消除电气干扰时,最好将电路上拉至工作电压。为了上拉按钮电路而不损坏高电流输入引脚,需要将一个 10kΩ上拉电阻与按钮一起使用。为此,将 ADK 电路板的+5V Vcc 引脚连接到 10kω电阻的一条引线。另一根引线连接到数字引脚 4。数字引脚 4 也连接到按钮的一个引线。相反的导线接地(GND)。按钮电路如图图 10-7 所示。

images

图 10-7。项目 12:按钮电路设置

倾斜开关电路

由于倾斜开关的工作原理与按钮类似,您可以像连接按钮一样连接它。将 ADK 板的+5V Vcc 引脚连接到 10kΩ电阻的一根引线上。另一根引线连接到数字引脚 5。数字引脚 5 也连接到倾斜开关的一个引线。相反的导线接地(GND)。倾斜开关电路设置如图 10-8 所示。

images

图 10-8。项目 12:倾斜开关电路设置

完成电路设置

现在你知道如何连接每个组件,看看图 10-9 所示的完整电路设置。

images

图 10-9。项目 12:完成电路设置

那么这个报警系统是如何工作的呢?想象一下,倾斜开关安装在门把手或天窗上。当手柄被按下或存水弯窗被打开时,倾斜开关将倾斜到导电球接触内部引线的位置,从而使开关闭合。现在,报警被记录,压电蜂鸣器和 LED 开始发出声音和视觉报警反馈。要重置警报以便再次触发,必须按下按钮。

软件

现在您已经设置好了一切,是时候编写必要的软件来启动和运行警报系统了。Arduino 草图将监控倾斜开关是否已倾斜至其触点闭合位置。如果倾斜开关触发了警报,Arduino tone方法将用于使压电蜂鸣器振荡,从而产生高音。此外,红色 LED 将通过使用analogWrite方法产生脉冲,该方法调制电压输出并使 LED 以不同的照明强度点亮。为了重置警报,使其可以再次被触发,一个简单的按钮的状态被读取。一旦按下该按钮,所有必要的变量都会重置,报警系统可以再次记录报警。

如果警报被触发,如果 Android 设备已连接,则会向其发送一条消息。一旦 Android 设备收到警报消息,它会以红屏背景和警报消息的形式给出视觉反馈。警报消息和时间戳将保存在设备的外部存储系统中。但是,不要被术语外部所误导。Android 环境下的外部并不一定意味着存储,比如可移动 SD 卡。术语外部也可以描述内部不可移动存储。这是一种表述,描述存储在其中的文件可以被用户读取和修改,并且该存储可以被另一个操作系统安装为用于文件浏览的大容量存储。

如果连接的 Android 设备支持电话功能,则会向预先配置的电话号码发送包含警报消息的短信。请记住,大多数 Android 平板电脑都没有电话功能,因为它们的主要用途是浏览互联网,而不是给人打电话。你可以想象,没有人会在公共场合拿着一个 10 英寸的平板电脑贴着脸颊打电话,看起来很酷。

Arduino 草图

如软件部分所述,您将使用一些在前面的示例中使用过的众所周知的方法。现在唯一的不同是,您将使用多个组件。在我详细描述之前,先看一下完整的清单 10-1 。

清单 10-1。项目 12: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

#define LED_OUTPUT_PIN 2
#define PIEZO_OUTPUT_PIN 3
#define BUTTON_INPUT_PIN 4
#define TILT_SWITCH_INPUT_PIN 5

#define NOTE_C7 2100

#define COMMAND_ALARM 0x9
#define ALARM_TYPE_TILT_SWITCH 0x1
#define ALARM_OFF 0x0
#define ALARM_ON 0x1

int tiltSwitchValue;
int buttonValue;
int ledBrightness;
int fadeSteps = 5;

boolean alarm = false;

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial"); byte sntmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_ALARM;
sntmsg[1] = ALARM_TYPE_TILT_SWITCH;
}

void loop() {
acc.isConnected();
tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN);
if((tiltSwitchValue == LOW) && !alarm) {
startAlarm();
}
buttonValue = digitalRead(BUTTON_INPUT_PIN);
if((buttonValue == LOW) && alarm) {
stopAlarm();
}
if(alarm) {
fadeLED();
}
delay(10);
}

void startAlarm() {
alarm = true;
tone(PIEZO_OUTPUT_PIN, NOTE_C7);
ledBrightness = 0;
//inform Android device
sntmsg[2] = ALARM_ON;
sendAlarmStateMessage();
}

void stopAlarm() {
alarm = false;
//turn off piezo buzzer
noTone(PIEZO_OUTPUT_PIN);
//turn off LED
digitalWrite(LED_OUTPUT_PIN, LOW);
//inform Android device
sntmsg[2] = ALARM_OFF;
sendAlarmStateMessage();
}

void sendAlarmStateMessage() {
if (acc.isConnected()) {
acc.write(sntmsg, 3);
}
} void fadeLED() {
analogWrite(LED_OUTPUT_PIN, ledBrightness);
//increase or decrease brightness
ledBrightness = ledBrightness + fadeSteps;
//change fade direction when reaching max or min of analog values
if (ledBrightness < 0 || ledBrightness > 255) {
fadeSteps = -fadeSteps ;
}
}`

首先让我们看看变量的定义和声明。如果您按照我在项目硬件设置一节中描述的那样连接了所有输入和输出组件,那么您的引脚定义应该如下所示。

`#define LED_OUTPUT_PIN 2

define PIEZO_OUTPUT_PIN 3

define BUTTON_INPUT_PIN 4

define TILT_SWITCH_INPUT_PIN 5`

你看到的下一个定义是警报发生时压电蜂鸣器应该产生的高音频率。2100 Hz 的频率定义了音符 C7,这是大多数音乐键盘上的最高音符,除了古典 88 键钢琴。音符提供了完美的高音,人耳比任何低音都能听得更清楚。这就是为什么像火警这样的系统使用高音报警声的原因。

#define NOTE_C7 2100

接下来,您将看到通常的数据消息字节定义。为报警命令选择了一个新的字节值,一个类型字节定义了本项目中用于触发报警的倾斜开关。如果您打算以后在报警系统中添加额外的开关或传感器,则定义类型字节。最后两个字节定义仅定义报警是否已触发或是否已关闭。

`#define COMMAND_ALARM 0x9

define ALARM_TYPE_TILT_SWITCH 0x1

define ALARM_OFF 0x0

define ALARM_ON 0x1`

当您使用digitalRead方法读取按钮或倾斜开关的数字状态时,返回值将是一个int值,稍后可与常量HIGHLOW进行比较。所以你需要两个变量来存储按钮和倾斜开关的读数。

int tiltSwitchValue; int buttonValue;

请记住,当警报发生时,您希望让 LED 发出脉冲。为此,您需要使用analogWrite方法来调制 LED 的电源电压。analogWrite方法接受从 0 到 255 范围内的值。这就是为什么你将当前亮度值存储为一个int值。当您增加或降低 LED 的亮度时,您可以定义渐变过程的步长值。步长值越低,LED 的衰减越平滑越慢,因为达到analogWrite范围的最大值或最小值需要更多的循环周期。

int ledBrightness; int fadeSteps = 5;

最后一个新变量是一个boolean标志,它仅存储报警系统的当前状态,以确定报警当前是激活还是关闭。它在开始时被初始化为关闭状态。

boolean alarm = false;

变量就是这样。除了用新的命令字节和类型字节填充数据消息的前两个字节之外,setup方法没有任何新的功能。有趣的部分是loop方法。

void loop() { acc.isConnected(); tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN); if((tiltSwitchValue == LOW) && !alarm) { startAlarm(); } buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) { stopAlarm(); } if(alarm) { fadeLED(); } delay(10); }

在之前的项目中,循环方法中的代码被 if 子句包围,该子句检查 Android 设备是否连接,然后才执行程序逻辑。由于这是一个报警系统,所以我认为即使没有连接 Android 设备,也最好让它至少在 Arduino 端工作。在循环开始时调用isConnected方法的原因是,该方法中的逻辑确定设备是否连接,并向 Android 设备发送消息,以便启动相应的应用。循环逻辑的其余部分非常简单。首先,你读倾斜开关的状态。

tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN);

如果倾斜开关闭合其电路,数字状态将为LOW,因为它在闭合时接地。只有到那时,如果闹钟还没有打开,你会希望闹钟启动。稍后将解释startAlarm方法的实现。

if((tiltSwitchValue == LOW) && !alarm) { startAlarm(); }

按钮被按下时的代码正好相反。它应该停止报警并重置报警系统,以便能够再次被激活。本章后面还将描述stopAlarm方法的实现。

buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) { stopAlarm(); }

如果系统当前处于警报状态,您需要淡化 LED 以显示警报。接下来是fadeLED方法的实现。

if(alarm) { fadeLED(); }

现在让我们看看从startAlarm方法开始的其他方法实现。

void startAlarm() { alarm = true; tone(PIEZO_OUTPUT_PIN, NOTE_C7); ledBrightness = 0; //inform Android device sntmsg[2] = ALARM_ON; sendAlarmStateMessage(); }

如您所见,alarm标志已经被设置为true,这样该方法就不会在下一个循环中被意外调用。在《??》第五章中已经使用了tone方法。这里,它用于在压电蜂鸣器上产生音符 C7 。当警报启动时,需要重置ledBrightness变量,以启动 LED 从暗到亮的淡入淡出。最后,用于描述警报被触发的消息字节被设置在数据消息上,并且如果 Android 设备被连接,则该消息被发送到 Android 设备。

接下来是对比法stopAlarm

void stopAlarm() { alarm = false; //turn off piezo buzzer noTone(PIEZO_OUTPUT_PIN); //turn off LED digitalWrite(LED_OUTPUT_PIN, LOW); //inform Android device sntmsg[2] = ALARM_OFF; sendAlarmStateMessage(); }

首先,您将报警标志设置为false以允许再次触发报警。然后,您需要通过调用noTone方法来关闭压电蜂鸣器。它停止向压电蜂鸣器输出电压,使其不再振荡。通过调用digitalWrite方法并将其设置为LOW (0V)来关闭 LED。这里的最后一步也是设置相应的消息字节,如果 Android 设备已连接,则向其发送停止消息。

sendAlarmStateMessage方法只是检查是否连接了 Android 设备,如果连接了,则使用Accessory对象的write方法传输三字节消息。

void sendAlarmStateMessage() { if (acc.isConnected()) { acc.write(sntmsg, 3); } }

最后一个方法实现是 LED 淡入淡出的逻辑。

void fadeLED() { analogWrite(LED_OUTPUT_PIN, ledBrightness); //increase or decrease brightness ledBrightness = ledBrightness + fadeSteps; //change fade direction when reaching max or min of analog values if (ledBrightness < 0 || ledBrightness > 255) { fadeSteps = -fadeSteps ; } }

为了给 LED 提供不同的电压等级,这里必须使用analogWrite方法和当前亮度值。在每个循环周期中,当系统设置为报警模式时,调用fadeLED方法。要改变 LED 的亮度等级,您必须将当前的ledBrightness值加上fadeSteps值。如果您碰巧超过了 0 到 255 的可能的analogWrite限制,您需要否定fadeSteps值的符号。值 5 将变成-5,而不是在下一个循环中增加亮度值,而是现在减小它,将 LED 调暗到更暗的亮度水平。

这就是软件的 Arduino 部分。如果你现在运行你的草图,你实际上已经有了一个功能报警系统。不过,您会希望实现 Android 应用,以便通过使用 Android 设备作为短信和存储信息的网关,让您的警报系统变得更加强大。

Android 应用

Android 软件部分将向您展示如何使用 Android 设备的存储能力将日志文件写入设备的文件系统。当警报发生时,连接的 Android 设备收到触发消息,它会将消息和时间戳写入应用的存储文件夹,供以后检查。此外,如果您使用支持电话功能的设备,如 Android 手机,该设备将向预定义的号码发送短信,以远程通知警报。为了直观显示警报已经发生,屏幕的背景颜色将变为红色,并显示一条警报消息。如果在 ADK 板上重置警报,相应的消息将被发送到 Android 设备,应用也将被重置,以再次启用警报系统。

项目 12 活动 Java 文件

在我进入细节之前,看一下完整的清单 10-2 。

清单 10-2。项目 12:ProjectTwelveActivity.java

`package project.twelve.adk;

import …;

public class ProjectTwelveActivity extends Activity {

private PendingIntent smsSentIntent;
private PendingIntent logFileWrittenIntent;

private static final byte COMMAND_ALARM = 0x9;
private static final byte ALARM_TYPE_TILT_SWITCH = 0x1;
private static final byte ALARM_OFF = 0x0; private static final byte ALARM_ON = 0x1;

private static final String SMS_DESTINATION = "put_telephone_number_here";
private static final String SMS_SENT_ACTION = "SMS_SENT";
private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";

private PackageManager packageManager;
boolean hasTelephony;

private TextView alarmTextView;
private TextView smsTextView;
private TextView logTextView;
private LinearLayout linearLayout;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUsbManager = UsbManager.getInstance(this);
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
smsSentIntent = PendingIntent.getBroadcast(this, 0, new Intent(
SMS_SENT_ACTION), 0);
logFileWrittenIntent = PendingIntent.getBroadcast(this, 0, new Intent(
LOG_FILE_WRITTEN_ACTION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
filter.addAction(SMS_SENT_ACTION);
filter.addAction(LOG_FILE_WRITTEN_ACTION);
registerReceiver(broadcastReceiver, filter);

packageManager = getPackageManager();
hasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);

setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
alarmTextView = (TextView) findViewById(R.id.alarm_text);
smsTextView = (TextView) findViewById(R.id.sms_text);
logTextView = (TextView) findViewById(R.id.log_text);
}

/**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    /
    @Override
    public void onResume() {
    super.onResume();

    } /
    * Called when the activity is paused by the system. */
    @Override
    public void onPause() {
    super.onPause();
    closeAccessory();
    }

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(broadcastReceiver);
    }

private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
openAccessory(accessory);
} else {
Log.d(TAG, "permission denied for accessory " + accessory);
}
mPermissionRequestPending = false;
}
} else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
closeAccessory();
}
} else if (SMS_SENT_ACTION.equals(action)) {
smsTextView.setText(R.string.sms_sent_message);
} else if (LOG_FILE_WRITTEN_ACTION.equals(action)) {
logTextView.setText(R.string.log_written_message);
}
}
};

private void openAccessory(UsbAccessory accessory) {

}

private void closeAccessory() {

} Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[3];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_ALARM:

if (buffer[1] == ALARM_TYPE_TILT_SWITCH) {
final byte alarmState = buffer[2];
final String alarmMessage = getString(R.string.alarm_message,
getString(R.string.alarm_type_tilt_switch));
runOnUiThread(new Runnable() {

@Override
public void run() {
if(alarmState == ALARM_ON) {
linearLayout.setBackgroundColor(Color.RED);
alarmTextView.setText(alarmMessage);
} else if(alarmState == ALARM_OFF) {
linearLayout.setBackgroundColor(Color.WHITE);
alarmTextView.setText(R.string.alarm_reset_message);
smsTextView.setText("");
logTextView.setText("");
}
}
});
if(alarmState == ALARM_ON) {
sendSMS(alarmMessage);
writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString());
}
}
break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
} }
};

private void sendSMS(String smsText) {
if(hasTelephony) {
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(SMS_DESTINATION, null, smsText, smsSentIntent, null);
}
}

private void writeToLogFile(String logMessage) {
File logDirectory = getExternalLogFilesDir();
if(logDirectory != null) {
File logFile = new File(logDirectory, "ProjectTwelveLog.txt");
if(!logFile.exists()) {
try {
logFile.createNewFile();
} catch (IOException e) {
Log.d(TAG, "Log File could not be created.", e);
}
}
BufferedWriter bufferedWriter = null;
try {
bufferedWriter = new BufferedWriter(new FileWriter(logFile, true));
bufferedWriter.write(logMessage);
bufferedWriter.newLine();
Log.d(TAG, "Written message to file: " + logFile.toURI());
logFileWrittenIntent.send();
} catch (IOException e) {
Log.d(TAG, "Could not write to Log File.", e);
} catch (CanceledException e) {
Log.d(TAG, "LogFileWrittenIntent was cancelled.", e);
} finally {
if(bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
Log.d(TAG, "Could not close Log File.", e);
}
}
}
}
}

private File getExternalLogFilesDir() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return getExternalFilesDir(null);
} else {
return null;
} }
}`

正如您在浏览代码时可能已经看到的,您有多个 UI 元素来显示一些文本。所以在开始研究代码之前,先看看布局和文本是如何定义的。

XML 资源定义

main.xml布局文件包含三个用LinearLayout包装的TextViewTextView只是稍后显示通知消息。LinearLayout负责改变背景颜色。您还可以看到,短信和文件通知TextView有一个绿色定义(#00FF00),这样当背景变成红色时,它们会有更好的对比度。

清单 10-3。项目 12: main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> <TextView android:id="@+id/alarm_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000" android:text="@string/alarm_reset_message"/> <TextView android:id="@+id/sms_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> <TextView android:id="@+id/log_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> </LinearLayout>

布局中引用的文本,以及一旦触发警报时应显示的警报消息文本,在strings.xml文件中定义,如清单 10-4 所示。

清单 10-4。项目 12: strings.xml

<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">ProjectTwelve</string> <string name="alarm_message">%1$s triggered an alarm!</string> <string name="alarm_reset_message">Alarm system is reset and active!</string> <string name="alarm_type_tilt_switch">Tilt switch</string> <string name="sms_sent_message">SMS has been sent.</string> <string name="log_written_message">Log has been written.</string> </resources>

变量声明和定义

现在让我们详细谈谈来自清单 10-2 的实际代码。首先是变量。您可以看到两个额外的PendingIntent。这对于稍后通知活动相应的事件已经发生以更新相应的TextView是必要的。

private PendingIntent smsSentIntent; private PendingIntent logFileWrittenIntent;

然后是通常的消息数据字节,如 Arduino 草图中所定义的。

private static final byte COMMAND_ALARM = 0x9; private static final byte ALARM_TYPE_TILT_SWITCH = 0x1; private static final byte ALARM_OFF = 0x0; private static final byte ALARM_ON = 0x1;

接下来,您会看到一些字符串定义。SMS_DESTINATION是您要发送通知短信的目的地电话号码。你得把这个换成一个真实的电话号码。然后你有两个动作字符串,当它们广播它们相应的事件已经发生时,这两个字符串被用来标识PendingIntent

private static final String SMS_DESTINATION = "put_telephone_number_here"; private static final String SMS_SENT_ACTION = "SMS_SENT"; private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";

为了确定您的 Android 设备是否支持电话功能,您需要访问PackageManagerboolean标志hasTelephony用于存储支持电话的信息,这样你就不用每次都去查找了。

private PackageManager packageManager; private boolean hasTelephony;

在全局变量部分的末尾,您可以看到 UI 元素的LinearLayoutTextView声明,它们用于给出可视的报警反馈。

private TextView alarmTextView; private TextView smsTextView; private TextView logTextView; private LinearLayout linearLayout;

生命周期方法

变量就是这样。现在我们来看看onCreate方法。除了用于授予 USB 权限的PendingIntent之外,您还定义了两个新的PendingIntent,它们将用于广播它们的特定事件,将日志文件写入文件系统或发送 SMS,以便 UI 可以相应地更新。

smsSentIntent = PendingIntent.getBroadcast(this, 0, new Intent(SMS_SENT_ACTION), 0); logFileWrittenIntent = PendingIntent.getBroadcast( this, 0, new Intent(LOG_FILE_WRITTEN_ACTION), 0);

你可以看到它们定义了一个广播,其中特定事件的动作字符串常量被用来初始化它们的Intent。为了在稍后BroadcastReceiver处理广播时过滤这些意图,您需要向IntentFilter添加相应的动作,该动作与BroadcastReceiver一起在系统中注册。

IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); filter.addAction(SMS_SENT_ACTION); filter.addAction(LOG_FILE_WRITTEN_ACTION); registerReceiver(broadcastReceiver, filter);

onCreate方法中的下一件重要事情是获取对PackageManager的引用,以确定您的 Android 设备是否支持电话功能。

packageManager = getPackageManager(); hasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);

PackageManager是一个系统工具类,用于解析全局包信息。您可以检查您的设备是否支持某些功能,或者设备上是否安装了某些应用,或者您可以访问自己的应用信息。在这个项目中,我们只对电话支持感兴趣,所以您调用带有常量FEATURE_TELEPHONYhasSystemFeature方法。

方法的最后一部分定义了通常的 UI 初始化。

setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); alarmTextView = (TextView) findViewById(R.id.alarm_text); smsTextView = (TextView) findViewById(R.id.sms_text); logTextView = (TextView) findViewById(R.id.log_text);

广播接收器

其他生命周期方法没有改变,所以您可以继续使用BroadcastReceiveronReceive方法。如您所见,添加了两个新的 else-if 子句来评估触发广播的操作。

else if (SMS_SENT_ACTION.equals(action)) { smsTextView.setText(R.string.sms_sent_message); } else if (LOG_FILE_WRITTEN_ACTION.equals(action)) { logTextView.setText(R.string.log_written_message); }

根据已经触发广播的动作,相应的TextView被更新。BroadcastReceiveronReceive方法运行在 UI 线程上,所以在这里更新 UI 是安全的。

可运行的实现

下一个有趣的部分是Runnable实现中的警报消息评估。

final byte alarmState = buffer[2]; final String alarmMessage = getString(R.string.alarm_message, getString(R.string.alarm_type_tilt_switch)); runOnUiThread(new Runnable() { @Override public void run() { if(alarmState == ALARM_ON) { linearLayout.setBackgroundColor(Color.RED); alarmTextView.setText(alarmMessage); } else if(alarmState == ALARM_OFF) { linearLayout.setBackgroundColor(Color.WHITE); alarmTextView.setText(R.string.alarm_reset_message); smsTextView.setText(""); logTextView.setText(""); } } }); if(alarmState == ALARM_ON) { sendSMS(alarmMessage); writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString()); }

检查报警系统的当前状态后,可以在runOnUiThread方法中更新相应的TextView s。如果报警被触发,将LinearLayout的背景颜色设置为红色,并将报警文本设置在alarmTextView上。如果警报被取消并重置,您将LinearLayout的背景颜色设置回白色,用文本更新alarmTextView以告知用户系统再次重置,并清除通知 SMS 和日志文件事件的TextView。根据警报的当前条件更新用户界面后,如果警报已被触发,您可以继续发送短信和写入日志文件。该实现封装在单独的方法中,我们将在接下来看到。

发送文字信息(SMS)

首先让我们看看如何在 Android 中发送短信。

private void sendSMS(String smsText) { if(hasTelephony) { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(SMS_DESTINATION, null, smsText, smsSentIntent, null); } }

这里,您在开始时设置的boolean标志用于检查连接的 Android 设备是否能够发送短信。如果不是,调用代码就没有任何意义了。为了能够发送 SMS,您需要首先获得对系统的SmsManager的引用。SmsManager类提供了一个方便的静态方法来获得系统的默认SmsManager实现。一旦你有了对SmsManager的引用,你就可以调用sendTextMessage方法,它需要几个参数。首先,你必须提供短信目的地号码。然后你可以提供一个服务中心地址。通常,您可以使用 null,以便使用默认的服务中心。第三个参数是您想要通过 SMS 发送的实际消息。最后两个参数是PendingIntent s,您可以提供在发送短信和收到短信时得到通知。您已经定义了PendingIntent来通知短信已经发送,所以您将在这里使用它来通知短信的发送。一旦发生这种情况,就会通知BroadcastReceiver相应地更新 UI。

将日志文件写入文件系统

顾名思义,writeToLogFile方法负责将日志文件写入设备文件系统上应用的存储目录。

private void writeToLogFile(String logMessage) { File logDirectory = getExternalLogFilesDir(); if(logDirectory != null) { File logFile = new File(logDirectory, "ProjectTwelveLog.txt"); if(!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { Log.d(TAG, "Log File could not be created.", e); } } BufferedWriter bufferedWriter = null; try { bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); bufferedWriter.write(logMessage); bufferedWriter.newLine(); Log.d(TAG, "Written message to file: " + logFile.toURI()); logFileWrittenIntent.send(); } catch (IOException e) { Log.d(TAG, "Could not write to Log File.", e); } catch (CanceledException e) { Log.d(TAG, "LogFileWrittenIntent was cancelled.", e); } finally { if(bufferedWriter != null) { try { bufferedWriter.close(); } catch (IOException e) { Log.d(TAG, "Could not close Log File.", e); } } } } }

在写入 Android 设备的外部存储器之前,您需要检查该存储器是否安装在 Android 系统中,并且当前未被其他系统使用,例如,当连接到计算机以传输文件时。为此,您可以使用另一种方法来检查外部存储的当前状态,并返回应用文件存储目录的路径。

private File getExternalLogFilesDir() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return getExternalFilesDir(null); } else { return null; } }

如果该方法返回有效的目录路径,您可以通过提供目录和文件名来创建一个File对象。

File logFile = new File(logDirectory, "ProjectTwelveLog.txt");

如果目录中不存在该文件,您应该先创建它。

if(!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { Log.d(TAG, "Log File could not be created.", e); } }

为了写入文件本身,您将创建一个BufferedWriter对象,它将一个FileWriter对象作为参数。通过提供对File对象的引用和一个boolean标志来创建FileWriterboolean标志定义要写入的文本是否应该附加到文件中,或者文件是否应该被覆盖。如果你想添加文本,你应该使用boolean标志true

bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); bufferedWriter.write(logMessage); bufferedWriter.newLine();

如果您完成了对文件的写入,您可以通过调用logFileWrittenIntent对象上的send方法来触发相应的广播。

logFileWrittenIntent.send();

总是关闭打开的连接以释放内存和文件句柄是很重要的,所以不要忘记在 finally 块中的BufferedWriter对象上调用close方法。这将关闭所有底层打开的连接。

bufferedWriter.close();

权限

java 编码部分到此为止。但是,如果您现在运行应用,当试图发送 SMS 或写入文件系统时,它会崩溃。这是因为这些任务需要特殊权限。您需要将android.permission.SEND_SMS权限和android.permission.WRITE_EXTERNAL_STORAGE权限添加到您的AndroidManifest.xml中。看看清单 10-5 中的是如何做到的。

清单 10-5。项目 12: AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" **package="project.twelve.adk"** android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <uses-feature android:name="android.hardware.usb.accessory" /> **<uses-permission android:name="android.permission.SEND_SMS" />** **<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />** `

<activity android:name=".ProjectTwelveActivity"
android:label="@string/app_name" android:screenOrientation="portrait">









`

最终结果

Android 应用现在可以部署到设备上了。也上传 Arduino 草图,连接两个设备,并查看您的报警系统的运行情况。如果你正确地设置了所有的东西,并把你的倾斜开关倾斜到一个垂直的位置,你应该得到一个看起来像图 10-10 的结果。

images

图 10-10。项目 12:最终结果

项目 13:带红外线光栅的摄像机报警系统

最终项目将是一个摄像头报警系统,当警报触发时,它能够快速拍摄入侵者的照片。硬件将或多或少与前一个项目相同。唯一的区别是,你将使用红外光栅触发警报,而不是倾斜开关。拍照后,照片将与记录警报事件的日志文件一起保存在应用的外部存储中。

零件

为了将 IR 挡光板集成到您之前项目的硬件设置中,您需要一些额外的部件。除了您已经使用的部件,您还需要一个额外的 220ω电阻、一个红外发射器或红外 LED 以及一个红外探测器。完整的零件清单如下所示(参见图 10-11 ):

  • ADK 董事会
  • 试验板
  • 2×220ω电阻
  • 2 X10 kω电阻
  • 一些电线
  • 纽扣
  • 红外挡光板(红外发射器、红外探测器)
  • 工作电压为 5V 的 LED
  • 压电蜂鸣器

images

图 10-11。项目 13 个部分(ADK 板、试验板、电线、电阻器、按钮、红外发射器(透明)、红外探测器(黑色)、压电蜂鸣器、红色 LED)

ADK 棋盘

红外线光栅电路将连接到 ADK 板的模拟输入端。模拟输入引脚将用于检测测量电压电平的突然变化。当 IR 检测器暴露于红外光波长的光时,所连接的模拟输入引脚上测得的输入电压将非常低。如果红外线照射中断,测得的电压将显著增加。

红外线光栅

你将在这个项目中建立的红外光栅将由两部分组成,一个红外发射器和一个红外探测器。发射器通常是普通的红外发光二极管,它发射波长约为 940 纳米的红外光。你会发现不同形式的红外发光二极管。单个红外 LED 的通常形式是标准的灯泡形 LED,但也可以发现类似晶体管形状的 LED。两者都显示在图 10-12 中。

images

图 10-12。灯泡形红外发光二极管(左),晶体管形红外发光二极管(右)

红外探测器通常是一个只有一个集电极和一个发射极连接器的两条腿光电晶体管。通常这两种元件作为匹配对出售,以创建 ir 光屏障电路。这种匹配装置通常以晶体管形状出售。(参见图 10-13 。)

images

图 10-13。配套 TEMIC K153P 中的红外探测器(左)和发射器(右)

匹配对组的优点是两个元件在光学和电学上匹配,以提供最佳的兼容性。我在这个项目中使用的红外发射器和探测器被称为 TEMIC K153P。你不必使用完全相同的一套。所有你需要的是一个红外 LED 和一个光电晶体管,你会达到同样的最终结果。您可能只需在稍后的代码中调整 IR 光栅电路中的电阻或警报触发阈值。红外光栅的典型电路如图图 10-14 所示。

images

图 10-14。普通红外线光栅电路

如前所述,其工作原理是,如果检测器(光电晶体管)暴露在红外光下,输出电压会降低。因此,如果您将手指或任何其他物体放在发射器和检测器之间,检测器对 IR 光的暴露就会中断,输出电压就会增加。一旦达到自定义的阈值,您可以触发警报。

设置

对于这个项目的设置,您基本上只需将您的倾斜开关电路与之前的项目断开,并将其替换为图 10-15 所示的 IR 电路。

images

图 10-15。项目 13:红外线光栅电路设置

如您所见,红外 LED(发射器)像普通 LED 一样连接。只需将+5V 电压连接到 220ω电阻的一根引线上,并将电阻的另一根引线连接到 IR LED 的正极引线上。红外 LED 的负极引线接地(GND)。红外光电晶体管的发射极引线接地(GND)。集电极引线必须通过一个 10k 电阻连接到+5V,此外还要连接到模拟输入 A0。如果不确定哪个引脚是哪个引脚,请查看元件的数据手册。

完整的电路设置,结合之前项目中的其他报警系统组件,看起来类似于图 10-16 。

images

图 10-16。项目 13:完成电路设置

软件

Arduino 软件部分只会略有变化。您将读取连接到红外光栅红外探测器的模拟输入引脚的输入值,而不是读取之前连接倾斜开关的数字输入引脚的状态。如果测得的输入值达到预定义的阈值,则会触发警报,并且与之前的项目一样,会发出警报声音,红色 LED 也会淡入淡出。一旦警报发送到连接的 Android 设备,应用的外部存储目录中将存储一个日志文件。如果有摄像头的话,这种设备现在可以拍照,而不是发送短信来通知可能的入侵者。一旦拍摄了照片,它也将与日志文件一起保存在应用的外部存储目录中。

Arduino 草图

正如我刚才描述的,这个项目的 Arduino 草图与项目 12 中使用的非常相似。它只需要一些微小的变化,以符合红外光栅电路。先看看完整的清单 10-6;之后我会解释必要的改变。

清单 10-6。项目 13: Arduino 草图

`#include <Max3421e.h>

include <Usb.h>

include <AndroidAccessory.h>

define LED_OUTPUT_PIN 2

define PIEZO_OUTPUT_PIN 3

define BUTTON_INPUT_PIN 4

#define IR_LIGHT_BARRIER_INPUT_PIN A0

#define IR_LIGHT_BARRIER_THRESHOLD 511

define NOTE_C7 2100

define COMMAND_ALARM 0x9

#define ALARM_TYPE_IR_LIGHT_BARRIER 0x2

define ALARM_OFF 0x0

define ALARM_ON 0x1

int irLightBarrierValue;
int buttonValue;
int ledBrightness = 0;
int fadeSteps = 5;

boolean alarm = false;

AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial");

byte sntmsg[3];

void setup() {
Serial.begin(19200);
acc.powerOn();
sntmsg[0] = COMMAND_ALARM;
sntmsg[1] = ALARM_TYPE_IR_LIGHT_BARRIER;
}

void loop() {
acc.isConnected();
irLightBarrierValue = analogRead(IR_LIGHT_BARRIER_INPUT_PIN);
if((irLightBarrierValue > IR_LIGHT_BARRIER_THRESHOLD) && !alarm) {
startAlarm();
}
buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) {
stopAlarm();
}
if(alarm) {
fadeLED();
}
delay(10);
}

void startAlarm() {
alarm = true;
tone(PIEZO_OUTPUT_PIN, NOTE_C7);
ledBrightness = 0;
//inform Android device
sntmsg[2] = ALARM_ON;
sendAlarmStateMessage();
}

void stopAlarm() {
alarm = false;
//turn off piezo buzzer
noTone(PIEZO_OUTPUT_PIN);
//turn off LED
digitalWrite(LED_OUTPUT_PIN, LOW);
//inform Android device
sntmsg[2] = ALARM_OFF;
sendAlarmStateMessage();
}

void sendAlarmStateMessage() {
if (acc.isConnected()) {
acc.write(sntmsg, 3);
}
}

void fadeLED() {
analogWrite(LED_OUTPUT_PIN, ledBrightness);
//increase or decrease brightness
ledBrightness = ledBrightness + fadeSteps;
//change fade direction when reaching max or min of analog values
if (ledBrightness < 0 || ledBrightness > 255) {
fadeSteps = -fadeSteps ;
}
}`

您可以看到,红外光栅的模拟引脚定义已经取代了倾斜开关引脚定义。

#define IR_LIGHT_BARRIER_INPUT_PIN A0

下一个新定义是红外光栅上电压变化的阈值。当红外探测器暴露在红外发射器下时,测得的电压输出非常低。读取的 ADC 值通常在低位两位数范围内。一旦 IR 曝光中断,电压输出会显著增加。现在,读取的 ADC 值通常在接近最大 ADC 值 1023 的范围内。介于 0 和 1023 之间的值是触发警报的理想阈值。如果您希望您的警报触发器仅对红外照明的微小变化做出更快的响应,您应该降低阈值。不过,值 511 是一个好的开始。

#define IR_LIGHT_BARRIER_THRESHOLD 511

要在引脚 A0 上存储 IR 光栅的读取 ADC 值,只需使用一个整数变量。

int irLightBarrierValue;

剩下的代码非常简单,在项目 12 中已经很熟悉了。在环路方法中,您需要做的唯一一件新事情是读取 IR 光栅模拟输入引脚上的 ADC 值,并检查它是否超过预定义的阈值。如果有,并且之前没有触发警报,您可以启动警报程序。

irLightBarrierValue = analogRead(IR_LIGHT_BARRIER_INPUT_PIN); if((irLightBarrierValue > IR_LIGHT_BARRIER_THRESHOLD) && !alarm) { startAlarm(); }

这并不难,是吗?让我们来看看在你的报警系统的 Android 软件方面你必须做些什么。

Android 应用

一旦 Android 应用接收到表示警报已经发生的数据消息,它将通过视觉方式通知用户该警报,并在应用的外部存储目录中另外写入一个日志文件。为了能够识别可能触发警报的入侵者,如果设备有内置摄像头,Android 应用将利用 Android camera API 来拍照。如果前置摄像头存在,它将是首选摄像头。如果设备只有一个后置摄像头,将使用这个摄像头。

为了在最后一个项目中提供一个更好的概述,我将清单分开来单独讨论。

变量和生命周期方法

在我进入细节之前,请看一下清单 10-7 。

清单 10-7。【项目 13:ProjectThirteenActivity.java(第一部分)

`package project.thirteen.adk;

import …;

public class ProjectThirteenActivity extends Activity {

private PendingIntent photoTakenIntent;
private PendingIntent logFileWrittenIntent; private static final byte COMMAND_ALARM = 0x9;
private static final byte ALARM_TYPE_IR_LIGHT_BARRIER = 0x2;
private static final byte ALARM_OFF = 0x0;
private static final byte ALARM_ON = 0x1;

private static final String PHOTO_TAKEN_ACTION = "PHOTO_TAKEN";
private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";

private PackageManager packageManager;
private boolean hasFrontCamera;
private boolean hasBackCamera;

private Camera camera;
private SurfaceView surfaceView;

private TextView alarmTextView;
private TextView photoTakenTextView;
private TextView logTextView;
private LinearLayout linearLayout;
private FrameLayout frameLayout;

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

mUsbManager = UsbManager.getInstance(this);
mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent(
ACTION_USB_PERMISSION), 0);
photoTakenIntent = PendingIntent.getBroadcast(this, 0, new Intent(
PHOTO_TAKEN_ACTION), 0);
logFileWrittenIntent = PendingIntent.getBroadcast(this, 0, new Intent(
LOG_FILE_WRITTEN_ACTION), 0);
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);
filter.addAction(PHOTO_TAKEN_ACTION);
filter.addAction(LOG_FILE_WRITTEN_ACTION);
registerReceiver(broadcastReceiver, filter);

packageManager = getPackageManager();
hasFrontCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT);
hasBackCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA);

setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
frameLayout = (FrameLayout) findViewById(R.id.camera_preview);
alarmTextView = (TextView) findViewById(R.id.alarm_text);
photoTakenTextView = (TextView) findViewById(R.id.photo_taken_text);
logTextView = (TextView) findViewById(R.id.log_text);
} /**

  • Called when the activity is resumed from its paused state and immediately
  • after onCreate().
    */
    @Override
    public void onResume() {
    super.onResume();

camera = getCamera();
dummySurfaceView = new CameraPreview(this, camera);
frameLayout.addView(dummySurfaceView);


}

/** Called when the activity is paused by the system. */
@Override
public void onPause() {
super.onPause();
closeAccessory();
if(camera != null) {
camera.release();
camera = null;
frameLayout.removeAllViews();
}
}

/**

  • Called when the activity is no longer needed prior to being removed from
  • the activity stack.
    */
    @Override
    public void onDestroy() {
    super.onDestroy();
    unregisterReceiver(broadcastReceiver);
    }

…`

ProjectThirteenActivity的第一部分显示了最终项目需要调整的初始化和生命周期方法。让我们快速浏览一下变量声明和定义。您可以看到,您再次使用PendingIntent s 用于通知目的。您将在日志文件写入事件和相机拍照事件中使用它们。

private PendingIntent photoTakenIntent; private PendingIntent logFileWrittenIntent;

接下来,您会看到与 Arduino 草图中使用的相同的警报类型字节标识符,用于将 IR 光栅识别为警报的触发源。

private static final byte ALARM_TYPE_IR_LIGHT_BARRIER = 0x2;

您还必须定义一个新的动作常量来标识稍后照片事件的广播。

private static final String PHOTO_TAKEN_ACTION = "PHOTO_TAKEN";

在这个项目中再次使用PackageManager来确定设备是否有前置摄像头和后置摄像头。

private PackageManager packageManager; private boolean hasFrontCamera; private boolean hasBackCamera;

您还将持有对设备摄像头的引用,因为您需要调用Camera对象本身的某些生命周期方法来拍照。SurfaceView是一个特殊的View元素,它会在你拍照前显示当前的相机预览。

private Camera camera; private SurfaceView surfaceView;

您可能还注意到有两个新的 UI 元素。一个是TextView,显示一段文字,表明照片已经被拍摄。第二个是一个FrameLayout View容器。这种容器用于将多个View叠加在一起以达到叠加效果。

private TextView photoTakenTextView; private FrameLayout frameLayout;

现在让我们看看在ProjectThirteenActivity的生命周期方法中你必须做什么。在onCreate方法中,你可以进行通常的初始化。同样,您必须为照片事件定义新的Pendingintent,并在IntentFilter注册广播动作。

photoTakenIntent = PendingIntent.getBroadcast(this, 0, new Intent(PHOTO_TAKEN_ACTION), 0); filter.addAction(PHOTO_TAKEN_ACTION);

再次使用PackageManager来检查设备特性。只是这次你要检查设备上的前置摄像头和后置摄像头。

hasFrontCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); hasBackCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA);

最后一步是通常的 UI 初始化。

setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); frameLayout = (FrameLayout) findViewById(R.id.camera_preview); alarmTextView = (TextView) findViewById(R.id.alarm_text); photoTakenTextView = (TextView) findViewById(R.id.photo_taken_text); logTextView = (TextView) findViewById(R.id.log_text);

这些是在创建Activity时必需的步骤。当应用暂停和恢复时,您还必须注意某些事情。当应用恢复运行时,您必须获取设备摄像头的引用。您还需要准备一个类型为SurfaceView的预览View元素,这样设备就可以呈现当前的摄像机预览并显示给用户。这个预览SurfaceView然后被添加到你的FrameLayout容器中显示。关于SurfaceView的实现细节,以及如何获得实际的摄像机参考,将在后面显示。

camera = getCamera(); dummySurfaceView = new CameraPreview(this, camera); frameLayout.addView(dummySurfaceView);

分别需要在应用暂停时释放资源。文档指出,您应该释放相机本身的句柄,以便其他应用能够使用相机。此外,您应该从FrameLayout中移除SurfaceView,这样当应用再次恢复时,容器中只存在一个新创建的SurfaceView

if(camera != null) { camera.release(); camera = null; frameLayout.removeAllViews(); }

这就是生命周期方法。

XML 资源定义

您看到您需要再次定义一个新的布局和一些新的文本,如清单 10-8 所示。

清单 10-8。项目 13: main.xml

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> **<FrameLayout android:id="@+id/camera_preview"** **android:layout_width="fill_parent"** **android:layout_height="fill_parent"** **android:layout_weight="1"/>** <TextView android:id="@+id/alarm_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000" android:text="@string/alarm_reset_message"/> **<TextView android:id="@+id/photo_taken_text"** **android:layout_width="wrap_content"** **android:layout_height="wrap_content"** **android:textColor="#00FF00"/>** <TextView android:id="@+id/log_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> </LinearLayout>

参考文本在strings.xml文件中定义,如清单 10-9 所示。

清单 10-9。项目 13: strings.xml

<?xml version="1.0" encoding="utf-8"?> <resources> **<string name="app_name">ProjectThirteen</string>** <string name="alarm_message">%1$s triggered an alarm!</string> <string name="alarm_reset_message">Alarm system is reset and active!</string> **<string name="alarm_type_ir_light_barrier">IR Light Barrier</string>** **<string name="photo_taken_message">Photo has been taken.</string>** <string name="log_written_message">Log has been written.</string> </resources>

BroadcastReceiver 和 Runnable 实现

现在让我们看看处理通信部分的BroadcastReceiverRunnable实现(清单 10-10 )。

清单 10-10。【项目 13:ProjectThirteenActivity.java(第二部分)

`…

private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (ACTION_USB_PERMISSION.equals(action)) {
synchronized (this) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
openAccessory(accessory);
} else {
Log.d(TAG, "permission denied for accessory " + accessory);
}
mPermissionRequestPending = false;
}
} else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) {
UsbAccessory accessory = UsbManager.getAccessory(intent);
if (accessory != null && accessory.equals(mAccessory)) {
closeAccessory();
}
} else if (PHOTO_TAKEN_ACTION.equals(action)) {
photoTakenTextView.setText(R.string.photo_taken_message);
} else if (LOG_FILE_WRITTEN_ACTION.equals(action)) {
logTextView.setText(R.string.log_written_message);
}
}
};

private void openAccessory(UsbAccessory accessory) {
}

private void closeAccessory() {

}

Runnable commRunnable = new Runnable() {

@Override
public void run() {
int ret = 0;
byte[] buffer = new byte[3];

while (ret >= 0) {
try {
ret = mInputStream.read(buffer);
} catch (IOException e) {
Log.e(TAG, "IOException", e);
break;
}

switch (buffer[0]) {
case COMMAND_ALARM:

if (buffer[1] == ALARM_TYPE_IR_LIGHT_BARRIER) {
final byte alarmState = buffer[2];
final String alarmMessage = getString(R.string.alarm_message,
getString(R.string.alarm_type_ir_light_barrier));
runOnUiThread(new Runnable() {

@Override
public void run() {
if(alarmState == ALARM_ON) {
linearLayout.setBackgroundColor(Color.RED);
alarmTextView.setText(alarmMessage);
} else if(alarmState == ALARM_OFF) {
linearLayout.setBackgroundColor(Color.WHITE);
alarmTextView.setText(R.string.alarm_reset_message);
photoTakenTextView.setText("");
logTextView.setText("");
}
}
});
if(alarmState == ALARM_ON) {
takePhoto();
writeToLogFile(new StringBuilder(alarmMessage).append(" - ")
.append(new Date()).toString());
} else if(alarmState == ALARM_OFF){
camera.startPreview();
}
} break;

default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
}
}
};

…`

一旦接收到相应的广播,只需增强BroadcastReceiver也对照片事件做出反应。您将更新photoTakenTextView以向用户显示已经拍摄了一张照片。

else if (PHOTO_TAKEN_ACTION.equals(action)) { photoTakenTextView.setText(R.string.photo_taken_message); }

Runnable实现评估接收到的消息。确定当前报警状态并设置报警消息后,您可以在runOnUiThread方法中相应地更新 UI 元素。

if(alarmState == ALARM_ON) { linearLayout.setBackgroundColor(Color.RED); alarmTextView.setText(alarmMessage); } else if(alarmState == ALARM_OFF) { linearLayout.setBackgroundColor(Color.WHITE); alarmTextView.setText(R.string.alarm_reset_message); photoTakenTextView.setText(""); logTextView.setText(""); }

在 UI 线程之外,您继续执行拍照和写入文件系统的额外任务。

if(alarmState == ALARM_ON) { takePhoto(); writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString()); } else if(alarmState == ALARM_OFF){ camera.startPreview(); }

这些方法调用不应该在 UI 线程上进行,因为它们处理可能阻塞 UI 本身的 IO 操作。在出现警报的情况下,拍照和写日志文件的实现将在下面的清单中显示。重置警报时,您还必须重置相机的生命周期,并开始新的相机图片预览。注意,在拍照之前必须调用startPreview方法。否则,您的应用将会崩溃。

使用相机

现在让我们看看新的 Android 应用真正有趣的部分:如何用设备的集成摄像头拍照(清单 10-11 )。

清单 10-11。【项目 13:ProjectThirteenActivity.java(第三部分)

`…

private Camera getCamera(){
Camera camera = null;
try {
if(hasFrontCamera) {
int frontCameraId = getFrontCameraId();
if(frontCameraId != -1) {
camera = Camera.open(frontCameraId);
}
}
if((camera == null) && hasBackCamera) {
camera = Camera.open();
}
} catch (Exception e){
Log.d(TAG, "Camera could not be initialized.", e);
}
return camera;
}

private int getFrontCameraId() {
int cameraId = -1;
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
CameraInfo cameraInfo = new CameraInfo();
Camera.getCameraInfo(i, cameraInfo);
if (CameraInfo.CAMERA_FACING_FRONT == cameraInfo.facing) {
cameraId = i;
break;
}
}
return cameraId;
}

private void takePhoto() {
if(camera != null) {
camera.takePicture(null, null, pictureTakenHandler);
}
}

private PictureCallback pictureTakenHandler = new PictureCallback() {

@Override
public void onPictureTaken(byte[] data, Camera camera) { writePictureDataToFile(data);
}
};

`

getCamera方法展示了两种获取设备摄像头引用的方法。Camera类提供了两个静态方法来获取引用。这里显示的第一个方法是open方法,它使用一个int参数通过 id 获取特定的摄像机。

camera = Camera.open(frontCameraId)

第二个 open 方法不带参数,返回设备的默认摄像机引用。这通常是后置摄像头。

camera = Camera.open();

不幸的是,要确定前置摄像头的 id,您必须检查设备提供的每个摄像头,并检查其方向以找到正确的摄像头,如getFrontCameraId方法所示。

takePhoto方法显示了如何指示相机拍照。为此,您调用了camera对象上的takePicture方法。takePicture方法有三个参数。这些参数是回调接口,提供了图片拍摄过程生命周期的挂钩。第一个是类型为ShutterCallback的接口,它在照相机捕捉到图片时被调用。第二个参数是PictureCallback接口,一旦相机准备好未压缩的原始图片数据,就会调用该接口。我只提供最后一个参数,也是一个PictureCallback,一旦当前图片的 jpeg 数据处理完毕,就会调用这个参数。

camera.takePicture(null, null, pictureTakenHandler);

PictureCallback接口的实现相当容易。你只需要实现onPictureTaken方法。

`private PictureCallback pictureTakenHandler = new PictureCallback() {

@Override
public void onPictureTaken(byte[] data, Camera camera) {
writePictureDataToFile(data);
}
};`

回调方法以字节数组的形式提供经过处理的 jpeg 数据,这些数据可以写入文件系统上的图片文件。

文件系统操作

文件系统操作如清单 10-12 中的所示。

清单 10-12。【项目 13:ProjectThirteenActivity.java(第四部分)

`…

private void writeToLogFile(String logMessage) {
File logFile = getFile("ProjectThirteenLog.txt"); if(logFile != null) {
BufferedWriter bufferedWriter = null;
try {
bufferedWriter = new BufferedWriter(new FileWriter(logFile, true));
bufferedWriter.write(logMessage);
bufferedWriter.newLine();
Log.d(TAG, "Written message to file: " + logFile.toURI());
logFileWrittenIntent.send();
} catch (IOException e) {
Log.d(TAG, "Could not write to Log File.", e);
} catch (CanceledException e) {
Log.d(TAG, "LogFileWrittenIntent was cancelled.", e);
} finally {
if(bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
Log.d(TAG, "Could not close Log File.", e);
}
}
}
}
}

private void writePictureDataToFile(byte[] data) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
String currentDateAndTime = dateFormat.format(new Date());
File pictureFile = getFile(currentDateAndTime + ".jpg");
if(pictureFile != null) {
BufferedOutputStream bufferedOutputStream = null;
try {
bufferedOutputStream = new BufferedOutputStream( new FileOutputStream(pictureFile));
bufferedOutputStream.write(data);
Log.d(TAG, "Written picture data to file: " + pictureFile.toURI());
photoTakenIntent.send();
} catch (IOException e) {
Log.d(TAG, "Could not write to Picture File.", e);
} catch (CanceledException e) {
Log.d(TAG, "photoTakenIntent was cancelled.", e);
} finally {
if(bufferedOutputStream != null) {
try {
bufferedOutputStream.close();
} catch (IOException e) {
Log.d(TAG, "Could not close Picture File.", e);
}
}
}
}
} private File getFile(String fileName) {
File file = new File(getExternalDir(), fileName);
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, "File could not be created.", e);
}
}
return file;
}

private File getExternalDir() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return getExternalFilesDir(null);
} else {
return null;
}
}
}`

获取或创建指定的File对象的任务已经被提取到它自己的方法中,称为getFile,以便在写入日志文件或图片文件时可以重用。写日志文件已经在之前的项目中描述过了,所以我将只关注writePictureDataToFile方法,它处理将图片数据写到应用的外部存储目录中的文件。

第一步是创建一个文件来写入数据。使用当前日期和时间作为文件名是一个好主意,这样您就可以很快看到照片是何时拍摄的。

SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); String currentDateAndTime = dateFormat.format(new Date()); File pictureFile = getFile(currentDateAndTime + ".jpg");

SimpleDateFormat类是一个工具类,用于将日期表示格式化为特定的形式。假设您的当前日期是 2012 年 12 月 23 日,您的时间是凌晨 1 点。格式化的String表示将是 2012-12-23-01-00-00。现在你只需要添加文件类型 jpeg 的文件结尾并创建你的File对象。

创建的File被提供给由BufferedOutputStream包裹的FileOutputStream,以将图片数据写入File。如果一切顺利,就可以发送描述照片已经拍摄并保存的广播。

bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(pictureFile)); bufferedOutputStream.write(data); Log.d(TAG, "Written picture data to file: " + pictureFile.toURI()); photoTakenIntent.send();

Activity的编码到此为止。

SurfaceView 实现

请记住,您仍然需要实现一个类型为SurfaceView的类,以便可以在您的应用中呈现相机预览。看看清单 10-13 中的,它显示了扩展了SurfaceView类的CameraPreview类。

清单 10-13。项目 13:CameraPreview.java

`package project.thirteen.adk;

import java.io.IOException;

import android.content.Context;
import android.hardware.Camera;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {
private static final String TAG = CameraPreview.class.getSimpleName();
private SurfaceHolder mHolder;
private Camera mCamera;

public CameraPreview(Context context, Camera camera) {
super(context);
mCamera = camera;

// Add a SurfaceHolder.Callback so we get notified when the
// underlying surface is created.
mHolder = getHolder();
mHolder.addCallback(this);
// deprecated setting, but required on Android versions prior to 3.0
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

public void surfaceCreated(SurfaceHolder holder) {
try {
mCamera.setPreviewDisplay(holder);
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
} catch (IOException e) {
Log.d(TAG, "Error setting camera preview: " + e.getMessage());
}
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// not implemented
}

@Override
public void surfaceDestroyed(SurfaceHolder holder) { // not implemented
}
}`

CameraPreview类只有两个字段:对设备摄像头的引用和一个所谓的SurfaceHolderSurfaceHolder是一个SurfaceView显示表面的接口,它将显示相机拍摄的预览图片。

private SurfaceHolder mHolder; private Camera mCamera;

CameraPreview的构造函数中,你通过调用SurfaceViewgetHolder方法来初始化SurfaceHolder,并分配一个回调接口来挂钩它的生命周期。

mHolder = getHolder(); mHolder.addCallback(this);

回调是必要的,因为您需要正确设置Camera对象,将完全初始化的SurfaceHolder作为预览显示。CameraPreview类本身实现了SurfaceHolder.Callback接口。你必须处理它的所有三个方法,但是你只需要完全实现surfaceCreated方法。当调用surfaceCreated回调方法时,SurfaceHolder被完全初始化,您可以将其设置为预览显示。此外,相机的方向在这里被设置为 90 度,以便在纵向模式下的 Android 手机将以通常的方向显示预览图片。请注意,平板电脑有另一个自然方向,因此您可能需要调整该方向值来满足您的需求。如果方向有问题,应该使用另一个旋转值。旋转值以度表示,可能的值为 0、90、180 和 270。在这里你也可以开始相机图片的第一次预览。记住,在你可以拍照之前,你必须调用startPreview方法来遵守Camera生命周期。

mCamera.setPreviewDisplay(holder); mCamera.setDisplayOrientation(90); mCamera.startPreview();

这就是 Java 编码部分,但是正如您从前面的项目中了解到的,您可能需要在您的AndroidManifest.xml文件中添加额外的权限定义。

权限

你已经知道你需要android.permission.WRITE_EXTERNAL_STORAGE许可。为了使用设备的摄像头拍照,您还需要获得android.permission.CAMERA许可。完整的AndroidManifest.xml文件显示在清单 10-14 中。

清单 10-14。项目 13: AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" **package="project.thirteen.adk"** android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <uses-feature android:name="android.hardware.usb.accessory" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> **<uses-permission android:name="android.permission.CAMERA" />** `

<activity android:name=".ProjectThirteenActivity"
android:label="@string/app_name" android:screenOrientation="portrait">









`

最终结果

现在,您已经为最终的项目测试运行做好了一切准备。将 Arduino 草图上传到 ADK 板上,在您的 Android 设备上部署您的 Android 应用,并查看您新的支持摄像头的报警系统(图 10-17 )。

images

图 10-17。项目 13:最终结果

总结

在这最后一章中,你建立了你自己的警报系统,包括你在整本书中了解的一些部分。你建造了两个版本的警报系统,一个由倾斜开关触发,另一个由自建的红外光栅触发。报警系统借助压电蜂鸣器和红色 LED 发出声音和视觉反馈。一个连接的 Android 设备通过提供发送通知短信或拍摄可能的入侵者的照片的可能性,增强了警报系统。您学习了如何以编程方式发送这些 SMS 消息,以及如何使用相机 API 来指示相机拍照。通过将警报事件保存到日志文件中,您还了解了在 Android 中保存数据的一种方法。

posted @ 2024-08-13 14:02  绝不原创的飞龙  阅读(231)  评论(0)    收藏  举报