这本书解释了如何在Android开放附件开发工具包(ADK)的帮助下为与外部硬件通信的Android设备建立项目。您将学习如何配置您的开发环境,如何选择硬件并相应地设置电路,以及如何对Android应用和硬件进行编程。这本书将教你实现自己想法所需的基础知识。通过几个项目,您将了解ADK兼容的硬件板、传感器和致动器的功能,以及如何通过Android应用与它们进行交互。
一般来说,任何对移动编程和硬件修补感兴趣的人都会喜欢ADK提供的众多可能性。有Android编程经验者优先,但不是完全必要。但是,您应该对Java编程语言和一般编程基础和算法有一个基本的了解。如果你以前做过电路实验,也会有所帮助。但是,如果您还没有,请不要担心,我将指导您完成电路设置,这样您就不会意外损坏您的硬件。这些项目被设计成建立在彼此的基础上,这样你就可以在这个过程中应用你已经学到的东西。总而言之,你应该享受实验和创新的乐趣。
除了你的计算机(你需要它来编程)和一个ADK兼容的硬件板,你还需要一套软件和硬件组件来完成本书中解释的项目。
硬件组件大多是基本部件,如led、电线以及其他无源和有源组件。我试图通过选择只需要基本的、负担得起的和容易获得的部件的项目来保持硬件成本最低。在特定项目中需要的地方详细描述了硬件部分。我预先编制了一份你在整本书中需要的所有必要硬件部件的清单,这样你就可以先把所有部件组装起来。没有什么比开始一个项目,却要等一个星期才能收到某些零件更让人恼火的了。
这本书分为十章。在介绍了开发环境和ADK兼容板的设置后,您将立即投入到第一个项目中,熟悉设置ADK实验的过程。在很大程度上,以下章节相互借鉴,将教会你从制作简单的LED闪烁到设计利用不同传感器的高级报警系统,以及使用Android设备的功能。下面是章节的快速总结:
2011年5月,谷歌举行了年度开发者大会GoogleIO,向大约5000名与会者展示了其最新技术。除了对谷歌API或核心搜索技术等已经众所周知的技术进行改进,谷歌还将重点放在了两大主题上:Chrome和Android。一如既往,Android平台的最新进展得到了展示和讨论,但谷歌稍后在Androidkeynote上宣布的内容有点令人惊讶:谷歌的第一个Android设备与外部硬件通信的标准。Android开放附件标准和附件开发工具包(ADK)将是与硬件通信和为Android设备构建外部附件的关键。为了鼓励开发,谷歌向感兴趣的与会者分发了ADK硬件包,并展示了一些ADK项目的例子,如将数据传输到连接的安卓设备的跑步机和可以用安卓设备控制的巨大倾斜迷宫。活动结束后不久,第一个DIY项目浮出水面,这已经显示了ADK的巨大潜力。
您可能只想一头扎进去,但是首先您应该了解ADK的具体情况,并设置您的开发环境。你不会在不知道怎么做或者没有合适的工具的情况下盖房子,是吗?
附件开发套件(ADK)基本上是一个微控制器开发板,它遵循Google创建的简单开放附件标准协议作为参考实现。尽管这可能是任何符合ADK兼容规范的主板,但大多数主板都基于Arduino设计,这是2005年创建的开放式硬件平台。这些板是基于ArduinoMega2560和Circuits@HomeUSBHostShield实现的支持USB的微控制器板。然而,还有其他已知的ADK兼容板设计,如基于PIC的板,甚至普通USB主机芯片板,如FTDI的VNCII。Google决定在ArduinoMega2560设计的基础上构建其参考套件,并以开源方式提供软件和硬件资源。这是一个聪明的举动,因为Arduino社区在过去的几年里发展迅速,使得设计师、爱好者和普通人可以很容易地将他们的想法变成现实。随着Android和Arduino爱好者群体的不断增长,ADK有了一个很好的开始。
为了与硬件板通信,支持Android的设备需要满足某些标准。随着AndroidHoneycomb版本3.1和backported版本2.3.4的推出,引入了必要的软件API。然而,这些设备还必须配备合适的USB驱动程序。该驱动程序支持通用USB功能,但特别支持所谓的附件模式。附件模式允许没有USB主机功能的Android设备与外部硬件通信,外部硬件反过来充当USB主机部分。
开放附件标准的规范规定,USB主机必须为USB总线提供电源,并且可以枚举连接的设备。根据USB2.0规范,外部设备必须在5V下提供500mA用于Android设备的充电目的。
ADK还为开发板提供固件,固件以一组源代码文件、库和一个演示套件草图的形式出现,演示套件草图是Arduino术语,指项目或源代码文件。固件关心USB总线的枚举,并寻找与附件模式兼容的连接设备。
谷歌还为Android设备提供了一个示例应用,可以轻松访问和演示参考板及其传感器和执行器的功能。如果您使用的衍生板没有相同种类的传感器,您仍然可以使用示例应用,但您可能希望将代码剥离到通信的基本部分。
当你在ADK建立一个硬件项目时,你是在打造一个所谓的安卓配件。您的硬件项目是Android设备的附件,例如,键盘是PC的附件,不同之处在于您的附件为整个系统提供动力。配件需要支持已经提到的设备电源,并且它们必须遵守Android配件协议。该协议规定附件遵循四个基本步骤来建立与Android设备的通信:
本节将概述目前市场上各种兼容ADK的开发板。请注意,我不能保证这个列表的完整性,因为社区发展的速度如此之快,以至于新的委员会可能会随时出现。在撰写本文时,我将集中讨论最受欢迎的主板。
谷歌ADK是在2011年5月的谷歌IO上展示的参考套件,它是第一个遵循开放附件标准的主板。该套件带有ADK基板和演示屏蔽,如图图1-1所示。
图1-1。谷歌ADK板和演示盾
基板(图1-2)包含DC电源连接器、连接手机或平板电脑的USB连接器(A型插座)以及连接电脑用于编程和调试的微型USB连接器(微型B型插座)。它的顶部安装了Atmel的ATmega2560AVR芯片,针对C编译代码进行了优化,这使得它非常快速且易于编程,而不是必须用汇编语言编程的可比微控制器。ATmega2560有一个256千字节的内部闪存和一个8位CPU,工作频率为16MHz。它提供8KB的SRAM和4KB的EEPROM。ATmega芯片的IO端口控制16个模拟引脚,提供10位输入分辨率,支持1,024个不同值的模数转换。默认情况下,它们的测量范围是从地到5V。该芯片有54个数字引脚,其中14个是PWM(脉宽调制)使能的,例如,允许led变暗或控制伺服系统。板的中间是一个复位按钮,用于复位板上的程序执行。该板的工作电压为5V。虽然您可以通过USB电缆为电路板供电,但如果您打算控制伺服系统或驱动电机,则应该考虑使用电源适配器。
图1-2。近距离观察谷歌ADK董事会
DemoShield是一个附加板,包含各种不同的传感器和执行器。Shield是Arduino术语,指可以放在Arduino基板上的扩展板。这种连接是通过可堆叠的引脚接头实现的。基板的IO引脚大多委托给屏蔽层的引脚,因此可以重复使用。然而,某些屏蔽可能会占用引脚来操作它们的传感器。演示屏蔽罩本身预焊有插头,因此没有额外的屏蔽罩可以堆叠在上面。这并不令人惊讶,因为shield使用大多数引脚来让基板与其所有传感器进行通信。由于屏蔽隐藏了基板的重置按钮,它本身包含一个按钮,因此您仍然可以使用重置功能。然而,最重要的部分是传感器和致动器,而且数量很多。
ArduinoADK(图1-3)是Arduino系列制造商自己生产的ADK兼容基板。它也基于ATmega2560,与Google参考板仅略有不同。
图1-3。ArduinoADK板
ArduinoADK板还有一个DC电源连接器和一个USB连接器(A型插座),用于连接Android设备。然而,编程和调试连接器与标准USB连接器不同(B型插座)。重置按钮位于电路板的远端,ATmega芯片位于电路板的中间。IO引脚布局与Googleboard中的完全相同,并且具有相同的模拟和数字引脚特性。然而,ArduinoADK有两个ICSP6针接头,用于微芯片的在线串行编程(ICSP)。ArduinoADK和谷歌ADK共享相同的引脚布局和外形,与演示盾和其他基于Arduino的盾兼容。
它的价格约为90美元(不包括可能的运输成本和税收),对于普通爱好者和硬件黑客来说,它比谷歌ADK更实惠。
IOIO(发音为yo-yo)板(图1-4)是一种基于PIC微控制器的开发板,由SparkfunElectronics在开放附件标准公布之前开发。
图1-4。火花乐趣IOIO板
IOIO板设计用于所有版本为1.5及以上的Android设备。最初的固件设计旨在与AndroidDebugBridge(ADB)配合使用,后者通常在Android应用的开发过程中用于调试过程和文件系统操作。在开放附件标准公布后,IOIO被更新为新的固件,以支持开放附件协议和作为后备的ADB协议,从而仍然支持较旧的设备。在撰写本书时,固件仍处于测试阶段。因为你需要通过PIC编程器更新板的固件,以使板ADK兼容,它可能不是一个没有经验的修补程序的完美选择。
该板的硬件特性如下。IOIO的外形尺寸约为常规ADK兼容板的四分之一,这使它成为目前最小的板之一。然而,它几乎跟上了它的老大哥的众多IO引脚。总共48个IO引脚中有许多都有几种工作模式,这可能会使引脚分配有点混乱。
在48个IO引脚中,所有引脚都可以用作通用输入输出端口。此外,其中16个引脚可用作模拟输入,3对引脚可用于IC通信,1个引脚可用作外设输入,28个引脚可用于外设输入和输出。通常,这些引脚只能承受3.3V电压,但22个引脚能够承受5V输入和输出。IC引脚提供快速简单的双线接口,与传感器板等外部集成电路通信。
除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可编程存储器和96KBRAM。
运费和税之前的价格约为50美元,这是最便宜的主板之一,但对初学者来说不是最友好的。
seedeuinoADK板(图1-5)也源自ATmega板,看起来与标准的ArduinoADK板非常相似,但乍一看,它有一些不错的额外功能。
图1-5。SeeeduinoADK董事会(图片由Seeedstudio提供)
它有56个数字IO引脚,其中14个支持PWM,16个模拟输入引脚和1个ICSP接头。板上的连接器与最初谷歌设计中的连接器类型相同。它有一个DC电源连接器、一个USB连接器(A型插座)和一个微型USB连接器(微型B型插座)。
与大多数其他类似Atmega的板的最大区别是,SeeeduinoADK板已经随MicroBridge固件一起发运,因此它可以在操作系统版本2.3.4及以上的Android设备上以ADK模式工作,在操作系统版本2.3.4之前的设备上以ADB模式工作,这与IOIO非常相似。
它的售价为79美元(不含运费和税),这使它成为一款非常实惠但功能强大的主板。
在你看到了最常见的支持ADK的主板后,你可能会想知道是否只有这些。虽然开放附件标准只有大约一年的历史,但已经可用的板的数量是令人难以置信的,在这个年轻但快速发展的开源硬件领域中还有许多。使用开放附件标准进行开发还有很多其他的可能性。一些代表纯粹的DIY(自己动手)方法,而另一些则是在ADK问世之前就已经使用的主板的扩展。
一种早期的方法是将ADK港移植到通用的ArduinoUno或Duemilanove。你唯一需要的是一个额外的USB主机屏蔽来连接Android设备。我是早期的DIY黑客中的一员,也是朝着这个方向发展的。当时,它是最初的谷歌参考板的唯一负担得起的替代品。如今,我不会推荐它;已经有完美的一体化主板,不需要额外的屏蔽、黑客攻击或剥离代码。如果你仍然想使用你的普通Arduino,有很多商店出售USB主机保护罩,你可以使用:
一些一体式主板还捆绑成套件,让您可以摆弄一堆传感器。这些套件通常提供一些与谷歌演示盾功能相同的传感器。
现在,您已经了解了支持开放附件标准的各种电路板,您可能想知道哪种电路板最适合您自己的项目。这总是一个难题,没有唯一的答案。你应该提前计划好你的项目,分析哪种板最合适。
如果你是硬件开发和ADK的初学者,你应该坚持使用最常用的板。在撰写本文时,这将是谷歌ADK董事会,它被分发给数百名参加谷歌IO2011的开发者。如果你不是幸运地收到这些主板中的一个,并且你的预算相当紧张——这是通常的情况——考虑标准的ArduinoADK主板。到目前为止,我所见过的大多数黑客和创客项目都使用这两种板,如果你有需要,它们周围有一个巨大的社区可以帮助你。
表1-1给出了正在讨论的电路板的概况。
随着越来越多支持Android的平板电脑的推出,在AndroidHoneycomb版本中,开放附件标准作为AndroidAPI的一部分被引入。为了不仅支持蜂窝设备,Google决定将必要的类移植到2.3.4版本,使它们也可以用于手机。较新的功能作为GoogleAPI附加库被反向移植。这个库基本上是一个JAR文件,必须包含在Android项目的构建路径中。
第一批获得必要版本更新并支持开放配件模式的候选产品是摩托罗拉Xoom和谷歌NexusS。其他设备很快就会跟进,这很快导致了众所周知的片段问题。通常,当涉及不同版本的操作系统时,片段通常是一个问题,但现在的问题是,即使设备有必要的操作系统版本2.3.4或3.1,开放附件模式仍有可能无法在设备上工作。怎么会这样呢?问题是仅仅更新系统软件是不够的。设备的USB驱动程序必须与开放附件模式兼容。很多开发者更新自己的设备甚至rooted安装了Cyanogenmod这样的自制Mod最终运行2.3.4版本,却发现设备厂商的USB驱动不兼容。
然而,有很多设备已经过测试,据说可以在开放附件模式下完美工作,其中一些是官方的,另一些是由DIYmods驱动的。以下是一些已经过社区验证可与ADK配合使用的设备列表:
就个人而言,我建议使用NexusS这样的谷歌开发者设备,因为这些设备对最新的API和功能提供了最好的支持。
你对ADK的历史和技术细节了如指掌,但是在你的想法变成现实之前,你需要建立你的工作环境。您需要为您的Android设备和硬件板编写软件,让双方能够相互通信,并控制执行器或读取传感器值。编程是在两个集成开发环境(ide)的帮助下完成的。要编写Android应用,Google建议使用EclipseIDE。EclipseIDE是最常见的Java开发IDE,拥有最大的社区之一、各种插件和出色的支持。由于硬件板基于Arduino设计,您将使用ArduinoIDE对它们进行编程,以编写所谓的草图,这些草图将被上传到板上。为了让这些ide正常工作,您还需要Java开发工具包(JDK),它比普通的Java运行时环境(JRE)具有更多的功能,并且您的系统可能已经安装了它。您还需要AndroidSDK来编写您的Android应用。
这个分步指南将帮助您建立必要的开发环境。请严格按照您选择的操作系统的步骤操作。如果您遇到任何问题,也可以参考软件网站上的官方安装指南。
图1-6。JDK下载页面
接受许可协议并为您的操作系统选择文件(图1-7)。x86文件适用于32位操作系统,x64文件必须安装在64位系统上,因此请确保选择正确的文件。
图1-7。JDK平台下载量
您可能会注意到,MacOS上没有JDK文件。这些并没有在甲骨文网站上发布,因为苹果提供了自己版本的JDK。JDK应该预装在您的MacOSX系统上。您可以通过在终端窗口中键入java-version来验证这一点。您应该会在终端窗口中看到您当前安装的Java版本。
下载完可执行文件后,打开它并按照指导您完成安装过程的说明进行操作。之后,您应该将JDK路径设置为您的WindowsPATH变量。path变量用于方便地从系统中的任何地方运行可执行文件。否则,您将不得不总是键入完整的路径,以便从命令行执行某些东西,比如C:\ProgramFiles\Java\jdk1.7.0\bin\java。
Eclipse也依赖于要设置的JAVA_HOME变量。要设置系统环境变量,您必须执行以下操作。
为您的系统(32位/64位)下载tar.gz文件,并将该文件移动到您想要安装JDK的位置。通常JDK安装在/usr/java/中,但是请记住,您需要root权限才能安装到该目录中。
打开包装并安装JDK,包括:
#tarzxvfjdk-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
exportPATHJAVA_HOMEexportCLASSPATH=.`
#chmod755java_env.sh
如前所述,Mac版JDK不是通过Oracle下载网站发布的。JDK预装在MacOSX上,但也可以通过AppleStore下载。如果您的环境变量JAVA_HOME和PATH尚未设置,您可以参考Linux安装中使用的相应步骤,因为MacOSX也是基于Unix的。您可以在终端窗口中使用以下命令检查变量是否已设置:
`#echo$JAVA_HOMEAndsimilarforthePATHvariable:
为了能够编写Android应用,你需要Android软件开发工具包,它提供了Google目前支持的所有Android版本的所有库和工具。
图1-11。AndroidSDK下载页面
该网站提供了适用于Windows、Linux和Mac的SDK的压缩文档。还有另一个Windows版本,它是一个可执行文件,应该会引导您完成安装过程。由于所有平台的初始设置都是相同的,因此没有特定于操作系统的步骤。
下载完SDK档案后,将其移动到您选择的位置并解压缩。您将看到add-ons和platforms目录都是空的。只有tools目录包含几个二进制文件。现在该目录中的重要文件是android脚本。它启动SDK和AVD管理器(图1-12)。
图1-12。SDK经理
SDK管理器是SDK的核心。它管理已安装的Android版本,并更新新版本和附加包。使用SDK和AVD管理器,你还可以设置模拟器来测试Android应用。
图1-13。SDK包安装
您可以通过单击“已安装和可用的软件包”来管理SDK安装,从而随时卸载和安装软件包。当SDK管理器下载完所有必要的包后,你会看到你的SDK目录已经增长,并且有了一些新的文件夹。我后面会讲到其中的一些,所以现在没有必要去理解。SDK现在已经可以开发了。
Eclipse集成开发环境是软件开发人员最常用的ide之一。它有一个巨大的支持社区和一些针对各种开发场景的最强大的插件。您将需要Eclipse来开发您的Android应用。编写Android应用的编程语言是Java。尽管您正在编写Java代码,但最终它将被编译成Android特有的dex代码,并打包成一个以文件结尾.apk的归档文件。这个存档文件将被放到您的Android设备上进行安装。
图1-14。月食下载网站
您应该选择Eclipse经典版,因为它没有针对其他目的进行预配置。单击您的系统类型(32位/64位)的下载按钮。现在选择下载的默认镜像页面或选择离你最近的镜像(图1-15)。
图1-15。下载镜像选择
根据您的操作系统,下载的文件可能是zip或tar.gz存档文件。您不需要安装Eclipse,因为它已经打包好可以运行了。将归档文件移动到您希望放置Eclipse的目录中,并提取它。EclipseIDE现在可以进行普通的Java开发了。然而,您需要安装一个额外的插件并进行一些配置,以便为Android开发准备Eclipse。
打开Eclipse,点击Help。选择InstallNewSoftware(图1-16)。
图1-16。Eclipse插件安装
图1-17。添加插件网站
将列出更新站点可用软件。选中DeveloperTools处的复选框,点击Next(图1-18)。
图1-18。插件包选择
将会显示一个安装摘要,您可以点击Next。最后一步是接受许可协议并点击Finish(图1-19)。
图1-19。许可协议
如果弹出一个对话框说真实性或有效性没有保证,点击OK。插件现在已经安装好了,Eclipse将提示您要么重启Eclipse,要么只应用更改。重启Eclipse总是一个好主意,这样在加载插件时就不会出现副作用。在您能够设置Android项目之前,您需要在Eclipse中设置一些首选项。在顶栏中选择Window和Preferences(图1-20)。
图1-20。配置首选项
在左侧列表中选择Android。如果你第一次点击它,会弹出一个对话框,要求你发送使用统计数据到谷歌服务器。您不需要允许这样做,但是您必须做出选择并点击Proceed关闭对话框。现在您必须配置您的AndroidSDK位置。点击Browse…,选择放置SDK安装的目录,如图图1-21所示。
图1-21。设置AndroidSDK路径
应用更改并点击OK。您已经完成了设置,现在准备开发Android应用。
与Eclipse相比,Arduino集成开发环境是一个非常小的IDE。您将使用它为基于Arduino的微控制器板编写代码。ArduinoIDE本身是基于Java的,但是您将使用c编写代码。IDE使用avr-gcc编译代码。Arduino的编写软件是一个所谓的草图。IDE(图1-22)本身包含一个带有语法高亮显示的代码编辑器,一个用于调试和信息目的的控制台,它通过串行连接(USB)连接到你的硬件板。IDE附带了许多方便的库,用于各种IO操作。Arduino社区有一个庞大的、不断增长的社区,由设计师、爱好者和开发人员组成,他们也制作各种各样的库和草图。
图1-22。ArduinoIDE
图1-23。Arduino下载网站
如果你使用的是Windows或者Linux,你必须下载一个存档文件,你可以把它放在任何你喜欢的目录下。之后,解压存档文件。不需要安装,IDE已经准备就绪。
如果你使用的是MacOSX系统,你必须下载一个.dmg文件,这是一个磁盘镜像。如果系统没有自动装载它,请双击该文件。安装后,您会看到一个名为Arduino.app的文件。将该文件移动到主目录中的应用文件夹中。IDE现在已经安装完毕,可以开始编码了。
外部硬件几乎总是需要所谓的驱动程序。驱动程序是一个软件,它让你的操作系统知道硬件。驱动程序使操作系统能够与外部设备通信。因为在编程阶段你有两个硬件设备要与之通信(ADK板和Android设备),所以你也必须为这些设备提供驱动程序。
归档文件只包含一个.inf文件。将你的ADK板连接到你的电脑上,你会被提示提供一个驱动程序或者让系统搜索一个。选择手动安装一个,指向提到的.inf文件。系统将安装驱动程序,并且系统知道您的ADK板。
Android设备还需要一个驱动程序,以便您能够部署和调试您编写的应用。在大多数情况下,您的AndroidSDK安装提供的通用驱动程序就足够了。当您在SDK管理器中安装额外的SDK包时,您选择了一个名为GoogleUSBDriverPackage,revisionx的包。当您第一次连接您的Android设备时,系统会提示您手动选择一个驱动程序或让系统搜索一个。再次选择手动分配驱动程序,并在%SDK_HOME%\extras\google\usb_driver的SDK安装目录中选择通用驱动程序。
虽然Fritzing是一个完全可选的开源软件组件,但并不一定需要,我想给你一个简短的概述,介绍它如何在以后的项目中帮助你。Fritzing是一个强大的原型制作工具,可以让您以不同的形式可视化您的电路和硬件设计。它旨在支持业余爱好者、设计师和制造商的项目文档、可视化和制造。
使用Fritzing,您可以创建如图1-24所示的试验板原理图。
图1-24。油炸面包板示意图
您可以将您的设计抽象为电路原理图,如图图1-25所示。
图1-25。烧结电路原理图
您甚至可以将您的设计转化为PCB设计(图1-26),以便日后生产。
图1-26。烧结PCB原理图
恭喜你!您已经完成了为Android开放附件开发套件设置开发环境的繁琐任务。你也了解了一些关于仍然年轻的ADK的历史,以及哪些硬件存在于野外。接下来的章节将带领你创建一个ADK项目。接下来,我将通过设置不同种类的实验和项目来描述您需要了解的关于ADK板和可能的外部元件的基础知识。每个项目教给你一个具体的细节,你可能需要在后续的项目。这些例子大部分是建立在彼此的基础上的;最后,您将利用目前所学的知识建立更复杂的项目。这些项目将基于最常见的ADK兼容板,ArduinoADK板。
现在你已经准备好了,让我们开始你的第一个项目。
既然你已经了解了Android和Arduino平台的基础知识,是时候让他们互相了解了。本章将指导你完成你的第一个Android开放附件开发包(ADK)项目。您将了解开放附件协议的细节以及它在双方是如何实现的。您将编写您将在本书中用于所有后续示例的代码基础。为了了解代码示例的生命周期和结构,您将从为这两个平台编写有史以来最受欢迎的“HelloWorld”开始。
您将从为Arduino和Android创建两个非常基本的项目开始,学习如何在这两个平台上设置项目。一步一步,你将实现必要的功能,让Android设备和ADK板相互识别时,他们连接。最后,您将实现实际的通信,从Android设备向ADK板发送文本消息,反之亦然。
首先,打开你安装的ArduinoIDE。如果您以前没有使用过ArduinoIDE,请不要害怕。它有一个非常清晰和基本的结构,它提供了足够的功能让你为Arduino平台进行适当的开发。
你会看到一个很大的文本可编辑区域,在这里你可以编写你的代码(图2-1)。代码编辑器提供了语法突出显示,但不幸的是,没有代码补全,这可能会使针对外部库的开发更加困难,因为您必须直接在库代码中查找函数定义。
图2-1。ArduinoIDE代码编辑器
编辑器的顶部是操作栏。操作栏可让您快速访问编译等功能,以及New、Open、Save和Upload等文件操作,并启动串行监视器进行调试或直接向连接的电路板发送命令。在顶部的系统栏中(如图图2-2所示),你可以选择更细粒度的操作,比如选择合适的板卡、通信端口、导入外部库等等。
图2-2。ArduinoIDE系统栏和操作栏
编辑器底部的状态栏显示你的编译和上传进度以及警告或编译错误(见图2-3)。关于ArduinoIDE更详细的解释,你可以参考Arduino网站上的原始文档。
图2-3。ArduinoIDE状态字段
Arduino草图有两个重要的方法。第一个是setup方法,它只在代码执行开始时运行一次。这是你初始化程序的地方。第二种是loop法。该方法无限循环运行,直到板复位。这是实现程序逻辑的地方。正如你在图2-4中看到的,Arduino草图的生命周期相当简单。
图2-4。草图生命周期
您也可以在同一个草图中定义自己的方法,或者链接其他源文件或库。要链接一段代码,可以使用C开发中已知的#include指令。包括放置在草图的最顶部。全局变量也必须在你的include指令下面的草图的顶部定义。由于ArduinoADK和大多数其他ADK板有256千字节的内存限制,你必须记住写干净的代码,这意味着没有代码重复,你必须坚持尽可能最小的数据类型,以便不耗尽内存。幸运的是,编译器总是向您显示编译后代码的确切大小,并在超出限制时发出警告。
现在是时候为Arduino编写你的“HelloWorld”程序了。在ArduinoIDE中,选择操作栏中的New。在新打开的编辑器窗口中,输入如清单2-1所示的代码。
清单2-1。ArduinoHelloWorld草图
`//includeswouldbeplacedhere
//constantdefinition
//globalvariabledefinitioncharhello[ARRAY_SIZE]={'h','e','l','l','o','','w','o','r','l','d','!'};
voidsetup(){//setbaudrateforserialcommunicationSerial.begin(115200);}
voidloop(){//printcharactersfromarraytoserialmonitorfor(intx=0;x 我们来谈谈代码是做什么的。首先,您为数组的大小定义了一个常数。然后定义了一个char数组,其大小为12。因为您的char数组中有12个字符的空间,所以您可以将“helloworld!”投入其中。 在设置方法中,为串行通信准备电路板。串行对象提供了简化串行通信的方法。要开始通信,您可以使用参数115200调用begin方法。这就是所谓的波特率,定义了每秒可以传输多少位。串行通信中的接收方必须配置相同的波特率,以正确读取传输数据。 如前所述,循环方法会无休止地运行,直到电路板复位。在loop方法中,在for循环的帮助下打印char数组的每个字符,for循环遍历所有元素。使用Serial类的print方法将元素打印到串行监视器上。您还会看到在每个字符打印输出之间调用了一个delay方法来降低输出速度,以获得更好的可读性。延迟参数以毫秒为单位。 当您输入所有内容后,单击操作栏中的Verify,查看您的代码是否可以编译而不出现错误。如果一切正常,你可以连接你的ADK板到你的电脑。您需要告诉IDE您将ADK板连接到了哪个端口,以及它实际上是哪种类型的板,这样IDE才能以正确的方式将程序代码传输到板上。 图2-5。ArduinoIDE板选择 图2-6。ArduinoIDE串口选择 完成所有配置后,点击操作栏中的上传。你的代码将被编译并转移到ADK董事会。完成后,状态字段将显示“上传完成”。代码现在由您的ADK板处理并开始执行。单击IDE操作栏中的串行监视器,打开串行监视器并查看打印输出。要正确查看传输的数据,请确保将波特率正确设置为代码中的115200。这是在串行监视器的右下角完成的。你现在应该看到“你好,世界!”在串行监视器窗口中反复打印,如图图2-7所示。 图2-7。ArduinoIDE串行监视器 恭喜你!你已经写好了你的第一张Arduino草图,并且已经做好了一切准备来开始真正的ADK部分。 如果你以前没有使用过Android,那么最好先熟悉一下这个平台的基础知识,了解一下它的关键组件。由于这本身就可以写满另一本书,我强烈建议你看看Android开发者页面,通读基础知识和基本原理,这样当我提到活动或广播接收者时,你就知道我在说什么了。下面简单介绍一下我刚才提到的基本组件。更多详情请参考Android开发指南。 图2-8。EclipseIDE新项目创建 一个新的对话框将会弹出,里面有很多项目类型可供选择。您需要选择AndroidProject,点击Next,如图图2-9所示。 图2-9。EclipseIDE项目类型选择 在下一个对话框中,您可以配置项目设置。输入项目的名称,如HelloWorld。然后选择一个构建目标。目标取决于您使用的设备。如果您使用Android版本2.3.4或更高版本的设备,则选择GoogleAPIsplatform2.3.3APIlevel10。如果您使用的是Android或更高版本的设备,请选择GoogleAPIsplatform3.1APIlevel12。(参见图2-10。) 图2-10。EclipseIDEAndroid项目向导(上半部分) 接下来,定义您的应用名称。也可以输入HelloWorld。现在选择你的包名。包名将是AndroidMarket中的唯一标识符,通常反映您的公司或组织的域名。对于这个例子,你可以输入helloworld.adk。检查显示CreateActivity的复选标记,并键入HelloWorldActivity。最小SDK版本字段描述了应用兼容的API级别。因为您将需要USBAPI特性,如果您使用2.3.4设备,您应该为API级别10选择10,否则为API级别12选择12。其余的设置可以保持不变。(参见图2-11。) 图2-11。EclipseIDEAndroid项目向导(下部分) 点击Finish,等待Eclipse建立您的第一个AndroidADK项目。你新创建的项目应该看起来像图2-12。 图2-12。helloworld项目的EclipseIDE包浏览器视图 如果您看一下左边的PackageExplorer区域,您会看到Eclipse用几个文件夹和文件创建了一个新项目。让我们看看他们都是什么意思。 首先你会看到的是src文件夹,它包含了你将要编写的所有Java源代码。您已经可以看到Eclipse已经构建了您配置的包结构,并将给定的HelloWorldActivity.java文件放入其中。在查看那个项目之前,您将进一步探索生成的项目。 除了src文件夹,还有一个文件夹叫gen。如果项目中的资源发生了变化,则每次构建项目时都会自动生成该文件夹及其内容。在那个文件夹里有一个叫做R.java的文件。这是一个文件,其中所有的资源都被索引并映射到整数。这些静态数字用于在代码中以一种简单的方式访问您的资源。你不应该接触gen文件夹,因为Android构建过程直接管理R.java文件,你手动做的每一个改变都会在下一次构建中被覆盖。 接下来你看到的是将在你的构建路径中使用的引用库。您应该看到GoogleAPIs,旁边是您配置的目标版本。如果您展开节点,您会看到项目引用了一个android.jar,它包含您选择的Android版本的系统类文件;一个usb.jar,它包含您与ADK设备进行USB通信所需的USB特定类;以及一个maps.jar,它用于GoogleMaps集成。 下一个文件夹叫做assets,它通常包含你必须自己管理的额外资源。您必须通过它们在应用中的相对路径来引用它们,因为构建系统不会将引用放入R.java文件中。 第二个资源文件夹是一个名为res的托管文件夹。放入该文件夹的每个资源都会在R.java文件中获得一个自动生成的id,以便于访问。你已经可以在res文件夹中看到几个子文件夹。drawable文件夹有一个带有屏幕密度标识符的后缀。如果在这些文件夹中为不同的屏幕密度提供不同的图像,系统将在执行应用时选择正确的文件。还有一个文件夹叫做layout。该文件夹包含定义应用UI的xml布局文件。一个布局文件可以有多个视图元素,这些元素构成了您的用户界面。您看到的最后一个文件夹是values文件夹。在values文件夹中,你可以为静态资源定义xml文件,比如字符串、维度、数组等等。 除了所有这些文件夹,你还会看到一些独立的文件:AndroidManifest.xml、default.properties和proguard.cfg。您唯一感兴趣的文件是AndroidManifest.xml,它是您的应用的中央注册表。关于活动、服务、权限、强制设备特性和许多其他特性的细节必须在这个文件中定义。 生成的项目不仅仅是一个骨架。它已经是一个成熟的应用,尽管非常初级。它只显示“HelloWorld,HelloWorldActivity”。让我们来看看这个应用的结构和生命周期。每个Android应用的起点是AndroidManifest.xml。打开它,看看它的结构(清单2-2)。 清单2-2。AndroidManifest.xml 您可以看到一个application标签,它的属性定义了应用的icon和label。这两个资源都用xml资源语法@resource-type/id引用。用@string/app_name引用的应用名称将取自res/values文件夹中strings.xml文件中定义的字符串。相同的资源查找语法适用于drawables和您将在任何xml文件中看到的所有其他资源。 活动节点中重要的是intent-filter。intent-filter定义了系统如何启动活动以及如何触发活动。 在action标签中,您可以看到该活动被标记为应用的主活动,并且将在用户启动应用时首先启动。 category标签指定了启动器类别,这意味着应用链接将放入设备上的应用概览菜单中,将启动您的活动。 在与application标签相同的层次级别上,您可以看到uses-sdk标签,它定义了AndroidAPI级别版本,并且是必需的。在该层级中,您还可以定义用户在安装过程中必须授予的权限,例如访问附件。 您已经知道应用的起点在哪里。现在让我们看看你的主要活动实际上是做什么的。打开HelloWorldActivity.java文件并查看其内容(清单2-3)。 清单2-3。HelloWorldActivity.java `packagehelloworld.adk; importandroid.app.Activity;importandroid.os.Bundle; publicclassHelloWorldActivityextendsActivity{/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);}}` 如你所见,这个文件很小。该类必须扩展Activity类,它是Android系统的一部分。因为活动负责提供用户界面,所以您必须提供一些视图来显示给用户。视图通常在活动创建时加载。Android平台提供了活动生命周期的挂钩,这样您就可以创建资源、运行应用逻辑,并在活动的相应阶段进行清理。一个Android活动的生命周期比一个简单的Arduino草图要复杂一些,如图2-13所示。 你可以看到setContentView方法是以布局资源为参数调用的。该方法采用layout/main.xml中的布局定义,并将其所有视图呈现在设备屏幕上。如果你打开这个main.xml文件,你可以看到它只定义了两个视图元素(见清单2-4)。 清单2-4。布局文件main.xml ` LinearLayout是一个可以容纳其他视图或容器的容器视图。layout_width和layout_height属性被设置为在设备的整个屏幕上延伸。orientation属性指定所包含的元素应该垂直对齐。目前包含在LinearLayout中的唯一元素是TextView元素。从它的属性可以看出,它应该填充屏幕的宽度,但应该只与它自己的内容一样高。TextView的文本由来自strings.xml文件的@string/hello引用解析。如果您在Eclipse中从xml编辑器切换到图形布局编辑器,您应该已经在虚拟设备屏幕上看到文本“HelloWorld,HelloWorldActivity!”。现在够了。让我们在真实设备上看看这个应用。 将您的Android设备连接到您的计算机,右键单击该项目,选择RunAs,然后选择AndroidApplication。您的应用应该被打包成一个apk文件,并被推送到设备上进行安装。如果一切正常,您应该看到应用在您的设备上启动。例如,如果系统由于缺少驱动程序而无法识别你的设备,它将使用默认的Android虚拟设备(AVD)启动一个模拟器。当应用启动后,你应该会看到类似图2-14的内容。 图2-14。运行在安卓设备上的HelloWorld应用 恭喜你!您已经编写了您的第一个Arduino应用和第一个Android应用。现在,您对设置项目和为两个平台编写代码已经稍微熟悉了一些,让我们看看这两个设备如何在开放附件协议的帮助下相互识别。 您先前下载的ADK参考包包含两个库,您将需要它们来建立USB通信。一个是USB_Host_Shield库的修改版本,最初是由OlegMazurov为Circuits@Home创建的。该库最初设计用于ArduinoUSB主机保护。由于ADK兼容板上的USB芯片相当于USBhostshield,因此只对库进行了一些小的修改。第二个库是AndroidAccessory库,负责实现开放附件协议。将在ADK_release_xxxx\firmware\arduino_libs\找到的两个库文件夹复制到arduino-xxxx\libraries的ArduinoIDE安装的库文件夹中。修改你的ArduinoHelloWorld草图,如清单2-5所示。 清单2-5。HelloWorldSketch扩展识别安卓设备 `#include #include AndroidAccessoryacc("Manufacturer","Model","Description","Version","URI","Serial"); charhello[ARRAY_SIZE]={'h','e','l','l','o','','w','o','r','l','d','!'}; voidsetup(){Serial.begin(115200);acc.powerOn();} voidloop(){if(acc.isConnected()){for(intx=0;x 如您所见,准备开放式配件沟通的草图只需三处改动。这里你要做的第一件事是初始化一个AndroidAccessory对象,这个实现了开放附件协议,这样你就不用担心了。你用一些描述性的字符串来初始化它,这些字符串定义了你创建的附件。这些字段是不言自明的。最重要的参数是Manufacturer、Model和Version。它们将在Android应用中用于验证您是否与正确的ADK板通信。此外,如果没有安装安卓应用,安卓系统使用URI参数来寻找合适的安卓应用。这可以是一个链接到Android市场或产品页面。 在setup例程中,您用powerOn方法将对象设置为活动状态。loop例程在每个循环中检查是否有东西连接到附件,然后才执行其中的代码。在这个方便的方法中,实现了实际的连接协议。 只用了三行代码,ADK板就能识别支持附件模式的连接的Android设备。就这么简单。如果你把代码上传到你的ADK板上,你会看到“你好,世界!”只有当您将Android设备连接到ADK板时,才会打印到串行监视器。您将在串行监视器上看到如下内容: `Deviceaddressed...Requestingdevicedescriptor.foundpossibledevice.switchingtoserialmodedevicesupportsprotcol1 Deviceaddressed...Requestingdevicedescriptor.foundandroidaccessorydeviceconfigdescinterfacedescinterfacedesc57` 这个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包中的类。 如您所见,这里添加了第二个意图过滤器,它对USB_ACCESSORY_ATTACHED动作做出反应。当您将Android设备连接到ADK兼容板时,这将触发HelloWorldActivity。还添加了一个新元素。元数据标签引用额外的资源,这些资源可被提供给意图过滤器以进一步细化过滤机制。这里引用的accessory_filter.xml定义了一个更细粒度的过滤标准,只匹配您的附件,不匹配其他附件。在/res文件夹中创建一个名为xml的文件夹。在创建的文件夹中添加一个名为accessory_filter.xml的新文件。现在将清单2-6中的内容添加到xml文件中。 清单2-6。定义附件过滤的元文件 现在,您已经确保了两台设备能够相互识别,但您希望它们能够真正相互通话。通信是在一个相当简单的自定义协议中完成的。消息通过字节流发送和接收。在Android应用中,这是通过读写一个特殊文件的输入输出流来完成的。在Arduino端,AndroidAccessory类提供了读写消息的方法。对于通信协议应该是什么样子没有限制。在示例demokit应用中,Google将消息定义为3字节长的字节数组。(参见图2-15)。第一个字节是命令类型。它定义了传输哪种类型的消息。在demokit应用中使用的命令是伺服系统、发光二极管、温度传感器和许多其他设备的命令类型。第二个字节是该命令的实际目标。谷歌演示盾有多个发光二极管和伺服连接器,并解决适当的,目标字节使用。第三个字节是应该发送到该目标或从该目标接收的值。一般来说,当您自己实现这些消息时,您可以选择您想要的任何消息结构,但是我建议您坚持使用示例,因为您可能会在整个Web上找到基于相同消息结构的教程和示例。请记住,您只能传输字节,因此您必须相应地转换更大的数据类型。在大多数例子中,你也会遵循这个惯例。 图2-15。Google在demokit应用中定义的默认消息协议 然而,对于第一个例子,你必须稍微改变一下规则,定义一个自定义协议来传输文本信息(见图2-16)。传输的数据也是字节数组,但形式略有不同。第一个字节将定义命令类型,第二个字节将定义目标,第三个字节定义文本消息的长度(不超过252个字节),最后剩余的字节定义实际的文本消息。 图2-16。发送和接收短信的自定义短信协议 Arduino草图中的通信实现非常简单。如清单2-7中的所示扩展草图。 清单2-7。helloworldSketch中的通信实现 **#defineARRAY_SIZE25 charhello[ARRAY_SIZE]={'H','e','l','l','o','','W','o','r','l','d','','f','r','o','m','','A','r','d','u','i','n','o','!'}; bytercvmsg[255];bytesntmsg[3+ARRAY_SIZE]; **voidloop(){if(acc.isConnected()){//readthesenttextmessageintothebytearrayintlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_TEXT){if(rcvmsg[1]==TARGET_DEFAULT){//getthetextLengthfromthechecksumbytebytetextLength=rcvmsg[2];inttextEndIndex=3+textLength;//printeachcharactertotheserialoutputfor(intx=3;x sntmsg[0]=COMMAND_TEXT;sntmsg[1]=TARGET_DEFAULT;sntmsg[2]=ARRAY_SIZE;for(intx=0;x 让我们看看这里有什么新内容。因为您想要向Android设备发送更具体的文本,所以您需要更改文本消息及其大小常量。 #defineARRAY_SIZE25charhello[ARRAY_SIZE]={'H','e','l','l','o','','W','o','r','l','d','','f','r','o','m','','A','r','d','u','i','n','o','!'}; 请注意,要发送的字节数组的大小等于消息本身加上命令类型、目标和校验和的附加字节。命令类型字节和目标字节也可以定义为常量。 `#defineCOMMAND_TEXT0xF 在循环方法中,您将处理消息的接收和发送。首先看一下信息是如何被接收的: if(acc.isConnected()){//readthesenttextmessageintothebytearrayintlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_TEXT){if(rcvmsg[1]==TARGET_DEFAULT){//getthetextLengthfromthechecksumbytebytetextLength=rcvmsg[2];inttextEndIndex=3+textLength;//printeachcharactertotheserialoutputfor(intx=3;x AndroidAccessory对象的read方法读取inputstream并将其内容复制到提供的字节数组中。作为参数,read方法采用应该填充的字节数组、该字节数组的长度以及一个阈值,以防传输未被确认。之后,执行检查以查看是否传输了正确的命令和目标类型;只有这样,才能确定传输消息的长度。从for循环中的字节数组读取实际的文本消息,并逐字符打印到串行输出。收到一条消息后,另一条消息被发送到Android设备,如下所示: if(acc.isConnected()){…sntmsg[0]=COMMAND_TEXT;sntmsg[1]=TARGET_DEFAULT;sntmsg[2]=ARRAY_SIZE;for(intx=0;x 同样,要发送的字节数组是根据自定义协议构建的。第一个字节设置为命令类型常量,第二个字节设置为目标常量,第三个字节设置为作为校验和的实际文本消息的大小。现在,程序遍历hellochar数组,用文本消息填充字节数组。当字节数组设置完毕后,调用AndroidAccessory对象的write方法,通过outputstream将数据传输到Android设备。write方法有两个参数:要传输的字节数组和传输的字节大小。 正如你所看到的,Arduino草图非常简单,AndroidAccessory对象为你做了所有的脏工作。现在让我们来看看Android通信部分。 在Android中实现通信部分比在Arduino端需要更多的工作。扩展HelloWorldActivity类,如清单2-8中的所示。此后,您将了解每个代码片段的作用。 清单2-8。【HelloWorldActivity.java(进口和变量)】 importjava.io.FileDescriptor;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.IOException; importandroid.app.Activity;importandroid.app.PendingIntent;importandroid.content.BroadcastReceiver;importandroid.content.Context;importandroid.content.Intent;importandroid.content.IntentFilter;importandroid.os.Bundle;importandroid.os.ParcelFileDescriptor;importandroid.util.Log;importandroid.widget.TextView; importcom.android.future.usb.UsbAccessory;importcom.android.future.usb.UsbManager; publicclassHelloWorldActivityextendsActivity{ privatestaticfinalStringTAG=HelloWorldActivity.class.getSimpleName(); privatePendingIntentmPermissionIntent;privatestaticfinalStringACTION_USB_PERMISSION="com.android.example.USB_PERMISSION";privatebooleanmPermissionRequestPending; privateUsbManagermUsbManager;privateUsbAccessorymAccessory;privateParcelFileDescriptormFileDescriptor;privateFileInputStreammInputStream;privateFileOutputStreammOutputStream; privatestaticfinalbyteCOMMAND_TEXT=0xF;privatestaticfinalbyteTARGET_DEFAULT=0xF; privateTextViewtextView; …` 图2-17。月食日志视图 COMMAND_TEXT和TARGET_DEFAULT与Arduino草图中使用的常量相同。它们构成了数据协议的前两个字节。 与外部设备建立连接必须得到用户的许可。当用户授予连接到您的ADK板的权限时,PendingIntent将广播ACTION_USB_PERMISSION,并带有一个反映用户是确认还是拒绝访问的标志。布尔变量mPermissionRequestPending仅用于在用户交互仍未完成时不再显示权限对话框。 UsbManager是一项系统服务,用于管理与设备USB端口的所有交互。它用于列举连接的设备,并请求和检查连接到附件的许可。UsbManager还负责打开与外部设备的连接。USB存储器是连接附件的参考。ParcelFileDescriptor是在与附件建立连接时获得的。它用于访问附件的输入和输出流。 唯一用户可见的UI元素是textView,它应该显示从ADK板传输的消息。 清单2-9。【HelloWorldActivity.java(生命周期方法)】 `/**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); mUsbManager=UsbManager.getInstance(this);mPermissionIntent=PendingIntent.getBroadcast(this,0,newIntent(ACTION_USB_PERMISSION),0);IntentFilterfilter=newIntentFilter(ACTION_USB_PERMISSION);filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);registerReceiver(mUsbReceiver,filter); setContentView(R.layout.main);textView=(TextView)findViewById(R.id.textView);} /**CalledwhentheactivityisresumedfromitspausedstateandimmediatelyafteronCreate().*/@OverridepublicvoidonResume(){super.onResume(); if(mInputStream!=null&&mOutputStream!=null){return;} UsbAccessory[]accessories=mUsbManager.getAccessoryList();UsbAccessoryaccessory=(accessories==nullnull: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,"mAccessoryisnull");}} /**Calledwhentheactivityispausedbythesystem.*/@OverridepublicvoidonPause(){super.onPause();closeAccessory();} /**Calledwhentheactivityisnolongerneededpriortobeingremovedfromtheactivitystack.*/@OverridepublicvoidonDestroy(){super.onDestroy();unregisterReceiver(mUsbReceiver);}` 每个活动类的第一个生命周期回调方法是onCreate方法。这个方法通常是你进行基本初始化的地方。不过,要小心。onCreate方法只在系统创建活动时调用一次。该活动将继续存在,直到系统需要释放内存并终止它,或者如果您显式调用该活动的finish方法来告诉系统不再需要该活动。 setContentView(R.layout.main);textView=(TextView)findViewById(R.id.textView);}` 在onCreate方法中,您需要做的最后一件事是设置您的UI元素,以便用户可以实际看到正在发生的事情。您已经了解到Android中的UI布局大部分是在xml文件中定义的。再次使用setContentView方法加载布局。代码中的最后一行用于从布局中获取对视图元素的引用,以便可以在代码中对其进行管理。findViewById方法获取一个视图标识符,并返回该引用的通用视图元素。这就是为什么需要对视图元素的正确实现进行强制转换的原因。为了能够从布局xml文件中引用视图,这些视图需要定义一个标识符。在res/layout/打开main.xml文件,并将id属性添加到TextView。 这里可以看到动态资源生成的新语法。语法@+id/textView意味着该视图元素应该分配有idtextView。id前面的加号意味着,如果这个id在R.java文件中不存在,那么应该在那里创建一个新的引用。 `@OverridepublicvoidonResume(){super.onResume(); UsbAccessory[]accessories=mUsbManager.getAccessoryList();UsbAccessoryaccessory=(accessories==nullnull: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,"mAccessoryisnull");}}` 如果输入和输出流仍然是活动的,那么你可以进行通信,并且可以从onResume方法中提前返回。否则,您必须从UsbManager获取附件的参考。如果您已经拥有与设备通信的用户权限,您可以打开并重新分配输入和输出流。这部分是在一个名为openAccessory的方法中实现的,稍后会详细介绍。这里我要讲的最后两个生命周期方法是onPause方法和onDestroy方法。 `@OverridepublicvoidonPause(){super.onPause();closeAccessory();} @OverridepublicvoidonDestroy(){super.onDestroy();unregisterReceiver(mUsbReceiver);}` onResume方法的反面是onPause方法。您已经了解了活动的暂停状态,因为您在onResume方法中打开了到附件的连接,所以您应该注意在onPause方法中关闭连接以释放内存。 如果调用生命周期方法onDestroy,您的活动将被终止,并且不再出现在应用活动堆栈中。这个生命周期阶段可以描述为onCreate方法的对立面。尽管您在onCreate阶段完成了所有的初始化工作,但是您将在onDestroy阶段完成反初始化和清理工作。因为应用只有这一个活动,所以当onDestroy在活动上被调用时,应用也会被系统杀死。您在创建时注册了一个广播接收器,以收听特定于配件的事件。由于应用不再存在,你应该在这里注销这个广播接收器。为此,使用广播接收器作为参数调用unregisterReceiver方法。 生命周期方法到此结束。到目前为止,这相当容易。通信部分的实现一开始看起来有点棘手,但是不要担心。我会引导你完成它(见清单2-10)。 清单2-10。【HelloWorldActivity.java(建立附件连接) privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(ACTION_USB_PERMISSION.equals(action)){synchronized(this){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,false)){openAccessory(accessory);}else{Log.d(TAG,"permissiondeniedforaccessory"+accessory);}mPermissionRequestPending=false;}}elseif(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(accessory!=null&&accessory.equals(mAccessory)){`closeAccessory();}}}}; privatevoidopenAccessory(UsbAccessoryaccessory){mFileDescriptor=mUsbManager.openAccessory(accessory);if(mFileDescriptor!=null){mAccessory=accessory;FileDescriptorfd=mFileDescriptor.getFileDescriptor();mInputStream=newFileInputStream(fd);mOutputStream=newFileOutputStream(fd);Threadthread=newThread(null,commRunnable,TAG);thread.start();Log.d(TAG,"accessoryopened");}else{Log.d(TAG,"accessoryopenfail");}} privatevoidcloseAccessory(){try{if(mFileDescriptor!=null){mFileDescriptor.close();}}catch(IOExceptione){}finally{mFileDescriptor=null;mAccessory=null;}}` 这里首先看到的是BroadcastReceiver的实现。现在您知道您需要在各自的生命周期方法中注册和注销广播接收器,但是现在让我们看看应该如何实现广播接收器。 `privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){ @OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(ACTION_USB_PERMISSION.equals(action)){synchronized(this){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,false)){openAccessory(accessory);}else{Log.d(TAG,"permissiondeniedforaccessory"+accessory);}mPermissionRequestPending=false;}}elseif(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(accessory!=null&&accessory.equals(mAccessory)){closeAccessory();}}}};` 广播接收器是作为broadcastreceiver类型的匿名内部类实现的。您必须覆盖的唯一方法是onReceive方法,如果该广播接收器已注册并与提供的intent-filter匹配,则系统会调用该方法。记住,在意图过滤器中定义了两个动作。您必须检查在调用广播接收器时发生了什么操作。如果您收到描述许可请求已被回应的操作,您必须检查用户是否被授予与您的配件通信的许可。如果是这样,您可以打开附件的通信通道。可能已经触发广播接收器的第二个动作是附件已经从Android设备分离的通知。在这种情况下,你需要清理并关闭你的沟通渠道。如您所见,BroadcastReceiver调用openAccessory方法和closeAccessory方法来打开和关闭附件的通信通道。接下来我们来看看那些方法。 在openAccessory方法中,您委托USB服务方法(也称为openAccessory)来获取附件的FileDescriptor。FileDescriptor管理您将用来与设备通信的输入和输出流。一旦流被分配,你也将启动一个单独的线程,该线程将实际接收和发送消息,但稍后会详细介绍。 如果您还没有权限连接到您的附件,并且您没有处于用户权限的挂起状态,您必须通过调用USB服务上的requestpermission方法来请求您的附件的权限。requestPermission方法有两个参数,您请求权限的附件和待定意向。这个挂起的意图是您在onCreate方法中定义的mPermissionIntent,它负责在用户授予或拒绝与附件通信的权限时发送带有ACTION_USB_PERMISSION的广播。您可能还记得,您还在onCreate方法中注册了一个广播接收器,该方法为完全相同的操作提供了一个intent-filter。一旦广播被发送,广播接收器将对其做出反应。 closeAccessory方法负责关闭附件的所有剩余的打开连接。 privatevoidcloseAccessory(){try{if(mFileDescriptor!=null){mFileDescriptor.close();}}catch(IOExceptione){}finally{mFileDescriptor=null;mAccessory=null;}} 当您最终打开与附件的连接时,您可以来回发送和接收数据。清单2-11显示了实际的通信实现。 清单2-11。【HelloWorldActivity.java(通信实施) `RunnablecommRunnable=newRunnable(){ @Overridepublicvoidrun(){intret=0;byte[]buffer=newbyte[255]; while(ret>=0){try{ret=mInputStream.read(buffer);}catch(IOExceptione){break;} switch(buffer[0]){caseCOMMAND_TEXT: finalStringBuildertextBuilder=newStringBuilder();inttextLength=buffer[2];inttextEndIndex=3+textLength;for(intx=3;x runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){textView.setText(textBuilder.toString());}});sendText(COMMAND_TEXT,TARGET_DEFAULT,"HelloWorldfromAndroid!");break; default:Log.d(TAG,"unknownmsg:"+buffer[0]);break;} }}};publicvoidsendText(bytecommand,bytetarget,Stringtext){inttextLength=text.length();byte[]buffer=newbyte[3+textLength];if(textLength<=252){buffer[0]=command;buffer[1]=target;buffer[2]=(byte)textLength;byte[]textInBytes=text.getBytes();for(intx=0;x 一旦建立了与附件的连接,就可以开始实际发送和接收消息了。您可能还记得,openAccessory方法中启动了一个单独的线程,负责消息处理。 Threadthread=newThread(null,commRunnable,TAG);thread.start(); 传递给线程的Runnable对象也是您必须实现的匿名内部类。只要您拥有来自附件的活动inputstream,就会执行它的run方法。 switch(buffer[0]){caseCOMMAND_TEXT:finalStringBuildertextBuilder=newStringBuilder();inttextLength=buffer[2];inttextEndIndex=3+textLength;for(intx=3;x default:Log.d(TAG,"unknownmsg:"+buffer[0]);break;}}}};` 在每个迭代步骤中,inputstream的内容被读入一个字节数组。如果第一个字节表示您收到了COMMAND_TEXT类型的消息,那么将使用StringBuilder从发送的剩余字节中构建消息。 既然您已经完成了消息,那么您需要向用户显示它。记住你仍然在一个单独的线程中。系统只允许UI更新发生在UI线程上。要更新TextViewUI元素的文本,可以使用方便的方法runOnUiThread,该方法在系统UI线程上执行给定的Runnable对象。 这就是消息处理的接收部分。收到一条消息后,另一条消息会立即发送回公告板。为此,您将编写自己的名为sendText的方法,该方法采用前两个标识符字节和实际消息来构建您的消息数据结构,您可以通过outputstream将其发送到附件。 publicvoidsendText(bytecommand,bytetarget,Stringtext){inttextLength=text.length();byte[]buffer=newbyte[3+textLength];if(textLength<=252){buffer[0]=command;buffer[1]=target;buffer[2]=(byte)textLength;byte[]textInBytes=text.getBytes();for(intx=0;x 恭喜你!您已经完成了通信双方的实现。现在,将Arduino草图上传到您的ADK板上,并将Android应用部署到您的设备上。如果你把你的Android设备连接到你的ADK板,你应该看到你的Android应用自动启动,它将打印ADK板发送的信息。如果您在开发板连接到PC时打开串行监视器,您可以看到来自Android设备的信息。每个设备显示对方的信息,如图图2-18和图2-19所示。 图2-18。Android设备上的HelloWorld应用接收消息 图2-19。接收消息的Arduino应用的串行监视器输出 您已经了解了Android设备和Arduino配件在连接时如何相互识别。您了解了在Arduino端和通信的Android端实现开放附件协议的必要条件。Arduino端的实现相当简单,因为大部分工作已经在AndroidAccessoryArduino库的帮助下完成了。你已经看到,软件的挑战在于Android设备的编码,因为有更多的工作要做。本章向您展示了如何使用自定义的数据结构跨两个平台传输文本消息。接下来的章节将基于你已经学到的知识,使你能够用ADK板读取传感器值或控制执行器。您在此完成的项目将是本书中后续示例的基础。 源自原始Arduino设计的ADK板有几个引脚和连接器。这些引脚大多数是数字引脚。这种ADK板上的数字引脚可以配置为输入或输出。本章描述了如何将数字引脚配置为输出引脚。 在这种特殊情况下,输出意味着什么?这意味着当设置为输出模式时,引脚将发射功率。源自Arduino的ADK板上的数字引脚可以发射高达5V的电压。它们可以用在数字环境中,可以有两种状态,HIGH和LOW。将输出引脚设置为HIGH意味着它将发出5V。如果设置为LOW,则发出0V,因此没有电压。一些输出引脚也可用于模拟环境。这意味着它们能够发出从0V到5V的输出值范围。 以下两个项目将在实际应用中解释这两种用例。 这是您将使用附加硬件部件的许多项目中的第一个。您将利用ADK板的数字引脚作为输出端口来为发光二极管(LED)供电,并编写一个Android应用来打开和关闭LED。 您将在本项目中使用以下硬件(如图3-1所示): 图3-1。项目1零件(ADK板、试验板、电阻器、LED、电线) 一个发光二极管(LED)是一个充当光源的小半导体(见图3-2)。你家里几乎所有的电子设备上都可以找到led。大多数情况下,它们被用作状态指示器。led被设计成非常节能和可靠。这就是为什么他们也找到了进入艺术装置、汽车前灯和普通家庭照明解决方案的方法,这里仅举几个例子。 图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电压。如果你应用欧姆定律并重新排列,你就可以计算出所需电阻的电阻值。 电阻有标准化的范围,你找不到像166ω这样的特定值。您应该始终使用下一个可用的较高电阻值,而不要使用较低值,因为您不想因过载而永久损坏您的组件。下一个更高的电阻值是220ω电阻。 你已经学会了如何确定在这个项目中你需要的电阻值。现在,我们来看看常见的电阻种类,以及如何通过观察来识别它们的值。 电阻有多种形式和尺寸,但除了小型表面贴装器件(SMD),最常用的电阻是碳化合物电阻和薄膜电阻,如图图3-3所示。 图3-3。碳化合物电阻器(下),薄膜电阻器(上) 碳化合物电阻器由碳和其他化合物组成,因此得名。电阻值取决于混合物中的碳含量。碳化合物电阻器通常比其他电阻器更耐用,因为它们可以更好地处理高脉冲,而不会对其电阻值产生长期影响。缺点是它们不是最精确的电阻。 薄膜电阻器具有由金属薄膜覆盖的绝缘陶瓷棒或基底。金属涂层的厚度决定了电阻器的电阻特性。薄膜电阻器不如碳化合物电阻器坚固,因为它们容易受到高脉冲和过载的影响,这会损害它们的电阻能力。这些电阻的优点是比碳化合物电阻更精确。 上述标准应在生产电路设计中考虑,但不适用于我们的简单项目。 两种类型的电阻器表面都涂有彩色条纹。这些条带有助于识别电阻器的电阻值。碳化合物电阻器具有4段颜色编码,而薄膜电阻器具有5段颜色编码。 表3-1给你一个颜色编码的概述。 您可能想知道应该从电阻的哪一端读取色带。如果你仔细观察,你会发现一个波段与其他波段的距离稍大。这是公差带。图3-4显示了一个4频段碳化合物220ω电阻,公差为+-5%。第一个频段为红色(2),第二个频段为红色(2),乘法器频段为棕色(10),即22×10ω=220ω。公差带为金色(+-5%)。 图3-4。220ω+-5%碳化合物电阻器 一个试验板,也称为原型板,是一种不需要焊接的原型板。它通常是一块有穿孔的塑料。这些孔的标准间距通常为0.1英寸(2.54毫米)。 图3-5。试验板/原型板 嵌入电路板的是以特殊布局排列的导电触点。这些板允许即插即用机制,以便您可以专注于电路设置,而不是将所有东西焊接在一起。这样,如果你在设置中犯了错误,你可以快速调整电路。电路板有多种形式和尺寸,但基本布局基本相同。顶部和底部导轨上的触点主要用于连接电源的正极和负极端口。电路板中间的区域是实际的原型制作区域。嵌入试验板的连接布局如图图3-6所示。 图3-6。试验板触点布局 在第一章中,您了解了ADK板的规格。Arduino衍生的ADK板有几个数字输入和输出引脚。您将使用其中一个引脚作为输出端口来开关LED。输出端口可以提供高达5V的电压。您将使用在图3-7中看到的数字输出引脚2,并且您将在数字环境(HIGH/LOW)中设置输出值。 图3-7。数字输出引脚2 你需要一些电线将试验板上的电阻和LED连接到ADK板上。对于原型制作和试验板工作,有特殊的试验板或跳线。使用这些电线的好处是你不必自己剥开它们,它们有不同的长度,并且有公母接头。 图3-8。左起:电子线、跳线(公对公)、跳线(母对公) 如果你不想买现成的电线,你也可以使用电子或贝尔线。您必须剥去这些电线上的电线末端,露出大约3/16英寸至5/16英寸(5毫米至8毫米)的电线,以便与嵌入试验板的触点良好接触。你可以用小刀小心地切开电线绝缘体,然后把它剥掉,但我强烈推荐使用电缆剥线钳,这是一种更安全、更容易操作的工具。你只需抓住电线,施加一些软压力,将隔离器从电线上剥离。(参见图3-9。) 图3-9。剥线器 你需要把电阻串联到发光二极管上。ADK板的数字输出引脚2将连接到您的电阻器,电阻器连接到LED的阳极,ADK板的地(GND)将连接到LED的阴极(负极引线)。如图3-10所示连接所有部件。 图3-10。项目1设置 硬件设置已经完成,是时候编写控制LED的代码了。您将编写一个Arduino草图,它接收切换命令,并根据Android应用发送的命令切换LED。Android应用将由一个控制开关状态的切换按钮组成。 以第二章中写的Arduino草图作为这个草图的基础。您已经在其中实现了特定于ADK的部分,您只需通过为LED切换场景定义另一个数据协议来更改通信部分。创建一个新的草图,并输入清单3-1中所示的代码。之后,我会解释发生了什么变化。 清单3-1。项目一:Arduino草图 #defineCOMMAND_LED0x2#defineTARGET_PIN_20x2#defineVALUE_ON0x1#defineVALUE_OFF0x0 #definePIN2 bytercvmsg[3]; voidsetup(){Serial.begin(19200);acc.powerOn();pinMode(PIN,OUTPUT);} voidloop(){if(acc.isConnected()){//readthereceiveddataintothebytearrayintlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_LED){if(rcvmsg[1]==TARGET_PIN_2){//gettheswitchstatebytevalue=rcvmsg[2];//setoutputpintoaccordingstateif(value==VALUE_ON){digitalWrite(PIN,HIGH);}elseif(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方法。它的参数是要设置的引脚和它应该切换到的状态,HIGH或LOW。 Arduino部分到此为止。让我们继续Android软件部分。 对于Android部分,你也将建立在你在第二章中从你的Android应用中学到的原则之上。您还必须调整数据协议并引入一个新的UI元素,一个ToggleButton,它允许用户打开和关闭LED。让我们看看清单3-2中的类,以及您必须做出的更改。 清单3-2。项目一:ProjectOneActivity.java `packageproject.one.adk; importandroid.app.Activity;importandroid.app.PendingIntent;importandroid.content.BroadcastReceiver;importandroid.content.Context;importandroid.content.Intent;importandroid.content.IntentFilter;importandroid.os.AsyncTask;importandroid.os.Bundle;importandroid.os.ParcelFileDescriptor;importandroid.util.Log;importandroid.widget.CompoundButton;importandroid.widget.CompoundButton.OnCheckedChangeListener;importandroid.widget.ToggleButton; publicclassProjectOneActivityextendsActivity{ privatestaticfinalStringTAG=ProjectOneActivity.class.getSimpleName(); privatestaticfinalbyteCOMMAND_LED=0x2;privatestaticfinalbyteTARGET_PIN_2=0x2;privatestaticfinalbyteVALUE_ON=0x1;privatestaticfinalbyteVALUE_OFF=0x0; privateToggleButtonledToggleButton; /**Calledwhentheactivityisfirstcreated.*/@OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.main);ledToggleButton=(ToggleButton)findViewById(R.id.led_toggle_button);ledToggleButton.setOnCheckedChangeListener(toggleButtonCheckedListener);} /** OnCheckedChangeListenertoggleButtonCheckedListener=newOnCheckedChangeListener(){ @OverridepublicvoidonCheckedChanged(CompoundButtonbuttonView,booleanisChecked){if(buttonView.getId()==R.id.led_toggle_button){ newAsyncTask @OverrideprotectedVoiddoInBackground(Boolean...params){sendLedSwitchCommand(TARGET_PIN_2,params[0]);returnnull;}}.execute(isChecked);}}}; privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(ACTION_USB_PERMISSION.equals(action)){synchronized(this){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,false)){openAccessory(accessory);}else{Log.d(TAG,"permissiondeniedforaccessory"+accessory);}mPermissionRequestPending=false;}}elseif(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(accessory!=null&&accessory.equals(mAccessory)){closeAccessory();}}}}; privatevoidopenAccessory(UsbAccessoryaccessory){mFileDescriptor=mUsbManager.openAccessory(accessory);if(mFileDescriptor!=null){mAccessory=accessory;FileDescriptorfd=mFileDescriptor.getFileDescriptor();mInputStream=newFileInputStream(fd);mOutputStream=newFileOutputStream(fd);Log.d(TAG,"accessoryopened");}else{Log.d(TAG,"accessoryopenfail");}} publicvoidsendLedSwitchCommand(bytetarget,booleanisSwitchedOn){byte[]buffer=newbyte[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(IOExceptione){Log.e(TAG,"writefailed",e);}}}}` 如果您像这里一样更改了活动名和包名,请确保您也更改了AndroidManifest.xml条目以反映这种重命名。 清单3-3。项目1:AndroidManifest.xml 数据协议常量已从4字节协议更改为3字节协议。正如您在Arduino草图中所做的那样,您还为LED切换数据消息定义了常数。 这个项目的范围是开关LED,所以你不需要显示任何文本。因此,TextViewUI元素已被ToggleButton替换。 `privateToggleButtonledToggleButton; @OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState);…setContentView(R.layout.main);ledToggleButton=(ToggleButton)findViewById(R.id.led_toggle_button);ledToggleButton.setOnCheckedChangeListener(toggleButtonCheckedListener);}` 您可以看到一个OnCheckedChangeListener被分配给了ledToggleButton,它实现了一个回调方法,每次按钮被按下时都会触发这个回调方法。ToggleButton是Button的一个特殊的有状态实现,这意味着它知道自己是否被选中。OnCheckedChangeListener的实现是在匿名内部类中完成的。唯一需要实现的方法是onCheckedChange方法,它有两个参数:触发事件的按钮和指示按钮新状态的布尔标志。 `OnCheckedChangeListenertoggleButtonCheckedListener=newOnCheckedChangeListener(){ @OverridepublicvoidonCheckedChanged(CompoundButtonbuttonView,booleanisChecked){if(buttonView.getId()==R.id.led_toggle_button){newAsyncTask @OverrideprotectedVoiddoInBackground(Boolean...params){sendLedSwitchCommand(TARGET_PIN_2,params[0]);returnnull;}}.execute(isChecked);}}};` 如果您有多个按钮使用同一个监听器,您应该总是验证是否按下了正确的按钮。这里就是这么做的。在这个特定的项目中,您不需要检查这一点,因为您只使用了一个按钮,但是如果您计划将侦听器用于更多的组件,这是一个很好的实践。在确认正确的按钮触发了事件后,您可以开始向ADK板发送命令来切换LED。sendText方法已经被移除,因为在这个项目中不需要它。实现了一种新方法,将3字节数据消息发送到名为sendLedSwitchCommand的板。它有两个参数,LED连接的ADK板的目标引脚和它应该切换到的状态。 sendLedSwitchCommand方法看起来类似于您已经知道的sendText方法,但是它实现了3字节数据协议。 publicvoidsendLedSwitchCommand(bytetarget,booleanisSwitchedOn){byte[]buffer=newbyte[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(IOExceptione){Log.e(TAG,"writefailed",e);}}} 代码改动到此为止。您记得应该向用户显示一个ToggleButton,所以您也需要在布局文件main.xml中做一些更改(清单3-4)。 清单3-4。项目一:main.xml 如您所见,TextView元素被ToggleButton元素所取代。它被定义为只和它自己的内容一样宽一样高;勾选或取消勾选时显示的文本在strings.xml文件中被引用。如果你在Eclipse中切换到图形布局编辑器,你已经可以在你的布局中间看到这个按钮了(图3-11)。 图3-11。项目1:main.XML的Eclipse图形化布局 这就是这个项目的Android部分要做的全部工作。项目1现在已经完成,可以测试了。上传两个设备的应用,将它们连接在一起,你应该会得到类似于你在图3-12中看到的东西。 图3-12。项目1:最终结果 在本项目中,您将了解ADK板上数字IO引脚的另一个特性,即脉宽调制。你将学习什么是脉宽调制,以及如何用它来调暗LED。您将编写一个Android应用,在滑块的帮助下控制调光过程。 这个项目的部件与项目1中的完全相同。你不需要任何新的硬件部件。 这些部件已经在项目1中解释过了,但你将使用ADK板的一个新功能,我将在下面解释。 ADK板的一些数字IO引脚有一个称为PWM的附加功能。PWM代表脉宽调制。具有该特性的管脚在Arduino衍生的ADK板上有标记,如图图3-13所示。 图3-13。Arduino板上的PWM标记 PWM可以被描述为数字输出的快速高-低来回切换。切换时,数字引脚产生方波信号(参见图3-14)。 图3-14。占空比为50%的脉宽调制示例 引脚的快速状态变化直接影响模拟特性,即引脚提供的电压。占空比为100%时,该引脚产生约5V的模拟值。Arduino衍生板将256个值映射到0V和5V之间的范围。因此,值127将导致引脚产生占空比为50%的方波,产生约2.5V的电压。 为了控制Arduino草图中管脚的脉冲宽度,使用了analogWrite方法,其参数是要使用的数字管脚,值在0到256之间。 电路设置与项目1完全相同,参见图3-10以供参考。 两个平台的大部分代码都可以保持原样。您将只更改较小的细节来传输更宽的脉冲宽度值范围,并且您将在Android代码中引入一个新的UI元素SeekBar,用于选择PWM值。 支持PWM输出的更改后的Arduino代码可以在清单3-5中看到。 清单3-5。项目二:Arduino草图 **#defineCOMMAND_LED0x2 voidloop(){if(acc.isConnected()){//readthereceiveddataintothebytearrayintlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_LED){if(rcvmsg[1]==TARGET_PIN_2){//gettheanalogvaluebytevalue=rcvmsg[2];//setoutputpintoaccordinganalogvalueanalogWrite(PIN,value);}}}}}` 您可以看到,LED状态的常量(VALUE_ON/VALUE_OFF)已被删除,因为您现在使用的是模拟值,而不是数字状态。Android应用传输的字节值被读取并直接输入到analogWrite方法中。如果数字引脚支持PWM,这种方法会触发数字引脚产生具有特定占空比的方波。作为参数,它采用要使用的引脚和一个0到255的字节值,该值映射到0V到5V范围内的模拟值。 清单3-6。项目二:ProjectTwoActivity.java `packageproject.two.adk; importandroid.app.Activity;importandroid.app.PendingIntent;importandroid.content.BroadcastReceiver;importandroid.content.Context;importandroid.content.Intent;importandroid.content.IntentFilter;importandroid.os.AsyncTask;importandroid.os.Bundle;importandroid.os.ParcelFileDescriptor;importandroid.util.Log;importandroid.widget.SeekBar;importandroid.widget.SeekBar.OnSeekBarChangeListener;importandroid.widget.TextView;importcom.android.future.usb.UsbAccessory;importcom.android.future.usb.UsbManager; **publicclassProjectTwoActivityextendsActivity{ privatestaticfinalStringTAG=ProjectTwoActivity.class.getSimpleName();** privatestaticfinalbyteCOMMAND_LED=0x2;privatestaticfinalbyteTARGET_PIN_2=0x2; **privateTextViewledIntensityTextView;privateSeekBarledIntensitySeekBar; @OverridepublicvoidonCreate(BundlesavedInstanceState){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("LEDintensity:"+ledIntensitySeekBar.getProgress());}** @OverridepublicvoidonResume(){super.onResume();…} @OverridepublicvoidonPause(){super.onPause();…} @OverridepublicvoidonDestroy(){super.onDestroy();…} **OnSeekBarChangeListenerledIntensityChangeListener=newOnSeekBarChangeListener(){@OverridepublicvoidonProgressChanged(SeekBarseekBar,intprogress,booleanfromUser){ledIntensityTextView.setText("LEDintensity:"+ledIntensitySeekBar.getProgress());newAsyncTask @OverrideprotectedVoiddoInBackground(Byte...params){sendLedIntensityCommand(TARGET_PIN_2,params[0]);returnnull;}}.execute((byte)progress);} @OverridepublicvoidonStartTrackingTouch(SeekBarseekBar){//notimplemented} @OverridepublicvoidonStopTrackingTouch(SeekBarseekBar){//notimplemented}};** privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){…}; privatevoidopenAccessory(UsbAccessoryaccessory){…} privatevoidcloseAccessory(){…} publicvoidsendLedIntensityCommand(bytetarget,bytevalue){byte[]buffer=newbyte[3];buffer[0]=COMMAND_LED;buffer[1]=target;buffer[2]=value;if(mOutputStream!=null){try{mOutputStream.write(buffer);}catch(IOExceptione){Log.e(TAG,"writefailed",e);}}}}` 你可以看到,这里也删除了LED开关状态的字节常量。在这个项目中,向用户显示了两个UI元素。第一个是一个TextView,它应该显示当前传输到ADK板的选定值。第二个元素是一个SeekBar,它是一个滑块控件,让用户可以轻松地在预定义的范围内选择一个值。 `privateTextViewledIntensityTextView;privateSeekBarledIntensitySeekBar; @OverridepublicvoidonCreate(BundlesavedInstanceState){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("LEDintensity:"+ledIntensitySeekBar.getProgress());}` 像所有其他的View元素一样,SeekBar可以注册一组广泛的监听器,当某些事件发生时,这些监听器会得到通知。一个专用于SeekBar的监听器是在onCreate方法中注册的OnSeekBarChangeListener。如果滑块收到第一个触摸手势,如果滑块改变其值,如果触摸被释放,它会得到通知。您只关心SeekBar的变化状态,因此实现如下: `OnSeekBarChangeListenerledIntensityChangeListener=newOnSeekBarChangeListener(){ @OverridepublicvoidonProgressChanged(SeekBarseekBar,intprogress,booleanfromUser){ledIntensityTextView.setText("LEDintensity:"+ledIntensitySeekBar.getProgress());newAsyncTask @OverridepublicvoidonStopTrackingTouch(SeekBarseekBar){//notimplemented}};` 当调用onProgressChanged方法时,它从系统接收三个参数。第一个是触发事件的实际的SeekBar元素,第二个是SeekBar的当前进度,第三个是一个布尔标志,指示进度的改变是由用户滑过SeekBar造成的,还是进度是通过编程设置的。实现非常简单。在TextView的帮助下,你向用户显示数值的变化,然后你将数值传送到ADK板。注意progress的数据类型是byte。稍后你会看到SeekBar的范围被配置为从0到255。然而,数据类型字节的范围是从-128到127。实际情况是,进度值被转换成一个字节,如果该值大于127,它就变成负数。这与位算术和所谓的符号位有关。这不应该是你现在关心的问题,因为在Arduino端,当一个可能的负字节值被提供给analogWrite方法时,它将被转换回原来的表示。请注意,一般来说,这种强制转换是不安全的,尽管在这个例子中它是有效的。 您已经了解到IO操作应该在UI独立的线程中进行,因此您将再次使用AsyncTask来实现这一目的。实际的通信逻辑封装在sendLedIntensityCommand方法中。 publicvoidsendLedIntensityCommand(bytetarget,bytevalue){byte[]buffer=newbyte[3];buffer[0]=COMMAND_LED;buffer[1]=target;buffer[2]=value;if(mOutputStream!=null){try{mOutputStream.write(buffer);}catch(IOExceptione){Log.e(TAG,"writefailed",e);}}} 实现几乎等于来自项目1的sendLedSwitchCommand。您将传输SeekBar的当前值,其范围从0到255,而不是只传输两种可能的状态。 这就是项目2的所有代码实现。您仍然需要更改main.xml文件,以便向用户实际显示TextView和SeekBar。新的main.xml文件看起来像清单3-7。 清单3-7。项目2–main.XML 您已经了解了TextView元素的属性,所以让我们看看SeekBar有什么特别之处。除了已经知道的id、layout_width、layout_height等属性之外,你看到一个叫做max的属性。该属性定义了SeekBar可以达到的最大值。初始值0是默认值,您不必自己定义。所以在布局中,您已经定义了从0到255的范围。如果您切换到图形布局编辑器,您已经可以看到该用户界面的预览(图3-15)。 图3-15。项目2:main.XML的Eclipse图形化布局 项目2现已完成,准备测试。将应用上传到您的设备并启动它们。你完成的项目应该看起来像图3-16。 图3-16。项目二:最终结果 在这一章中,你学习了什么是ADK板上的输出引脚以及它的功能。在本章的第一个项目中,您看到了如何在数字环境中使用输出引脚,当输出切换到HIGH或LOW时,通过提供5V或0V来开关简单的LED。第二个项目引入了数字输出的PWM或脉宽调制模式,其中一个输出引脚可以发出0V至5V范围内的输出电压。为了让用户控制传输到ADK板的值,您使用了两个不同的AndroidUI元素:ToggleButton在数字环境中打开和关闭LED,而SeekBar在模拟环境中从一系列值中选择来调暗LED。 ADK板上的大多数引脚都可以用作输入引脚。请记住,数字引脚可以配置为输出和输入。默认情况下,数字引脚配置为输入引脚。您可以使用pinMode方法将它们设置为输入模式,但您不一定需要这样做。 此外,ADK板有专用的模拟输入引脚。使用模拟输入引脚,您可以测量这些引脚上施加电压的变化。测得的模拟电压被映射为数字表示,您可以在代码中进行处理。 以下两个项目描述了两种输入引脚类型及其使用情况。 在这个项目中,您将学习如何使用ADK板上的数字输入引脚来检测按钮或开关的状态。对于额外的硬件,你需要一个按钮或一个开关和一个电阻。你可以在这个项目中使用按钮或开关,因为它们的工作方式基本相同。这两个元件都可以用来闭合或断开电路。您将编写一个Arduino草图,它读取按钮的当前状态,并将状态更改传输到Android应用。接收状态变化的Android应用将在TextView中传播该变化,并且每当按下按钮时,您的Android设备的振动器将被触发振动。 到现在为止,你已经知道了这个项目的大部分内容。不过,我将解释按钮或开关的原理、所谓上拉电阻的使用以及配置为输入引脚的数字引脚的使用。在该项目中,您将需要以下硬件(如图4-1所示): 图4-1。项目3部分(ADK板、试验板、电阻、按钮、电线) 按钮或开关是用于控制电路状态的元件。电路可以是闭合的,这意味着电源有回路,也可以是断开的,这意味着电路的回路被阻断或没有连接到电路。为了实现从开路到闭路的转换,需要使用按钮或开关。在ON状态下,按钮或开关本身没有电压降,也没有限流特性。在其OFF状态,按钮或开关理想地没有电压限制和无穷大的电阻值。在一个简单的电路图中,一个闭合电路看起来像图4-2中的所示。 图4-2。闭路 如你所见,功率可以通过电路的元件流向回路。如果您将一个开关或按钮连接到该电路,您可以控制该电路是断开还是闭合。通过按下按钮或将开关切换到ON位置,您可以闭合电路,这样电力就可以流过电路。如果您松开按钮或将开关切换回其OFF位置,您将断开电路,从而使其保持打开状态。按钮或开关的电路图符号在电路中显示为开路部分。在图4-3的电路图中可以看到开关的符号。 图4-3。带开关的电路 按钮和开关有多种类型和尺寸。典型的按钮可以是按钮,您需要按住它来闭合电路,松开它来打开电路,或者它们可以是拨动按钮,在被按下后保持其当前状态。开关也有几种形状和应用类型,但最常见的是众所周知的定义两种状态的ON/OFF开关,以及可以在多种状态之间切换的拨动开关(见图4-4)。 图4-4。按钮和开关 你已经用一个电阻来限制电路中的电流。在这个项目中,您将使用一个电阻和一个按钮或开关将输入引脚拉至LOW(0V)或HIGH(5V)。这可以通过特殊的电路设置来实现。 在某些情况下,您可能希望输入引脚处于定义的状态。因此,例如,当一个数字引脚被配置为输入,并且没有元件与之相连时,您仍然会测量到电压波动。这些波动是外部信号或其他电干扰的结果。引脚上测得的电压将介于0V和5V之间,这将导致引脚状态的数字读数连续变化(LOW/HIGH)。为了消除这些干扰,您需要将该输入引脚上的电压拉高。在这种用例中,电阻器被称为上拉电阻器。 上拉电阻必须放置在电路内的电压源和输入引脚之间。按钮或开关位于输入引脚和地之间。该设置的简单示意图如图4-5所示。 图4-5。上拉电阻电路 这里发生的事情的一个简单解释是,如果开关或按钮没有按下,输入只连接到Vcc(5V),线被拉高,输入被设置为HIGH。当按下开关或按钮且输入连接到Vcc和GND(0V)时,电流在10kΩ电阻处的电阻大于开关或按钮处的电阻,后者的电阻非常低(通常远低于1ω)。在这种情况下,输入被设置为LOW,因为到GND的连接强于到Vcc的连接。 还需要高阻值电阻来限制电路中的总电流。如果你按下开关或按钮,你直接连接Vcc到GND。如果没有高阻值电阻,会让太多的电流直接流向GND,从而导致短路。高电流会导致热量积聚,在大多数情况下,会永久性地损坏您的部件。 您已经使用了配置为输出引脚的ADK板的数字引脚。在这个项目中,您将使用处于输入模式的引脚。通过使用数字引脚作为输入引脚,您可以测量数字信号:数字HIGH表示输入引脚上大约5V的电压,而数字LOW接近0V。您已经了解到,上拉电阻可用于稳定输入引脚,通过将引脚稳定上拉至5V电源电压,使其不受干扰影响。ADK电路板的一个特点是,嵌入式ATmega芯片集成了可以通过代码激活的上拉电阻。要激活集成上拉电阻,只需将引脚设置为输入模式,并将其设置为HIGH。 pinMode(pin,INPUT);//setdigitalpintoinputmodedigitalWrite(pin,HIGH);//turnonpullupresistorforpin 不过,我不建议在这个项目中使用这种技术,这样您可以直接了解上拉电阻的基本原理。如果您手头没有高值电阻,您仍然可以如上所示更改该项目的代码来激活内部上拉电阻。请注意,如果在代码中之前输入引脚被用作输出引脚,那么您只需使用pinMode方法来定义输入引脚。默认情况下,所有数字引脚都被配置为输入,因此,如果该引脚始终仅用作输入,则不必显式设置pinMode。 您刚刚了解到需要将想要使用的数字输入引脚连接到上拉电阻电路。在图4-6中可以看到,ADK板的+5VVcc引脚必须连接到10kΩ上拉电阻的一个引线上。另一根引线连接到数字输入引脚2。数字引脚2也连接到开关或按钮的一个引线。相反的引线接地。就这么简单。通过这种设置,当按钮或开关未按下时,将输入引脚拉至5V,使数字输入引脚测量数字HIGH。如果现在按下按钮或开关,数字输入引脚被拉至GND,导致输入测量数字LOW。 图4-6。项目3设置 如本章开头的项目描述所述,您将编写一个Arduino草图,持续监控数字输入引脚的状态。每当pin码的状态从HIGH变为LOW,或者相反,你就会向连接的Android设备发送一条消息。Android应用将监听传入的状态变化,并在一个TextView中显示当前状态。此外,只要按下按钮,Android设备的振动器就会被激活。 和以前一样,Arduinosketch实现非常简单。看看清单4-1中的,稍后我会解释细节。 清单4-1。项目三:Arduino草图 #defineCOMMAND_BUTTON0x1#defineTARGET_BUTTON0x1#defineVALUE_ON0x1#defineVALUE_OFF0x0#defineINPUT_PIN2 bytesntmsg[3];intlastButtonState;intcurrentButtonState; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_BUTTON;sntmsg[1]=TARGET_BUTTON;} voidloop(){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);}}` 这里要做的第一件事是为按钮状态消息定义一些新的消息字节。 `#defineCOMMAND_BUTTON0x1 因为消息的前两个字节不会改变,所以您已经可以在您的setup方法中设置它们了。 sntmsg[0]=COMMAND_BUTTON;sntmsg[1]=TARGET_BUTTON; 注意,没有必要在setup方法中调用pinMode方法,因为默认情况下数字引脚是输入引脚。 第一种新方法是digitalRead方法,它测量输入引脚上施加的电压,并将其转换为两种可能的数字状态:HIGH或LOW。提供给该方法的唯一参数是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应用没有引入新的UI元素。在已知的TextView的帮助下,您将看到按钮或开关的状态变化。但是,您将学习如何调用系统服务来处理某些系统或硬件功能。对于这个项目,Android设备的振动器服务将负责控制设备中的振动器电机。首先,看看清单4-2中的代码。我将在后面解释新的功能。同样,没有改变的已知代码部分被缩短了,这样您就可以专注于重要的部分。 清单4-2。项目三:ProjectThreeActivity.java `packageproject.three.adk; import…;publicclassProjectThreeActivityextendsActivity{ … privatestaticfinalbyteCOMMAND_BUTTON=0x1;privatestaticfinalbyteTARGET_BUTTON=0x1;privatestaticfinalbyteVALUE_ON=0x1;privatestaticfinalbyteVALUE_OFF=0x0; privatestaticfinalStringBUTTON_PRESSED_TEXT="TheButtonispressed!";privatestaticfinalStringBUTTON_NOT_PRESSED_TEXT="TheButtonisnotpressed!"; privateTextViewbuttonStateTextView; privateVibratorvibrator;privatebooleanisVibrating; @OverridepublicvoidonCreate(BundlesavedInstanceState){super.onCreate(savedInstanceState); setContentView(R.layout.main);buttonStateTextView=(TextView)findViewById(R.id.button_state_text_view); vibrator=((Vibrator)getSystemService(VIBRATOR_SERVICE));} @OverridepublicvoidonPause(){super.onPause();closeAccessory();stopVibrate();} @OverridepublicvoidonDestroy(){super.onDestroy();unregisterReceiver(mUsbReceiver);} privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){…}}; RunnablecommRunnable=newRunnable(){ @Overridepublicvoidrun(){intret=0;finalbyte[]buffer=newbyte[3]; switch(buffer[0]){caseCOMMAND_BUTTON: if(buffer[1]==TARGET_BUTTON){if(buffer[2]==VALUE_ON){startVibrate();}elseif(buffer[2]==VALUE_OFF){stopVibrate();}runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){buttonStateTextView.setText(buffer[2]==VALUE_ONBUTTON_PRESSED_TEXT:BUTTON_NOT_PRESSED_TEXT);}});}break; default:Log.d(TAG,"unknownmsg:"+buffer[0]); break;}}}}; publicvoidstartVibrate(){if(vibrator!=null&&!isVibrating){isVibrating=true;vibrator.vibrate(newlong[]{0,1000,250},0);}} publicvoidstopVibrate(){if(vibrator!=null&&isVibrating){isVibrating=false;vibrator.cancel();}}}` 看看这个项目增加了哪些变量: `privatestaticfinalbyteCOMMAND_BUTTON=0x1;privatestaticfinalbyteTARGET_BUTTON=0x1;privatestaticfinalbyteVALUE_ON=0x1;privatestaticfinalbyteVALUE_OFF=0x0; privateVibratorvibrator;privatebooleanisVibrating;` 您应该已经认识到稍后验证发送的消息所需的协议字节。然后你会看到两个String常量,如果按钮或开关的状态改变了,它们用来更新TextView的文本。最后两个变量用于引用系统振动器服务,并检查振动器是否已被激活。 在onCreate方法中,您请求设备振动器的系统服务: vibrator=((Vibrator)getSystemService(VIBRATOR_SERVICE)); getSystemService方法返回Android设备系统服务的句柄。这个方法可以从Context类的每个子类中调用,或者直接从Context引用中调用。所以你可以从一个Activity或者一个Service以及一个Application子类中访问系统服务。Context类还定义了访问系统服务的常量。 在第二章中,您已经了解了从您的HelloWorld应用接收数据消息的实现细节。一个单独的线程检查传入的数据并处理消息。根据接收到的按钮状态值,调用startVibrate或stopVibrate方法。startVibrate方法检查您是否仍然拥有系统服务的有效句柄,以及振动器是否已经停止振动。然后,它设置布尔标志来描述振动器被激活,并定义要立即开始的振动模式。 如果你的应用暂停了,你应该确保不要留下不必要的资源分配,所以如果发生这种情况,一定要停止振动器。这就是为什么在onPause生命周期方法中调用stopVibrate方法的原因。实现很简单。 publicvoidstopVibrate(){if(vibrator!=null&&isVibrating){isVibrating=false;vibrator.cancel();}} 首先检查你是否仍然有一个有效的服务参考,振动器是否还在振动。然后重置布尔标志并取消振动。 现在,将Arduino草图上传到您的ADK板上,并将Android应用部署到您的设备上。如果你做的一切都正确,你的项目应该看起来像图4-7中的所示,并且你的Android设备应该在你每次按下连接到你的ADK板的按钮或开关时振动并改变它的TextView。 图4-7。项目3:最终结果 模拟输入测量用于识别模拟输入引脚上施加电压的变化。许多传感器和部件通过改变它们的输出电压来表示值的变化。这个项目将教你如何使用电路板的模拟输入引脚,以及如何将模拟输入映射到你可以在代码中使用的数字值。为了改变模拟输入,你将使用一种叫做电位计的新元件。您将更改模拟值,该值将被转换为数字值,并传输到Android应用。在Android应用中,您将使用一个ProgressBarUI元素来可视化接收到的值的变化。 对于这个项目,您只需要一个电位计和一些电线作为附加硬件组件(如图图4-8所示): 图4-8。项目4部分(ADK板、试验板、电位器、电线) 这是您第一次不用ADK板的数字IO引脚。相反,您将使用电路板上的模拟输入引脚。顾名思义,它们只能用作输入。这些引脚的特殊之处在于它们可以测量模拟值,即施加电压的变化。ADK板能够将这些测量值转换成数字值。这个过程被称为模数转换。这是由一个叫做ADC的内部组件完成的,ADC是一个模数转换器。在ADK板的情况下,这意味着从0V到5V的值被映射到从0到1023的数字值,因此它能够可视化10位范围内的值的变化。模拟输入引脚位于电路板上数字引脚的另一侧,通常标有模拟输入和引脚编号前缀a,因此模拟引脚5应标为A5。你可以在图4-9中看到那些针脚。 图4-9。模拟输入引脚 电位计是一种可变电阻器。它有三根导线可以连接到电路上。它有两种功能,取决于你如何连接它。如果您只是将一个外部端子和一个中间端子连接到您的电路,它只是一个简单的可变电阻器,如图图4-10所示。 图4-10。电位器作为可变电阻 如果你还连接了第三根引线,它就充当了所谓的分压器。分压器(也称为分压器)是一种特殊的电路设置,顾名思义,它能够将电路中的电压分成电路组件之间的不同电压电平。典型的分压电路由两个串联电阻或一个电位计组成。在图4-11中可以看到电路可视化。 图4-11。带电位计的分压器(左),带两个串联电阻的分压器(右) Vin是施加在两个串联电阻上的电压,Vout是第二个电阻(R2)上的电压。确定输出电压的公式如下: 让我们看一个例子。考虑这样一个使用案例,您有一个9V电池,但您的一个电子元件只能在5V电压下工作。您已经确定了Vin(9V)和Vout(5V)。唯一缺少的是电阻值,这是你需要的。 让我们尝试使用一个27k电阻来测量R2。现在唯一缺少的是R1。将这些值输入公式,结果如下: 重新排列公式,以便确定缺失的变量R1。 由于您找不到这样一个特定的电阻值,因此可以采用下一个更高的值,即22k。对于R1的那个值,你将得到4.96V,这非常接近于目标5V。 如果你扭动电位器,你基本上改变了它的内阻比例,也就是说如果左边端子和中间端子之间的电阻减小,右边端子和中间端子之间的电阻增大,反之亦然。因此,如果你将这个原理应用于分压公式,这意味着如果R1值增加,R2值就会减少,反之亦然。因此,当电位计内的电阻比例发生变化时,会导致Vout发生变化。电位计有几种形状和电阻范围。最常见的类型是微调器,其通过使用螺丝刀或类似的装配物体来调整,以及旋转电位计,其具有轴或旋钮来调整电阻值(如图图4-12所示)。在这个项目中,我使用微调类型,因为它通常比旋转电位器便宜一点。 图4-12。电位器:微调(左),旋转电位器(右) 这个项目的设置很简单。只需将+5V引脚连接到电位计的一个外部引线,并将GND引脚连接到相反的外部引线。将模拟引脚A0连接到电位计的中间引线,就大功告成了。你的设置应该看起来像图4-13。如果调整电位计,模拟引脚上的测量值将会改变。 图4-13。项目4设置 Arduino草图负责读取模拟引脚的ADC值。传输的10位值将由Android应用接收,值的变化将显示在TextView和ProgressBarUI元素中。您还将学习传输大值的转换技术。 看看清单4-3中完整的Arduino草图。之后我会讨论新的内容。 清单4-3。项目4:Arduino草图 #defineCOMMAND_ANALOG0x3#defineTARGET_PIN0x0#defineINPUT_PINA0 bytesntmsg[6];intanalogPinReading; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_ANALOG;sntmsg[1]=TARGET_PIN;} voidloop(){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。正如您已经知道的,这个值是一个整数。该值的二进制表示如下: 00000000000000000000000100101100=300 正确的数学表达式更短,不需要你写所有的前导零。只是前缀是0b。 0b100101100=300 如果将该值简单地转换为一个字节,则只有最后八位将构成字节值。在这种情况下,您最终得到的值是44。 00101100=44 那只是整个价值的一部分。要转换其余的位,您需要首先将它们放到适当的位置。这就是使用移位的地方。您可以使用运算符<>,向两个方向移动位,将它们移动到右侧。在这种情况下,您需要右移,所以您使用>>操作符。在将该值转换为新的字节之前,需要将它向右移动八次。因为您需要将它移位几次来构造所有四个字节,所以完整的语法应该是这样的: (byte)(300>>24)(byte)(300>>16)(byte)(300>>8)(byte)300 在其新的二进制表示中,上述值如下所示: 00000000000000000000000100101100 可以看到,移出的位被简单地忽略了。现在,您可以传输所有四个数据字节,并在另一端将它们重新转换回初始整数。 在Android应用中,接收到的四字节值将被转换回整数值,测量值的变化将通过显示当前值的TextView可视化。第二个可视指示器是ProgressBarUI元素。它看起来与已经推出的SeekBar相似,但是这里用户没有与工具条交互的可能性。看看清单4-4中的代码。稍后我会解释细节。 清单4-4。项目四:ProjectFourActivity.java `packageproject.four.adk; import…; publicclassProjectFourActivityextendsActivity{ privatestaticfinalbyteCOMMAND_ANALOG=0x3;privatestaticfinalbyteTARGET_PIN=0x0; privateTextViewadcValueTextView;privateProgressBaradcValueProgressBar; setContentView(R.layout.main);adcValueTextView=(TextView)findViewById(R.id.adc_value_text_view);adcValueProgressBar=(ProgressBar)findViewById(R.id.adc_value_bar);} @Overridepublicvoidrun(){intret=0;byte[]buffer=newbyte[6]; while(ret>=0){try{ret=mInputStream.read(buffer);}catch(IOExceptione){Log.e(TAG,"IOException",e);break;} switch(buffer[0]){caseCOMMAND_ANALOG: if(buffer[1]==TARGET_PIN){finalintadcValue=((buffer[2]&0xFF)<<24)+((buffer[3]&0xFF)<<16)+((buffer[4]&0xFF)<<8)+(buffer[5]&0xFF);runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){adcValueProgressBar.setProgress(adcValue);adcValueTextView.setText(getString(R.string.adc_value_text,adcValue));}});}break; default:Log.d(TAG,"unknownmsg:"+buffer[0]);break;}}}};}` 正如您所看到的,这个代码片段中的新变量与Arduino草图中的消息定义字节相同,还有我在开始时描述的两个UI元素。 `privatestaticfinalbyteCOMMAND_ANALOG=0x3;privatestaticfinalbyteTARGET_PIN=0x0; privateTextViewadcValueTextView;privateProgressBaradcValueProgressBar;` 看看需要在清单4-5所示的main.xml布局文件中进行的UI元素定义。除了这两个元素通常的布局属性之外,您还必须定义ProgressBar的max值属性,以便可以在从0到1023的正确范围内进行图形可视化。 你可以看到还有第二个重要的属性。属性告诉系统以某种风格呈现UI元素的外观。如果省略该属性,ProgressBar将以默认样式呈现,这是一个加载类型的旋转轮。这不是你想要的,所以你可以用另一个样式覆盖它。这种特殊样式查找的语法看起来有点奇怪。前缀android:意味着这个特殊的资源不能在当前项目的res文件夹中找到,但是可以在Android系统资源中找到。 清单4-5。项目4:main.xml 在项目3中,您对接收到的输入感兴趣,因此接收数据的逻辑基本保持不变。一个单独的线程负责读取inputstream并处理接收到的消息。您可以看到,通过使用移位技术,接收到的消息的最后四个字节被再次转换为一个整数值—只是这一次,移位发生在另一个方向。 `finalintadcValue=((buffer[2]&0xFF)<<24) 您还可以看到,字节值在进行位移之前已经改变。这种操作称为按位AND。通过应用值0xFF,可以消除处理负数和正数时可能出现的符号位错误。 如果您考虑前面的示例,并假设测得的值为300,那么四个接收到的字节在没有移位的情况下将具有以下值: 00000000=000000000=000000001=100101100=44 要重建原始的整数值,你需要像上面那样左移字节值。 00000000<<24=00000000000000000000000000000000=000000000<<16=00000000000000000000000000000000=000000001<<8=00000000000000000000000100000000=25600101100=00000000000000000000000000101100=44 现在,如果您将接收到的字节值相加,您将再次得到原始的整数值。 0+0+256+44=300 最后要做的是将价值可视化给用户。使用helper方法runOnUiThread,两个UI元素都被更新。TextView相应地获取其文本设置,ProgressBar设置其新的进度值。 上传Arduinosketch和Android应用,查看调整电位计后数值如何变化。最终结果如图4-14所示 图4-14。项目4:最终结果 本章展示了如何从ADK板的输入引脚读取数值。您使用输入配置中的数字引脚来读取HIGH和LOW的数字输入。按钮或开关用于在这两种状态之间切换,每当按钮被按下或开关关闭时,Android应用就会通过振动来表达当前状态。您还了解了通过将ADK板模拟输入引脚上的模拟电压读数转换为0到1023范围内的数字表达式来测量数值范围的第二种可能性。一个Android应用用一个新的UI元素ProgressBar将当前阅读可视化。您通过应用不同的样式更改了UI元素的外观。在此过程中,您了解了分压器和上拉电阻的原理,并了解到移位可以作为一种数据转换方式。 ADK板本身不能产生或检测声音。幸运的是,有一个组件可以帮助完成这两项任务:压电蜂鸣器。 声音的定义是什么?一般来说,声音是一组可以通过固体、液体和气体传播的压力波。压电蜂鸣器通过不同频率的振动在空气中传播声音。这些波的不同频率组成了你能听到的不同声音。人类能够听到20Hz到20,000Hz范围内的频率。频率的单位是赫兹。它定义了每秒的周期数。所以人耳每秒探测到的声波越多,感知到的声音就越高。如果你曾经站在一个大的音频音箱附近,你可能会看到扬声器的薄膜在振动。这实质上是扬声器产生不同频率的压力波。 在接下来的两个项目中,你将学习如何使用压电蜂鸣器发声,以及如何探测附近的声音。第一个项目将为您提供一种为自己的项目生成声音的方法,以便您可以构建音频警报系统、通知设备或简单的乐器。第二个项目将向你展示一种检测近距离声音甚至振动的方法。例如,这些功能用于爆震传感器项目或测量可能伤害敏感商品的振动。 这个项目将向你展示如何使用压电蜂鸣器来产生声音。它将解释逆压电效应的原理。您将使用您的Android设备来选择一个音符的频率值,该值将被传输到您的ADK板,以通过压电蜂鸣器产生声音。 对于这个项目,你需要一个新的组件:压电组件,也就是压电蜂鸣器。除此之外,您只需要以下组件(如图图5-1所示): 图5-1。项目5部分(ADK板、试验板、电线、压电蜂鸣器) 您将使用支持脉宽调制(PWM)的ADK板的一个数字引脚。您已经使用了数字引脚的PWM功能来调暗LED。再次使用PWM特性来产生方波,稍后将应用于压电蜂鸣器。方波特性的变化将导致压电蜂鸣器产生不同的振荡频率,从而产生不同的声音。 压电蜂鸣器是一种可以利用压电效应和逆压电效应的压电元件。这意味着它可以感知和产生声音。典型的压电蜂鸣器由放置在金属板上的陶瓷片组成。陶瓷晶片包含对振荡敏感的压电晶体。 压电效应描述了压力等机械力导致压电元件上产生电荷。压力波让陶瓷晶片膨胀和收缩。它与金属板一起引起振动,由此产生的压电晶体变形产生可测量的电荷。(参见图5-2。)在第二个项目中,压电效应被用于感应其附近的振动。 图5-2。压电效应(压电元件的膨胀和收缩) 逆压电效应描述了当施加电势时产生机械力(例如压力波)的压电元件的效应。在电势的刺激下,压电元件再次收缩和膨胀,由此产生的振动产生声波,该声波甚至可以被共振的中空壳体放大。产生的不同声波取决于振荡的频率。这种效果将在本章的第一个项目中演示,以生成不同频率的声音。 最常见的压电蜂鸣器装在塑料外壳中,但你也可以找到陶瓷压电蜂鸣器板(图5-3)。 图5-3。压电蜂鸣器 压电蜂鸣器用于家用电器、工业机器,甚至音乐设备。你可能在火警系统、无障碍系统中听到过它们,或者当你的洗衣机或烘干机试图告诉你它们的工作完成了。有时你会看到它们作为拾音器连接在原声吉他上,将共鸣吉他琴体的振动转换成电信号。 这个项目的设置非常简单(见图5-4)。你只需要将压电蜂鸣器的一个连接到GND,另一个连接到你的ADK板的数字引脚2。请记住,一些压电蜂鸣器可能有一定的极性。通常它们被相应地标记或者它们已经连接了相应的电线。在这种情况下,将负极线连接到GND,正极线连接到数字引脚2。 图5-4。项目5设置 对于这个项目,您将编写一个Android应用,让用户通过SpinnerUI元素选择一个注释,这是一个类似下拉列表的东西,您可能从Web上了解到。音符将被映射到其代表频率,其值将被传输到ADK板。在Arduino端,您利用Arduinotone方法,它是ArduinoIDE的一部分,在连接的压电蜂鸣器上生成相应的声音。 这个项目的Arduino草图与项目2中使用的非常相似。只是这一次,您将使用tone方法,而不是使用analogWrite方法直接写入输出引脚,这种方法会生成必要的波形来产生所需的声音。在内部,它利用寻址的数字PWM引脚的能力来产生波形。看看完整的清单5-1。我将在后面解释tone方法的作用。 清单5-1。项目5:Arduino草图 bytercvmsg[6]; voidsetup(){Serial.begin(19200);pinMode(TARGET_PIN_2,OUTPUT);acc.powerOn();} voidloop(){if(acc.isConnected()){intlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_ANALOG){if(rcvmsg[1]==TARGET_PIN_2){intoutput=((rcvmsg[2]&0xFF)<<24)+((rcvmsg[3]&0xFF)<<16)+((rcvmsg[4]&0xFF)<<8)+(rcvmsg[5]&0xFF);//setthefrequencyforthedesiredtoneinHztone(TARGET_PIN_2,output);}}}}}` ArduinoIDE提供了一个名为tone的重载特殊方法来生成方波,它可以用来通过扬声器或压电蜂鸣器产生声音。在其第一个变体中,tone方法接受两个参数,蜂鸣器连接的数字PWM引脚和以Hz为单位的频率。 tone(pin,frequency); tone(pin,frequency,duration); 在内部,tone方法实现使用analogWrite方法利用ADK板的PWM功能来产生波形。正如你所看到的,这个例子中使用了双参数的tone方法来产生一个稳定连续的音调。在将接收到的频率值馈送到音调方法之前,通过使用移位技术对其进行转换。 对于Android部分,您将使用一个名为Spinner的类似下拉列表的UI元素,让用户选择一个将被映射到其相应频率的音符。您将学习如何初始化类似列表的UI元素,以及如何使用它们。在我解释细节之前,请看一下完整的清单5-2。 清单5-2。项目五:ProjectFiveActivity.java `packageproject.five.adk; publicclassProjectFiveActivityextendsActivity{ privatestaticfinalbyteCOMMAND_ANALOG=0x3;privatestaticfinalbyteTARGET_PIN_2=0x2; privateSpinnernotesSpinner;privateArrayAdapteradapter;privateint[]notes={/C3/131,/D3/147,/E3/165,/F3/175,/G3/196,/A3/220,/B3/247}; 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);} /**Calledwhentheactivityispausedbythesystem./@OverridepublicvoidonPause(){super.onPause();closeAccessory();}/* OnItemSelectedListeneronItemSelectedListener=newOnItemSelectedListener(){ @OverridepublicvoidonItemSelected(AdapterView<>adapterView,Viewview,intposition,longid){newAsyncTask @OverrideprotectedVoiddoInBackground(Integer...params){sendAnalogValueCommand(TARGET_PIN_2,notes[params[0]]);returnnull;}}.execute(position);} @OverridepublicvoidonNothingSelected(AdapterView<>arg0){//notimplemented}}; publicvoidsendAnalogValueCommand(bytetarget,intvalue){byte[]buffer=newbyte[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(IOExceptione){Log.e(TAG,"writefailed",e);}}}}` 让我们先来看看新的变量。 privateSpinnernotesSpinner;privateArrayAdapter 您将使用一个名为Spinner的UI元素为用户提供选择注释的可能性。Spinner是一个列表元素,非常类似于下拉列表。它是一个input元素,单击时会展开一个列表。列表中的元素是可以选择的可能输入值。类似列表的UI元素用适配器管理它们的内容。这些适配器负责用内容填充列表,并在以后访问它。您在这里看到的ArrayAdapter就是这样一个适配器,可以保存内容元素的类型化数组。这里的最后一件事是一个映射数组,它将所选的音符映射到它以后的频率表示。这些值非常接近相应音符的频率,单位为赫兹(Hz)。 在你给变量分配新的视图元素之前,你必须在你的布局main.xml文件中定义它(见清单5-3)。 清单5-3。项目5:main.xml Spinner有一个新的属性叫做prompt,定义了显示Spinner的列表内容时的提示。您可以在该属性中引用的strings.xml文件中定义一个简短的描述性标签。 现在您可以在onCreate方法中正确初始化视图元素了。 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文件中进行的。您只需要定义一个字符串项数组,如下所示。 必须给它一个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); notesSpinner.setAdapter(adapter); 初始步骤已经完成,是时候实现负责处理值已被选择的情况的监听器了。 `OnItemSelectedListeneronItemSelectedListener=newOnItemSelectedListener(){ @OverridepublicvoidonNothingSelected(AdapterView<>arg0){//notimplemented}};` 当实现OnItemSelectedListener时,您将不得不处理两个方法。一个是onNothingSelected方法,在这种情况下不感兴趣;另一个是onItemSelected方法,当用户做出选择时被触发。当它被系统调用时,它提供四个参数:带有底层适配器的AdapterView、被选择的视图元素、被选择的项目在列表中的位置以及列表项目的id。现在您已经知道选择了哪个项目,您可以将音符映射到它的实际频率,并将值发送到ADK板。这是在一个AsyncTask中完成的,这样IO操作就不会发生在UI线程上。 `newAsyncTask @OverrideprotectedVoiddoInBackground(Integer...params){sendAnalogValueCommand(TARGET_PIN_2,notes[params[0]]);returnnull;}}.execute(position);` 在将频率整数值作为四字节数据包传输之前,必须在sendAnalogValueCommand方法中对其进行位移。 图5-5。项目5:最终结果 本章的第二个项目将向你展示压电效应的原理。您将使用压电蜂鸣器构建一个爆震传感器,当压电元件振荡时,它会产生电荷。您将编写一个Android应用,在该应用中,每次检测到敲门声时,背景都会发生变化。一个简单的ProgressBarUI元素将显示已检测到的当前ADC值。 这个项目唯一需要的额外部件是一个高阻值电阻。您将使用一个1Mω的下拉电阻。其他组件已经在之前的项目中使用过(见图5-6): 图5-6。项目6部分(ADK板、试验板、电线、1Mω电阻器、压电蜂鸣器) 由于需要测量压电蜂鸣器振荡时的电压变化,因此需要使用ADK板上的一个模拟输入引脚。模拟输入将被转换成数字值(ADC),稍后可以在您的Android应用中进行处理。 正如已经提到的,你将在这个项目中利用压电蜂鸣器的压电效应。爆震或突然的压力波以某种方式影响压电元件,使其振荡。振荡频率对压电元件上产生的电荷有影响。所以振荡的频率与产生的电荷成正比。 在前一章中,您使用上拉电阻将数字输入引脚稳定地拉至状态HIGH(+5V),以避免电路处于空闲状态时产生静态噪声。当按下连接按钮,电路连接到GND(0V)时,电阻最小的路径通向GND,输入引脚设置为0V。 由于现在需要测量模拟引脚上施加的电压,将输入引脚上拉至5V毫无意义。您无法正确测量压电蜂鸣器引起的电压变化,因为输入引脚会持续在5V左右浮动。为了继续避免空闲状态下产生的静态噪声,同时能够测量电压变化,您可以将输入引脚拉低至GND(0V),并在压电元件产生负载时测量电压。该用例的简单电路原理图如图5-7中的所示。 图5-7。压电蜂鸣器输入测量下拉电阻电路 该项目的设置(如图图5-8所示)仅与之前略有不同。你只需要将高阻值电阻并联到压电蜂鸣器上。压电蜂鸣器的正极引线连接到电阻器的一端和ADK板的模拟输入引脚A0。负极引线连接到电阻器和GND的另一端。 图5-8。项目6设置 您将编写一个读取模拟输入引脚A0的Arduino草图。如果压电蜂鸣器振荡,并在该引脚上测量到电压,相应的值将被转换为数字值,并可以传输到Android设备。Android应用将通过ProgressBarUI元素可视化传输的值,如果达到某个阈值,容器视图元素的背景颜色将变为随机颜色。所以每次敲击最终都会产生一个新的背景色。 这个项目的Arduino草图与项目4中的基本相同。您将测量引脚A0上的模拟输入,并将转换后的ADC值(范围为0至1023)传输至连接的Android设备。参见完整的清单5-4。 清单5-4。项目6:Arduino草图 bytesntmsg[6]; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_ANALOG;sntmsg[1]=INPUT_PIN_0;} voidloop(){if(acc.isConnected()){intcurrentValue=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应用对接收到的消息进行解码,并将接收到的字节转换回测得的整数值。如果达到阈值100,LinearLayout视图容器将随机改变它的背景颜色。作为第二个可视化元素,您将为LinearLayout添加一个ProgressBar,这样如果用户在压电蜂鸣器附近敲门,就可以看到测量中的尖峰。 清单5-5。项目六:ProjectSixActivity.java `packageproject.six.adk; publicclassProjectSixActivityextendsActivity{ privateLinearLayoutlinearLayout;privateTextViewadcValueTextView;privateProgressBaradcValueProgressBar;privateRandomrandom;privatefinalintTHRESHOLD=100; 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=newRandom(System.currentTimeMillis());} privatefinalBroadcastReceivermUsbReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){…}};privatevoidopenAccessory(UsbAccessoryaccessory){mFileDescriptor=mUsbManager.openAccessory(accessory);if(mFileDescriptor!=null){mAccessory=accessory;FileDescriptorfd=mFileDescriptor.getFileDescriptor();mInputStream=newFileInputStream(fd);mOutputStream=newFileOutputStream(fd);Threadthread=newThread(null,commRunnable,TAG);thread.start();Log.d(TAG,"accessoryopened");}else{Log.d(TAG,"accessoryopenfail");}} if(buffer[1]==TARGET_PIN){finalintadcValue=((buffer[2]&0xFF)<<24) 这里对你来说唯一新的东西是Random类。Random类提供了为各种数字数据类型返回伪随机数的方法。特别是nextInt方法有一个重载的方法签名,它接受一个上限整数n,因此它只返回从0到n的值。在从ADK爆震传感器接收到的值被重新转换成整数后,它将对照阈值进行检查。如果该值超过阈值,则调用random对象的nextInt方法来生成三个随机整数。这些数字用于产生RGB颜色(红、绿、蓝),其中每个整数定义相应色谱的强度以形成新的颜色。屏幕的linearLayout视图容器用新的颜色更新,这样它的背景颜色在每次敲门时都会改变。 如果您已经完成了Arduino草图和Android应用的编写,请将它们部署到设备上并查看您的最终结果。它应该看起来像图5-9。 图5-9。项目6:最终结果 在本章中,您学习了压电效应和反向压电效应的原理,从而能够感知并产生声音。你影响了压电蜂鸣器产生声音的振荡频率。您还使用压电蜂鸣器来检测由蜂鸣器附近的压力波或振动引起的压电元件的振荡。在这个过程中,您学习了Arduinotone方法以及如何使用AndroidSpinnerUI元素。您再次利用ADK板的模拟功能读取模拟值并将其转换为数字值,以感测您附近的声音或振动。你可以在自己的进一步项目中使用所有这些知识,例如,给出听觉反馈或感知振动。 在这一章中,你将学会如何感知你周围环境中的光线强度。为了做到这一点,你将需要另一个新的组件,称为光敏电阻或光敏电阻(LDR)。我将在“部件”一节中解释这个组件的工作原理但是首先你需要理解光本身的描述。 那么光到底是什么?在我们的日常生活中,它无处不在。我们星球的完整生态系统依赖于光。它是所有生命的源泉,然而我们大多数人从未真正费心去理解光到底是什么。我不是一个物理学家,也不声称对它的物理原理提供了最好的解释,但我想至少提供一个关于光是什么的简要描述,让你对本章项目的目标有所了解。 光物理上描述为电磁辐射。辐射是高能波或粒子穿过介质的术语。在这种情况下,光是能量波的任何波长。人眼只能看到一定范围的波长。它可以对波长为390纳米到750纳米的光做出响应。当检测到特定波长和频率的光时,会感觉到不同的光色。表6-1给出了人眼能看到的光的色谱的概述。 人眼看不到的光的一个很好的例子是电视遥控器上的小红外LED。红外光谱在700纳米到1000纳米的范围内。LED的光波长通常在980纳米左右,因此超过了人眼可见的光谱。LED以取决于制造商的模式与电视的接收器单元进行通信。由于太阳光覆盖的波长范围很广,红外光也是其中一部分,因此通常会干扰通信。为了避免这个问题,电视制造商使用了在阳光中找不到的特定频率的红外光。 这一章的项目应该为你提供一种方法来轻松地感知你周围的光线变化。您将在ADK板的模拟输入引脚上测量光敏电阻的光照强度引起的电压变化。由此产生的转换后的数字值将被发送到Android设备,以根据周围的光线条件调整Android设备的屏幕亮度。大多数Android设备已经内置了这样的传感器来实现这一点,但这个项目应该可以帮助你了解你的设备是如何操作的,以及你如何自己影响它的光线设置。 这个项目的新部件是光敏电阻。其余部分对您来说并不陌生(参见图6-1): 图6-1。项目7部分(ADK板、试验板、电线、光敏电阻、10k电阻) 又到了使用ADK板的模拟输入引脚来测量电压变化的时候了。这个项目的电路设置将最终建立一个与光敏电阻连接的分压器。在模拟输入引脚上测量电压变化时,会用数字ADC值表示。稍后,您将使用数字值对相对环境照明进行假设。 一个光敏电阻是一个电阻,当它暴露在光线下时,电阻会减小(见图6-2)。这种行为是由所谓的光电效应造成的。 图6-2。光敏电阻 诸如光敏电阻的半导体的电子可以具有不同的状态。这些状态由能带描述。能带由价带、电子束缚在单个原子上、带隙,没有电子态存在,以及导带,电子可以自由移动。如果电子从吸收的光的光子中获得足够的能量,它们就会被从它们的原子上撞下来,从价带移动到导带,在那里它们可以自由移动。这个过程对光敏电阻的电阻值有直接影响。这就是光电效应的原理,如图图6-3所示。 图6-3。光电效应 光敏电阻通常用于需要感应照明变化的项目。例如,夜灯是光敏电阻的完美用例。当环境光线非常暗时,你需要打开夜灯,这样人们在晚上就能更好地辨别方向,而不必打开主灯。白天,当照明条件好得多的时候,你会想关掉夜灯以节约能源。这里可以使用光敏电阻将照明变化传播到微控制器,微控制器可以依次打开或关闭小夜灯。 另一种情况是使用光敏电阻和其他环境传感器来建立一个气象站,以监测全天的天气变化。例如,你可以判断天气是多云还是晴朗。如果你将这种气象站与Android设备结合使用,你可以保存你的数据,甚至将它发送到远程位置。如你所见,可能性是无限的。 需要额外的电阻器来创建分压器电路。你在第四章中学习了分压电路的原理。当光敏电阻暴露于光下时,需要分压器来测量电压变化。如果光敏电阻的阻值变化,电路的输出电压也会变化。如果您只是将光敏电阻单独连接到模拟输入引脚,您将不会测量到引脚上的电压变化,因为暴露在光线下只会改变光敏电阻的电阻特性,因此只会影响通过的电流。如果太多的电流通过,你也可能最终损坏你的ADK板,因为未使用的能量将在大量热量积累中表现出来。 如上所述,您需要为这个项目构建一个分压器电路。为此,您需要将光敏电阻的一根引线连接到+5V,另一根引线连接到附加电阻和模拟输入引脚A0。电阻器的一条引线连接到光敏电阻和模拟输入引脚A0,另一条引线连接到GND。项目设置见图6-4。 图6-4。项目7设置 您将编写一个Arduino草图,在模拟输入引脚A0获取模拟读数,并将其转换为10位数字值。该值被映射到0到100之间的较低值,并发送到Android设备。Android应用将根据接收到的值计算新的屏幕亮度。 同样,您将读取一个模拟引脚,只是这次您不会利用位移位技术来传输ADC值。您将首先使用utilitymap方法来转换您的测量值,稍后会详细介绍。首先看看完整的清单6-1。 清单6-1。项目7:Arduino草图 #defineCOMMAND_LIGHT_INTENSITY0x5#defineINPUT_PIN_00x0 bytesntmsg[3]; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_LIGHT_INTENSITY;sntmsg[1]=INPUT_PIN_0;} voidloop(){if(acc.isConnected()){intcurrentValue=analogRead(INPUT_PIN_0);sntmsg[2]=map(currentValue,0,1023,0,100);acc.write(sntmsg,3);delay(100);}}` 如您所见,新的命令字节和所用的模拟输入引脚是在开始时定义的。 `#defineCOMMAND_LIGHT_INTENSITY0x5 在这个项目中,您只需要一个三字节的消息,因为您不需要对测得的ADC值进行位移。您不需要对该值进行比特移位,因为您将在传输消息之前使用map方法。map方法的作用是将一个范围的值转换成另一个范围的值。您将把ADC值(范围为0到1023)映射到0到100的范围。例如,ADC值511将被转换为值50。转换时,测量值不会大于100,100小到可以放入一个字节。构建完整的三字节消息后,您可以简单地将其传输到Android设备。 intcurrentValue=analogRead(INPUT_PIN_0);sntmsg[2]=map(currentValue,0,1023,0,100);acc.write(sntmsg,3);delay(100); Arduino部分到此为止。让我们看看在Android端有什么要做的。 同样,Android应用负责接收来自ADK板的消息。当Android应用收到该值时,它会计算屏幕亮度的新强度。之后,设置新的屏幕亮度。清单6-2只强调了重要的部分;代码很短,你现在应该知道了。 清单6-2。项目7:ProjectSevenActivity.java `packageproject.seven.adk; publicclassProjectSevenActivityextendsActivity{ privatestaticfinalbyteCOMMAND_LIGHT_INTENSITY=0x5;privatestaticfinalbyteTARGET_PIN=0x0; privateTextViewlightIntensityTextView;privateLayoutParamswindowLayoutParams; setContentView(R.layout.main);lightIntensityTextView=(TextView)findViewById(R.id.light_intensity_text_view);} RunnablecommRunnable=newRunnable(){` `**@Overridepublicvoidrun(){intret=0;byte[]buffer=newbyte[3];**while(ret>=0){try{ret=mInputStream.read(buffer);}catch(IOExceptione){Log.e(TAG,"IOException",e);break;} switch(buffer[0]){caseCOMMAND_LIGHT_INTENSITY:if(buffer[1]==TARGET_PIN){finalbytelightIntensityValue=buffer[2];runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){lightIntensityTextView.setText(getString(R.string.light_intensity_value,lightIntensityValue));windowLayoutParams=getWindow().getAttributes();windowLayoutParams.screenBrightness=lightIntensityValue/100.0f;getWindow().setAttributes(windowLayoutParams);}});}break; 首先,您必须定义相同的命令字节和pin字节,以便稍后匹配接收到的消息。 `privatestaticfinalbyteCOMMAND_LIGHT_INTENSITY=0x5;privatestaticfinalbyteTARGET_PIN=0x0; privateTextViewlightIntensityTextView;privateLayoutParamswindowLayoutParams;` 类型Runnable的内部类实现了上面描述的屏幕亮度调整逻辑。从ADK板收到值后,更新TextViewUI元素,向用户提供文本反馈。 lightIntensityTextView.setText(getString(R.string.light_intensity_value,lightIntensityValue)); 为了调整屏幕的亮度,你首先要获得一个对当前Window的LayoutParams对象的引用。 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。 图6-5。项目7:最终结果 有时,像这个项目中所做的那样,仅仅使用相对值是不够的。测量光强度的更科学的方法是测量给定区域的照度。照度的单位是勒克斯;它的符号是lx。 许多Android设备都内置了光线传感器,可以根据周围的环境照明来调整屏幕亮度。这些传感器返回以勒克斯(lx)为单位的测量值。要请求这些值,首先必须获得对SensorManager类的引用,该类充当设备传感器的一种注册表。之后,您可以通过使用光传感器Sensor.TYPE_LIGHT的传感器类型常量调用SensorManager上的getDefaultSensor方法来获得对光传感器本身的引用。 SensorManagersensorManager=(SensorManager)getSystemService(Context.SENSOR_SERVICE);SensorlightSensor=sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); sensorManager.registerListener(lightSensorEventListener,lightSensor,SensorManager.SENSOR_DELAY_NORMAL); lightSensorEventListener的实现如下: `SensorEventListenerlightSensorEventListener=newSensorEventListener(){ @OverridepublicvoidonAccuracyChanged(Sensorsensor,intaccuracy){//nothingtoimplementhere} @OverridepublicvoidonSensorChanged(SensorEventsensorEvent){if(sensorEvent.sensor.getType()==Sensor.TYPE_LIGHT){Log.i("Lightinlx",sensorEvent.values[0]);}}};` 您只需要实现onSensorChanged方法,因为这是您感兴趣的事件。系统传递给该方法的SensorEvent对象包含一个值数组。根据您正在读取的传感器类型,您会在该数组中获得不同的值。传感器类型光的值位于该阵列的索引0处,它以勒克斯为单位反映当前的环境光。 你也可以把光敏电阻的测量值转换成勒克斯。然而,如果光敏电阻是非线性的(大多数光敏电阻都是非线性的),这需要更深入地理解数据手册和对数函数的使用。由于这涉及太多的细节,我不会在这里覆盖它。然而,如果你用你选择的搜索引擎搜索“计算勒克斯光敏电阻”,你可以在网上找到详细的信息和教程。 如果你不是数学的狂热爱好者,你也可以使用简单实用的方法。您可以尝试照明条件,并将从您的Android设备的光传感器接收到的结果与项目7中使用光敏电阻进行测量的结果进行比较。然后,您可以将lux值映射到相对值,并定义自己的查找表以供将来参考。请注意,这更多的是一个近似值,而不是精确的计算。 本章向你展示了光电效应的原理,以及如何在光敏电阻的帮助下,利用它来测量光强的变化。为此,您应用了分压器电路布局。您还学习了如何在Arduino平台上将一个范围的值映射到另一个范围的值,并根据周围的光线强度更改了Android设备屏幕的亮度。作为一个小奖励,您看到了如何在Android设备的内置光传感器上请求当前的环境照度(以勒克斯为单位)。 温度传感器广泛用于许多家用设备和工业机械中。它们的目的是测量附近的当前温度。它们通常用于预防目的,例如防止敏感部件过热,或者仅仅是监测温度的变化。 本章将向您展示如何使用热敏电阻,因为它是测量温度的最便宜、最普遍的元件。您将了解如何在元件数据手册和一些公式的帮助下计算温度。您将编写一个Android应用,通过使用定制的视图组件在设备屏幕上直接绘制形状和文本来可视化温度的变化。 项目8将指导您完成构建温度传感器的过程。您将使用热敏电阻来计算与其电阻值相对应的温度。为此,您必须设置一个分压器电路,并将其连接到ADK板的模拟输入引脚。您将测量电压的变化,并应用一些公式来计算温度。您将了解如何借助器件数据手册和施泰因哈特-哈特方程计算温度。之后,您将把确定的值传输到Android设备。Android应用将通过在屏幕上绘制温度计和文本值来可视化测量的温度。 除了前面提到的热敏电阻之外,您还需要一个10k的电阻、ADK板、试验板和一些电线。在本项目描述中,我将使用4.7k热敏电阻。4.7k电阻值是25摄氏度时的电阻。选择哪个电阻值并不重要,但热敏电阻的系数是负还是正,以及数据手册提供的规格值都很重要(稍后将详细介绍)。本项目所需的零件如图7-1所示: 图7-1。项目8部分(ADK板、试验板、电线、4.7k热敏电阻、10k电阻) 热敏电阻是一个可变电阻,其电阻值取决于环境温度。它的名字是由热敏和电阻两个字组成。热敏电阻没有方向性,也就是说,它与普通电阻一样,与电路的连接方式无关。它们可以具有负的或正的系数,这意味着当它们具有负的系数时,它们对应于温度的电阻增加,而当它们具有正的系数时,电阻减小。与光敏电阻一样,它们也依赖于能带理论,详见第六章光敏电阻一节。温度变化对热敏电阻的电子有直接影响,促使它们进入导电带,导致电导率和电阻发生变化。热敏电阻有不同的形状,但最常见的是带引线的盘形热敏电阻,它类似于典型的陶瓷电容器。(参见图7-2。) 图7-2。热敏电阻 选择热敏电阻时,最重要的事情是先看看它的数据手册。数据手册需要包含温度计算的一些重要细节。有些数据手册包含查找表,每个电阻值都映射到一个温度值。虽然您可以使用这样的表,但是将它转换到您的代码中是一项繁琐的任务。 更好的方法是用施泰因哈特-哈特方程计算当前温度。下面的摘要将向你展示计算温度的必要方程。不要害怕这里的数学。一旦你知道你要把哪些值放进方程,这就相当容易了。 施泰因哈特-哈特方程描述了一种模型,其中半导体的电阻取决于当前温度T。该公式如下所示: 为了应用这个公式,你需要三个系数——a、b、和c——此外,还有热敏电阻的当前电阻值R。如果您的热敏电阻数据手册包含这些值,您可以很好地使用它们,但大多数数据手册只提供所谓的B或β系数。幸运的是,对于特定的温度T.,施泰因哈特-哈特方程还有另一种表示,它与这个B参数和一对温度和电阻R0一起工作 这个方程中不同的参数只是代表了a、b和c。 a=(1/)-(1/B)×ln(R0) b=1/B c=0 R0指定为T0处的电阻,通常为298.15开尔文,等于25摄氏度。下面是B参数方程的简化公式: r=r∞e〖〗b/t〖〗 R∞描述了趋于无穷大的电阻,可通过下式计算: R∞=R0×e-B/ 现在,您可以计算所有必要的值,您可以重新排列之前的公式,以最终计算温度。 这些等式将在稍后的Arduino草图中应用,因此您稍后会再次遇到它们。 当热敏电阻的电阻变化时,你必须设置一个分压电路来测量电压的变化。分压器的组成取决于您使用的热敏电阻的类型。如果您使用负系数热敏电阻(NTC),您的基本电路设置如图7-3所示。 图7-3。NTC热敏电阻分压器 如果你使用正系数热敏电阻(PTC),你需要一个如图图7-4所示的电路。 图7-4。PTC热敏电阻分压器 在这个项目中,温度上升时,模拟输入引脚上测得的电压会增加,温度下降时,测得的电压会降低。因此,请确保根据您使用的热敏电阻构建您的分压器电路,如上所示。图7-5显示了NTC热敏电阻的项目设置。 图7-5。项目8设置 这个项目的Arduino草图将使用Arduino平台的一些数学函数。您将使用自己编写的方法来表达公式,以计算当前温度。温度值将随后传输到Android设备。Android应用将演示如何在Android设备的屏幕上绘制简单的形状和文本,以可视化测量的温度。 您将首次在Arduino草图中编写自己的自定义方法。自定义方法必须在强制设置和循环方法之外编写。它们可以有返回类型和输入参数。 此外,您将使用Arduino平台的一些数学函数。您将需要log和exp函数来应用施泰因哈特-哈特方程计算温度。计算出的温度值需要进行比特移位,以便正确传输到Android设备。看一下完整的清单7-1;我描述一下上市后的细节。 清单7-1。项目8:Arduino草图 #defineCOMMAND_TEMPERATURE0x4#defineINPUT_PIN_00x0//-----//changethosevaluesaccordingtoyourthermistor'sdatasheetlongr0=4700;longbeta=3980;//----- double=298.15;longadditional_resistor=10000;floatv_in=5.0;doubler_inf;doublecurrentThermistorResistance; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_TEMPERATURE;sntmsg[1]=INPUT_PIN_0;r_inf=r0*(exp((-beta)/));} voidloop(){if(acc.isConnected()){intcurrentADCValue=analogRead(INPUT_PIN_0);floatvoltageMeasured=getCurrentVoltage(currentADCValue);doublecurrentThermistorResistance=getCurrentThermistorResistance(voltageMeasured);doublecurrentTemperatureInDegrees=getCurrentTemperatureInDegrees(currentThermistorResistance); //multiplythefloatvalueby10toretainonevaluebehindthedecimalpointbefore//convertingtoanintegerforbettervaluetransmissionintconvertedValue=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);}} //"reverseADCcalculation"floatgetCurrentVoltage(intcurrentADCValue){returnv_in*currentADCValue/1024;} //rearrangedvoltagedividerformulaforthermistorresistancecalculationdoublegetCurrentThermistorResistance(floatvoltageMeasured){return((v_in*additional_resistor)-(voltageMeasured*additional_resistor))/voltageMeasured;} //Steinhart-HartBequationfortemperaturecalculationdoublegetCurrentTemperatureInDegrees(doublecurrentThermistorResistance){return(beta/log(currentThermistorResistance/r_inf))-273.15;}` 让我们看看草图顶部定义的变量。您在这里看到的第一个变量是数据协议的定义。为了确认传输了温度数据,选择了字节常数COMMAND_TEMPERATURE0x4。用于测量的模拟输入引脚被定义为INPUT_PIN_00x0。 现在已经定义了特定于数据表的值: longr0=4700;longbeta=3980; 我在这个项目中使用了一个4.7kω的热敏电阻,这意味着热敏电阻在25摄氏度时的电阻(R0)为4.7kω。这就是为什么r0被定义为4700。在我的例子中,热敏电阻的数据表只定义了B值,即3980。查看热敏电阻数据表,必要时调整这些值。 接下来,您将看到用于计算目的的常量值的一些定义: double=298.15;longadditional_resistor=10000;floatv_in=5.0; 你需要25摄氏度时的开尔文温度()来计算R∞。此外,还需要分压器电路中的第二个电阻值(10k)和输入电压来计算热敏电阻的电流电阻。 计算当前温度时,施泰因哈特-哈特方程的B参数变量中需要最后两个变量。 现在让我们看看程序流中发生了什么。在设置方法中,您将计算R∞的值,因为它只需要在开始时计算一次。 r_inf=r0*(exp((-beta)/)); 循环方法中的重复步骤可描述如下: 现在让我们来看看单个步骤的详细描述。 analogRead方法返回当前读取的ADC值。您将使用它来计算施加于模拟输入引脚的实际电压。为此,您可以使用自己编写的自定义方法: floatgetCurrentVoltage(intcurrentADCValue){returnv_in*currentADCValue/1024;} getCurrentVoltage方法将currentADCValue作为输入参数,并将计算出的电压作为float返回。由于Arduino平台将0V到5V的电压范围映射为1024个值,所以你只需将currentADCValue乘以5.0V,再除以1024,就可以计算出当前的电压。 现在你已经有了测量的电压,你可以用自己编写的方法getCurrentThermistorResistance计算热敏电阻的实际电阻。 doublegetCurrentThermistorResistance(floatvoltageMeasured){return((v_in*additional_resistor)-(voltageMeasured*additional_resistor))/voltageMeasured;} getCurrentThermistorResistance方法将测得的电压作为输入参数,计算电阻,并将其作为double返回。 最后可以进行最重要的计算。你用自己写的方法getCurrentTemperatureInDegrees来计算温度。 doublegetCurrentTemperatureInDegrees(doublecurrentThermistorResistance){return(beta/log(currentThermistorResistance/r_inf))-273.15;} 该方法将当前热敏电阻的电阻作为输入参数。它使用施泰因哈特-哈特方程的B参数变量来计算当前温度,单位为开尔文。要把它转换成摄氏度,你必须减去273.15。该方法以摄氏度为单位返回当前温度作为double。这里使用的Arduinolog函数是上面公式中使用的自然对数函数ln。 在将数据传输到Android设备之前,剩下的最后一步是转换温度值,以便于传输。例如,您可能已经计算出22.52摄氏度的双精度值。因为您只传输字节,所以您必须将值转换成非浮点数。小数点后有一个数字的精度就足够了,因此转换就像将该值乘以10一样简单,从而得到225。 intconvertedValue=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应用的第一步是建立与ADK板的通信,读取传输的数据并将其转换回原来的整数值。完成后,你可以通过在设备屏幕上绘制2D图形来可视化当前温度。这个应用将向你展示如何使用一些2D图形类和方法在屏幕的画布上绘制简单的形状。清单7-2显示了当前项目活动的一个片段,重点是新的和重要的部分。 清单7-2。项目八:ProjectEightActivity.java `packageproject.eight.adk; publicclassProjectEightActivityextendsActivity{ privatestaticfinalbyteCOMMAND_TEMPERATURE=0x4;privatestaticfinalbyteTARGET_PIN=0x0; privateTemperatureViewtemperatureView; setContentView(R.layout.main);temperatureView=(TemperatureView)findViewById(R.id.temperature_view);} switch(buffer[0]){caseCOMMAND_TEMPERATURE:if(buffer[1]==TARGET_PIN){finalfloattemperatureValue=(((buffer[2]&0xFF)<<24)+((buffer[3]&0xFF)<<16)+((buffer[4]&0xFF)<<8)+(buffer[5]&0xFF))/10;runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){temperatureView.setCurrentTemperature(temperatureValue);}});}break; 首先看看变量的定义。前两个消息字节必须与Arduino草图中定义的字节相匹配,因此您可以按如下方式定义它们: 然后你可以看到另一个类型为TemperatureView的变量。 TemperatureView顾名思义,是扩展安卓系统View类的自编自定义View。我们很快就会看到这个类,但是首先让我们继续activity类的剩余代码。 在读取接收到的消息后,您必须将字节数组转换回它原来的整数值。您只需反转Arduino部分中完成的位移来获得整数值。此外,您需要将整数除以10,以获得您最初计算的浮点值。 `finalfloattemperatureValue=(((buffer[2]&0xFF)<<24) 收到的值225现在将被转换为22.5。 最后要做的事情是将值传递给TemperatureView,这样您就可以在它的画布上绘制温度可视化。 `runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){temperatureView.setCurrentTemperature(temperatureValue);}});` 请记住,您应该只在UI线程上更新UI元素。您必须在runOnUIThread方法中设置TemperatureView的温度值,因为它会在以后重新绘制时使自己失效。 2D绘图是在TemperatureView类中实现的,所以先看看完整的清单7-3。 清单7-3。项目八:TemperatureView.java importandroid.content.Context;importandroid.content.res.TypedArray;importandroid.graphics.Canvas;importandroid.graphics.Color;importandroid.graphics.Paint;importandroid.graphics.RectF;importandroid.util.AttributeSet;importandroid.view.View; publicclassTemperatureViewextendsView{privatefloatcurrentTemperature;privatePainttextPaint=newPaint();privatePaintthermometerPaint=newPaint();privateRectFthermometerOval=newRectF();privateRectFthermometerRect=newRectF(); privateintavailableWidth;privateintavailableHeight; privatefinalfloatdeviceDensity; privateintovalLeftBorder;privateintovalTopBorder;privateintovalRightBorder;privateintovalBottomBorder; privateintrectLeftBorder;privateintrectTopBorder;privateintrectRightBorder;privateintrectBottomBorder; publicTemperatureView(Contextcontext,AttributeSetattrs){super(context,attrs);textPaint.setColor(Color.BLACK);thermometerPaint.setColor(Color.RED);deviceDensity=getResources().getDisplayMetrics().density;TypedArrayattributeArray=context.obtainStyledAttributes(attrs,R.styleable.temperature_view_attributes);inttextSize=attributeArray.getInt(R.styleable.temperature_view_attributes_textSize,18);textSize=(int)(textSize*deviceDensity+0.5f);textPaint.setTextSize(textSize);} @OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){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);//setupovalwithitspositioncenteredhorizontallyandatthebottomofthescreenthermometerOval.set(ovalLeftBorder,ovalTopBorder,ovalRightBorder,ovalBottomBorder); rectLeftBorder=(availableWidth/2)-(availableWidth/15);rectRightBorder=(availableWidth/2)+(availableWidth/15);rectBottomBorder=ovalBottomBorder-((ovalBottomBorder-ovalTopBorder)/2);} publicvoidsetCurrentTemperature(floatcurrentTemperature){this.currentTemperature=currentTemperature;//onlydrawathermometerintherangeof-50to50degreescelsiusfloatthermometerRectTop=currentTemperature+50;if(thermometerRectTop<0){thermometerRectTop=0;}elseif(thermometerRectTop>100){thermometerRectTop=100;}rectTopBorder=(int)(rectBottomBorder-(thermometerRectTop*(availableHeight/140)));//updaterectbordersthermometerRect.set(rectLeftBorder,rectTopBorder,rectRightBorder,rectBottomBorder);invalidate();} @OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas);//drawshapescanvas.drawOval(thermometerOval,thermometerPaint);canvas.drawRect(thermometerRect,thermometerPaint);//drawtextintheupperleftcornercanvas.drawText(getContext().getString(R.string.temperature_value,currentTemperature),availableWidth/10,availableHeight/10,textPaint);}}` 先看一下变量。如前所述,currentTemperature变量将由包含TemperatureView的活动设置。 privatefloatcurrentTemperature; 接下来你可以看到两个Paint参考。一个Paint对象定义了颜色、大小、笔划宽度等等。当你在绘制图形或文本时,你可以为相应的方法调用提供一个Paint对象来优化绘制结果。您将使用两个Paint对象,一个用于文本可视化,另一个用于稍后将绘制的形状。 privatePainttextPaint=newPaint();privatePaintthermometerPaint=newPaint(); RectF对象可以理解为用于定义形状边界的边界框。 privateRectFthermometerOval=newRectF();privateRectFthermometerRect=newRectF(); 你将画一个温度计,所以你将画两个不同的形状:一个椭圆形的底部和一个矩形的温度条(见图7-6)。 图7-6。2D形状创建温度计(椭圆形+长方形=温度计) 接下来的两个变量将包含View的宽度和高度。它们被用来计算你将画温度计的位置。 为了能够根据设备的屏幕属性调整文本大小,您必须确定您的屏幕密度(稍后会详细介绍)。 您将绘制的描绘温度计的2D图形具有定义的边界。这些边界需要动态计算,以适应任何屏幕尺寸,因此您也可以将它们保存在全局变量中。 `privateintovalLeftBorder;privateintovalTopBorder;privateintovalRightBorder;privateintovalBottomBorder; privateintrectLeftBorder;privateintrectTopBorder;privateintrectRightBorder;privateintrectBottomBorder;` 变量就是这样。现在我们将看看方法的实现,从TemperatureView的构造函数开始。 如果您想将自定义的View嵌入到一个XML布局文件中,您需要实现一个构造函数,它不仅接受一个Context对象,还接受一个AttributeSet。一旦View充气,这些将由系统设置。AttributeSet包含您可以进行的XML定义,比如宽度和高度,甚至自定义属性。您还需要调用父View的构造函数来正确设置属性。该构造函数也用于设置Paint对象。这只需要一次,所以你可以在这里设置颜色和文本大小。 当定义文本大小时,你必须考虑到设备有不同的屏幕属性。它们可以有从小到特大的不同尺寸,每种尺寸也可以有不同的密度,从低密度到超高密度。尺寸描述了以屏幕对角线测量的实际物理尺寸。密度描述了定义的物理区域中的像素数量,通常表示为每英寸点数(dpi)。如果您要为文本定义一个固定的像素大小,它将在具有相同大小的设备之间显示非常不同。在低密度的设备上,它可以呈现得非常大,而相同大小的其他设备会呈现得非常小,因为它们具有更高的密度。 要解决这个问题,你需要做几件事。首先,您必须确定设备的密度,以计算您需要设置的实际像素大小,以便文本在不同设备上看起来一致。 deviceDensity=getResources().getDisplayMetrics().density; 现在你有了密度,你只需要文本的相对大小来计算实际的像素大小。当编写自己的View元素时,您也可以为该视图定义自定义属性。在这个例子中,您将为TemperatureView定义属性textSize。为了做到这一点,您必须创建一个新文件,定义所有的定制属性TemperatureView可以有。在res/values中创建一个名为attributes.xml的XML文件。文件的名字没有限制,你可以选择随便叫;只要确保它以.xml结尾。在这个XML文件中,你必须定义如清单7-4所示的属性。 清单7-4。项目8:attributes.xml 接下来,您需要将TemperatureView添加到布局中,并设置它的textSize属性。如果您在XML布局文件中使用您自己的定制视图,您必须用它们的完全限定类名来定义它们,即它们的包名加上它们的类名。这个项目的main.xml布局文件看起来像清单7-5中的。 清单7-5。项目8:main.xml 因为您不仅添加了系统属性,还添加了自己的属性,所以除了标准的系统名称空间之外,您还必须定义自己的名称空间。 temperatureview:textSize=”18”> 现在您已经成功配置了自定义属性textSize。让我们看看如何在初始化TemperatureView时访问它的值。 TypedArrayattributeArray=context.obtainStyledAttributes(attrs,R.styleable.temperature_view_attributes);inttextSize=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方法。 `@OverrideprotectedvoidonMeasure(intwidthMeasureSpec,intheightMeasureSpec){super.onMeasure(widthMeasureSpec,heightMeasureSpec);availableWidth=getMeasuredWidth();availableHeight=getMeasuredHeight(); rectLeftBorder=(availableWidth/2)-(availableWidth/15);rectRightBorder=(availableWidth/2)+(availableWidth/15);rectBottomBorder=ovalBottomBorder-((ovalBottomBorder-ovalTopBorder)/2);}` onMeasure方法继承自View系统类。系统调用它来计算显示View所需的尺寸。它在这里被覆盖以获得View的当前宽度和高度,以便以后可以以适当的比例绘制形状。注意,同样调用父类的onMeasure方法是很重要的,否则系统将抛出一个IllegalStateException。一旦你有了宽度和高度,你就可以定义椭圆形的边界框了,因为它以后不会改变。您还可以计算矩形四个边框中的三个。唯一依赖于当前温度的边界是顶部边界,因此将在以后进行计算。边界的计算定义了在每个设备上看起来成比例的形状。 为了更新测量的温度值以便可视化,您编写了一个名为setCurrentTemperature的setter方法,它将当前温度作为一个参数。setCurrentTemperature方法不仅仅是一个简单的变量设置器。它还用于更新温度计栏矩形的边界框,并使视图无效,以便重新绘制视图。 更新矩形的边界后,你需要使TemperatureView无效。从TemperatureView的超类View继承而来的invalidate方法告诉系统这个特定的视图元素是无效的,需要重新绘制。 最后一种方法是负责2D图形绘制的实际方法。每次需要更新时,在一个View上调用onDraw方法。你可以像在setCurrentTemperature方法中一样,通过调用invalidate方法告诉系统它需要重画。让我们来看看它的实现。 `@OverrideprotectedvoidonDraw(Canvascanvas){super.onDraw(canvas); //drawshapescanvas.drawOval(thermometerOval,thermometerPaint);canvas.drawRect(thermometerRect,thermometerPaint); //drawtextintheupperleftcornercanvas.drawText(getContext().getString(R.string.temperature_value,currentTemperature),availableWidth/10,availableHeight/10,textPaint);}` 当系统调用onDraw方法时,它提供一个与View关联的Canvas对象。Canvas对象用于在其表面上绘图。draw方法调用的顺序很重要。你可以把它想象成现实生活中的画布,你可以在上面一层一层地画。在这里你可以看到,首先一个椭圆形及其预定义的RectF和Paint对象被绘制。接下来,绘制象征温度计条的矩形。最后通过定义要绘制的文本来绘制文本可视化,其坐标和原点是左上角及其关联的Paint对象。编码部分到此为止。 算了这么多,终于到了看你自建温度计是否管用的时候了。部署您的应用,如果您用指尖加热热敏电阻,应该会看到温度升高。最终结果应该看起来像图7-7。 图7-7。项目8:最终结果 在这一章中,你学会了如何制作自己的温度计。您还学习了热敏电阻的基本知识,以及如何借助施泰因哈特-哈特方程计算环境温度。出于可视化的目的,您编写了自己的自定义UI元素。您还使用2D图形绘制了一个虚拟温度计以及当前测量温度的文本表示。 触摸用户界面已经越来越成为我们日常生活的一部分。我们在自动售货机、家用电器、手机和电脑上看到它们。触摸界面让日常活动看起来更有未来感和时尚感。当你在看老科幻电影时,你会注意到,即使在那时,触摸也是想象未来用户输入的首选方式。如今,孩子们是在这种技术的陪伴下成长的。 有许多不同类型的触摸界面技术,每种技术都有其优点和缺点。三种最普遍的技术是电阻触摸感应、电容触摸感应和红外(IR)触摸感应。 电容式触摸是另一种触摸感应方式,被更新的智能手机等现代设备所采用。其原理依赖于人体的电容特性。电容式触摸表面形成电场,该电场在被触摸时被人体扭曲,并被测量为电容的变化。电容式触摸系统的优势在于,您不必触摸表面就能感受到触摸。当系统没有足够高的绝缘时,当您靠近传感器时,可能会影响传感器。然而,触摸屏有玻璃绝缘,需要你直接触摸。电容式触摸系统不需要力量来感知输入。缺点是不是每个物体都可以与电容式触摸系统交互。你可能已经注意到,当你戴上普通的冬季手套时,你无法控制你的智能手机。那是因为你没有导电性,手指上包裹了太多绝缘材料。除了手指之外,您只能使用特殊的触笔或等效物来控制电容式触摸系统。 你可能听说过的最后一个系统是红外(IR)触摸系统。这种触摸系统主要用于户外亭或大型多点触摸桌,你可能听说过。红外系统的工作原理是由红外发光二极管发出的红外光投射到屏幕或玻璃表面的边缘。红外光束在屏幕内以一定的模式反射,当物体放在屏幕表面时,这种模式会被破坏。IRLEDs被定位成覆盖x轴和y轴,以便可以确定放置在屏幕上的对象的正确位置。红外系统的优势在于,每个物体都可以用来与系统进行交互,因为该系统对电导率或电容等属性没有要求。一个缺点是,便宜的系统会受到阳光直射的影响,阳光中含有红外光谱。然而,大多数工业或消费系统都有适当的滤波机制来避免这些干扰。 这个项目将释放你自己动手(DIY)的精神,使你能够建立自己的定制电容式触摸传感器。电容式触摸传感器是迄今为止最容易和最便宜的为自己制作的,这就是为什么你将在本章的项目中使用它。您将使用铝箔制作一个自定义传感器,它将成为项目电路的一部分。您将使用ADK板的一个数字输入引脚来感知用户何时触摸传感器或影响其电场。触摸信息将被传播到一个Android应用,通过振动和播放一个简单的声音文件,让你的Android设备成为一个游戏节目蜂鸣器。 如您所知,您不会在本章的项目中使用预建的传感器。这一次,您将使用任何家庭中都能找到的零件来制作自己的传感器。为了制作一个可以连接到电路的电容式触摸传感器,你需要胶带、铝箔和一根电线。以下是该项目的完整零件清单(如图图8-1): 图8-1。项目9件(ADK板、试验板、电线、铝箔、胶带、10k电阻) 铝箔是铝压制成的薄片(图8-2)。家用床单通常具有大约0.2毫米的厚度。在一些地区,它仍然被错误地称为锡箔,因为锡箔是铝箔的前身。铝箔具有导电的特性,因此可以用作电路的一部分。你也可以在本章的项目中使用一根简单的线,但是箔片提供了一个更大的触摸目标区域,并且可以按照你喜欢的方式形成。不过,如果想将铝箔集成到电路中,它有一个缺点。用普通的焊锡将电线焊接到箔片上是不可能的。铝箔有一层薄的氧化层,防止焊锡与铝形成化合物。然而,有一些方法可以在焊接时减缓铝的氧化,迫使化合物与焊锡结合。这些方法是繁琐的,大多数时候你会得到一张损坏的铝箔,所以你不会在这个项目中这样做。相反,你会建立一个松散的铝箔连接。 图8-2。铝箔 如前所述,您需要在连接到项目电路的电线和一片铝箔之间建立松散连接。为了将电线紧紧地固定在铝箔上,您将使用胶带(图8-3)。你可以使用任何类型的胶带,比如管道胶带,所以只要用你喜欢的胶带就可以了。 图8-3。胶带 你要做的第一件事是构建电容式触摸传感器。你先把一小块铝箔切成一定的形状。保持它相当小,以获得最佳结果;手掌大小的四分之一应该足够了。(参见图8-4。) 图8-4。电线、铝箔片、胶带 接下来,在箔片上放一根电线,并用一条胶带粘上。你要确保电线接触到金属箔并且牢牢地固定在上面。这种方法的一种替代方法是使用鳄鱼夹连接到可以夹在箔片上的金属丝上。如果您的连接有问题,您可以使用这些作为替代。但是你可能想先用胶带试试。(参见图8-5。) 图8-5。电容式触摸传感器 现在你的传感器已经准备好了,你只需要把它连接到电路上。电路设置非常简单。您只需通过一个高阻值电阻将一个配置为输出的数字引脚连接到一个数字输入引脚。使用大约10k的电阻值。由于电路中没有消耗大量电流的实际用电设备,所以需要电阻,这样只有非常小的电流流过电路。触摸传感器就像一根分叉的电线一样连接到电路上。你可以在图8-6中看到完整的设置。 图8-6。项目9设置 该项目的软件部分将向您展示如何使用CapSenseArduino库,通过您新构建的电容式触摸传感器来感知触摸。当达到某个阈值时,您将识别触摸事件,并将该信息传播到Android设备。如果发生触摸,运行的Android应用将播放蜂鸣器声音并振动,就像游戏节目蜂鸣器一样。 先看看完整的清单8-1。我将在清单之后更详细地介绍CapSense库。 清单8-1。项目9:Arduino草图 #include #defineCOMMAND_TOUCH_SENSOR0x6#defineSENSOR_ID0x0;#defineTHRESHOLD50 CapSensetouchSensor=CapSense(4,6); voidsetup(){Serial.begin(19200);acc.powerOn();//disablesautocalibrationtouchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);sntmsg[0]=COMMAND_TOUCH_SENSOR;sntmsg[1]=SENSOR_ID;}voidloop(){if(acc.isConnected()){//takes30measurementstoreducefalsereadingsanddisturbanceslongvalue=touchSensor.capSense(30);if(value>THRESHOLD){sntmsg[2]=0x1;}else{sntmsg[2]=0x0;}acc.write(sntmsg,3);delay(100);}}` 除了附件通信所需的库之外,您还需要包含带有以下指令的CapSense库: 由于电容式触摸按钮是一种特殊的按钮,我对数据消息使用了新的命令字节0x6。您可以使用与常规按钮相同的命令字节,即0x1,但是您必须在代码中进一步区分这两种类型。这里定义的第二个字节是触摸传感器的id: `#defineCOMMAND_TOUCH_SENSOR0x6 #defineTHRESHOLD50 对于这个项目,我选择了值50,因为对于这个电路设置,CapSense测量返回的值范围相当小。如果您使用一些Serial.println()方法调用来监控测量值,以查看触摸电容式传感器时该值如何变化,会有所帮助。如果您在设置中使用另一个电阻,或者您发现50不是您的设置的最佳阈值,那么您可以简单地调整THRESHOLD值。 你在草图中看到的下一个东西是CapSense对象的定义。CapSense类的构造函数将两个整数值作为输入参数。第一个定义了数字输出引脚,它在数字状态HIGH和LOW之间交替。第二个参数定义数字输入引脚,该引脚被拉至与输出引脚相同的电流状态。 看完变量定义之后,让我们来看看设置方法。除了通常的初始化步骤,您现在已经知道了,还有对CapSense对象的第一个方法调用。 touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF); 该方法关闭了感测程序的自动校准,否则在测量期间可能会发生自动校准。 循环法实现触摸检测。首先,调用capSense方法,其中必须提供样本数量的参数。30个样本的值似乎足够了。该方法以任意单位返回值。如果返回的检测值超过您之前定义的阈值,则检测到触摸,并在返回消息中设置相应的字节。 longvalue=touchSensor.capSense(30);if(value>THRESHOLD){sntmsg[2]=0x1;}else{sntmsg[2]=0x0;} 最后要做的是将当前数据消息发送到连接的Android设备。 Android应用使用了一些你已经知道的功能,比如使用振动器服务。这个应用的一个新功能是音频播放。当接收到触摸传感器数据消息时,代码评估数据以确定触摸按钮是否被按下。如果它被按下,背景颜色会变成红色,一个TextView会显示哪个触摸按钮被按下,以防您添加其他按钮。与此同时,该设备的振动器打开,并播放蜂鸣器声音,以获得最终游戏节目蜂鸣器般的感觉。清单8-2中的这个项目的Android代码显示了应用逻辑。 清单8-2。项目9:ProjectNineActivity.java `packageproject.nine.adk; publicclassProjectNineActivityextendsActivity{ privatestaticfinalbyteCOMMAND_TOUCH_SENSOR=0x6;privatestaticfinalbyteSENSOR_ID=0x0; privateLinearLayoutlinearLayout;privateTextViewbuzzerIdentifierTextView; privateSoundPoolsoundPool;privatebooleanisSoundPlaying;privateintsoundId; privatefloatstreamVolumeMax; setContentView(R.layout.main);linearLayout=(LinearLayout)findViewById(R.id.linear_layout);buzzerIdentifierTextView=(TextView)findViewById(R.id.buzzer_identifier); soundPool=newSoundPool(1,AudioManager.STREAM_MUSIC,0);soundId=soundPool.load(this,R.raw.buzzer,1); AudioManagermgr=(AudioManager)getSystemService(Context.AUDIO_SERVICE);streamVolumeMax=mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);} /**Calledwhentheactivityispausedbythesystem.*/@OverridepublicvoidonPause(){super.onPause();closeAccessory();stopVibrate();stopSound();} @Overridepublicvoidrun(){intret=0;byte[]buffer=newbyte[3]; switch(buffer[0]){caseCOMMAND_TOUCH_SENSOR: if(buffer[1]==SENSOR_ID){finalbytebuzzerId=buffer[1];finalbooleanbuzzerIsPressed=buffer[2]==0x1;runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){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,"unknownmsg:"+buffer[0]);break;}}}}; privatevoidstartVibrate(){if(vibrator!=null&&!isVibrating){isVibrating=true;vibrator.vibrate(newlong[]{0,1000,250},0);}} privatevoidstopVibrate(){if(vibrator!=null&&isVibrating){isVibrating=false;vibrator.cancel();}} privatevoidplaySound(){if(!isSoundPlaying){soundPool.play(soundId,streamVolumeMax,streamVolumeMax,1,0,1.0F);isSoundPlaying=true;}}privatevoidstopSound(){if(isSoundPlaying){soundPool.stop(soundId);isSoundPlaying=false;}} privatevoidreleaseSoundPool(){if(soundPool!=null){stopSound();soundPool.release();soundPool=null;}}}` 首先,像往常一样,让我们看看变量的定义。 `privatestaticfinalbyteCOMMAND_TOUCH_SENSOR=0x6;privatestaticfinalbyteSENSOR_ID=0x0; privatefloatstreamVolumeMax;` 数据消息字节与Arduino草图中的相同。LinearLayout是容器视图,它稍后会填充整个屏幕。它用于通过将背景颜色更改为红色来指示触摸按钮被按下。TextView显示当前按钮的标识符。接下来的两个变量负责保存对Android系统的Vibrator服务的引用,并负责确定振动器当前是否在振动。最后一个变量负责媒体回放。Android有几种媒体播放的可能性。一种简单的低延迟播放短声音片段的方法是使用SoundPool类,它甚至能够一次播放多个流。SoundPool对象在初始化后负责加载和播放声音。在本例中,您将需要一个布尔标志,即isSoundPlaying标志,这样,如果蜂鸣器已经在播放,您就不会再次触发它。一旦声音文件被加载,soundId将保存一个对声音文件的引用。最后一个变量用于设置稍后播放声音时的音量。 这里要做的最后一件事是确定您所使用的流类型的最大可能音量。稍后,当您播放声音时,您可以定义音量级别。因为你想要一个相当响的蜂鸣器,所以最好把音量调到最大。 `setContentView(R.layout.main);linearLayout=(LinearLayout)findViewById(R.id.linear_layout);buzzerIdentifierTextView=(TextView)findViewById(R.id.buzzer_identifier); AudioManagermgr=(AudioManager)getSystemService(Context.AUDIO_SERVICE);streamVolumeMax=mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);` 和往常一样,在接收消息时,分配给接收工作线程的Runnable对象实现评估逻辑,并最终触发类似蜂鸣器的行为。 `switch(buffer[0]){caseCOMMAND_TOUCH_SENSOR:if(buffer[1]==SENSOR_ID){finalbytebuzzerId=buffer[1];finalbooleanbuzzerIsPressed=buffer[2]==0x1;runOnUiThread(newRunnable(){ default:Log.d(TAG,"unknownmsg:"+buffer[0]);break;}` 您可以看到LinearLayout的背景颜色根据按钮的状态而改变,并且TextView也相应地更新。startVibrate和stopVibrate方法在第四章的项目3中已经熟悉。 `privatevoidstartVibrate(){if(vibrator!=null&&!isVibrating){isVibrating=true;vibrator.vibrate(newlong[]{0,1000,250},0);}} privatevoidstopVibrate(){if(vibrator!=null&&isVibrating){isVibrating=false;vibrator.cancel();}}` startVibrate和stopVibrate方法只是在开始振动或取消当前振动之前检查振动器是否已经在振动。 根据触摸按钮的状态,开始或停止蜂鸣声播放。这里可以看到方法的实现: `privatevoidplaySound(){if(!isSoundPlaying){soundPool.play(soundId,streamVolumeMax,streamVolumeMax,1,0,1.0F);isSoundPlaying=true;}} privatevoidstopSound(){if(isSoundPlaying){soundPool.stop(soundId);isSoundPlaying=false;}}` 要播放声音,你必须调用SoundPool对象上的play方法。它的参数是soundId,这是您之前在加载声音文件时检索到的,左右声道的音量定义,声音优先级,循环模式,以及当前声音的回放速率。要停止声音,你只需调用SoundPool对象上的stop方法,并提供相应的soundId。您不需要在您的AndroidManifest.xml中为SoundPool的工作定义任何额外的权限。 当应用关闭时,你也应该清理一下。要释放SoundPool分配的资源,只需调用release方法。 privatevoidreleaseSoundPool(){if(soundPool!=null){stopSound();soundPool.release();soundPool=null;}} 由于屏幕布局与上一个项目有所不同,你应该看看这个项目的main.xml布局文件,如清单8-3所示。 清单8-3。项目9:main.xml 您可以看到该布局只定义了一个嵌入到LinearLayout容器中的TextView。 这就是本章项目编码的全部内容。如果你愿意,你可以用额外的蜂鸣器来扩展这个项目,这样你就可以在和你的朋友和家人玩游戏时使用你自己定制的蜂鸣器。部署您的应用,并对项目进行测试。你的最终结果应该看起来像图8-7。 图8-7。项目9:最终结果 您已经看到,构建一个简单的DIY电容式触摸传感器既不困难也不昂贵。你可以很容易地想象,这种技术在爱好社区中被大量使用来构建具有漂亮和酷的交互用户界面的项目。为了让你自己的创意源源不断,我想借此机会向你展示我的一个项目,这是我为2011年柏林谷歌开发者日做的,只是可以实现的一个例子。 2011年7月,谷歌宣布公开呼吁谷歌开发者日。谷歌开发者日是谷歌在美国以外最大的开发者大会。它在全球几个大城市举办。2011年的比赛地点是阿根廷、澳大利亚、巴西、捷克共和国、德国、以色列、日本和俄罗斯。这次公开征集让开发者有机会向大约2000名开发者展示他们的技能和项目。两个挑战是公开呼吁的一部分:HTML5挑战和ADK挑战。参与者首先必须回答一些关于他们挑战的相应技术的基本问题。当他们成功回答这些问题时,他们就有资格参加第二轮挑战。现在,我不知道HTML5挑战赛的流程到底是如何运作的,但ADK挑战赛的第二轮要求拿出一个合理的项目计划。项目计划应该将ADK技术与Android设备结合起来,创造一些有趣的东西,如机器人、乐器,甚至是解决日常问题的设备。我的项目计划是用纸做一架带有电容式触摸键的钢琴,ADK纸钢琴。当用户触摸一个键时,连接的ADK板应该会识别它,并在连接的Android设备的帮助下播放该键的相应音符。 我从一个只有四个电容式触摸键的小型原型开始。我想看看我用铝箔制成的电容式触摸传感器,当我用纸的顶层和底层绝缘时,它是否会有足够的响应。我使用了CapSense库来识别触摸时不同的键,并使用了SoundPool类来回放每个键对应的音符。设计示意图如图图8-8所示。 图8-8。钢琴键构造示意图 原型运行得非常好,我的项目计划被认为是十大提交项目之一,所以我在谷歌开发者日的展览区获得了一席之地。谷歌提供了谷歌ADK董事会和演示盾牌,以实现该项目的活动。 正如我对原型所做的那样,我决定用覆有纸的铝箔条来制作电容式触摸钢琴键。每个钥匙下面都有自己的铝箔条。我必须确保这些条带覆盖了钥匙的大部分区域,而不会碰到附近的其他条带。 图8-9。钢琴键布局表 总共需要对61个按键进行切割并粘贴在纸键下方的繁琐工作(参见图8-10)。). 图8-10。完成琴键布局 如果你还记得的话,谷歌ADK板是基于ArduinoMega设计的,只有54个数字引脚。光凭黑板,我无法识别目标的61个键。这就是为什么我建立了自己的扩展板,能够提供更多的输入。电路板上有所谓的8位输入移位寄存器。这些IC提供八个输入通道,只需使用ADK板的三个引脚即可读取。有了8个这样的IC,我只用了大约一半的ADK数字引脚就能有64路输入。(参见图8-11。) 图8-11。自定义输入移位寄存器板 由于我不能将输入连接直接焊接到铝箔条上,我使用鳄鱼夹来建立连接(图8-12)。 图8-12。成品纸钢琴建造 在截止日期前完成项目(如图8-13所示)后,我不得不一路穿过柏林将钢琴运送到场地(图8-14)。ADK项目的展览区受到了热烈的欢迎,ADK纸钢琴给人留下了深刻的印象。它甚至被当地报纸的一篇报道选中。在活动中展示的ADK项目引起了社区的极大兴趣,并很好地概述了可以用ADK做些什么,并且希望激发一些人自己尝试一下。 图8-13。柏林2011年谷歌开发者日的ADK纸钢琴 图8-14。【2011年柏林谷歌开发者日在柏林国际商会举行 你也应该看看这个网站,它给出了在谷歌开发者日之旅的不同地点展示的所有ADK项目的概述。 自从你开始写这本书以来,你第一次用家用物品制作了自己的传感器。您已经了解了构建自己的电容式触摸传感器有多简单。对于Arduino部分,您学习了如何使用ArduinoCapSense库来感测触摸事件。通过结合一些以前学到的Android功能,比如使用Vibrator服务和通过SoundPool类添加回放声音的能力,您创建了自己的游戏节目蜂鸣器。您还了解到在一个更大的项目中使用DIY电容式触摸传感器的个人实例。 业余电子爱好最有趣的方面之一可能是制造机器人或让你的项目移动。根据使用情况,有许多方法可以实现一般的移动。推动项目的一种常见方式是通过马达。马达被称为致动器,因为它们作用于某物,而不是像传感器那样感知某物。不同种类的马达提供不同程度的运动自由度和动力。三种最常见的电机是DC电机、伺服电机和步进电机(见图9-1)。 DC汽车是靠直流电(DC)运行的电动机,主要用于遥控车等玩具。它们提供轴的连续旋转,轴可以连接到齿轮以实现不同的动力传输。它们没有位置反馈,这意味着你不能确定电机转动了多少度。 例如,伺服系统通常用于机器人移动手臂或腿的关节。它们的旋转大多被限制在一定的度数范围内。大多数伺服系统不提供连续旋转,仅支持180度范围内的运动。然而,有特殊的伺服能够旋转360度,甚至有黑客的限制伺服压制他们的180度限制。伺服系统具有位置反馈,这使得通过发送特定信号将它们设置到某个位置成为可能。 步进电机主要用于扫描仪或打印机中的精密机器运动。它们提供带有精确位置反馈的全旋转。当齿轮或传送带连接到步进电机上时,它们可以移动到准确的位置。 图9-1。电机(DC电机、伺服电机、步进电机) 伺服系统非常适合控制有限的运动。为了控制一个伺服系统,你需要通过你的ADK板的数字引脚发送不同的波形给伺服系统。为了定义您的伺服系统应该向哪个方向移动,您将编写一个利用您设备的加速度传感器的Android应用。因此,当你沿着x轴的某个方向倾斜你的设备时,你的伺服系统也会反映相对运动。 设备的加速度计实际上做的是测量施加到设备上的加速度。加速度是相对于一组轴的速度变化率。重力影响测得的加速度。当Android设备放在桌子上时,不会测量到加速度。当沿设备的一个轴倾斜设备时,沿该轴的加速度发生变化。(参见图9-2)。 幸运的是,伺服系统不需要复杂的电路或额外的部件。它们只有一条数据线接收波形脉冲,以将伺服设置在正确的位置。所以除了一个伺服系统,你只需要你的ADK板和一些电线(如图9-3所示)。 图9-3。项目10部分(ADK板、电线、伺服) 如前所述,伺服机构是一种马达,在很大程度上只能在预定范围内转动其轴。业余爱好伺服的范围通常是180度。当内部齿轮到达预定位置时,通过阻止内部齿轮机械地实现限制。你可以在网上找到黑客来摆脱这种阻碍,但你必须打开你的伺服来打破这种阻碍,然后做一些焊接。获得更多旋转自由度的另一种可能性是使用特殊的360度伺服系统。这些往往有点贵,这就是为什么黑客的低预算180度伺服似乎是一个很好的选择,对一些人来说。无论如何,在大多数情况下,你不会需要一个完整的旋转伺服大多数项目。在这些情况下,你最好使用DC电机或步进电机。 图9-4。伺服控制信号波形(上:最左位置,下:最右位置) 对于不同的使用情况,伺服系统有许多不同的外形规格(图9-5)。在业余爱好电子产品中,你会发现模型飞机或机器人的小型伺服系统。这些伺服系统可以区分大小和速度,但它们通常很小,很容易安装。 图9-5。不同的伺服外形尺寸 大多数伺服系统都有不同的驱动轴附件,所以你可以用它们作为机器人的关节或控制模型飞机或轮船的方向舵。(参见图9-6)。 图9-6。伺服驱动轴附件 因为你不需要一个特殊的电路来控制一个伺服系统,如果你有一个能够产生所需波形的微控制器,连接一个伺服系统是非常简单的。一个伺服系统有三根电线与之相连。通常它们以某种方式着色。红线是Vin,它连接到+5V。但是,您应该阅读数据手册,看看您的伺服系统是否有不同的输入电压额定值。黑线必须接地(GND)。最后一根线是数据线,通常为橙色、黄色或白色。这是连接微控制器数字输出引脚的地方。它用于将脉冲传输到伺服系统,伺服系统进而移动到所需的位置。 由于你不必为这个项目建立一个特殊的电路,你可以直接连接你的伺服到你的ADK板。如前所述,将红线连接到+5V,黑线连接到GND,橙线、黄线或白线连接到数字引脚2。伺服系统通常带有母连接器,所以你要么直接将电线连接到连接器,要么在中间使用公对公连接器。(参见图9-7)。 图9-7。项目10设置 为了控制伺服系统,您将编写一个Android应用,请求更新其加速度计传感器在x轴上的当前倾斜度。当你向左或向右倾斜你的设备时,你将把产生的方向更新发送到ADK板。Arduino草图接收新的倾斜值,并向伺服系统发送相应的定位脉冲。 Arduino草图负责接收加速度计数据,并将伺服设置到正确的位置。你有两种可能做那件事。您可以实现自己的方法来创建想要发送到伺服系统的波形,也可以使用ArduinoIDE附带的Servo库。就个人而言,我更喜欢使用库,因为它更精确,但是我将在这里向您展示这两种方法。您可以决定哪种解决方案更符合您的需求。 第一种方法是自己实现波形生成。先看看完整的清单9-1。 清单9-1。项目10:Arduino草图(自定义波形实现) #defineCOMMAND_SERVO0x7#defineSERVO_ID_10x1#defineSERVO_ID_1_PIN2 inthighSignalTime;floatmicroSecondsPerDegree;//defaultboundaries,changethemforyourspecificservointleftBoundaryInMicroSeconds=1000;intrightBoundaryInMicroSeconds=2000; voidsetup(){Serial.begin(19200);pinMode(SERVO_ID_1_PIN,OUTPUT);acc.powerOn();microSecondsPerDegree=(rightBoundaryInMicroSeconds–leftBoundaryInMicrosSeconds)/180.0;} voidloop(){if(acc.isConnected()){intlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_SERVO){if(rcvmsg[1]==SERVO_ID_1){intposInDegrees=((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);}}}}} voidmoveServo(intservoPulsePin,intpos){//calculatetimeforhighsignalhighSignalTime=leftBoundaryInMicroSeconds+(pos*microSecondsPerDegree);//setServotoHIGHdigitalWrite(servoPulsePin,HIGH);//waitforcalculatedamountofmicrosecondsdelayMicroseconds(highSignalTime);//setServotoLOWdigitalWrite(servoPulsePin,LOW);//delaytocompletewaveformdelayMicroseconds(20000–highSignalTime);}` 像往常一样,让我们从草图的变量开始。 `#defineCOMMAND_SERVO0x7 您可以看到为伺服控制消息定义了一个新的命令类型常量。第二个常量是伺服的ID,如果你想在你的ADK板上安装多个伺服,应该控制这个ID。第三个常量是连接伺服系统的ADK板上的相应引脚。接下来,您有一些变量来指定以后的波形。 inthighSignalTime;floatmicroSecondsPerDegree;intleftBoundaryInMicroSeconds=1000;intrightBoundaryInMicroSeconds=2000; 在setup方法中,您必须将伺服信号引脚的pinMode设置为输出。 pinMode(SERVO_ID_1_PIN,OUTPUT); 您还应该在这里计算每度的微秒数,以便您可以在以后的定位计算中使用该值。 microSecondsPerDegree=(rightBoundaryInMicroSeconds-leftBoundaryInMicroSeconds)/180.0; 计算很简单。由于您的伺服很可能只有180度的范围,您只需要将右边界值和左边界值之差除以180。 接下来是loop方法。在从Android应用中读取接收到的数据消息后,您需要使用移位技术对传输的值进行解码。 `intposInDegrees=((rcvmsg[2]&0xFF)<<24) 您将在后面编写的Android应用将从-100到100的范围内为左侧位置和右侧位置传输值。因为您需要为伺服位置提供相应的度数,所以您需要首先使用map函数。 posInDegrees=map(posInDegrees,-100,100,0,180); 位置值现在可以与相应伺服系统的信号引脚一起提供给自定义的moveServo方法。 moveServo(SERVO_ID_1_PIN,posInDegrees); moveServo方法的实现描述了控制伺服所需波形的构造。 voidmoveServo(intservoPulsePin,intpos){//calculatetimeforhighsignalhighSignalTime=leftBoundaryInMicroSeconds+(pos*microSecondsPerDegree);//setServotoHIGHdigitalWrite(servoPulsePin,HIGH);//waitforcalculatedamountofmicrosecondsdelayMicroseconds(highSignalTime);//setServotoLOWdigitalWrite(servoPulsePin,LOW);//delaytocompletewaveformdelayMicroseconds(20000-highSignalTime);} highSignalTime=1000+(90*5.55556); 如您所见,实现波形生成并不特别困难,但是如果您使用ArduinoIDE附带的Servo库,它会变得更加容易。清单9-2显示了使用Servo库重写的草图。 清单9-2。项目10:Arduino草图(使用伺服库) #include Servoservo; voidsetup(){Serial.begin(19200);servo.attach(SERVO_ID_1_PIN);acc.powerOn();} voidloop(){if(acc.isConnected()){intlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_SERVO){if(rcvmsg[1]==SERVO_ID_1){intposInDegrees=((rcvmsg[2]&0xFF)<<24) 乍一看,您可以看到代码变得短了很多。您将不再需要任何计算或自定义方法。要使用Servo库,你首先必须将它包含在你的草图中。 通过将它包含在你的草图中,你可以用一个Servo物体来为你做所有繁重的工作。 要初始化Servo对象,你必须调用attach方法,并提供连接伺服信号线的数字引脚。 servo.attach(SERVO_ID_1_PIN); 为了实际控制伺服系统,你只需要调用它的write方法以及你想要的位置值(以度为单位)。 servo.write(posInDegrees);delay(20); Arduino部分到此为止。请记住,在实现Arduino草图时,您可以选择最适合您需求的方法。 大多数Android设备都有识别其在三维空间中的方向的方法。通常,他们通过向加速度计传感器和磁场传感器请求传感器更新来实现这一点。对于Android部分,您还将从加速度计请求方向更新,这将直接关系到稍后的伺服运动。清单9-3显示了活动的实现,我将在后面详细讨论。 清单9-3。项目10:ProjectTenActivity.java `packageproject.ten.adk; publicclassProjectTenActivityextendsActivity{ privatestaticfinalbyteCOMMAND_SERVO=0x7;privatestaticfinalbyteSERVO_ID_1=0x1; privateTextViewservoDirectionTextView; privateSensorManagersensorManager;privateSensoraccelerometer; 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);} } /**Calledwhentheactivityispausedbythesystem.*/@OverridepublicvoidonPause(){super.onPause();closeAccessory();sensorManager.unregisterListener(sensorEventListener);} privatefinalSensorEventListenersensorEventListener=newSensorEventListener(){ intx_acceleration; @OverridepublicvoidonAccuracyChanged(Sensorsensor,intaccuracy){//notimplemented} @OverridepublicvoidonSensorChanged(SensorEventevent){x_acceleration=(int)(-event.values[0]*10);moveServoCommand(SERVO_ID_1,x_acceleration);runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){servoDirectionTextView.setText(getString(R.string.x_axis_tilt_text_placeholder,x_acceleration));}});}}; publicvoidmoveServoCommand(bytetarget,intvalue){byte[]buffer=newbyte[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(IOExceptione){Log.e(TAG,"writefailed",e);}}}}` 正如Arduino草图中所做的那样,您首先必须为您打算稍后发送的控制消息定义相同的命令和目标id。 您将使用的唯一可视组件是一个简单的TextView元素,用于调试目的,并为您提供倾斜设备时x轴方向值如何变化的概述。 为了从你的Android设备的任何传感器请求更新,你首先需要获得对SensorManager和Sensor本身的引用。从某些传感器获取数据还有其他方法,但SensorManager是大多数传感器的通用注册表。 在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); 现在让我们来看看监听器本身,因为它负责处理传感器更新。 `privatefinalSensorEventListenersensorEventListener=newSensorEventListener(){ @Overridepublicvoidrun(){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元素来给出视觉反馈。 @Overridepublicvoidrun(){servoDirectionTextView.setText(getString(R.string.x_axis_tilt_text_placeholder,x_acceleration));}});` 最后要做的事情是向ADK板发送控制消息。为此,您将使用自定义方法moveServoCommand,提供要控制的伺服ID和实际加速度数据。 moveServoCommand(SERVO_ID_1,x_acceleration); 该方法的实现非常简单。您只需设置基本的数据结构,将整数加速度值位移到四个单字节,并通过输出流将完整的消息发送到ADK板。 publicvoidmoveServoCommand(bytetarget,intvalue){byte[]buffer=newbyte[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(IOExceptione){Log.e(TAG,"writefailed",e);}}} Android部分到此为止,最终结果如图图9-8所示。当你准备好了,部署这两个应用,看看每当你倾斜你的Android设备时,你的伺服如何转动。 图9-8。项目10:最终结果 下一个项目将向您展示如何控制另一种电机,即所谓的DC电机。正如我在本章开始时已经解释过的,DC马达提供连续的旋转,不像伺服马达那样受到人为的限制。您将再次使用设备的加速度计来控制DC电机,只是这次您将处理设备y轴上的加速度变化。因此,当你向前倾斜设备时,电机将开始旋转。你也可以在这样做的时候控制它的速度。注意,我没有涉及旋转方向的改变,这需要更多的硬件。在开始之前,你需要了解控制DC发动机所需的部件以及发动机是如何运转的。 你不需要很多零件就能让马达运转起来。事实上,你将要建造的电路的唯一新元件是一个所谓的NPN晶体管。我一会儿会解释它的目的。 零件清单如下所示(图9-9): 图9-9。项目11个零件(ADK板、试验板、电线、NPN晶体管、DC电机) 稍后,您将使用输出引脚的脉宽调制(PWM)功能来产生影响电机速度的不同电压。 DC汽车靠直流电运行,因此得名。有两种类型的DC电机:有刷DC电机和无刷DC电机。 有刷DC电机的工作原理是固定磁铁干扰电磁场。驱动轴上安装的线圈在其电枢周围产生电磁场,不断被周围的固定磁铁吸引和排斥,导致驱动轴旋转。有刷DC电机通常比无刷DC电机便宜,这就是为什么它们更广泛地应用于爱好电子产品。 无刷DC电机的制造与有刷DC电机正好相反。它们的驱动轴上安装有固定的磁铁,当电磁场作用于它们时,磁铁开始运动。它们的不同之处还在于电机控制器将直流电(DC)转换为交流电(AC)。无刷DC电机比他们的同类产品贵一点。不同的DC电机外形如图9-10所示。 图9-10。不同外形的DC汽车 大多数爱好DC汽车有一个双线连接,Vin和GND。要改变旋转的方向,通常只需改变连接的极性。当你想在飞行中改变马达的旋转方向时,你需要一个比你在这里将要建立的电路更复杂的电路。出于这些目的,你需要一个特殊的电路设置,称为H桥或一个特殊的电机驱动器集成电路。关于这些的进一步细节,只需搜索网页,在那里你会找到大量的教程和信息。为了避免使项目复杂化,我将只坚持一个马达方向。电机的速度会受到所施加的电压水平的影响。你提供的越多,它通常会变得越快,但是注意不要提供超过它能处理的。快速浏览一下电机的数据表,你会对工作电压范围有一个大致的了解。 DC发动机通常与齿轮和变速器一起使用,这样它们的扭矩就可以传递到齿轮上,从而转动例如车轮或其他机械结构。(参见图9-11。) 图9-11。带齿轮附件的DC电机 请注意,大多数DC汽车不附带预先连接的电线,所以你可能必须先焊接电线。 晶体管是能够在电路中开关和放大功率的半导体。它们通常有三个连接器,称为基极、集电极和发射极(见图9-12)。 图9-12。晶体管(平面朝向:发射极、基极、集电极) 晶体管能够通过向另一对施加较小的电压或电流来影响一对连接之间的电流。例如,当电流从集电极流向发射极时,可以将一小部分电流施加于流经发射极的基极,以控制更高的电流。所以基本上晶体管可以用作电路中电源的开关或放大器。 有几种不同类型的晶体管用于不同的任务。在这个项目中,你需要一个NPN晶体管。一个NPN晶体管在基极连接器被拉高的情况下工作,这意味着当高电压或电流施加到基极时,它被设置为“on”。所以当基极上的电压增加时,从集电极到发射极的连接会让更多的电流通过。NPN晶体管的电气符号如图图9-13所示。 图9-13。NPN晶体管的电气符号 与NPN晶体管相反的是PNP晶体管,其工作方式完全相反。要在集电极和发射极之间切换电流,需要将基极拉低。因此,随着电压的降低,更多的电流通过集电极-发射极连接。PNP晶体管的电气符号如图9-14所示。 图9-14。PNP晶体管的电气符号 稍后,您将使用NPN晶体管来控制应用于电机的电源,以便控制其速度。 本章第二个项目的连接设置也相当简单(图9-15)。根据您使用的DC电机,您将电机的一根电线连接到+3.3V或5V,这由电机的额定电压决定。如果你碰巧使用一个额定电压更高的电机,你可能需要连接一个外部电池。在这种情况下,你必须确保连接电池和ADK板到一个共同的地面(GND)。电机的第二根线需要连接到晶体管的集电极。晶体管的发射极连接到GND,晶体管的基极连接到数字引脚2。如果你在晶体管上找不到正确的连接,只要把晶体管平的一面朝向你就行了。右边的引脚是集电极,中间的引脚是基极,左边的引脚是发射极。 图9-15。项目11设置 NPN晶体管已被添加到电路中,以防您的电机需要比ADK板所能提供的更多的功率。因此,如果你遇到一个非常慢的电机运行或根本没有运动,你可以很容易地将外部电池连接到电路上。如果这样做,还应确保在数字引脚2上增加一个1kω至10k范围内的高阻值电阻,以保护ADK板免受来自高功率电路的功率异常影响。如果你需要一个外部电池,你的电路看起来会像图9-16。 图9-16。使用外部电池的项目11设置 你应该先尝试使用第一个电路设置,但如果你的电机需要更多的电力,很容易切换到第二个电路设置。 这个小项目的软件部分相当简单。您将再次使用Android设备的加速度计来获取设备倾斜时倾斜值的变化,只是这次您感兴趣的是y轴而不是x轴的倾斜值。y轴倾斜描述的是设备屏幕朝上时的倾斜运动,其顶部边缘被向下推,底部边缘被向上拉。这就好像你要向前推一架飞机的油门杆。倾斜值将被传输到ADK板,运行的Arduino草图将把接收到的值映射到0到255之间的一个值,该值将只在0到100的范围内,因为您对另一个倾斜方向不感兴趣,以输入到analogWrite方法中。这导致数字引脚2以PWM模式工作,该引脚的输出电压将相应改变。请记住,晶体管的基极连接器连接到引脚2,因此随着电压的变化,您将影响为电机提供的总功率,速度将根据您的倾斜而变化。 Arduino草图会比你写的控制伺服系统的草图短一点(如清单9-4所示)。ArduinoIDE没有官方的DC汽车库,但是对于这个例子,你不需要库,因为代码非常简单。在网站上有社区编写的自定义库,用于使用H桥控制旋转方向的情况,但这超出了本例的范围。您只需使用analogWrite方法,根据您将从连接的Android设备接收到的倾斜值,更改数字引脚2上的电压输出。 清单9-4。项目11:Arduino草图 #defineCOMMAND_DC_MOTOR0x8#defineDC_MOTOR_ID_10x1#defineDC_MOTOR_ID_1_PIN2 voidsetup(){Serial.begin(19200);pinMode(DC_MOTOR_ID_1_PIN,OUTPUT);acc.powerOn();} voidloop(){if(acc.isConnected()){intlen=acc.read(rcvmsg,sizeof(rcvmsg),1);if(len>0){if(rcvmsg[0]==COMMAND_DC_MOTOR){if(rcvmsg[1]==DC_MOTOR_ID_1){intmotorSpeed=rcvmsg[2]&0xFF;motorSpeed=map(motorSpeed,0,100,0,255);analogWrite(DC_MOTOR_ID_1_PIN,motorSpeed);}}}}}` 这里要做的第一件事是像往常一样更改命令和目标字节。 `#defineCOMMAND_DC_MOTOR0x8 接下来,将数字引脚2配置为输出引脚。 pinMode(DC_MOTOR_ID_1_PIN,OUTPUT); 一旦你从连接的Android设备收到有效信息,你必须将原始倾斜值从byte转换为int。 intmotorSpeed=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应用与之前的伺服项目几乎相同。唯一需要改变的是消息字节和当SensorEventListener接收到SensorEvent时得到的值。让我们看看需要做些什么(清单9-5)。 清单9-5。项目11:ProjectElevenActivity.java `packageproject.eleven.adk; publicclassProjectElevenActivityextendsActivity{ privatestaticfinalbyteCOMMAND_DC_MOTOR=0x8;privatestaticfinalbyteDC_MOTOR_ID_1=0x1; privateTextViewmotorSpeedTextView; 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);}/** inty_acceleration; @OverridepublicvoidonSensorChanged(SensorEventevent){y_acceleration=(int)(-event.values[1]*10);if(y_acceleration<0){y_acceleration=0;}elseif(y_acceleration>100){y_acceleration=100;}moveMotorCommand(DC_MOTOR_ID_1,y_acceleration);runOnUiThread(newRunnable(){@Overridepublicvoidrun(){motorSpeedTextView.setText(getString(R.string.y_axis_tilt_text_placeholder,y_acceleration));}});}}; publicvoidmoveMotorCommand(bytetarget,intvalue){byte[]buffer=newbyte[3];buffer[0]=COMMAND_DC_MOTOR;buffer[1]=target;buffer[2]=(byte)value;if(mOutputStream!=null){try{mOutputStream.write(buffer);}catch(IOExceptione){Log.e(TAG,"writefailed",e);}}}}` 此处命令和目标消息字节已被更改,以匹配Arduino草图中的字节。 在伺服示例中,您将使用一个简单的TextView元素为用户提供视觉反馈,该元素显示设备沿y轴的当前倾斜值。 实际上,这里唯一有趣的新部分是SensorEventListener的实现。 privatefinalSensorEventListenersensorEventListener=newSensorEventListener(){`inty_acceleration; @OverridepublicvoidonSensorChanged(SensorEventevent){y_acceleration=(int)(-event.values[1]*10);if(y_acceleration<0){y_acceleration=0;}elseif(y_acceleration>100){y_acceleration=100;}moveMotorCommand(DC_MOTOR_ID_1,y_acceleration);runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){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;}elseif(y_acceleration>100){y_acceleration=100;} 现在你已经有了最终的倾斜值,你可以更新TextView并将数据信息发送到ADK板。传输是通过一种叫做moveMotorCommand的独立方法完成的。 publicvoidmoveMotorCommand(bytetarget,intvalue){byte[]buffer=newbyte[3];buffer[0]=COMMAND_DC_MOTOR;buffer[1]=target;buffer[2]=(byte)value;if(mOutputStream!=null){try{mOutputStream.write(buffer);}catch(IOExceptione){Log.e(TAG,"writefailed",e);}}} Android代码这次没有太大的麻烦,因为您只需要调整上一个示例中的一些代码行。然而,你已经完成了机器人的部分,现在你可以看到你的DC汽车在运行了(图9-17)。部署Arduinosketch和Android应用,当您向前倾斜设备时,可以看到您的电机在旋转。 图9-17。项目11:最终结果 这一章给了你一个简单的概述,让你的项目以任何方式前进。这一概述远未完成。有很多方法可以将运动带入游戏,但使用伺服或DC马达是最常见的方法,它给你进一步的实验提供了一个完美的开始。您学习了这些致动器如何操作,以及如何使用ADK板驱动它们。您还学习了如何处理Android设备的加速度传感器,以及如何使用它来读取其三个轴中任何一个轴的当前加速度或倾斜值。最后,您使用设备的加速度传感器来控制执行器。 现在你已经对你的ADK板和你已经使用过的不同的传感器和组件感到满意了,是时候来点更大的了。在最后一章中,您将结合前几章中使用的一些组件来构建两个版本的报警系统。您将了解新组件—倾斜开关和由红外LED和红外探测器组成的红外挡光板—在现实世界中广泛应用。在两个独立的项目中,您将学习如何将这些组件集成到一个小型报警系统中,以便它们触发警报。在硬件方面,警报将通过闪烁的红色LED和产生高音的压电蜂鸣器来表达。您的Android设备会将警报事件保存在一个日志文件中,并且根据当前的项目,它还会发送一条通知短信,或者给入侵者拍照并保存到您的文件系统中。 你的第一个报警系统需要几个部件。您将使用一个红色LED来发出警报发生的视觉信号。对于听觉反馈,您将使用一个压电蜂鸣器产生报警声。如果倾斜到关闭位置,倾斜开关将触发警报。要重置报警系统,以便它可以报告下一个报警,您将使用一个简单的按钮。以下是您的报警系统所需部件的完整列表(参见图10-1): 图10-1。项目12部分(ADK板、试验板、电线、电阻器、按钮、倾斜开关、压电蜂鸣器、红色LED) 本项目将使用ADK板,提供两种输入方式,即倾斜开关和按钮,以及两种输出方式,即LED和压电蜂鸣器。由于您没有任何模拟输入要求,您将只使用ADK板的数字引脚。您将使用数字输入功能来感应按钮的按下或倾斜开关的闭合。数字输出功能,尤其是PWM功能,将用于脉冲LED,并通过压电蜂鸣器产生声音。 正如您在第三章中所学,您可以通过使用ADK板的PWM功能来调暗LED。当警报发生时,LED将用于给出视觉反馈。这将通过让LED脉冲来实现,这意味着它将在其最亮的最大水平和最暗的最小水平之间持续变暗,直到警报被重置。 第五章向您展示了当向压电蜂鸣器的压电元件供电时,您可以产生声音,这被描述为反向压电效应。压电元件的振荡产生压力波,该压力波被人耳感知为声音。振荡是通过使用ADK板的PWM功能来调制输出到压电蜂鸣器的电压来实现的。 在第四章中,您学习了如何将按钮或开关用作项目的输入。原理很简单。当按钮或开关被按下或闭合时,电路闭合。 倾斜开关与普通开关非常相似。不同的是,用户没有真正的开关来按下或翻转以闭合连接的电路。最常见的倾斜开关通常以部件内的连接器引线彼此分离的方式构造。此外,在组件内还有一个由导电材料制成的球。(参见图10-2)。 图10-2。打开倾斜开关(内部视图) 当开关以某种方式倾斜时,导电球移动并接触开路连接。球关闭了连接,电流可以从一端流到另一端。由于球受重力影响,你只需倾斜倾斜开关,使连接点指向地面。(参见图10-3)。 图10-3。关闭倾斜开关(内部视图) 就这么简单。当你摇动倾斜开关时,你可以听到球在组件内移动。 倾斜开关也被称为水银开关,因为在过去,大多数倾斜开关内部都有一个水银滴来关闭连接。水银的优点是它不像其他材料那样容易反弹,所以它不会受到振动的太大影响。然而,缺点是水银毒性很强,所以如果倾斜开关损坏,就会对环境造成危险。如今,其他导电材料通常用于倾斜开关。 倾斜开关在许多实际应用中使用。汽车工业广泛使用它们。汽车的行李箱灯只是一个例子:如果你把行李箱打开到一个特定的角度,灯就会打开。另一个例子是经典的弹球机,如果用户过度倾斜机器以获得优势,它会进行记录。你也可以很容易地想象一个报警系统的用例,比如当门把手被按下时进行感应。 一些倾斜开关可能带有焊接引脚,您不能直接将其插入试验板,因此您可能需要在这些引脚上焊接一些电线,以便稍后将倾斜开关连接到电路。(参见图10-4)。有时倾斜开关可以有两个以上的连接器,以便您可以将它们连接到多个电路。你只需要确保你总是连接正确的引脚对。查看数据手册或构建一个简单电路来测试哪些连接相互关联。 图10-4。带焊接连接器引脚的倾斜开关和库存倾斜开关 设置将通过一次连接一个组件来完成,这样当您启动项目时,您就不会弄混并最终损坏某些东西。我将首先展示每个独立的电路设置,然后展示包含所有元件的完整电路设置。 先说LED。众所周知,led通常以大约20mA到30mA的电流工作。为了将电流限制在至少20mA的值,您需要在LED上连接一个电阻。ADK板的输出引脚提供5V电压,因此您需要应用欧姆定律来计算电阻值。 r=5v/0.02ar=250ω 最接近的普通电阻是220ω电阻。有了这个电阻,你最终会得到一个大约23mA的电流,这很好。现在将电阻的一端连接到数字引脚2,另一端连接到LED的阳极(长引线)。发光二极管的阴极接地(GND)。LED电路设置如图图10-5所示。 图10-5。项目12:LED电路设置 接下来,您将压电蜂鸣器连接到ADK板。这非常简单,因为你不需要额外的组件。如果你碰巧有一个压电蜂鸣器,确保按照标记正确连接正负引线。否则,只需将一根引线连接到数字引脚3,另一根引线接地(GND)。压电蜂鸣器电路设置如图10-6所示。 图10-6。项目12:压电蜂鸣器电路设置 您可能还记得第四章中的内容,使用按钮消除电气干扰时,最好将电路上拉至工作电压。为了上拉按钮电路而不损坏高电流输入引脚,需要将一个10kΩ上拉电阻与按钮一起使用。为此,将ADK电路板的+5VVcc引脚连接到10kω电阻的一条引线。另一根引线连接到数字引脚4。数字引脚4也连接到按钮的一个引线。相反的导线接地(GND)。按钮电路如图图10-7所示。 图10-7。项目12:按钮电路设置 由于倾斜开关的工作原理与按钮类似,您可以像连接按钮一样连接它。将ADK板的+5VVcc引脚连接到10kΩ电阻的一根引线上。另一根引线连接到数字引脚5。数字引脚5也连接到倾斜开关的一个引线。相反的导线接地(GND)。倾斜开关电路设置如图10-8所示。 图10-8。项目12:倾斜开关电路设置 现在你知道如何连接每个组件,看看图10-9所示的完整电路设置。 图10-9。项目12:完成电路设置 那么这个报警系统是如何工作的呢?想象一下,倾斜开关安装在门把手或天窗上。当手柄被按下或存水弯窗被打开时,倾斜开关将倾斜到导电球接触内部引线的位置,从而使开关闭合。现在,报警被记录,压电蜂鸣器和LED开始发出声音和视觉报警反馈。要重置警报以便再次触发,必须按下按钮。 现在您已经设置好了一切,是时候编写必要的软件来启动和运行警报系统了。Arduino草图将监控倾斜开关是否已倾斜至其触点闭合位置。如果倾斜开关触发了警报,Arduinotone方法将用于使压电蜂鸣器振荡,从而产生高音。此外,红色LED将通过使用analogWrite方法产生脉冲,该方法调制电压输出并使LED以不同的照明强度点亮。为了重置警报,使其可以再次被触发,一个简单的按钮的状态被读取。一旦按下该按钮,所有必要的变量都会重置,报警系统可以再次记录报警。 如软件部分所述,您将使用一些在前面的示例中使用过的众所周知的方法。现在唯一的不同是,您将使用多个组件。在我详细描述之前,先看一下完整的清单10-1。 清单10-1。项目12:Arduino草图 #defineLED_OUTPUT_PIN2#definePIEZO_OUTPUT_PIN3#defineBUTTON_INPUT_PIN4#defineTILT_SWITCH_INPUT_PIN5 #defineNOTE_C72100 #defineCOMMAND_ALARM0x9#defineALARM_TYPE_TILT_SWITCH0x1#defineALARM_OFF0x0#defineALARM_ON0x1 inttiltSwitchValue;intbuttonValue;intledBrightness;intfadeSteps=5; booleanalarm=false; AndroidAccessoryacc("Manufacturer","Model","Description","Version","URI","Serial");bytesntmsg[3]; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_ALARM;sntmsg[1]=ALARM_TYPE_TILT_SWITCH;} voidloop(){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);} voidstartAlarm(){alarm=true;tone(PIEZO_OUTPUT_PIN,NOTE_C7);ledBrightness=0;//informAndroiddevicesntmsg[2]=ALARM_ON;sendAlarmStateMessage();} voidstopAlarm(){alarm=false;//turnoffpiezobuzzernoTone(PIEZO_OUTPUT_PIN);//turnoffLEDdigitalWrite(LED_OUTPUT_PIN,LOW);//informAndroiddevicesntmsg[2]=ALARM_OFF;sendAlarmStateMessage();} voidsendAlarmStateMessage(){if(acc.isConnected()){acc.write(sntmsg,3);}}voidfadeLED(){analogWrite(LED_OUTPUT_PIN,ledBrightness);//increaseordecreasebrightnessledBrightness=ledBrightness+fadeSteps;//changefadedirectionwhenreachingmaxorminofanalogvaluesif(ledBrightness<0||ledBrightness>255){fadeSteps=-fadeSteps;}}` `#defineLED_OUTPUT_PIN2 你看到的下一个定义是警报发生时压电蜂鸣器应该产生的高音频率。2100Hz的频率定义了音符C7,这是大多数音乐键盘上的最高音符,除了古典88键钢琴。音符提供了完美的高音,人耳比任何低音都能听得更清楚。这就是为什么像火警这样的系统使用高音报警声的原因。 接下来,您将看到通常的数据消息字节定义。为报警命令选择了一个新的字节值,一个类型字节定义了本项目中用于触发报警的倾斜开关。如果您打算以后在报警系统中添加额外的开关或传感器,则定义类型字节。最后两个字节定义仅定义报警是否已触发或是否已关闭。 `#defineCOMMAND_ALARM0x9 当您使用digitalRead方法读取按钮或倾斜开关的数字状态时,返回值将是一个int值,稍后可与常量HIGH和LOW进行比较。所以你需要两个变量来存储按钮和倾斜开关的读数。 inttiltSwitchValue;intbuttonValue; 请记住,当警报发生时,您希望让LED发出脉冲。为此,您需要使用analogWrite方法来调制LED的电源电压。analogWrite方法接受从0到255范围内的值。这就是为什么你将当前亮度值存储为一个int值。当您增加或降低LED的亮度时,您可以定义渐变过程的步长值。步长值越低,LED的衰减越平滑越慢,因为达到analogWrite范围的最大值或最小值需要更多的循环周期。 intledBrightness;intfadeSteps=5; 最后一个新变量是一个boolean标志,它仅存储报警系统的当前状态,以确定报警当前是激活还是关闭。它在开始时被初始化为关闭状态。 变量就是这样。除了用新的命令字节和类型字节填充数据消息的前两个字节之外,setup方法没有任何新的功能。有趣的部分是loop方法。 在之前的项目中,循环方法中的代码被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方法开始的其他方法实现。 如您所见,alarm标志已经被设置为true,这样该方法就不会在下一个循环中被意外调用。在《》第五章中已经使用了tone方法。这里,它用于在压电蜂鸣器上产生音符C7。当警报启动时,需要重置ledBrightness变量,以启动LED从暗到亮的淡入淡出。最后,用于描述警报被触发的消息字节被设置在数据消息上,并且如果Android设备被连接,则该消息被发送到Android设备。 接下来是对比法stopAlarm。 首先,您将报警标志设置为false以允许再次触发报警。然后,您需要通过调用noTone方法来关闭压电蜂鸣器。它停止向压电蜂鸣器输出电压,使其不再振荡。通过调用digitalWrite方法并将其设置为LOW(0V)来关闭LED。这里的最后一步也是设置相应的消息字节,如果Android设备已连接,则向其发送停止消息。 sendAlarmStateMessage方法只是检查是否连接了Android设备,如果连接了,则使用Accessory对象的write方法传输三字节消息。 voidsendAlarmStateMessage(){if(acc.isConnected()){acc.write(sntmsg,3);}} 最后一个方法实现是LED淡入淡出的逻辑。 voidfadeLED(){analogWrite(LED_OUTPUT_PIN,ledBrightness);//increaseordecreasebrightnessledBrightness=ledBrightness+fadeSteps;//changefadedirectionwhenreachingmaxorminofanalogvaluesif(ledBrightness<0||ledBrightness>255){fadeSteps=-fadeSteps;}} 为了给LED提供不同的电压等级,这里必须使用analogWrite方法和当前亮度值。在每个循环周期中,当系统设置为报警模式时,调用fadeLED方法。要改变LED的亮度等级,您必须将当前的ledBrightness值加上fadeSteps值。如果您碰巧超过了0到255的可能的analogWrite限制,您需要否定fadeSteps值的符号。值5将变成-5,而不是在下一个循环中增加亮度值,而是现在减小它,将LED调暗到更暗的亮度水平。 这就是软件的Arduino部分。如果你现在运行你的草图,你实际上已经有了一个功能报警系统。不过,您会希望实现Android应用,以便通过使用Android设备作为短信和存储信息的网关,让您的警报系统变得更加强大。 在我进入细节之前,看一下完整的清单10-2。 清单10-2。项目12:ProjectTwelveActivity.java `packageproject.twelve.adk; publicclassProjectTwelveActivityextendsActivity{ privatePendingIntentsmsSentIntent;privatePendingIntentlogFileWrittenIntent; privatestaticfinalbyteCOMMAND_ALARM=0x9;privatestaticfinalbyteALARM_TYPE_TILT_SWITCH=0x1;privatestaticfinalbyteALARM_OFF=0x0;privatestaticfinalbyteALARM_ON=0x1; privatestaticfinalStringSMS_DESTINATION="put_telephone_number_here";privatestaticfinalStringSMS_SENT_ACTION="SMS_SENT";privatestaticfinalStringLOG_FILE_WRITTEN_ACTION="LOG_FILE_WRITTEN"; privatePackageManagerpackageManager;booleanhasTelephony; privateTextViewalarmTextView;privateTextViewsmsTextView;privateTextViewlogTextView;privateLinearLayoutlinearLayout; mUsbManager=UsbManager.getInstance(this);mPermissionIntent=PendingIntent.getBroadcast(this,0,newIntent(ACTION_USB_PERMISSION),0);smsSentIntent=PendingIntent.getBroadcast(this,0,newIntent(SMS_SENT_ACTION),0);logFileWrittenIntent=PendingIntent.getBroadcast(this,0,newIntent(LOG_FILE_WRITTEN_ACTION),0);IntentFilterfilter=newIntentFilter(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);} privatefinalBroadcastReceiverbroadcastReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(ACTION_USB_PERMISSION.equals(action)){synchronized(this){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,false)){openAccessory(accessory);}else{Log.d(TAG,"permissiondeniedforaccessory"+accessory);}mPermissionRequestPending=false;}}elseif(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(accessory!=null&&accessory.equals(mAccessory)){closeAccessory();}}elseif(SMS_SENT_ACTION.equals(action)){smsTextView.setText(R.string.sms_sent_message);}elseif(LOG_FILE_WRITTEN_ACTION.equals(action)){logTextView.setText(R.string.log_written_message);}}}; privatevoidcloseAccessory(){…}RunnablecommRunnable=newRunnable(){ switch(buffer[0]){caseCOMMAND_ALARM: if(buffer[1]==ALARM_TYPE_TILT_SWITCH){finalbytealarmState=buffer[2];finalStringalarmMessage=getString(R.string.alarm_message,getString(R.string.alarm_type_tilt_switch));runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){if(alarmState==ALARM_ON){linearLayout.setBackgroundColor(Color.RED);alarmTextView.setText(alarmMessage);}elseif(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(newStringBuilder(alarmMessage).append("-").append(newDate()).toString());}}break; privatevoidsendSMS(StringsmsText){if(hasTelephony){SmsManagersmsManager=SmsManager.getDefault();smsManager.sendTextMessage(SMS_DESTINATION,null,smsText,smsSentIntent,null);}} privatevoidwriteToLogFile(StringlogMessage){FilelogDirectory=getExternalLogFilesDir();if(logDirectory!=null){FilelogFile=newFile(logDirectory,"ProjectTwelveLog.txt");if(!logFile.exists()){try{logFile.createNewFile();}catch(IOExceptione){Log.d(TAG,"LogFilecouldnotbecreated.",e);}}BufferedWriterbufferedWriter=null;try{bufferedWriter=newBufferedWriter(newFileWriter(logFile,true));bufferedWriter.write(logMessage);bufferedWriter.newLine();Log.d(TAG,"Writtenmessagetofile:"+logFile.toURI());logFileWrittenIntent.send();}catch(IOExceptione){Log.d(TAG,"CouldnotwritetoLogFile.",e);}catch(CanceledExceptione){Log.d(TAG,"LogFileWrittenIntentwascancelled.",e);}finally{if(bufferedWriter!=null){try{bufferedWriter.close();}catch(IOExceptione){Log.d(TAG,"CouldnotcloseLogFile.",e);}}}}} privateFilegetExternalLogFilesDir(){Stringstate=Environment.getExternalStorageState();if(Environment.MEDIA_MOUNTED.equals(state)){returngetExternalFilesDir(null);}else{returnnull;}}}` 正如您在浏览代码时可能已经看到的,您有多个UI元素来显示一些文本。所以在开始研究代码之前,先看看布局和文本是如何定义的。 main.xml布局文件包含三个用LinearLayout包装的TextView。TextView只是稍后显示通知消息。LinearLayout负责改变背景颜色。您还可以看到,短信和文件通知TextView有一个绿色定义(#00FF00),这样当背景变成红色时,它们会有更好的对比度。 清单10-3。项目12:main.xml 布局中引用的文本,以及一旦触发警报时应显示的警报消息文本,在strings.xml文件中定义,如清单10-4所示。 清单10-4。项目12:strings.xml 现在让我们详细谈谈来自清单10-2的实际代码。首先是变量。您可以看到两个额外的PendingIntent。这对于稍后通知活动相应的事件已经发生以更新相应的TextView是必要的。 然后是通常的消息数据字节,如Arduino草图中所定义的。 privatePackageManagerpackageManager;privatebooleanhasTelephony; 变量就是这样。现在我们来看看onCreate方法。除了用于授予USB权限的PendingIntent之外,您还定义了两个新的PendingIntent,它们将用于广播它们的特定事件,将日志文件写入文件系统或发送SMS,以便UI可以相应地更新。 smsSentIntent=PendingIntent.getBroadcast(this,0,newIntent(SMS_SENT_ACTION),0);logFileWrittenIntent=PendingIntent.getBroadcast(this,0,newIntent(LOG_FILE_WRITTEN_ACTION),0); 你可以看到它们定义了一个广播,其中特定事件的动作字符串常量被用来初始化它们的Intent。为了在稍后BroadcastReceiver处理广播时过滤这些意图,您需要向IntentFilter添加相应的动作,该动作与BroadcastReceiver一起在系统中注册。 IntentFilterfilter=newIntentFilter(ACTION_USB_PERMISSION);filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED);filter.addAction(SMS_SENT_ACTION);filter.addAction(LOG_FILE_WRITTEN_ACTION);registerReceiver(broadcastReceiver,filter); 方法的最后一部分定义了通常的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); 其他生命周期方法没有改变,所以您可以继续使用BroadcastReceiver的onReceive方法。如您所见,添加了两个新的else-if子句来评估触发广播的操作。 elseif(SMS_SENT_ACTION.equals(action)){smsTextView.setText(R.string.sms_sent_message);}elseif(LOG_FILE_WRITTEN_ACTION.equals(action)){logTextView.setText(R.string.log_written_message);} 根据已经触发广播的动作,相应的TextView被更新。BroadcastReceiver的onReceive方法运行在UI线程上,所以在这里更新UI是安全的。 下一个有趣的部分是Runnable实现中的警报消息评估。 finalbytealarmState=buffer[2];finalStringalarmMessage=getString(R.string.alarm_message,getString(R.string.alarm_type_tilt_switch));runOnUiThread(newRunnable(){@Overridepublicvoidrun(){if(alarmState==ALARM_ON){linearLayout.setBackgroundColor(Color.RED);alarmTextView.setText(alarmMessage);}elseif(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(newStringBuilder(alarmMessage).append("-").append(newDate()).toString());} 检查报警系统的当前状态后,可以在runOnUiThread方法中更新相应的TextViews。如果报警被触发,将LinearLayout的背景颜色设置为红色,并将报警文本设置在alarmTextView上。如果警报被取消并重置,您将LinearLayout的背景颜色设置回白色,用文本更新alarmTextView以告知用户系统再次重置,并清除通知SMS和日志文件事件的TextView。根据警报的当前条件更新用户界面后,如果警报已被触发,您可以继续发送短信和写入日志文件。该实现封装在单独的方法中,我们将在接下来看到。 首先让我们看看如何在Android中发送短信。 这里,您在开始时设置的boolean标志用于检查连接的Android设备是否能够发送短信。如果不是,调用代码就没有任何意义了。为了能够发送SMS,您需要首先获得对系统的SmsManager的引用。SmsManager类提供了一个方便的静态方法来获得系统的默认SmsManager实现。一旦你有了对SmsManager的引用,你就可以调用sendTextMessage方法,它需要几个参数。首先,你必须提供短信目的地号码。然后你可以提供一个服务中心地址。通常,您可以使用null,以便使用默认的服务中心。第三个参数是您想要通过SMS发送的实际消息。最后两个参数是PendingIntents,您可以提供在发送短信和收到短信时得到通知。您已经定义了PendingIntent来通知短信已经发送,所以您将在这里使用它来通知短信的发送。一旦发生这种情况,就会通知BroadcastReceiver相应地更新UI。 顾名思义,writeToLogFile方法负责将日志文件写入设备文件系统上应用的存储目录。 在写入Android设备的外部存储器之前,您需要检查该存储器是否安装在Android系统中,并且当前未被其他系统使用,例如,当连接到计算机以传输文件时。为此,您可以使用另一种方法来检查外部存储的当前状态,并返回应用文件存储目录的路径。 privateFilegetExternalLogFilesDir(){Stringstate=Environment.getExternalStorageState();if(Environment.MEDIA_MOUNTED.equals(state)){returngetExternalFilesDir(null);}else{returnnull;}} 如果该方法返回有效的目录路径,您可以通过提供目录和文件名来创建一个File对象。 FilelogFile=newFile(logDirectory,"ProjectTwelveLog.txt"); 如果目录中不存在该文件,您应该先创建它。 if(!logFile.exists()){try{logFile.createNewFile();}catch(IOExceptione){Log.d(TAG,"LogFilecouldnotbecreated.",e);}} 为了写入文件本身,您将创建一个BufferedWriter对象,它将一个FileWriter对象作为参数。通过提供对File对象的引用和一个boolean标志来创建FileWriter。boolean标志定义要写入的文本是否应该附加到文件中,或者文件是否应该被覆盖。如果你想添加文本,你应该使用boolean标志true。 bufferedWriter=newBufferedWriter(newFileWriter(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 Android应用现在可以部署到设备上了。也上传Arduino草图,连接两个设备,并查看您的报警系统的运行情况。如果你正确地设置了所有的东西,并把你的倾斜开关倾斜到一个垂直的位置,你应该得到一个看起来像图10-10的结果。 图10-10。项目12:最终结果 最终项目将是一个摄像头报警系统,当警报触发时,它能够快速拍摄入侵者的照片。硬件将或多或少与前一个项目相同。唯一的区别是,你将使用红外光栅触发警报,而不是倾斜开关。拍照后,照片将与记录警报事件的日志文件一起保存在应用的外部存储中。 为了将IR挡光板集成到您之前项目的硬件设置中,您需要一些额外的部件。除了您已经使用的部件,您还需要一个额外的220ω电阻、一个红外发射器或红外LED以及一个红外探测器。完整的零件清单如下所示(参见图10-11): 图10-11。项目13个部分(ADK板、试验板、电线、电阻器、按钮、红外发射器(透明)、红外探测器(黑色)、压电蜂鸣器、红色LED) 红外线光栅电路将连接到ADK板的模拟输入端。模拟输入引脚将用于检测测量电压电平的突然变化。当IR检测器暴露于红外光波长的光时,所连接的模拟输入引脚上测得的输入电压将非常低。如果红外线照射中断,测得的电压将显著增加。 你将在这个项目中建立的红外光栅将由两部分组成,一个红外发射器和一个红外探测器。发射器通常是普通的红外发光二极管,它发射波长约为940纳米的红外光。你会发现不同形式的红外发光二极管。单个红外LED的通常形式是标准的灯泡形LED,但也可以发现类似晶体管形状的LED。两者都显示在图10-12中。 图10-12。灯泡形红外发光二极管(左),晶体管形红外发光二极管(右) 红外探测器通常是一个只有一个集电极和一个发射极连接器的两条腿光电晶体管。通常这两种元件作为匹配对出售,以创建ir光屏障电路。这种匹配装置通常以晶体管形状出售。(参见图10-13。) 图10-13。配套TEMICK153P中的红外探测器(左)和发射器(右) 匹配对组的优点是两个元件在光学和电学上匹配,以提供最佳的兼容性。我在这个项目中使用的红外发射器和探测器被称为TEMICK153P。你不必使用完全相同的一套。所有你需要的是一个红外LED和一个光电晶体管,你会达到同样的最终结果。您可能只需在稍后的代码中调整IR光栅电路中的电阻或警报触发阈值。红外光栅的典型电路如图图10-14所示。 图10-14。普通红外线光栅电路 如前所述,其工作原理是,如果检测器(光电晶体管)暴露在红外光下,输出电压会降低。因此,如果您将手指或任何其他物体放在发射器和检测器之间,检测器对IR光的暴露就会中断,输出电压就会增加。一旦达到自定义的阈值,您可以触发警报。 对于这个项目的设置,您基本上只需将您的倾斜开关电路与之前的项目断开,并将其替换为图10-15所示的IR电路。 图10-15。项目13:红外线光栅电路设置 如您所见,红外LED(发射器)像普通LED一样连接。只需将+5V电压连接到220ω电阻的一根引线上,并将电阻的另一根引线连接到IRLED的正极引线上。红外LED的负极引线接地(GND)。红外光电晶体管的发射极引线接地(GND)。集电极引线必须通过一个10k电阻连接到+5V,此外还要连接到模拟输入A0。如果不确定哪个引脚是哪个引脚,请查看元件的数据手册。 完整的电路设置,结合之前项目中的其他报警系统组件,看起来类似于图10-16。 图10-16。项目13:完成电路设置 Arduino软件部分只会略有变化。您将读取连接到红外光栅红外探测器的模拟输入引脚的输入值,而不是读取之前连接倾斜开关的数字输入引脚的状态。如果测得的输入值达到预定义的阈值,则会触发警报,并且与之前的项目一样,会发出警报声音,红色LED也会淡入淡出。一旦警报发送到连接的Android设备,应用的外部存储目录中将存储一个日志文件。如果有摄像头的话,这种设备现在可以拍照,而不是发送短信来通知可能的入侵者。一旦拍摄了照片,它也将与日志文件一起保存在应用的外部存储目录中。 正如我刚才描述的,这个项目的Arduino草图与项目12中使用的非常相似。它只需要一些微小的变化,以符合红外光栅电路。先看看完整的清单10-6;之后我会解释必要的改变。 清单10-6。项目13:Arduino草图 #defineIR_LIGHT_BARRIER_INPUT_PINA0 #defineIR_LIGHT_BARRIER_THRESHOLD511 #defineALARM_TYPE_IR_LIGHT_BARRIER0x2 intirLightBarrierValue;intbuttonValue;intledBrightness=0;intfadeSteps=5; voidsetup(){Serial.begin(19200);acc.powerOn();sntmsg[0]=COMMAND_ALARM;sntmsg[1]=ALARM_TYPE_IR_LIGHT_BARRIER;} voidloop(){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);} voidfadeLED(){analogWrite(LED_OUTPUT_PIN,ledBrightness);//increaseordecreasebrightnessledBrightness=ledBrightness+fadeSteps;//changefadedirectionwhenreachingmaxorminofanalogvaluesif(ledBrightness<0||ledBrightness>255){fadeSteps=-fadeSteps;}}` 您可以看到,红外光栅的模拟引脚定义已经取代了倾斜开关引脚定义。 下一个新定义是红外光栅上电压变化的阈值。当红外探测器暴露在红外发射器下时,测得的电压输出非常低。读取的ADC值通常在低位两位数范围内。一旦IR曝光中断,电压输出会显著增加。现在,读取的ADC值通常在接近最大ADC值1023的范围内。介于0和1023之间的值是触发警报的理想阈值。如果您希望您的警报触发器仅对红外照明的微小变化做出更快的响应,您应该降低阈值。不过,值511是一个好的开始。 要在引脚A0上存储IR光栅的读取ADC值,只需使用一个整数变量。 intirLightBarrierValue; 剩下的代码非常简单,在项目12中已经很熟悉了。在环路方法中,您需要做的唯一一件新事情是读取IR光栅模拟输入引脚上的ADC值,并检查它是否超过预定义的阈值。如果有,并且之前没有触发警报,您可以启动警报程序。 irLightBarrierValue=analogRead(IR_LIGHT_BARRIER_INPUT_PIN);if((irLightBarrierValue>IR_LIGHT_BARRIER_THRESHOLD)&&!alarm){startAlarm();} 这并不难,是吗?让我们来看看在你的报警系统的Android软件方面你必须做些什么。 一旦Android应用接收到表示警报已经发生的数据消息,它将通过视觉方式通知用户该警报,并在应用的外部存储目录中另外写入一个日志文件。为了能够识别可能触发警报的入侵者,如果设备有内置摄像头,Android应用将利用AndroidcameraAPI来拍照。如果前置摄像头存在,它将是首选摄像头。如果设备只有一个后置摄像头,将使用这个摄像头。 为了在最后一个项目中提供一个更好的概述,我将清单分开来单独讨论。 在我进入细节之前,请看一下清单10-7。 清单10-7。【项目13:ProjectThirteenActivity.java(第一部分) `packageproject.thirteen.adk; publicclassProjectThirteenActivityextendsActivity{ privatePendingIntentphotoTakenIntent;privatePendingIntentlogFileWrittenIntent;privatestaticfinalbyteCOMMAND_ALARM=0x9;privatestaticfinalbyteALARM_TYPE_IR_LIGHT_BARRIER=0x2;privatestaticfinalbyteALARM_OFF=0x0;privatestaticfinalbyteALARM_ON=0x1; privatestaticfinalStringPHOTO_TAKEN_ACTION="PHOTO_TAKEN";privatestaticfinalStringLOG_FILE_WRITTEN_ACTION="LOG_FILE_WRITTEN"; privatePackageManagerpackageManager;privatebooleanhasFrontCamera;privatebooleanhasBackCamera; privateCameracamera;privateSurfaceViewsurfaceView; privateTextViewalarmTextView;privateTextViewphotoTakenTextView;privateTextViewlogTextView;privateLinearLayoutlinearLayout;privateFrameLayoutframeLayout; mUsbManager=UsbManager.getInstance(this);mPermissionIntent=PendingIntent.getBroadcast(this,0,newIntent(ACTION_USB_PERMISSION),0);photoTakenIntent=PendingIntent.getBroadcast(this,0,newIntent(PHOTO_TAKEN_ACTION),0);logFileWrittenIntent=PendingIntent.getBroadcast(this,0,newIntent(LOG_FILE_WRITTEN_ACTION),0);IntentFilterfilter=newIntentFilter(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);}/** camera=getCamera();dummySurfaceView=newCameraPreview(this,camera);frameLayout.addView(dummySurfaceView); …} /**Calledwhentheactivityispausedbythesystem.*/@OverridepublicvoidonPause(){super.onPause();closeAccessory();if(camera!=null){camera.release();camera=null;frameLayout.removeAllViews();}} privatePendingIntentphotoTakenIntent;privatePendingIntentlogFileWrittenIntent; 接下来,您会看到与Arduino草图中使用的相同的警报类型字节标识符,用于将IR光栅识别为警报的触发源。 privatestaticfinalbyteALARM_TYPE_IR_LIGHT_BARRIER=0x2; 您还必须定义一个新的动作常量来标识稍后照片事件的广播。 privatestaticfinalStringPHOTO_TAKEN_ACTION="PHOTO_TAKEN"; 在这个项目中再次使用PackageManager来确定设备是否有前置摄像头和后置摄像头。 您还将持有对设备摄像头的引用,因为您需要调用Camera对象本身的某些生命周期方法来拍照。SurfaceView是一个特殊的View元素,它会在你拍照前显示当前的相机预览。 您可能还注意到有两个新的UI元素。一个是TextView,显示一段文字,表明照片已经被拍摄。第二个是一个FrameLayoutView容器。这种容器用于将多个View叠加在一起以达到叠加效果。 privateTextViewphotoTakenTextView;privateFrameLayoutframeLayout; 现在让我们看看在ProjectThirteenActivity的生命周期方法中你必须做什么。在onCreate方法中,你可以进行通常的初始化。同样,您必须为照片事件定义新的Pendingintent,并在IntentFilter注册广播动作。 photoTakenIntent=PendingIntent.getBroadcast(this,0,newIntent(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的实现细节,以及如何获得实际的摄像机参考,将在后面显示。 分别需要在应用暂停时释放资源。文档指出,您应该释放相机本身的句柄,以便其他应用能够使用相机。此外,您应该从FrameLayout中移除SurfaceView,这样当应用再次恢复时,容器中只存在一个新创建的SurfaceView。 if(camera!=null){camera.release();camera=null;frameLayout.removeAllViews();} 这就是生命周期方法。 您看到您需要再次定义一个新的布局和一些新的文本,如清单10-8所示。 清单10-8。项目13:main.xml 参考文本在strings.xml文件中定义,如清单10-9所示。 清单10-9。项目13:strings.xml 现在让我们看看处理通信部分的BroadcastReceiver和Runnable实现(清单10-10)。 清单10-10。【项目13:ProjectThirteenActivity.java(第二部分) `… privatefinalBroadcastReceiverbroadcastReceiver=newBroadcastReceiver(){@OverridepublicvoidonReceive(Contextcontext,Intentintent){Stringaction=intent.getAction();if(ACTION_USB_PERMISSION.equals(action)){synchronized(this){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED,false)){openAccessory(accessory);}else{Log.d(TAG,"permissiondeniedforaccessory"+accessory);}mPermissionRequestPending=false;}}elseif(UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)){UsbAccessoryaccessory=UsbManager.getAccessory(intent);if(accessory!=null&&accessory.equals(mAccessory)){closeAccessory();}}elseif(PHOTO_TAKEN_ACTION.equals(action)){photoTakenTextView.setText(R.string.photo_taken_message);}elseif(LOG_FILE_WRITTEN_ACTION.equals(action)){logTextView.setText(R.string.log_written_message);}}}; if(buffer[1]==ALARM_TYPE_IR_LIGHT_BARRIER){finalbytealarmState=buffer[2];finalStringalarmMessage=getString(R.string.alarm_message,getString(R.string.alarm_type_ir_light_barrier));runOnUiThread(newRunnable(){ @Overridepublicvoidrun(){if(alarmState==ALARM_ON){linearLayout.setBackgroundColor(Color.RED);alarmTextView.setText(alarmMessage);}elseif(alarmState==ALARM_OFF){linearLayout.setBackgroundColor(Color.WHITE);alarmTextView.setText(R.string.alarm_reset_message);photoTakenTextView.setText("");logTextView.setText("");}}});if(alarmState==ALARM_ON){takePhoto();writeToLogFile(newStringBuilder(alarmMessage).append("-").append(newDate()).toString());}elseif(alarmState==ALARM_OFF){camera.startPreview();}}break; 一旦接收到相应的广播,只需增强BroadcastReceiver也对照片事件做出反应。您将更新photoTakenTextView以向用户显示已经拍摄了一张照片。 elseif(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);}elseif(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(newStringBuilder(alarmMessage).append("-").append(newDate()).toString());}elseif(alarmState==ALARM_OFF){camera.startPreview();} 这些方法调用不应该在UI线程上进行,因为它们处理可能阻塞UI本身的IO操作。在出现警报的情况下,拍照和写日志文件的实现将在下面的清单中显示。重置警报时,您还必须重置相机的生命周期,并开始新的相机图片预览。注意,在拍照之前必须调用startPreview方法。否则,您的应用将会崩溃。 现在让我们看看新的Android应用真正有趣的部分:如何用设备的集成摄像头拍照(清单10-11)。 清单10-11。【项目13:ProjectThirteenActivity.java(第三部分) privateCameragetCamera(){Cameracamera=null;try{if(hasFrontCamera){intfrontCameraId=getFrontCameraId();if(frontCameraId!=-1){camera=Camera.open(frontCameraId);}}if((camera==null)&&hasBackCamera){camera=Camera.open();}}catch(Exceptione){Log.d(TAG,"Cameracouldnotbeinitialized.",e);}returncamera;} privateintgetFrontCameraId(){intcameraId=-1;intnumberOfCameras=Camera.getNumberOfCameras();for(inti=0;i privatevoidtakePhoto(){if(camera!=null){camera.takePicture(null,null,pictureTakenHandler);}} privatePictureCallbackpictureTakenHandler=newPictureCallback(){ @OverridepublicvoidonPictureTaken(byte[]data,Cameracamera){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方法。 `privatePictureCallbackpictureTakenHandler=newPictureCallback(){ @OverridepublicvoidonPictureTaken(byte[]data,Cameracamera){writePictureDataToFile(data);}};` 回调方法以字节数组的形式提供经过处理的jpeg数据,这些数据可以写入文件系统上的图片文件。 文件系统操作如清单10-12中的所示。 清单10-12。【项目13:ProjectThirteenActivity.java(第四部分) privatevoidwriteToLogFile(StringlogMessage){FilelogFile=getFile("ProjectThirteenLog.txt");if(logFile!=null){BufferedWriterbufferedWriter=null;try{bufferedWriter=newBufferedWriter(newFileWriter(logFile,true));bufferedWriter.write(logMessage);bufferedWriter.newLine();Log.d(TAG,"Writtenmessagetofile:"+logFile.toURI());logFileWrittenIntent.send();}catch(IOExceptione){Log.d(TAG,"CouldnotwritetoLogFile.",e);}catch(CanceledExceptione){Log.d(TAG,"LogFileWrittenIntentwascancelled.",e);}finally{if(bufferedWriter!=null){try{bufferedWriter.close();}catch(IOExceptione){Log.d(TAG,"CouldnotcloseLogFile.",e);}}}}} privatevoidwritePictureDataToFile(byte[]data){SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd-HH-mm-ss");StringcurrentDateAndTime=dateFormat.format(newDate());FilepictureFile=getFile(currentDateAndTime+".jpg");if(pictureFile!=null){BufferedOutputStreambufferedOutputStream=null;try{bufferedOutputStream=newBufferedOutputStream(newFileOutputStream(pictureFile));bufferedOutputStream.write(data);Log.d(TAG,"Writtenpicturedatatofile:"+pictureFile.toURI());photoTakenIntent.send();}catch(IOExceptione){Log.d(TAG,"CouldnotwritetoPictureFile.",e);}catch(CanceledExceptione){Log.d(TAG,"photoTakenIntentwascancelled.",e);}finally{if(bufferedOutputStream!=null){try{bufferedOutputStream.close();}catch(IOExceptione){Log.d(TAG,"CouldnotclosePictureFile.",e);}}}}}privateFilegetFile(StringfileName){Filefile=newFile(getExternalDir(),fileName);if(!file.exists()){try{file.createNewFile();}catch(IOExceptione){Log.d(TAG,"Filecouldnotbecreated.",e);}}returnfile;} privateFilegetExternalDir(){Stringstate=Environment.getExternalStorageState();if(Environment.MEDIA_MOUNTED.equals(state)){returngetExternalFilesDir(null);}else{returnnull;}}}` SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd-HH-mm-ss");StringcurrentDateAndTime=dateFormat.format(newDate());FilepictureFile=getFile(currentDateAndTime+".jpg"); 创建的File被提供给由BufferedOutputStream包裹的FileOutputStream,以将图片数据写入File。如果一切顺利,就可以发送描述照片已经拍摄并保存的广播。 bufferedOutputStream=newBufferedOutputStream(newFileOutputStream(pictureFile));bufferedOutputStream.write(data);Log.d(TAG,"Writtenpicturedatatofile:"+pictureFile.toURI());photoTakenIntent.send(); Activity的编码到此为止。 请记住,您仍然需要实现一个类型为SurfaceView的类,以便可以在您的应用中呈现相机预览。看看清单10-13中的,它显示了扩展了SurfaceView类的CameraPreview类。 清单10-13。项目13:CameraPreview.java importjava.io.IOException; importandroid.content.Context;importandroid.hardware.Camera;importandroid.util.Log;importandroid.view.SurfaceHolder;importandroid.view.SurfaceView; publicclassCameraPreviewextendsSurfaceViewimplementsSurfaceHolder.Callback{privatestaticfinalStringTAG=CameraPreview.class.getSimpleName();privateSurfaceHoldermHolder;privateCameramCamera; publicCameraPreview(Contextcontext,Cameracamera){super(context);mCamera=camera; //AddaSurfaceHolder.Callbacksowegetnotifiedwhenthe//underlyingsurfaceiscreated.mHolder=getHolder();mHolder.addCallback(this);//deprecatedsetting,butrequiredonAndroidversionspriorto3.0mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);} publicvoidsurfaceCreated(SurfaceHolderholder){try{mCamera.setPreviewDisplay(holder);mCamera.setDisplayOrientation(90);mCamera.startPreview();}catch(IOExceptione){Log.d(TAG,"Errorsettingcamerapreview:"+e.getMessage());}} @OverridepublicvoidsurfaceChanged(SurfaceHolderholder,intformat,intwidth,intheight){//notimplemented} @OverridepublicvoidsurfaceDestroyed(SurfaceHolderholder){//notimplemented}}` CameraPreview类只有两个字段:对设备摄像头的引用和一个所谓的SurfaceHolder。SurfaceHolder是一个SurfaceView显示表面的接口,它将显示相机拍摄的预览图片。 privateSurfaceHoldermHolder;privateCameramCamera; 在CameraPreview的构造函数中,你通过调用SurfaceView的getHolder方法来初始化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 现在,您已经为最终的项目测试运行做好了一切准备。将Arduino草图上传到ADK板上,在您的Android设备上部署您的Android应用,并查看您新的支持摄像头的报警系统(图10-17)。 图10-17。项目13:最终结果 在这最后一章中,你建立了你自己的警报系统,包括你在整本书中了解的一些部分。你建造了两个版本的警报系统,一个由倾斜开关触发,另一个由自建的红外光栅触发。报警系统借助压电蜂鸣器和红色LED发出声音和视觉反馈。一个连接的Android设备通过提供发送通知短信或拍摄可能的入侵者的照片的可能性,增强了警报系统。您学习了如何以编程方式发送这些SMS消息,以及如何使用相机API来指示相机拍照。通过将警报事件保存到日志文件中,您还了解了在Android中保存数据的一种方法。