ADK Arduino 入门指南(三)
八、触觉
触摸用户界面已经越来越成为我们日常生活的一部分。我们在自动售货机、家用电器、手机和电脑上看到它们。触摸界面让日常活动看起来更有未来感和时尚感。当你在看老科幻电影时,你会注意到,即使在那时,触摸也是想象未来用户输入的首选方式。如今,孩子们是在这种技术的陪伴下成长的。
有许多不同类型的触摸界面技术,每种技术都有其优点和缺点。三种最普遍的技术是电阻触摸感应、电容触摸感应和红外(IR)触摸感应。
电阻式触摸感应主要由两个电阻层系统实现,当按压时,它们在某一点相互接触。一层负责检测 x 轴上的位置,另一层负责 y 轴上的位置。电阻式触摸屏已经存在了相当长的一段时间,在早期的商务智能手机中达到了顶峰。然而,基于电阻式触摸原理的新型智能手机仍在不断问世。电阻式触摸的优势在于,你不仅可以用手指作为输入工具,还可以用任何物体作为输入工具。缺点是,你必须对触摸界面的表面施加压力,随着时间的推移,这可能会损坏系统或磨损系统。
电容式触摸是另一种触摸感应方式,被更新的智能手机等现代设备所采用。其原理依赖于人体的电容特性。电容式触摸表面形成电场,该电场在被触摸时被人体扭曲,并被测量为电容的变化。电容式触摸系统的优势在于,您不必触摸表面就能感受到触摸。当系统没有足够高的绝缘时,当您靠近传感器时,可能会影响传感器。然而,触摸屏有玻璃绝缘,需要你直接触摸。电容式触摸系统不需要力量来感知输入。缺点是不是每个物体都可以与电容式触摸系统交互。你可能已经注意到,当你戴上普通的冬季手套时,你无法控制你的智能手机。那是因为你没有导电性,手指上包裹了太多绝缘材料。除了手指之外,您只能使用特殊的触笔或等效物来控制电容式触摸系统。
你可能听说过的最后一个系统是红外(IR)触摸系统。这种触摸系统主要用于户外亭或大型多点触摸桌,你可能听说过。红外系统的工作原理是由红外发光二极管发出的红外光投射到屏幕或玻璃表面的边缘。红外光束在屏幕内以一定的模式反射,当物体放在屏幕表面时,这种模式会被破坏。IR LEDs 被定位成覆盖 x 轴和 y 轴,以便可以确定放置在屏幕上的对象的正确位置。红外系统的优势在于,每个物体都可以用来与系统进行交互,因为该系统对电导率或电容等属性没有要求。一个缺点是,便宜的系统会受到阳光直射的影响,阳光中含有红外光谱。然而,大多数工业或消费系统都有适当的滤波机制来避免这些干扰。
项目 9: DIY 电容式触摸游戏展示蜂鸣器
这个项目将释放你自己动手(DIY)的精神,使你能够建立自己的定制电容式触摸传感器。电容式触摸传感器是迄今为止最容易和最便宜的为自己制作的,这就是为什么你将在本章的项目中使用它。您将使用铝箔制作一个自定义传感器,它将成为项目电路的一部分。您将使用 ADK 板的一个数字输入引脚来感知用户何时触摸传感器或影响其电场。触摸信息将被传播到一个 Android 应用,通过振动和播放一个简单的声音文件,让你的 Android 设备成为一个游戏节目蜂鸣器。
零件
如您所知,您不会在本章的项目中使用预建的传感器。这一次,您将使用任何家庭中都能找到的零件来制作自己的传感器。为了制作一个可以连接到电路的电容式触摸传感器,你需要胶带、铝箔和一根电线。以下是该项目的完整零件清单(如图图 8-1 ):
- ADK 董事会
- 试验板
- 铝箔
- 胶带
- 10kω电阻
- 一些电线
***图 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 设置
那么这个电容传感器实际上是如何工作的呢?如您所见,您通过一个电阻将一个输出引脚连接到一个输入引脚,以读取输出引脚的当前状态。当输出引脚向电路供电时,输入引脚需要一段时间才能达到相同的电平。这是因为输入引脚具有电容特性,这意味着它在一定程度上充放电。触摸传感器是电路的一部分,当通电时,它会在其周围产生一个电场。当用户现在触摸传感器或者甚至非常接近传感器时,人体的水分子会干扰电场。这种干扰会导致输入引脚的电容发生变化,输入引脚需要更长时间才能达到与输出引脚相同的电压水平。我们将利用这个时间差来确定触摸是否发生。
软件
该项目的软件部分将向您展示如何使用CapSense Arduino 库,通过您新构建的电容式触摸传感器来感知触摸。当达到某个阈值时,您将识别触摸事件,并将该信息传播到 Android 设备。如果发生触摸,运行的 Android 应用将播放蜂鸣器声音并振动,就像游戏节目蜂鸣器一样。
Arduino 草图
幸运的是,您不必自己实现容性检测逻辑。有一个额外的 Arduino 库,名为CapSense,它可以为您完成这项工作。CapSense由 Paul Badger 撰写,旨在鼓励使用 DIY 电容式触摸界面。你可以在[www.arduino.cc/playground/Main/CapSense](http://www.arduino.cc/playground/main/capsense)下载它,并将其复制到 Arduino IDE 安装的 libraries 文件夹中。
CapSense的作用是通过反复改变相连输出引脚的状态来监控数字输入引脚的状态变化行为。原理如下。数字输出引脚被设置为数字状态HIGH,,这意味着它向电路施加 5V 电压。连接的数字输入引脚需要一定的时间才能达到相同的状态。经过一段时间后,测量输入引脚是否已经达到与输出引脚相同的状态。如果如预期的那样,则没有发生触摸。之后,再次将输出引脚设置为LOW (0V),并重复该过程。如果用户现在触摸附着的铝箔,电场会变形,导致电容值增加。这减缓了输入引脚上的电压积累过程。如果输出引脚现在再次将其状态变为HIGH,则输入引脚需要更长时间才能变为相同状态。现在对状态变化进行测量,输入引脚没有达到预期的新状态。这是发生触摸的指示器。
先看看完整的清单 8-1 。我将在清单之后更详细地介绍CapSense库。
***清单 8-1。*项目 9: Arduino 草图
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h> #include <CapSense.h>
#define COMMAND_TOUCH_SENSOR 0x6 #define SENSOR_ID 0x0; #define THRESHOLD 50
CapSense touchSensor = CapSense(4,6);
AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");
byte sntmsg[3];
void setup() {
Serial.begin(19200);
acc.powerOn();
//disables auto calibration
touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);
sntmsg[0] = COMMAND_TOUCH_SENSOR;
sntmsg[1] = SENSOR_ID;
} void loop() {
if (acc.isConnected()) {
//takes 30 measurements to reduce false readings and disturbances
long value = touchSensor.capSense(30);
if(value > THRESHOLD) {
sntmsg[2] = 0x1;
}
else {
sntmsg[2] = 0x0;
}
acc.write(sntmsg, 3);
delay(100);
}
}`
除了附件通信所需的库之外,您还需要包含带有以下指令的CapSense库:
#include <CapSense.h>
由于电容式触摸按钮是一种特殊的按钮,我对数据消息使用了新的命令字节0x6。您可以使用与常规按钮相同的命令字节,即0x1,但是您必须在代码中进一步区分这两种类型。这里定义的第二个字节是触摸传感器的 id:
#define COMMAND_TOUCH_SENSOR 0x6 #define SENSOR_ID 0x0;
另一个常量定义是阈值。需要指定触摸发生的时间。由于电干扰会使测量值失真,您需要为测量值定义一个阈值或变化量,以确定什么是触摸,什么是简单的电噪声。
#define THRESHOLD 50
对于这个项目,我选择了值 50,因为对于这个电路设置,CapSense测量返回的值范围相当小。如果您使用一些Serial.println()方法调用来监控测量值,以查看触摸电容式传感器时该值如何变化,会有所帮助。如果您在设置中使用另一个电阻,或者您发现 50 不是您的设置的最佳阈值,那么您可以简单地调整THRESHOLD值。
你在草图中看到的下一个东西是CapSense对象的定义。CapSense类的构造函数将两个整数值作为输入参数。第一个定义了数字输出引脚,它在数字状态HIGH和LOW之间交替。第二个参数定义数字输入引脚,该引脚被拉至与输出引脚相同的电流状态。
CapSense touchSensor = CapSense(4,6);
看完变量定义之后,让我们来看看设置方法。除了通常的初始化步骤,您现在已经知道了,还有对CapSense对象的第一个方法调用。
touchSensor.set_CS_AutocaL_Millis(0xFFFFFFFF);
该方法关闭了感测程序的自动校准,否则在测量期间可能会发生自动校准。
循环法实现触摸检测。首先,调用capSense方法,其中必须提供样本数量的参数。30 个样本的值似乎足够了。该方法以任意单位返回值。如果返回的检测值超过您之前定义的阈值,则检测到触摸,并在返回消息中设置相应的字节。
long value = touchSensor.capSense(30); if(value > THRESHOLD) { sntmsg[2] = 0x1; } else { sntmsg[2] = 0x0; }
最后要做的是将当前数据消息发送到连接的 Android 设备。
Android 应用
Android 应用使用了一些你已经知道的功能,比如使用振动器服务。这个应用的一个新功能是音频播放。当接收到触摸传感器数据消息时,代码评估数据以确定触摸按钮是否被按下。如果它被按下,背景颜色会变成红色,一个TextView会显示哪个触摸按钮被按下,以防您添加其他按钮。与此同时,该设备的振动器打开,并播放蜂鸣器声音,以获得最终游戏节目蜂鸣器般的感觉。清单 8-2 中的这个项目的 Android 代码显示了应用逻辑。
清单 8-2。项目 9:ProjectNineActivity.java
`package project.nine.adk;
import …;
public class ProjectNineActivity extends Activity {
…
private static final byte COMMAND_TOUCH_SENSOR = 0x6; private static final byte SENSOR_ID = 0x0;
private LinearLayout linearLayout; private TextView buzzerIdentifierTextView;
private Vibrator vibrator; private boolean isVibrating;
private SoundPool soundPool; private boolean isSoundPlaying; private int soundId;
private float streamVolumeMax;
/** Called when the activity is first created. */
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
…
setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); buzzerIdentifierTextView = (TextView) findViewById(R.id.buzzer_identifier);
vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));
soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); soundId = soundPool.load(this, R.raw.buzzer, 1);
AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); streamVolumeMax = mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC); }
/**
- Called when the activity is resumed from its paused state and immediately
- after onCreate(). */ @Override public void onResume() { super.onResume(); … }
/** Called when the activity is paused by the system. */ @Override public void onPause() { super.onPause(); closeAccessory(); stopVibrate(); stopSound(); }
/**
- Called when the activity is no longer needed prior to being removed from
- the activity stack. */ @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mUsbReceiver); releaseSoundPool(); }
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) { …
}
};
private void openAccessory(UsbAccessory accessory) { mFileDescriptor = mUsbManager.openAccessory(accessory); if (mFileDescriptor != null) { mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); Thread thread = new Thread(null, commRunnable, TAG); thread.start(); Log.d(TAG, "accessory opened"); } else { Log.d(TAG, "accessory open fail"); } }
private void closeAccessory() { try { if (mFileDescriptor != null) { mFileDescriptor.close(); } } catch (IOException e) { } finally { mFileDescriptor = null; mAccessory = null; } }
Runnable commRunnable = new Runnable() {
@Override public void run() { int ret = 0; byte[] buffer = new byte[3];
while (ret >= 0) { try { ret = mInputStream.read(buffer); } catch (IOException e) { Log.e(TAG, "IOException", e); break; }
switch (buffer[0]) { case COMMAND_TOUCH_SENSOR:
if (buffer[1] == SENSOR_ID) {
final byte buzzerId = buffer[1]; final boolean buzzerIsPressed = buffer[2] == 0x1;
runOnUiThread(new Runnable() {
@Override public void run() { if(buzzerIsPressed) { linearLayout.setBackgroundColor(Color.RED); buzzerIdentifierTextView.setText(getString( R.string.touch_button_identifier, buzzerId)); startVibrate(); playSound(); } else { linearLayout.setBackgroundColor(Color.WHITE); buzzerIdentifierTextView.setText(""); stopVibrate(); stopSound(); } } }); } break;
default: Log.d(TAG, "unknown msg: " + buffer[0]); break; } } } };
private void startVibrate() { if(vibrator != null && !isVibrating) { isVibrating = true; vibrator.vibrate(new long[]{0, 1000, 250}, 0); } }
private void stopVibrate() { if(vibrator != null && isVibrating) { isVibrating = false; vibrator.cancel(); } }
private void playSound() {
if(!isSoundPlaying) {
soundPool.play(soundId, streamVolumeMax, streamVolumeMax, 1, 0, 1.0F);
isSoundPlaying = true;
}
} private void stopSound() {
if(isSoundPlaying) {
soundPool.stop(soundId);
isSoundPlaying = false;
}
}
private void releaseSoundPool() { if(soundPool != null) { stopSound(); soundPool.release(); soundPool = null; } } }`
首先,像往常一样,让我们看看变量的定义。
`private static final byte COMMAND_TOUCH_SENSOR = 0x6; private static final byte SENSOR_ID = 0x0;
private LinearLayout linearLayout; private TextView buzzerIdentifierTextView;
private Vibrator vibrator; private boolean isVibrating;
private SoundPool soundPool; private boolean isSoundPlaying; private int soundId;
private float streamVolumeMax;`
数据消息字节与 Arduino 草图中的相同。LinearLayout是容器视图,它稍后会填充整个屏幕。它用于通过将背景颜色更改为红色来指示触摸按钮被按下。TextView显示当前按钮的标识符。接下来的两个变量负责保存对 Android 系统的Vibrator服务的引用,并负责确定振动器当前是否在振动。最后一个变量负责媒体回放。Android 有几种媒体播放的可能性。一种简单的低延迟播放短声音片段的方法是使用SoundPool类,它甚至能够一次播放多个流。SoundPool对象在初始化后负责加载和播放声音。在本例中,您将需要一个布尔标志,即isSoundPlaying标志,这样,如果蜂鸣器已经在播放,您就不会再次触发它。一旦声音文件被加载,soundId将保存一个对声音文件的引用。最后一个变量用于设置稍后播放声音时的音量。
接下来是进行必要初始化的onCreate方法。除了视图初始化之外,您可以看到这里分配了一个对系统振动器服务的引用。之后,初始化SoundPool。SoundPool类的构造函数有三个参数。第一个是SoundPool可以同时播放的流的数量。第二个定义了哪种流将与SoundPool相关联。您可以为音乐、系统声音、通知等分配流。最后一个参数指定源质量,目前没有影响。文档说明您现在应该使用 0 作为缺省值。一旦它被初始化,你必须将声音载入SoundPool。为此,您必须调用load方法,它的参数是一个Context对象、要加载的声音的资源 id 和一个优先级 id。load方法返回一个引用 id,稍后您将使用它来回放预加载的声音。将您的声音文件放在res/raw/buzzer.mp3下的res文件夹中。
注意您可以在 Android 系统上使用多种音频文件编码类型。完整的列表可以在
[developer.android.com/guide/appendix/media-formats.html](http://developer.android.com/guide/appendix/media-formats.html)的开发者页面中找到。
这里要做的最后一件事是确定您所使用的流类型的最大可能音量。稍后,当您播放声音时,您可以定义音量级别。因为你想要一个相当响的蜂鸣器,所以最好把音量调到最大。
`setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); buzzerIdentifierTextView = (TextView) findViewById(R.id.buzzer_identifier);
vibrator = ((Vibrator) getSystemService(VIBRATOR_SERVICE));
soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0); soundId = soundPool.load(this, R.raw.buzzer, 1);
AudioManager mgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE); streamVolumeMax = mgr.getStreamMaxVolume(AudioManager.STREAM_MUSIC);`
和往常一样,在接收消息时,分配给接收工作线程的Runnable对象实现评估逻辑,并最终触发类似蜂鸣器的行为。
`switch (buffer[0]) { case COMMAND_TOUCH_SENSOR: if (buffer[1] == SENSOR_ID) { final byte buzzerId = buffer[1]; final boolean buzzerIsPressed = buffer[2] == 0x1; runOnUiThread(new Runnable() {
@Override
public void run() {
if(buzzerIsPressed) {
linearLayout.setBackgroundColor(Color.RED);
buzzerIdentifierTextView.setText(getString(
R.string.touch_button_identifier, buzzerId));
startVibrate();
playSound();
} else {
linearLayout.setBackgroundColor(Color.WHITE);
buzzerIdentifierTextView.setText("");
stopVibrate();
stopSound();
}
} });
}
break;
default: Log.d(TAG, "unknown msg: " + buffer[0]); break; }`
您可以看到LinearLayout的背景颜色根据按钮的状态而改变,并且TextView也相应地更新。startVibrate和stopVibrate方法在第四章的项目 3 中已经熟悉。
`private void startVibrate() { if(vibrator != null && !isVibrating) { isVibrating = true; vibrator.vibrate(new long[]{0, 1000, 250}, 0); } }
private void stopVibrate() { if(vibrator != null && isVibrating) { isVibrating = false; vibrator.cancel(); } }`
startVibrate和stopVibrate方法只是在开始振动或取消当前振动之前检查振动器是否已经在振动。
根据触摸按钮的状态,开始或停止蜂鸣声播放。这里可以看到方法的实现:
`private void playSound() { if(!isSoundPlaying) { soundPool.play(soundId, streamVolumeMax, streamVolumeMax, 1, 0, 1.0F); isSoundPlaying = true; } }
private void stopSound() { if(isSoundPlaying) { soundPool.stop(soundId); isSoundPlaying = false; } }`
要播放声音,你必须调用SoundPool对象上的play方法。它的参数是soundId,这是您之前在加载声音文件时检索到的,左右声道的音量定义,声音优先级,循环模式,以及当前声音的回放速率。要停止声音,你只需调用SoundPool对象上的stop方法,并提供相应的soundId。您不需要在您的AndroidManifest.xml中为SoundPool的工作定义任何额外的权限。
当应用关闭时,你也应该清理一下。要释放SoundPool分配的资源,只需调用release方法。
private void releaseSoundPool() { if(soundPool != null) { stopSound(); soundPool.release(); soundPool = null; } }
由于屏幕布局与上一个项目有所不同,你应该看看这个项目的main.xml布局文件,如清单 8-3 所示。
***清单 8-3。*项目 9: main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> <TextView android:id="@+id/buzzer_identifier" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000"/> </LinearLayout>
您可以看到该布局只定义了一个嵌入到LinearLayout容器中的TextView。
这就是本章项目编码的全部内容。如果你愿意,你可以用额外的蜂鸣器来扩展这个项目,这样你就可以在和你的朋友和家人玩游戏时使用你自己定制的蜂鸣器。部署您的应用,并对项目进行测试。你的最终结果应该看起来像图 8-7 。
***图 8-7。*项目 9:最终结果
额外的实际例子:ADK 纸钢琴
您已经看到,构建一个简单的 DIY 电容式触摸传感器既不困难也不昂贵。你可以很容易地想象,这种技术在爱好社区中被大量使用来构建具有漂亮和酷的交互用户界面的项目。为了让你自己的创意源源不断,我想借此机会向你展示我的一个项目,这是我为 2011 年柏林谷歌开发者日做的,只是可以实现的一个例子。
2011 年 7 月,谷歌宣布公开呼吁谷歌开发者日。谷歌开发者日是谷歌在美国以外最大的开发者大会。它在全球几个大城市举办。2011 年的比赛地点是阿根廷、澳大利亚、巴西、捷克共和国、德国、以色列、日本和俄罗斯。这次公开征集让开发者有机会向大约 2000 名开发者展示他们的技能和项目。两个挑战是公开呼吁的一部分:HTML5 挑战和 ADK 挑战。参与者首先必须回答一些关于他们挑战的相应技术的基本问题。当他们成功回答这些问题时,他们就有资格参加第二轮挑战。现在,我不知道 HTML5 挑战赛的流程到底是如何运作的,但 ADK 挑战赛的第二轮要求拿出一个合理的项目计划。项目计划应该将 ADK 技术与 Android 设备结合起来,创造一些有趣的东西,如机器人、乐器,甚至是解决日常问题的设备。我的项目计划是用纸做一架带有电容式触摸键的钢琴,ADK 纸钢琴。当用户触摸一个键时,连接的 ADK 板应该会识别它,并在连接的 Android 设备的帮助下播放该键的相应音符。
我从一个只有四个电容式触摸键的小型原型开始。我想看看我用铝箔制成的电容式触摸传感器,当我用纸的顶层和底层绝缘时,它是否会有足够的响应。我使用了CapSense库来识别触摸时不同的键,并使用了SoundPool类来回放每个键对应的音符。设计示意图如图图 8-8 所示。
***图 8-8。*钢琴键构造示意图
原型运行得非常好,我的项目计划被认为是十大提交项目之一,所以我在谷歌开发者日的展览区获得了一席之地。谷歌提供了谷歌 ADK 董事会和演示盾牌,以实现该项目的活动。
在 2011 年 11 月 19 日之前,我有大约三个月的时间来完成这个项目。让我告诉你,三个月听起来像很多时间,但当你在全职工作的同时写一本书时,你往往没有多少时间留给这样的项目。尽管如此,我还是及时完成了这个项目,最后一切都很顺利。然而,建造过程本身就充满了挑战。
正如我对原型所做的那样,我决定用覆有纸的铝箔条来制作电容式触摸钢琴键。每个钥匙下面都有自己的铝箔条。我必须确保这些条带覆盖了钥匙的大部分区域,而不会碰到附近的其他条带。
***图 8-9。*钢琴键布局表
总共需要对 61 个按键进行切割并粘贴在纸键下方的繁琐工作(参见图 8-10 )。).
***图 8-10。*完成琴键布局
如果你还记得的话,谷歌 ADK 板是基于 Arduino Mega 设计的,只有 54 个数字引脚。光凭黑板,我无法识别目标的 61 个键。这就是为什么我建立了自己的扩展板,能够提供更多的输入。电路板上有所谓的 8 位输入移位寄存器。这些 IC 提供八个输入通道,只需使用 ADK 板的三个引脚即可读取。有了 8 个这样的 IC,我只用了大约一半的 ADK 数字引脚就能有 64 路输入。(参见图 8-11 。)
***图 8-11。*自定义输入移位寄存器板
由于我不能将输入连接直接焊接到铝箔条上,我使用鳄鱼夹来建立连接(图 8-12 )。
***图 8-12。*成品纸钢琴建造
构建阶段已经完成,我必须编写软件来识别触摸事件并播放相应的音符。我不能再使用CapSense库了,因为我现在使用 ADK 板的数字引脚来寻址输入移位寄存器。所以我实现了自己的类似于CapSense库的例程,它也依赖于输入到达某个状态的时间。对于 Android 部分,我还使用了SoundPool类,当相应的键被触摸时,播放预先载入的 MP3 音乐。当前纸币的波形也显示在设备的屏幕上。
在截止日期前完成项目(如图 8-13 所示)后,我不得不一路穿过柏林将钢琴运送到场地(图 8-14 )。ADK 项目的展览区受到了热烈的欢迎,ADK 纸钢琴给人留下了深刻的印象。它甚至被当地报纸的一篇报道选中。在活动中展示的 ADK 项目引起了社区的极大兴趣,并很好地概述了可以用 ADK 做些什么,并且希望激发一些人自己尝试一下。
***图 8-13。*柏林 2011 年谷歌开发者日的 ADK 纸钢琴
图 8-14。【2011 年柏林谷歌开发者日在柏林国际商会举行
注你可以在
[marioboehmer.blogspot.com/2011/11/adk-paper-piano-at-google-developer-day.html](http://marioboehmer.blogspot.com/2011/11/adk-paper-piano-at-google-developer-day.html)的我的博客里找到更多关于这个项目和活动的信息。
你也应该看看这个网站,它给出了在谷歌开发者日之旅的不同地点展示的所有 ADK 项目的概述。
总结
自从你开始写这本书以来,你第一次用家用物品制作了自己的传感器。您已经了解了构建自己的电容式触摸传感器有多简单。对于 Arduino 部分,您学习了如何使用 Arduino CapSense库来感测触摸事件。通过结合一些以前学到的 Android 功能,比如使用Vibrator服务和通过SoundPool类添加回放声音的能力,您创建了自己的游戏节目蜂鸣器。您还了解到在一个更大的项目中使用 DIY 电容式触摸传感器的个人实例。
九、让东西动起来
业余电子爱好最有趣的方面之一可能是制造机器人或让你的项目移动。根据使用情况,有许多方法可以实现一般的移动。推动项目的一种常见方式是通过马达。马达被称为致动器,因为它们作用于某物,而不是像传感器那样感知某物。不同种类的马达提供不同程度的运动自由度和动力。三种最常见的电机是 DC 电机、伺服电机和步进电机(见图 9-1 )。
DC 汽车是靠直流电(DC)运行的电动机,主要用于遥控车等玩具。它们提供轴的连续旋转,轴可以连接到齿轮以实现不同的动力传输。它们没有位置反馈,这意味着你不能确定电机转动了多少度。
例如,伺服系统通常用于机器人移动手臂或腿的关节。它们的旋转大多被限制在一定的度数范围内。大多数伺服系统不提供连续旋转,仅支持 180 度范围内的运动。然而,有特殊的伺服能够旋转 360 度,甚至有黑客的限制伺服压制他们的 180 度限制。伺服系统具有位置反馈,这使得通过发送特定信号将它们设置到某个位置成为可能。
步进电机主要用于扫描仪或打印机中的精密机器运动。它们提供带有精确位置反馈的全旋转。当齿轮或传送带连接到步进电机上时,它们可以移动到准确的位置。
由于步进电机项目不像 DC 电机项目和伺服项目那样受欢迎,所以在本章中我将只描述后者。不过,如果你想试试步进电机,你可以在 Arduino 网站的[arduino.cc/hu/Tutorial/StepperUnipolar](http://arduino.cc/hu/Tutorial/StepperUnipolar)和[arduino.cc/en/Tutorial/MotorKnob](http://arduino.cc/en/Tutorial/MotorKnob)找到一些教程。
***图 9-1。*电机(DC 电机、伺服电机、步进电机)
项目 10:控制伺服系统
伺服系统非常适合控制有限的运动。为了控制一个伺服系统,你需要通过你的 ADK 板的数字引脚发送不同的波形给伺服系统。为了定义您的伺服系统应该向哪个方向移动,您将编写一个利用您设备的加速度传感器的 Android 应用。因此,当你沿着 x 轴的某个方向倾斜你的设备时,你的伺服系统也会反映相对运动。
设备的加速度计实际上做的是测量施加到设备上的加速度。加速度是相对于一组轴的速度变化率。重力影响测得的加速度。当 Android 设备放在桌子上时,不会测量到加速度。当沿设备的一个轴倾斜设备时,沿该轴的加速度发生变化。(参见图 9-2 )。
***图 9-2。*设备轴概述(谷歌公司的图像财产,在知识共享 2.5 下,developer . Android . com/reference/Android/hardware/sensor event . html)
零件
幸运的是,伺服系统不需要复杂的电路或额外的部件。它们只有一条数据线接收波形脉冲,以将伺服设置在正确的位置。所以除了一个伺服系统,你只需要你的 ADK 板和一些电线(如图 9-3 所示)。
- 【ADK 板】
- servo
- Some wires
***图 9-3。*项目 10 部分(ADK 板、电线、伺服)
ADK 董事会
在之前的项目中,您必须生成波形,您将使用 ADK 板的一个数字引脚。ADK 板将向伺服系统发送电脉冲。这些脉冲的宽度负责将伺服设置到特定的位置。为了产生必要的波形,您可以通过在定义的时间周期内将数字引脚设置为交替的HIGH / LOW信号来自行实现逻辑,也可以使用 Arduino Servo库,稍后将详细介绍。
伺服
如前所述,伺服机构是一种马达,在很大程度上只能在预定范围内转动其轴。业余爱好伺服的范围通常是 180 度。当内部齿轮到达预定位置时,通过阻止内部齿轮机械地实现限制。你可以在网上找到黑客来摆脱这种阻碍,但你必须打开你的伺服来打破这种阻碍,然后做一些焊接。获得更多旋转自由度的另一种可能性是使用特殊的 360 度伺服系统。这些往往有点贵,这就是为什么黑客的低预算 180 度伺服似乎是一个很好的选择,对一些人来说。无论如何,在大多数情况下,你不会需要一个完整的旋转伺服大多数项目。在这些情况下,你最好使用 DC 电机或步进电机。
那么伺服系统是如何工作的呢?基本上,伺服系统只是一个电动机,它将其动力传递给一组齿轮,这些齿轮可以在预定义的角度范围内转动轴。旋转轴的范围受到机械限制。集成电路与电位计相结合,接收发送到伺服机构的波形脉冲,并确定伺服机构必须设置的角度。通常的业余爱好伺服系统用频率为 50Hz 的脉冲操作,这描述了大约 20 毫秒的信号周期。信号设置为HIGH的时间量指定了伺服角度。根据伺服行业标准,信号必须设置为HIGH1 ms,然后在剩余时间内设置为LOW,以将伺服移动到其最左边的位置。要将伺服移动到最右边的位置,必须将信号设置为HIGH2 ms,然后在剩余时间内设置为LOW。(参见图 9-4 。)注意,这些信号时间是缺省值,并且实际上它们可能因伺服机构而异,因此对于左侧位置,这些值可能更低,而对于右侧位置,这些值可能更高。在某些情况下,你甚至不需要坚持 20 毫秒的时间,所以即使这样也可以定义为更短。一般来说,您应该首先坚持使用默认值,只有在遇到问题时才更改这些值。
***图 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 草图
Arduino 草图负责接收加速度计数据,并将伺服设置到正确的位置。你有两种可能做那件事。您可以实现自己的方法来创建想要发送到伺服系统的波形,也可以使用 Arduino IDE 附带的Servo库。就个人而言,我更喜欢使用库,因为它更精确,但是我将在这里向您展示这两种方法。您可以决定哪种解决方案更符合您的需求。
手动波形生成
第一种方法是自己实现波形生成。先看看完整的清单 9-1 。
***清单 9-1。*项目 10: Arduino 草图(自定义波形实现)
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>
#define COMMAND_SERVO 0x7
#define SERVO_ID_1 0x1 #define SERVO_ID_1_PIN 2
int highSignalTime; float microSecondsPerDegree; // default boundaries, change them for your specific servo int leftBoundaryInMicroSeconds = 1000; int rightBoundaryInMicroSeconds = 2000;
AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");
byte rcvmsg[6];
void setup() { Serial.begin(19200); pinMode(SERVO_ID_1_PIN, OUTPUT); acc.powerOn(); microSecondsPerDegree = (rightBoundaryInMicroSeconds – leftBoundaryInMicrosSeconds) / 180.0; }
void loop() { if (acc.isConnected()) { int len = acc.read(rcvmsg, sizeof(rcvmsg), 1); if (len > 0) { if (rcvmsg[0] == COMMAND_SERVO) { if(rcvmsg[1] == SERVO_ID_1) { int posInDegrees = ((rcvmsg[2] & 0xFF) << 24) + ((rcvmsg[3] & 0xFF) << 16) + ((rcvmsg[4] & 0xFF) << 8) + (rcvmsg[5] & 0xFF); posInDegrees = map(posInDegrees, -100, 100, 0, 180); moveServo(SERVO_ID_1_PIN, posInDegrees); } } } } }
void moveServo(int servoPulsePin, int pos){ // calculate time for high signal highSignalTime = leftBoundaryInMicroSeconds + (pos * microSecondsPerDegree); // set Servo to HIGH digitalWrite(servoPulsePin, HIGH); // wait for calculated amount of microseconds delayMicroseconds(highSignalTime); // set Servo to LOW digitalWrite(servoPulsePin, LOW); // delay to complete waveform delayMicroseconds(20000 – highSignalTime); }`
像往常一样,让我们从草图的变量开始。
#define COMMAND_SERVO 0x7 #define SERVO_ID_1 0x1 #define SERVO_ID_1_PIN 2
您可以看到为伺服控制消息定义了一个新的命令类型常量。第二个常量是伺服的 ID,如果你想在你的 ADK 板上安装多个伺服,应该控制这个 ID。第三个常量是连接伺服系统的 ADK 板上的相应引脚。接下来,您有一些变量来指定以后的波形。
int highSignalTime; float microSecondsPerDegree; int leftBoundaryInMicroSeconds = 1000; int rightBoundaryInMicroSeconds = 2000;
变量highSignalTime稍后被计算,它描述了信号被设置为HIGH的时间量(以微秒计)。正如你所记得的,将信号设置到HIGH大约 1ms 意味着将伺服向左转,将信号设置到HIGH大约 2ms 导致伺服向右转。microSecondsPerDegree变量用于转换目的,顾名思义,它描述了每度需要添加到HIGH信号时间中的微秒数。最后两个变量是以微秒为单位的伺服边界。如前所述,约 1ms 的HIGH信号应导致伺服系统向左完全移动,2ms 应导致向右完全移动。然而,在实践中,情况通常并非如此。如果你用这些默认值工作,你可能会看到你的伺服不会把它的全部潜力。您应该试验边界值来调整代码以适应您的伺服,因为每个伺服都是不同的。我甚至不得不将我的一个伺服系统的默认值改为从 600 到 2100 的边界,这意味着如果我施加一个HIGH信号 0.6 毫秒,伺服系统将完全移动到左边,如果我施加一个HIGH信号大约 2.1 毫秒,它将完全移动到右边。正如你所看到的,你可以使用默认值,但是如果你遇到问题,你应该尝试伺服系统的边界。
在setup方法中,您必须将伺服信号引脚的pinMode设置为输出。
pinMode(SERVO_ID_1_PIN, OUTPUT);
您还应该在这里计算每度的微秒数,以便您可以在以后的定位计算中使用该值。
microSecondsPerDegree = (rightBoundaryInMicroSeconds - leftBoundaryInMicroSeconds) / 180.0;
计算很简单。由于您的伺服很可能只有 180 度的范围,您只需要将右边界值和左边界值之差除以 180。
接下来是loop方法。在从 Android 应用中读取接收到的数据消息后,您需要使用移位技术对传输的值进行解码。
`int posInDegrees = ((rcvmsg[2] & 0xFF) << 24)
- ((rcvmsg[3] & 0xFF) << 16)
- ((rcvmsg[4] & 0xFF) << 8)
- (rcvmsg[5] & 0xFF);`
您将在后面编写的 Android 应用将从-100 到 100 的范围内为左侧位置和右侧位置传输值。因为您需要为伺服位置提供相应的度数,所以您需要首先使用 map 函数。
posInDegrees = map(posInDegrees, -100, 100, 0, 180);
位置值现在可以与相应伺服系统的信号引脚一起提供给自定义的moveServo方法。
moveServo(SERVO_ID_1_PIN, posInDegrees);
moveServo方法的实现描述了控制伺服所需波形的构造。
void moveServo(int servoPulsePin, int pos){ // calculate time for high signal highSignalTime = leftBoundaryInMicroSeconds + (pos * microSecondsPerDegree); // set Servo to HIGH digitalWrite(servoPulsePin, HIGH); // wait for calculated amount of microseconds delayMicroseconds(highSignalTime); // set Servo to LOW digitalWrite(servoPulsePin, LOW); // delay to complete waveform delayMicroseconds(20000 - highSignalTime); }
让我们通过一个例子来解决这个问题。假设你得到了一个 90 度的理想位置。要确定HIGH信号的时间,你可以用 90 乘以microSecondsPerDegree并加上左边界值。如果您使用默认边界值,则您的计算如下所示:
highSignalTime = 1000 + (90 * 5.55556);
这导致HIGH信号时间约为 1500 微秒,所以应该是伺服的中间位置。你现在要做的是将伺服系统的信号引脚设置为数字HIGH等待计算好的时间,然后再次将其设置为LOW。现在可以计算一个 20ms 信号周期的剩余延迟,以完成一个完整的脉冲。仅此而已。
用伺服库产生波形
如您所见,实现波形生成并不特别困难,但是如果您使用 Arduino IDE 附带的Servo库,它会变得更加容易。清单 9-2 显示了使用Servo库重写的草图。
***清单 9-2。*项目 10: Arduino 草图(使用伺服库)
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h> #include <Servo.h>
#define COMMAND_SERVO 0x7 #define SERVO_ID_1 0x1 #define SERVO_ID_1_PIN 2
Servo servo;
AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");
byte rcvmsg[6];
void setup() { Serial.begin(19200); servo.attach(SERVO_ID_1_PIN); acc.powerOn(); }
void loop() { if (acc.isConnected()) { int len = acc.read(rcvmsg, sizeof(rcvmsg), 1); if (len > 0) { if (rcvmsg[0] == COMMAND_SERVO) { if(rcvmsg[1] == SERVO_ID_1) { int posInDegrees = ((rcvmsg[2] & 0xFF) << 24)
- ((rcvmsg[3] & 0xFF) << 16)
- ((rcvmsg[4] & 0xFF) << 8)
- (rcvmsg[5] & 0xFF); posInDegrees = map(posInDegrees, -100, 100, 0, 180); servo.write(posInDegrees); // give the servo time to reach its position delay(20); } } } } }`
乍一看,您可以看到代码变得短了很多。您将不再需要任何计算或自定义方法。要使用Servo库,你首先必须将它包含在你的草图中。
#include <Servo.h>
通过将它包含在你的草图中,你可以用一个Servo物体来为你做所有繁重的工作。
Servo servo;
要初始化Servo对象,你必须调用attach方法,并提供连接伺服信号线的数字引脚。
servo.attach(SERVO_ID_1_PIN);
为了实际控制伺服系统,你只需要调用它的write方法以及你想要的位置值(以度为单位)。
servo.write(posInDegrees); delay(20);
注意,你必须在这里调用delay方法,给伺服一些时间到达它的位置,并完成一个完整的脉冲。如果你不提供延迟,伺服会抖动,因为位置更新会太快。波形生成是在后台的Servo库中处理的,所以你再也不用担心了。简单多了,不是吗?
Arduino 部分到此为止。请记住,在实现 Arduino 草图时,您可以选择最适合您需求的方法。
Android 应用
大多数 Android 设备都有识别其在三维空间中的方向的方法。通常,他们通过向加速度计传感器和磁场传感器请求传感器更新来实现这一点。对于 Android 部分,您还将从加速度计请求方向更新,这将直接关系到稍后的伺服运动。清单 9-3 显示了活动的实现,我将在后面详细讨论。
清单 9-3。项目 10:ProjectTenActivity.java
`package project.ten.adk;
import …;
public class ProjectTenActivity extends Activity {
…
private static final byte COMMAND_SERVO = 0x7; private static final byte SERVO_ID_1 = 0x1;
private TextView servoDirectionTextView;
private SensorManager sensorManager; private Sensor accelerometer;
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
…
setContentView(R.layout.main); servoDirectionTextView = (TextView) findViewById(R.id.x_axis_tilt_text_view);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); }
/**
- Called when the activity is resumed from its paused state and immediately
- after onCreate().
*/
@Override
public void onResume() {
super.onResume();
sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);
…
}
/** Called when the activity is paused by the system. */ @Override public void onPause() { super.onPause(); closeAccessory(); sensorManager.unregisterListener(sensorEventListener); }
/**
- Called when the activity is no longer needed prior to being removed from
- the activity stack. */ @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mUsbReceiver); }
private final SensorEventListener sensorEventListener = new SensorEventListener() {
int x_acceleration;
@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // not implemented }
@Override public void onSensorChanged(SensorEvent event) { x_acceleration = (int)(-event.values[0] * 10); moveServoCommand(SERVO_ID_1, x_acceleration); runOnUiThread(new Runnable() {
@Override public void run() { servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration)); } }); } };
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override
public void onReceive(Context context, Intent intent) {
…
}
};
private void openAccessory(UsbAccessory accessory) { … }
private void closeAccessory() { … }
public void moveServoCommand(byte target, int value) { byte[] buffer = new byte[6]; buffer[0] = COMMAND_SERVO; buffer[1] = target; buffer[2] = (byte) (value >> 24); buffer[3] = (byte) (value >> 16); buffer[4] = (byte) (value >> 8); buffer[5] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } } }`
正如 Arduino 草图中所做的那样,您首先必须为您打算稍后发送的控制消息定义相同的命令和目标 id。
private static final byte COMMAND_SERVO = 0x7; private static final byte SERVO_ID_1 = 0x1;
您将使用的唯一可视组件是一个简单的TextView元素,用于调试目的,并为您提供倾斜设备时 x 轴方向值如何变化的概述。
private TextView servoDirectionTextView;
为了从你的 Android 设备的任何传感器请求更新,你首先需要获得对SensorManager和Sensor本身的引用。从某些传感器获取数据还有其他方法,但SensorManager是大多数传感器的通用注册表。
private SensorManager sensorManager; private Sensor accelerometer;
在onCreate()方法中完成内容视图元素的常规设置后,通过调用上下文方法getSystemService并提供常量SENSOR_SERVICE作为参数,您获得了前面提到的对SensorManager的引用。此方法提供对所有类型的系统服务的访问,例如连接服务、音频服务等等。
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
随着SensorManager准备就绪,您可以访问 Android 设备的传感器。您特别想要加速度传感器,所以当您在SensorManager上调用getDefaultSensor方法时,您必须将它指定为一个参数。
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
Android 系统提供了一种注册传感器变化的机制,因此您不必担心这一点。因为你不能直接从Sensor物体上获得传感器读数,你必须为那些SensorEvent注册。
在onResume()方法中,你调用SensorManager对象上的registerListener方法,并传递三个参数。第一个是SensorEventListener,它实现了对传感器变化做出反应的方法。(我一会儿会谈到这一点。)第二个参数是实际的Sensor参考,应该听听。最后一个参数是更新速率。
在SensorManager类中定义了不同的速率常数。我对SENSOR_DELAY_GAME常数有最好的体验。它的更新速度非常快。正常延迟在伺服运动中引起了一些滞后行为。我也不推荐使用SENSOR_DELAY_FASTEST常数,因为这通常会使伺服抖动很大;更新来得太快了。
sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);
因为您在onResume()方法中注册了监听器,所以您还应该确保应用释放了资源,并相应地取消了onPause()方法中的监听过程。
sensorManager.unregisterListener(sensorEventListener);
现在让我们来看看监听器本身,因为它负责处理传感器更新。
`private final SensorEventListener sensorEventListener = new SensorEventListener() {
int x_acceleration;
@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // not implemented }
@Override public void onSensorChanged(SensorEvent event) { x_acceleration = (int)(-event.values[0] * 10); moveServoCommand(SERVO_ID_1, x_acceleration); runOnUiThread(new Runnable() {
@Override public void run() { servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration)); } }); } };`
当你实现一个SensorEventListener时,你将不得不写两个方法,即onAccuracyChanged方法和onSensorChanged方法。在这个项目中,第一个不是你真正感兴趣的;你现在不关心准确性。然而,第二个函数提供了一个名为SensorEvent的参数,它由系统提供并保存您感兴趣的传感器值。加速度计的SensorEvent值包含三个值,x 轴上的加速度、y 轴上的加速度和 z 轴上的加速度。你只对 x 轴上的加速度感兴趣,所以你只需要担心第一个值。返回值的范围从 10.0(完全向左倾斜)到-10.0(完全向右倾斜)。对于稍后看到这些值的用户来说,这似乎有点违背直觉。这就是这个示例项目中的值被否定的原因。为了更容易地传输这些值,最好将这些值乘以 10,这样以后就可以传输整数而不是浮点数。通过这样做,可传输的数字将在从-100 到 100 的范围内。
x_acceleration = (int)(-event.values[0] * 10);
现在您已经有了 x 轴上的加速度数据,您可以更新TextView元素来给出视觉反馈。
`runOnUiThread(new Runnable() {
@Override public void run() { servoDirectionTextView.setText(getString( R.string.x_axis_tilt_text_placeholder, x_acceleration)); } });`
最后要做的事情是向 ADK 板发送控制消息。为此,您将使用自定义方法moveServoCommand,提供要控制的伺服 ID 和实际加速度数据。
moveServoCommand(SERVO_ID_1, x_acceleration);
该方法的实现非常简单。您只需设置基本的数据结构,将整数加速度值位移到四个单字节,并通过输出流将完整的消息发送到 ADK 板。
public void moveServoCommand(byte target, int value) { byte[] buffer = new byte[6]; buffer[0] = COMMAND_SERVO; buffer[1] = target; buffer[2] = (byte) (value >> 24); buffer[3] = (byte) (value >> 16); buffer[4] = (byte) (value >> 8); buffer[5] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }
Android 部分到此为止,最终结果如图图 9-8 所示。当你准备好了,部署这两个应用,看看每当你倾斜你的 Android 设备时,你的伺服如何转动。
***图 9-8。*项目 10:最终结果
项目 11:控制 DC 汽车公司
下一个项目将向您展示如何控制另一种电机,即所谓的 DC 电机。正如我在本章开始时已经解释过的,DC 马达提供连续的旋转,不像伺服马达那样受到人为的限制。您将再次使用设备的加速度计来控制 DC 电机,只是这次您将处理设备 y 轴上的加速度变化。因此,当你向前倾斜设备时,电机将开始旋转。你也可以在这样做的时候控制它的速度。注意,我没有涉及旋转方向的改变,这需要更多的硬件。在开始之前,你需要了解控制 DC 发动机所需的部件以及发动机是如何运转的。
零件
你不需要很多零件就能让马达运转起来。事实上,你将要建造的电路的唯一新元件是一个所谓的 NPN 晶体管。我一会儿会解释它的目的。
注意如果您使用基于 Arduino 设计的 ADK 板,并且碰巧使用工作电压高于 5V 的 DC 电机,或者如果其功耗超过 40mA,您可能需要额外的外部电源,因为板的输出引脚被限制在这些值。
零件清单如下所示(图 9-9 ):
- 网页净化器板
- breadboard
- Transistor NPN [BC 547 b]
- DC motor
- Partial wire
- Optional: 1-10kω resistor, external battery
***图 9-9。*项目 11 个零件(ADK 板、试验板、电线、NPN 晶体管、DC 电机)
ADK 董事会
稍后,您将使用输出引脚的脉宽调制(PWM)功能来产生影响电机速度的不同电压。
DC 汽车
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 汽车不附带预先连接的电线,所以你可能必须先焊接电线。
NPN 晶体管(BC547B)
晶体管是能够在电路中开关和放大功率的半导体。它们通常有三个连接器,称为基极、集电极和发射极(见图 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 草图
Arduino 草图会比你写的控制伺服系统的草图短一点(如清单 9-4 所示)。Arduino IDE 没有官方的 DC 汽车库,但是对于这个例子,你不需要库,因为代码非常简单。在网站上有社区编写的自定义库,用于使用 H 桥控制旋转方向的情况,但这超出了本例的范围。您只需使用analogWrite方法,根据您将从连接的 Android 设备接收到的倾斜值,更改数字引脚 2 上的电压输出。
***清单 9-4。*项目 11: Arduino 草图
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>
#define COMMAND_DC_MOTOR 0x8 #define DC_MOTOR_ID_1 0x1 #define DC_MOTOR_ID_1_PIN 2
AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");
byte rcvmsg[3];
void setup() { Serial.begin(19200); pinMode(DC_MOTOR_ID_1_PIN, OUTPUT); acc.powerOn(); }
void loop() { if (acc.isConnected()) { int len = acc.read(rcvmsg, sizeof(rcvmsg), 1); if (len > 0) { if (rcvmsg[0] == COMMAND_DC_MOTOR) { if(rcvmsg[1] == DC_MOTOR_ID_1) { int motorSpeed = rcvmsg[2] & 0xFF; motorSpeed = map(motorSpeed, 0, 100, 0, 255); analogWrite(DC_MOTOR_ID_1_PIN, motorSpeed); } } } } }`
这里要做的第一件事是像往常一样更改命令和目标字节。
#define COMMAND_DC_MOTOR 0x8 #define DC_MOTOR_ID_1 0x1 #define DC_MOTOR_ID_1_PIN 2
接下来,将数字引脚 2 配置为输出引脚。
pinMode(DC_MOTOR_ID_1_PIN, OUTPUT);
一旦你从连接的 Android 设备收到有效信息,你必须将原始倾斜值从byte转换为int。
int motorSpeed = rcvmsg[2] & 0xFF;
因为analogWrite方法处理 0 到 255 之间的值,而不是 0 到 100 之间的值,所以在将它们提供给analogWrite方法之前,必须先将它们映射到适当的范围。
motorSpeed = map(motorSpeed, 0, 100, 0, 255);
最后,您可以通过提供目标引脚 2 和电机速度的转换值来调用analogWrite方法。
analogWrite(DC_MOTOR_ID_1_PIN, motorSpeed);
这一小段代码是用来控制一个简单的 DC 马达的单向速度的。
Android 应用
Android 应用与之前的伺服项目几乎相同。唯一需要改变的是消息字节和当SensorEventListener接收到SensorEvent时得到的值。让我们看看需要做些什么(清单 9-5 )。
清单 9-5。项目 11:ProjectElevenActivity.java
`package project.eleven.adk;
import …;
public class ProjectElevenActivity extends Activity {
…
private static final byte COMMAND_DC_MOTOR = 0x8; private static final byte DC_MOTOR_ID_1 = 0x1;
private TextView motorSpeedTextView;
private SensorManager sensorManager; private Sensor accelerometer;
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
…
setContentView(R.layout.main); motorSpeedTextView = (TextView) findViewById(R.id.y_axis_tilt_text_view);
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
} /**
- Called when the activity is resumed from its paused state and immediately
- after onCreate(). */ @Override public void onResume() { super.onResume();
sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_GAME);
…
}
/** Called when the activity is paused by the system. */ @Override public void onPause() { super.onPause(); closeAccessory(); sensorManager.unregisterListener(sensorEventListener); }
/**
- Called when the activity is no longer needed prior to being removed from
- the activity stack. */ @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mUsbReceiver); }
private final SensorEventListener sensorEventListener = new SensorEventListener() {
int y_acceleration;
@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // not implemented }
@Override
public void onSensorChanged(SensorEvent event) {
y_acceleration = (int)(-event.values[1] * 10);
if(y_acceleration < 0) {
y_acceleration = 0;
} else if(y_acceleration > 100) {
y_acceleration = 100;
}
moveMotorCommand(DC_MOTOR_ID_1, y_acceleration);
runOnUiThread(new Runnable() { @Override
public void run() {
motorSpeedTextView.setText(getString( R.string.y_axis_tilt_text_placeholder, y_acceleration));
}
});
}
};
private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { … } };
private void openAccessory(UsbAccessory accessory) { … }
private void closeAccessory() { … }
public void moveMotorCommand(byte target, int value) { byte[] buffer = new byte[3]; buffer[0] = COMMAND_DC_MOTOR; buffer[1] = target; buffer[2] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } } }`
此处命令和目标消息字节已被更改,以匹配 Arduino 草图中的字节。
private static final byte COMMAND_DC_MOTOR = 0x8; private static final byte DC_MOTOR_ID_1 = 0x1;
在伺服示例中,您将使用一个简单的TextView元素为用户提供视觉反馈,该元素显示设备沿 y 轴的当前倾斜值。
private TextView motorSpeedTextView;
实际上,这里唯一有趣的新部分是SensorEventListener的实现。
private final SensorEventListener sensorEventListener = new SensorEventListener() { `int y_acceleration;
@Override public void onAccuracyChanged(Sensor sensor, int accuracy) { // not implemented }
@Override public void onSensorChanged(SensorEvent event) { y_acceleration = (int)(-event.values[1] * 10); if(y_acceleration < 0) { y_acceleration = 0; } else if(y_acceleration > 100) { y_acceleration = 100; } moveMotorCommand(DC_MOTOR_ID_1, y_acceleration); runOnUiThread(new Runnable() {
@Override public void run() { motorSpeedTextView.setText(getString( R.string.y_axis_tilt_text_placeholder, y_acceleration)); } }); } };`
同样,您不需要实现onAccuracyChanged方法,因为您只想知道当前的倾斜值,而不是它的准确性。在onSensorChanged方法中,您可以看到您访问了事件值的第二个元素。您可能还记得,SensorEvent为这种传感器类型提供了三个值:x 轴、y 轴和 z 轴的值。因为需要 y 轴上的值变化,所以必须访问第二个元素。
y_acceleration = (int)(-event.values[1] * 10);
正如在伺服示例中所做的那样,您需要调整该值,以获得更好的用户可读性,并便于以后的传输。通常,如果您将设备向前倾斜,您会收到-10.0 到 0.0 之间的值。为了避免混淆用户,您将首先否定该值,这样向前倾斜的增加将显示一个增加的数字,而不是减少的数字。为了便于传输,只需将该值乘以 10,并将其转换为整数数据类型,就像前面的项目一样。
当向后倾斜时,您仍然可以接收到传感器值,您不希望稍后将这些值传输到 ADK 板,因此只需定义边界并调整接收到的值。
if(y_acceleration < 0) { y_acceleration = 0; } else if(y_acceleration > 100) { y_acceleration = 100; }
现在你已经有了最终的倾斜值,你可以更新TextView并将数据信息发送到 ADK 板。传输是通过一种叫做moveMotorCommand的独立方法完成的。
public void moveMotorCommand(byte target, int value) { byte[] buffer = new byte[3]; buffer[0] = COMMAND_DC_MOTOR; buffer[1] = target; buffer[2] = (byte) value; if (mOutputStream != null) { try { mOutputStream.write(buffer); } catch (IOException e) { Log.e(TAG, "write failed", e); } } }
Android 代码这次没有太大的麻烦,因为您只需要调整上一个示例中的一些代码行。然而,你已经完成了机器人的部分,现在你可以看到你的 DC 汽车在运行了(图 9-17 )。部署 Arduino sketch 和 Android 应用,当您向前倾斜设备时,可以看到您的电机在旋转。
***图 9-17。*项目 11:最终结果
总结
这一章给了你一个简单的概述,让你的项目以任何方式前进。这一概述远未完成。有很多方法可以将运动带入游戏,但使用伺服或 DC 马达是最常见的方法,它给你进一步的实验提供了一个完美的开始。您学习了这些致动器如何操作,以及如何使用 ADK 板驱动它们。您还学习了如何处理 Android 设备的加速度传感器,以及如何使用它来读取其三个轴中任何一个轴的当前加速度或倾斜值。最后,您使用设备的加速度传感器来控制执行器。
十、报警系统
现在你已经对你的 ADK 板和你已经使用过的不同的传感器和组件感到满意了,是时候来点更大的了。在最后一章中,您将结合前几章中使用的一些组件来构建两个版本的报警系统。您将了解新组件—倾斜开关和由红外 LED 和红外探测器组成的红外挡光板—在现实世界中广泛应用。在两个独立的项目中,您将学习如何将这些组件集成到一个小型报警系统中,以便它们触发警报。在硬件方面,警报将通过闪烁的红色 LED 和产生高音的压电蜂鸣器来表达。您的 Android 设备会将警报事件保存在一个日志文件中,并且根据当前的项目,它还会发送一条通知短信,或者给入侵者拍照并保存到您的文件系统中。
项目 12:带倾斜开关的短信报警系统
在这个项目中,你将使用一个所谓的倾斜开关来触发警报,如果开关倾斜到其关闭位置。触发器将使红色 LED 发出脉冲,压电蜂鸣器发出警报声。此外,Android 设备将收到警报通知,并在其文件系统中写入日志文件。如果您的 Android 设备具有电话功能,它还会向指定的电话号码发送短信,显示报警时间和报警消息。
零件
你的第一个报警系统需要几个部件。您将使用一个红色 LED 来发出警报发生的视觉信号。对于听觉反馈,您将使用一个压电蜂鸣器产生报警声。如果倾斜到关闭位置,倾斜开关将触发警报。要重置报警系统,以便它可以报告下一个报警,您将使用一个简单的按钮。以下是您的报警系统所需部件的完整列表(参见图 10-1 ):
- ADK 董事会
- 试验板
- S7-1200 可编程控制器
- 2 X10 kω电阻
- 一些电线
- 纽扣
- 倾斜开关
- 工作电压为 5V 的 LED
- 压电蜂鸣器
***图 10-1。*项目 12 部分(ADK 板、试验板、电线、电阻器、按钮、倾斜开关、压电蜂鸣器、红色 LED)
ADK 董事会
本项目将使用 ADK 板,提供两种输入方式,即倾斜开关和按钮,以及两种输出方式,即 LED 和压电蜂鸣器。由于您没有任何模拟输入要求,您将只使用 ADK 板的数字引脚。您将使用数字输入功能来感应按钮的按下或倾斜开关的闭合。数字输出功能,尤其是 PWM 功能,将用于脉冲 LED,并通过压电蜂鸣器产生声音。
LED
正如您在第三章中所学,您可以通过使用 ADK 板的 PWM 功能来调暗 LED。当警报发生时,LED 将用于给出视觉反馈。这将通过让 LED 脉冲来实现,这意味着它将在其最亮的最大水平和最暗的最小水平之间持续变暗,直到警报被重置。
压电蜂鸣器
第五章向您展示了当向压电蜂鸣器的压电元件供电时,您可以产生声音,这被描述为反向压电效应。压电元件的振荡产生压力波,该压力波被人耳感知为声音。振荡是通过使用 ADK 板的 PWM 功能来调制输出到压电蜂鸣器的电压来实现的。
按钮
在第四章中,您学习了如何将按钮或开关用作项目的输入。原理很简单。当按钮或开关被按下或闭合时,电路闭合。
倾斜开关
倾斜开关与普通开关非常相似。不同的是,用户没有真正的开关来按下或翻转以闭合连接的电路。最常见的倾斜开关通常以部件内的连接器引线彼此分离的方式构造。此外,在组件内还有一个由导电材料制成的球。(参见图 10-2 )。
***图 10-2。*打开倾斜开关(内部视图)
当开关以某种方式倾斜时,导电球移动并接触开路连接。球关闭了连接,电流可以从一端流到另一端。由于球受重力影响,你只需倾斜倾斜开关,使连接点指向地面。(参见图 10-3 )。
***图 10-3。*关闭倾斜开关(内部视图)
就这么简单。当你摇动倾斜开关时,你可以听到球在组件内移动。
倾斜开关也被称为水银开关,因为在过去,大多数倾斜开关内部都有一个水银滴来关闭连接。水银的优点是它不像其他材料那样容易反弹,所以它不会受到振动的太大影响。然而,缺点是水银毒性很强,所以如果倾斜开关损坏,就会对环境造成危险。如今,其他导电材料通常用于倾斜开关。
倾斜开关在许多实际应用中使用。汽车工业广泛使用它们。汽车的行李箱灯只是一个例子:如果你把行李箱打开到一个特定的角度,灯就会打开。另一个例子是经典的弹球机,如果用户过度倾斜机器以获得优势,它会进行记录。你也可以很容易地想象一个报警系统的用例,比如当门把手被按下时进行感应。
一些倾斜开关可能带有焊接引脚,您不能直接将其插入试验板,因此您可能需要在这些引脚上焊接一些电线,以便稍后将倾斜开关连接到电路。(参见图 10-4 )。有时倾斜开关可以有两个以上的连接器,以便您可以将它们连接到多个电路。你只需要确保你总是连接正确的引脚对。查看数据手册或构建一个简单电路来测试哪些连接相互关联。
***图 10-4。*带焊接连接器引脚的倾斜开关和库存倾斜开关
设置
设置将通过一次连接一个组件来完成,这样当您启动项目时,您就不会弄混并最终损坏某些东西。我将首先展示每个独立的电路设置,然后展示包含所有元件的完整电路设置。
LED 电路
先说 LED。众所周知,led 通常以大约 20mA 到 30mA 的电流工作。为了将电流限制在至少 20mA 的值,您需要在 LED 上连接一个电阻。ADK 板的输出引脚提供 5V 电压,因此您需要应用欧姆定律来计算电阻值。
r = 5v/0.02a r = 250ω
最接近的普通电阻是 220ω电阻。有了这个电阻,你最终会得到一个大约 23mA 的电流,这很好。现在将电阻的一端连接到数字引脚 2,另一端连接到 LED 的阳极(长引线)。发光二极管的阴极接地(GND)。LED 电路设置如图图 10-5 所示。
***图 10-5。*项目 12: LED 电路设置
压电蜂鸣器电路
接下来,您将压电蜂鸣器连接到 ADK 板。这非常简单,因为你不需要额外的组件。如果你碰巧有一个压电蜂鸣器,确保按照标记正确连接正负引线。否则,只需将一根引线连接到数字引脚 3,另一根引线接地(GND)。压电蜂鸣器电路设置如图 10-6 所示。
***图 10-6。*项目 12:压电蜂鸣器电路设置
按钮电路
您可能还记得第四章中的内容,使用按钮消除电气干扰时,最好将电路上拉至工作电压。为了上拉按钮电路而不损坏高电流输入引脚,需要将一个 10kΩ上拉电阻与按钮一起使用。为此,将 ADK 电路板的+5V Vcc 引脚连接到 10kω电阻的一条引线。另一根引线连接到数字引脚 4。数字引脚 4 也连接到按钮的一个引线。相反的导线接地(GND)。按钮电路如图图 10-7 所示。
***图 10-7。*项目 12:按钮电路设置
倾斜开关电路
由于倾斜开关的工作原理与按钮类似,您可以像连接按钮一样连接它。将 ADK 板的+5V Vcc 引脚连接到 10kΩ电阻的一根引线上。另一根引线连接到数字引脚 5。数字引脚 5 也连接到倾斜开关的一个引线。相反的导线接地(GND)。倾斜开关电路设置如图 10-8 所示。
***图 10-8。*项目 12:倾斜开关电路设置
完成电路设置
现在你知道如何连接每个组件,看看图 10-9 所示的完整电路设置。
***图 10-9。*项目 12:完成电路设置
那么这个报警系统是如何工作的呢?想象一下,倾斜开关安装在门把手或天窗上。当手柄被按下或存水弯窗被打开时,倾斜开关将倾斜到导电球接触内部引线的位置,从而使开关闭合。现在,报警被记录,压电蜂鸣器和 LED 开始发出声音和视觉报警反馈。要重置警报以便再次触发,必须按下按钮。
软件
现在您已经设置好了一切,是时候编写必要的软件来启动和运行警报系统了。Arduino 草图将监控倾斜开关是否已倾斜至其触点闭合位置。如果倾斜开关触发了警报,Arduino tone方法将用于使压电蜂鸣器振荡,从而产生高音。此外,红色 LED 将通过使用analogWrite方法产生脉冲,该方法调制电压输出并使 LED 以不同的照明强度点亮。为了重置警报,使其可以再次被触发,一个简单的按钮的状态被读取。一旦按下该按钮,所有必要的变量都会重置,报警系统可以再次记录报警。
如果警报被触发,如果 Android 设备已连接,则会向其发送一条消息。一旦 Android 设备收到警报消息,它会以红屏背景和警报消息的形式给出视觉反馈。警报消息和时间戳将保存在设备的外部存储系统中。但是,不要被术语外部所误导。Android 环境下的外部并不一定意味着存储,比如可移动 SD 卡。术语外部也可以描述内部不可移动存储。这是一种表述,描述存储在其中的文件可以被用户读取和修改,并且该存储可以被另一个操作系统安装为用于文件浏览的大容量存储。
如果连接的 Android 设备支持电话功能,则会向预先配置的电话号码发送包含警报消息的短信。请记住,大多数 Android 平板电脑都没有电话功能,因为它们的主要用途是浏览互联网,而不是给人打电话。你可以想象,没有人会在公共场合拿着一个 10 英寸的平板电脑贴着脸颊打电话,看起来很酷。
Arduino 草图
如软件部分所述,您将使用一些在前面的示例中使用过的众所周知的方法。现在唯一的不同是,您将使用多个组件。在我详细描述之前,先看一下完整的清单 10-1 。
***清单 10-1。*项目 12: Arduino 草图
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>
#define LED_OUTPUT_PIN 2 #define PIEZO_OUTPUT_PIN 3 #define BUTTON_INPUT_PIN 4 #define TILT_SWITCH_INPUT_PIN 5
#define NOTE_C7 2100
#define COMMAND_ALARM 0x9 #define ALARM_TYPE_TILT_SWITCH 0x1 #define ALARM_OFF 0x0 #define ALARM_ON 0x1
int tiltSwitchValue; int buttonValue; int ledBrightness; int fadeSteps = 5;
boolean alarm = false;
AndroidAccessory acc("Manufacturer", "Model", "Description",
"Version", "URI", "Serial"); byte sntmsg[3];
void setup() { Serial.begin(19200); acc.powerOn(); sntmsg[0] = COMMAND_ALARM; sntmsg[1] = ALARM_TYPE_TILT_SWITCH; }
void loop() { acc.isConnected(); tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN); if((tiltSwitchValue == LOW) && !alarm) { startAlarm(); } buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) { stopAlarm(); } if(alarm) { fadeLED(); } delay(10); }
void startAlarm() { alarm = true; tone(PIEZO_OUTPUT_PIN, NOTE_C7); ledBrightness = 0; //inform Android device sntmsg[2] = ALARM_ON; sendAlarmStateMessage(); }
void stopAlarm() { alarm = false; //turn off piezo buzzer noTone(PIEZO_OUTPUT_PIN); //turn off LED digitalWrite(LED_OUTPUT_PIN, LOW); //inform Android device sntmsg[2] = ALARM_OFF; sendAlarmStateMessage(); }
void sendAlarmStateMessage() {
if (acc.isConnected()) {
acc.write(sntmsg, 3);
}
} void fadeLED() {
analogWrite(LED_OUTPUT_PIN, ledBrightness);
//increase or decrease brightness
ledBrightness = ledBrightness + fadeSteps;
//change fade direction when reaching max or min of analog values
if (ledBrightness < 0 || ledBrightness > 255) {
fadeSteps = -fadeSteps ;
}
}`
首先让我们看看变量的定义和声明。如果您按照我在项目硬件设置一节中描述的那样连接了所有输入和输出组件,那么您的引脚定义应该如下所示。
#define LED_OUTPUT_PIN 2 #define PIEZO_OUTPUT_PIN 3 #define BUTTON_INPUT_PIN 4 #define TILT_SWITCH_INPUT_PIN 5
你看到的下一个定义是警报发生时压电蜂鸣器应该产生的高音频率。2100 Hz 的频率定义了音符 *C7,*这是大多数音乐键盘上的最高音符,除了古典 88 键钢琴。音符提供了完美的高音,人耳比任何低音都能听得更清楚。这就是为什么像火警这样的系统使用高音报警声的原因。
#define NOTE_C7 2100
接下来,您将看到通常的数据消息字节定义。为报警命令选择了一个新的字节值,一个类型字节定义了本项目中用于触发报警的倾斜开关。如果您打算以后在报警系统中添加额外的开关或传感器,则定义类型字节。最后两个字节定义仅定义报警是否已触发或是否已关闭。
#define COMMAND_ALARM 0x9 #define ALARM_TYPE_TILT_SWITCH 0x1 #define ALARM_OFF 0x0 #define ALARM_ON 0x1
当您使用digitalRead方法读取按钮或倾斜开关的数字状态时,返回值将是一个int值,稍后可与常量HIGH和LOW进行比较。所以你需要两个变量来存储按钮和倾斜开关的读数。
int tiltSwitchValue; int buttonValue;
请记住,当警报发生时,您希望让 LED 发出脉冲。为此,您需要使用analogWrite方法来调制 LED 的电源电压。analogWrite方法接受从 0 到 255 范围内的值。这就是为什么你将当前亮度值存储为一个int值。当您增加或降低 LED 的亮度时,您可以定义渐变过程的步长值。步长值越低,LED 的衰减越平滑越慢,因为达到analogWrite范围的最大值或最小值需要更多的循环周期。
int ledBrightness; int fadeSteps = 5;
最后一个新变量是一个boolean标志,它仅存储报警系统的当前状态,以确定报警当前是激活还是关闭。它在开始时被初始化为关闭状态。
boolean alarm = false;
变量就是这样。除了用新的命令字节和类型字节填充数据消息的前两个字节之外,setup方法没有任何新的功能。有趣的部分是loop方法。
void loop() { acc.isConnected(); tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN); if((tiltSwitchValue == LOW) && !alarm) { startAlarm(); } buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) { stopAlarm(); } if(alarm) { fadeLED(); } delay(10); }
在之前的项目中,循环方法中的代码被 if 子句包围,该子句检查 Android 设备是否连接,然后才执行程序逻辑。由于这是一个报警系统,所以我认为即使没有连接 Android 设备,也最好让它至少在 Arduino 端工作。在循环开始时调用isConnected方法的原因是,该方法中的逻辑确定设备是否连接,并向 Android 设备发送消息,以便启动相应的应用。循环逻辑的其余部分非常简单。首先,你读倾斜开关的状态。
tiltSwitchValue = digitalRead(TILT_SWITCH_INPUT_PIN);
如果倾斜开关闭合其电路,数字状态将为LOW,因为它在闭合时接地。只有到那时,如果闹钟还没有打开,你会希望闹钟启动。稍后将解释startAlarm方法的实现。
if((tiltSwitchValue == LOW) && !alarm) { startAlarm(); }
按钮被按下时的代码正好相反。它应该停止报警并重置报警系统,以便能够再次被激活。本章后面还将描述stopAlarm方法的实现。
buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) { stopAlarm(); }
如果系统当前处于警报状态,您需要淡化 LED 以显示警报。接下来是fadeLED方法的实现。
if(alarm) { fadeLED(); }
现在让我们看看从startAlarm方法开始的其他方法实现。
void startAlarm() { alarm = true; tone(PIEZO_OUTPUT_PIN, NOTE_C7); ledBrightness = 0; //inform Android device sntmsg[2] = ALARM_ON; sendAlarmStateMessage(); }
如您所见,alarm标志已经被设置为true,这样该方法就不会在下一个循环中被意外调用。在《??》第五章中已经使用了tone方法。这里,它用于在压电蜂鸣器上产生音符 C7 。当警报启动时,需要重置ledBrightness变量,以启动 LED 从暗到亮的淡入淡出。最后,用于描述警报被触发的消息字节被设置在数据消息上,并且如果 Android 设备被连接,则该消息被发送到 Android 设备。
接下来是对比法stopAlarm。
void stopAlarm() { alarm = false; //turn off piezo buzzer noTone(PIEZO_OUTPUT_PIN); //turn off LED digitalWrite(LED_OUTPUT_PIN, LOW); //inform Android device sntmsg[2] = ALARM_OFF; sendAlarmStateMessage(); }
首先,您将报警标志设置为false以允许再次触发报警。然后,您需要通过调用noTone方法来关闭压电蜂鸣器。它停止向压电蜂鸣器输出电压,使其不再振荡。通过调用digitalWrite方法并将其设置为LOW (0V)来关闭 LED。这里的最后一步也是设置相应的消息字节,如果 Android 设备已连接,则向其发送停止消息。
sendAlarmStateMessage方法只是检查是否连接了 Android 设备,如果连接了,则使用Accessory对象的write方法传输三字节消息。
void sendAlarmStateMessage() { if (acc.isConnected()) { acc.write(sntmsg, 3); } }
最后一个方法实现是 LED 淡入淡出的逻辑。
void fadeLED() { analogWrite(LED_OUTPUT_PIN, ledBrightness); //increase or decrease brightness ledBrightness = ledBrightness + fadeSteps; //change fade direction when reaching max or min of analog values if (ledBrightness < 0 || ledBrightness > 255) { fadeSteps = -fadeSteps ; } }
为了给 LED 提供不同的电压等级,这里必须使用analogWrite方法和当前亮度值。在每个循环周期中,当系统设置为报警模式时,调用fadeLED方法。要改变 LED 的亮度等级,您必须将当前的ledBrightness值加上fadeSteps值。如果您碰巧超过了 0 到 255 的可能的analogWrite限制,您需要否定fadeSteps值的符号。值 5 将变成-5,而不是在下一个循环中增加亮度值,而是现在减小它,将 LED 调暗到更暗的亮度水平。
这就是软件的 Arduino 部分。如果你现在运行你的草图,你实际上已经有了一个功能报警系统。不过,您会希望实现 Android 应用,以便通过使用 Android 设备作为短信和存储信息的网关,让您的警报系统变得更加强大。
Android 应用
Android 软件部分将向您展示如何使用 Android 设备的存储能力将日志文件写入设备的文件系统。当警报发生时,连接的 Android 设备收到触发消息,它会将消息和时间戳写入应用的存储文件夹,供以后检查。此外,如果您使用支持电话功能的设备,如 Android 手机,该设备将向预定义的号码发送短信,以远程通知警报。为了直观显示警报已经发生,屏幕的背景颜色将变为红色,并显示一条警报消息。如果在 ADK 板上重置警报,相应的消息将被发送到 Android 设备,应用也将被重置,以再次启用警报系统。
项目 12 活动 Java 文件
在我进入细节之前,看一下完整的清单 10-2 。
清单 10-2。项目 12:ProjectTwelveActivity.java
`package project.twelve.adk;
import …;
public class ProjectTwelveActivity extends Activity {
…
private PendingIntent smsSentIntent; private PendingIntent logFileWrittenIntent;
private static final byte COMMAND_ALARM = 0x9;
private static final byte ALARM_TYPE_TILT_SWITCH = 0x1;
private static final byte ALARM_OFF = 0x0; private static final byte ALARM_ON = 0x1;
private static final String SMS_DESTINATION = "put_telephone_number_here"; private static final String SMS_SENT_ACTION = "SMS_SENT"; private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";
private PackageManager packageManager; boolean hasTelephony;
private TextView alarmTextView; private TextView smsTextView; private TextView logTextView; private LinearLayout linearLayout;
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
mUsbManager = UsbManager.getInstance(this); mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent( ACTION_USB_PERMISSION), 0); smsSentIntent = PendingIntent.getBroadcast(this, 0, new Intent( SMS_SENT_ACTION), 0); logFileWrittenIntent = PendingIntent.getBroadcast(this, 0, new Intent( LOG_FILE_WRITTEN_ACTION), 0); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); filter.addAction(SMS_SENT_ACTION); filter.addAction(LOG_FILE_WRITTEN_ACTION); registerReceiver(broadcastReceiver, filter);
packageManager = getPackageManager(); hasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); alarmTextView = (TextView) findViewById(R.id.alarm_text); smsTextView = (TextView) findViewById(R.id.sms_text); logTextView = (TextView) findViewById(R.id.log_text); }
/**
- Called when the activity is resumed from its paused state and immediately
- after onCreate().
/
@Override
public void onResume() {
super.onResume();
…
}
/* Called when the activity is paused by the system. */ @Override public void onPause() { super.onPause(); closeAccessory(); }
/**
- Called when the activity is no longer needed prior to being removed from
- the activity stack. */ @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(broadcastReceiver); }
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { openAccessory(accessory); } else { Log.d(TAG, "permission denied for accessory " + accessory); } mPermissionRequestPending = false; } } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (accessory != null && accessory.equals(mAccessory)) { closeAccessory(); } } else if (SMS_SENT_ACTION.equals(action)) { smsTextView.setText(R.string.sms_sent_message); } else if (LOG_FILE_WRITTEN_ACTION.equals(action)) { logTextView.setText(R.string.log_written_message); } } };
private void openAccessory(UsbAccessory accessory) { … }
private void closeAccessory() {
…
} Runnable commRunnable = new Runnable() {
@Override public void run() { int ret = 0; byte[] buffer = new byte[3];
while (ret >= 0) { try { ret = mInputStream.read(buffer); } catch (IOException e) { Log.e(TAG, "IOException", e); break; }
switch (buffer[0]) { case COMMAND_ALARM:
if (buffer[1] == ALARM_TYPE_TILT_SWITCH) { final byte alarmState = buffer[2]; final String alarmMessage = getString(R.string.alarm_message, getString(R.string.alarm_type_tilt_switch)); runOnUiThread(new Runnable() {
@Override public void run() { if(alarmState == ALARM_ON) { linearLayout.setBackgroundColor(Color.RED); alarmTextView.setText(alarmMessage); } else if(alarmState == ALARM_OFF) { linearLayout.setBackgroundColor(Color.WHITE); alarmTextView.setText(R.string.alarm_reset_message); smsTextView.setText(""); logTextView.setText(""); } } }); if(alarmState == ALARM_ON) { sendSMS(alarmMessage); writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString()); } } break;
default:
Log.d(TAG, "unknown msg: " + buffer[0]);
break;
}
} }
};
private void sendSMS(String smsText) { if(hasTelephony) { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(SMS_DESTINATION, null, smsText, smsSentIntent, null); } }
private void writeToLogFile(String logMessage) { File logDirectory = getExternalLogFilesDir(); if(logDirectory != null) { File logFile = new File(logDirectory, "ProjectTwelveLog.txt"); if(!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { Log.d(TAG, "Log File could not be created.", e); } } BufferedWriter bufferedWriter = null; try { bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); bufferedWriter.write(logMessage); bufferedWriter.newLine(); Log.d(TAG, "Written message to file: " + logFile.toURI()); logFileWrittenIntent.send(); } catch (IOException e) { Log.d(TAG, "Could not write to Log File.", e); } catch (CanceledException e) { Log.d(TAG, "LogFileWrittenIntent was cancelled.", e); } finally { if(bufferedWriter != null) { try { bufferedWriter.close(); } catch (IOException e) { Log.d(TAG, "Could not close Log File.", e); } } } } }
private File getExternalLogFilesDir() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return getExternalFilesDir(null);
} else {
return null;
} }
}`
正如您在浏览代码时可能已经看到的,您有多个 UI 元素来显示一些文本。所以在开始研究代码之前,先看看布局和文本是如何定义的。
XML 资源定义
main.xml布局文件包含三个用LinearLayout包装的TextView。TextView只是稍后显示通知消息。LinearLayout负责改变背景颜色。您还可以看到,短信和文件通知TextView有一个绿色定义(#00FF00),这样当背景变成红色时,它们会有更好的对比度。
***清单 10-3。*项目 12: main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> <TextView android:id="@+id/alarm_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000" android:text="@string/alarm_reset_message"/> <TextView android:id="@+id/sms_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> <TextView android:id="@+id/log_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> </LinearLayout>
布局中引用的文本,以及一旦触发警报时应显示的警报消息文本,在strings.xml文件中定义,如清单 10-4 所示。
***清单 10-4。*项目 12: strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <string name="app_name">ProjectTwelve</string> <string name="alarm_message">%1$s triggered an alarm!</string> <string name="alarm_reset_message">Alarm system is reset and active!</string> <string name="alarm_type_tilt_switch">Tilt switch</string> <string name="sms_sent_message">SMS has been sent.</string> <string name="log_written_message">Log has been written.</string> </resources>
变量声明和定义
现在让我们详细谈谈来自清单 10-2 的实际代码。首先是变量。您可以看到两个额外的PendingIntent。这对于稍后通知活动相应的事件已经发生以更新相应的TextView是必要的。
private PendingIntent smsSentIntent; private PendingIntent logFileWrittenIntent;
然后是通常的消息数据字节,如 Arduino 草图中所定义的。
private static final byte COMMAND_ALARM = 0x9; private static final byte ALARM_TYPE_TILT_SWITCH = 0x1; private static final byte ALARM_OFF = 0x0; private static final byte ALARM_ON = 0x1;
接下来,您会看到一些字符串定义。SMS_DESTINATION是您要发送通知短信的目的地电话号码。你得把这个换成一个真实的电话号码。然后你有两个动作字符串,当它们广播它们相应的事件已经发生时,这两个字符串被用来标识PendingIntent。
private static final String SMS_DESTINATION = "put_telephone_number_here"; private static final String SMS_SENT_ACTION = "SMS_SENT"; private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";
为了确定您的 Android 设备是否支持电话功能,您需要访问PackageManager。boolean标志hasTelephony用于存储支持电话的信息,这样你就不用每次都去查找了。
private PackageManager packageManager; private boolean hasTelephony;
在全局变量部分的末尾,您可以看到 UI 元素的LinearLayout和TextView声明,它们用于给出可视的报警反馈。
private TextView alarmTextView; private TextView smsTextView; private TextView logTextView; private LinearLayout linearLayout;
生命周期方法
变量就是这样。现在我们来看看onCreate方法。除了用于授予 USB 权限的PendingIntent之外,您还定义了两个新的PendingIntent,它们将用于广播它们的特定事件,将日志文件写入文件系统或发送 SMS,以便 UI 可以相应地更新。
smsSentIntent = PendingIntent.getBroadcast(this, 0, new Intent(SMS_SENT_ACTION), 0); logFileWrittenIntent = PendingIntent.getBroadcast( this, 0, new Intent(LOG_FILE_WRITTEN_ACTION), 0);
你可以看到它们定义了一个广播,其中特定事件的动作字符串常量被用来初始化它们的Intent。为了在稍后BroadcastReceiver处理广播时过滤这些意图,您需要向IntentFilter添加相应的动作,该动作与BroadcastReceiver一起在系统中注册。
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); filter.addAction(SMS_SENT_ACTION); filter.addAction(LOG_FILE_WRITTEN_ACTION); registerReceiver(broadcastReceiver, filter);
onCreate方法中的下一件重要事情是获取对PackageManager的引用,以确定您的 Android 设备是否支持电话功能。
packageManager = getPackageManager(); hasTelephony = packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY);
PackageManager是一个系统工具类,用于解析全局包信息。您可以检查您的设备是否支持某些功能,或者设备上是否安装了某些应用,或者您可以访问自己的应用信息。在这个项目中,我们只对电话支持感兴趣,所以您调用带有常量FEATURE_TELEPHONY的hasSystemFeature方法。
方法的最后一部分定义了通常的 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 子句来评估触发广播的操作。
else if (SMS_SENT_ACTION.equals(action)) { smsTextView.setText(R.string.sms_sent_message); } else if (LOG_FILE_WRITTEN_ACTION.equals(action)) { logTextView.setText(R.string.log_written_message); }
根据已经触发广播的动作,相应的TextView被更新。BroadcastReceiver的onReceive方法运行在 UI 线程上,所以在这里更新 UI 是安全的。
可运行的实现
下一个有趣的部分是Runnable实现中的警报消息评估。
final byte alarmState = buffer[2]; final String alarmMessage = getString(R.string.alarm_message, getString(R.string.alarm_type_tilt_switch)); runOnUiThread(new Runnable() { @Override public void run() { if(alarmState == ALARM_ON) { linearLayout.setBackgroundColor(Color.RED); alarmTextView.setText(alarmMessage); } else if(alarmState == ALARM_OFF) { linearLayout.setBackgroundColor(Color.WHITE); alarmTextView.setText(R.string.alarm_reset_message); smsTextView.setText(""); logTextView.setText(""); } } }); if(alarmState == ALARM_ON) { sendSMS(alarmMessage); writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString()); }
检查报警系统的当前状态后,可以在runOnUiThread方法中更新相应的TextView s。如果报警被触发,将LinearLayout的背景颜色设置为红色,并将报警文本设置在alarmTextView上。如果警报被取消并重置,您将LinearLayout的背景颜色设置回白色,用文本更新alarmTextView以告知用户系统再次重置,并清除通知 SMS 和日志文件事件的TextView。根据警报的当前条件更新用户界面后,如果警报已被触发,您可以继续发送短信和写入日志文件。该实现封装在单独的方法中,我们将在接下来看到。
发送文字信息(SMS)
首先让我们看看如何在 Android 中发送短信。
private void sendSMS(String smsText) { if(hasTelephony) { SmsManager smsManager = SmsManager.getDefault(); smsManager.sendTextMessage(SMS_DESTINATION, null, smsText, smsSentIntent, null); } }
这里,您在开始时设置的boolean标志用于检查连接的 Android 设备是否能够发送短信。如果不是,调用代码就没有任何意义了。为了能够发送 SMS,您需要首先获得对系统的SmsManager的引用。SmsManager类提供了一个方便的静态方法来获得系统的默认SmsManager实现。一旦你有了对SmsManager的引用,你就可以调用sendTextMessage方法,它需要几个参数。首先,你必须提供短信目的地号码。然后你可以提供一个服务中心地址。通常,您可以使用 null,以便使用默认的服务中心。第三个参数是您想要通过 SMS 发送的实际消息。最后两个参数是PendingIntent s,您可以提供在发送短信和收到短信时得到通知。您已经定义了PendingIntent来通知短信已经发送,所以您将在这里使用它来通知短信的发送。一旦发生这种情况,就会通知BroadcastReceiver相应地更新 UI。
将日志文件写入文件系统
顾名思义,writeToLogFile方法负责将日志文件写入设备文件系统上应用的存储目录。
private void writeToLogFile(String logMessage) { File logDirectory = getExternalLogFilesDir(); if(logDirectory != null) { File logFile = new File(logDirectory, "ProjectTwelveLog.txt"); if(!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { Log.d(TAG, "Log File could not be created.", e); } } BufferedWriter bufferedWriter = null; try { bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); bufferedWriter.write(logMessage); bufferedWriter.newLine(); Log.d(TAG, "Written message to file: " + logFile.toURI()); logFileWrittenIntent.send(); } catch (IOException e) { Log.d(TAG, "Could not write to Log File.", e); } catch (CanceledException e) { Log.d(TAG, "LogFileWrittenIntent was cancelled.", e); } finally { if(bufferedWriter != null) { try { bufferedWriter.close(); } catch (IOException e) { Log.d(TAG, "Could not close Log File.", e); } } } } }
在写入 Android 设备的外部存储器之前,您需要检查该存储器是否安装在 Android 系统中,并且当前未被其他系统使用,例如,当连接到计算机以传输文件时。为此,您可以使用另一种方法来检查外部存储的当前状态,并返回应用文件存储目录的路径。
private File getExternalLogFilesDir() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return getExternalFilesDir(null); } else { return null; } }
如果该方法返回有效的目录路径,您可以通过提供目录和文件名来创建一个File对象。
File logFile = new File(logDirectory, "ProjectTwelveLog.txt");
如果目录中不存在该文件,您应该先创建它。
if(!logFile.exists()) { try { logFile.createNewFile(); } catch (IOException e) { Log.d(TAG, "Log File could not be created.", e); } }
为了写入文件本身,您将创建一个BufferedWriter对象,它将一个FileWriter对象作为参数。通过提供对File对象的引用和一个boolean标志来创建FileWriter。boolean标志定义要写入的文本是否应该附加到文件中,或者文件是否应该被覆盖。如果你想添加文本,你应该使用boolean标志true。
bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); bufferedWriter.write(logMessage); bufferedWriter.newLine();
如果您完成了对文件的写入,您可以通过调用logFileWrittenIntent对象上的send方法来触发相应的广播。
logFileWrittenIntent.send();
总是关闭打开的连接以释放内存和文件句柄是很重要的,所以不要忘记在 finally 块中的BufferedWriter对象上调用close方法。这将关闭所有底层打开的连接。
bufferedWriter.close();
权限
java 编码部分到此为止。但是,如果您现在运行应用,当试图发送 SMS 或写入文件系统时,它会崩溃。这是因为这些任务需要特殊权限。您需要将android.permission.SEND_SMS权限和android.permission.WRITE_EXTERNAL_STORAGE权限添加到您的AndroidManifest.xml中。看看清单 10-5 中的是如何做到的。
***清单 10-5。*项目 12: AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" **package="project.twelve.adk"** android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <uses-feature android:name="android.hardware.usb.accessory" /> **<uses-permission android:name="android.permission.SEND_SMS" />** **<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />** `
<activity android:name=".ProjectTwelveActivity"
android:label="@string/app_name" android:screenOrientation="portrait">
`
最终结果
Android 应用现在可以部署到设备上了。也上传 Arduino 草图,连接两个设备,并查看您的报警系统的运行情况。如果你正确地设置了所有的东西,并把你的倾斜开关倾斜到一个垂直的位置,你应该得到一个看起来像图 10-10 的结果。
***图 10-10。*项目 12:最终结果
项目 13:带红外线光栅的摄像机报警系统
最终项目将是一个摄像头报警系统,当警报触发时,它能够快速拍摄入侵者的照片。硬件将或多或少与前一个项目相同。唯一的区别是,你将使用红外光栅触发警报,而不是倾斜开关。拍照后,照片将与记录警报事件的日志文件一起保存在应用的外部存储中。
零件
为了将 IR 挡光板集成到您之前项目的硬件设置中,您需要一些额外的部件。除了您已经使用的部件,您还需要一个额外的 220ω电阻、一个红外发射器或红外 LED 以及一个红外探测器。完整的零件清单如下所示(参见图 10-11 ):
- ADK 董事会
- 试验板
- 2×220ω电阻
- 2 X10 kω电阻
- 一些电线
- 纽扣
- 红外挡光板(红外发射器、红外探测器)
- 工作电压为 5V 的 LED
- 压电蜂鸣器
***图 10-11。*项目 13 个部分(ADK 板、试验板、电线、电阻器、按钮、红外发射器(透明)、红外探测器(黑色)、压电蜂鸣器、红色 LED)
ADK 棋盘
红外线光栅电路将连接到 ADK 板的模拟输入端。模拟输入引脚将用于检测测量电压电平的突然变化。当 IR 检测器暴露于红外光波长的光时,所连接的模拟输入引脚上测得的输入电压将非常低。如果红外线照射中断,测得的电压将显著增加。
红外线光栅
你将在这个项目中建立的红外光栅将由两部分组成,一个红外发射器和一个红外探测器。发射器通常是普通的红外发光二极管,它发射波长约为 940 纳米的红外光。你会发现不同形式的红外发光二极管。单个红外 LED 的通常形式是标准的灯泡形 LED,但也可以发现类似晶体管形状的 LED。两者都显示在图 10-12 中。
***图 10-12。*灯泡形红外发光二极管(左),晶体管形红外发光二极管(右)
红外探测器通常是一个只有一个集电极和一个发射极连接器的两条腿光电晶体管。通常这两种元件作为匹配对出售,以创建 ir 光屏障电路。这种匹配装置通常以晶体管形状出售。(参见图 10-13 。)
***图 10-13。*配套 TEMIC K153P 中的红外探测器(左)和发射器(右)
匹配对组的优点是两个元件在光学和电学上匹配,以提供最佳的兼容性。我在这个项目中使用的红外发射器和探测器被称为 TEMIC K153P。你不必使用完全相同的一套。所有你需要的是一个红外 LED 和一个光电晶体管,你会达到同样的最终结果。您可能只需在稍后的代码中调整 IR 光栅电路中的电阻或警报触发阈值。红外光栅的典型电路如图图 10-14 所示。
***图 10-14。*普通红外线光栅电路
如前所述,其工作原理是,如果检测器(光电晶体管)暴露在红外光下,输出电压会降低。因此,如果您将手指或任何其他物体放在发射器和检测器之间,检测器对 IR 光的暴露就会中断,输出电压就会增加。一旦达到自定义的阈值,您可以触发警报。
设置
对于这个项目的设置,您基本上只需将您的倾斜开关电路与之前的项目断开,并将其替换为图 10-15 所示的 IR 电路。
***图 10-15。*项目 13:红外线光栅电路设置
如您所见,红外 LED(发射器)像普通 LED 一样连接。只需将+5V 电压连接到 220ω电阻的一根引线上,并将电阻的另一根引线连接到 IR LED 的正极引线上。红外 LED 的负极引线接地(GND)。红外光电晶体管的发射极引线接地(GND)。集电极引线必须通过一个 10k 电阻连接到+5V,此外还要连接到模拟输入 A0。如果不确定哪个引脚是哪个引脚,请查看元件的数据手册。
完整的电路设置,结合之前项目中的其他报警系统组件,看起来类似于图 10-16 。
***图 10-16。*项目 13:完成电路设置
软件
Arduino 软件部分只会略有变化。您将读取连接到红外光栅红外探测器的模拟输入引脚的输入值,而不是读取之前连接倾斜开关的数字输入引脚的状态。如果测得的输入值达到预定义的阈值,则会触发警报,并且与之前的项目一样,会发出警报声音,红色 LED 也会淡入淡出。一旦警报发送到连接的 Android 设备,应用的外部存储目录中将存储一个日志文件。如果有摄像头的话,这种设备现在可以拍照,而不是发送短信来通知可能的入侵者。一旦拍摄了照片,它也将与日志文件一起保存在应用的外部存储目录中。
Arduino 草图
正如我刚才描述的,这个项目的 Arduino 草图与项目 12 中使用的非常相似。它只需要一些微小的变化,以符合红外光栅电路。先看看完整的清单 10-6;之后我会解释必要的改变。
***清单 10-6。*项目 13: Arduino 草图
`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>
#define LED_OUTPUT_PIN 2 #define PIEZO_OUTPUT_PIN 3 #define BUTTON_INPUT_PIN 4 #define IR_LIGHT_BARRIER_INPUT_PIN A0
#define IR_LIGHT_BARRIER_THRESHOLD 511 #define NOTE_C7 2100
#define COMMAND_ALARM 0x9 #define ALARM_TYPE_IR_LIGHT_BARRIER 0x2 #define ALARM_OFF 0x0 #define ALARM_ON 0x1
int irLightBarrierValue; int buttonValue; int ledBrightness = 0; int fadeSteps = 5;
boolean alarm = false;
AndroidAccessory acc("Manufacturer", "Model", "Description", "Version", "URI", "Serial");
byte sntmsg[3];
void setup() { Serial.begin(19200); acc.powerOn(); sntmsg[0] = COMMAND_ALARM; sntmsg[1] = ALARM_TYPE_IR_LIGHT_BARRIER; }
void loop() {
acc.isConnected();
irLightBarrierValue = analogRead(IR_LIGHT_BARRIER_INPUT_PIN);
if((irLightBarrierValue > IR_LIGHT_BARRIER_THRESHOLD) && !alarm) {
startAlarm();
}
buttonValue = digitalRead(BUTTON_INPUT_PIN); if((buttonValue == LOW) && alarm) {
stopAlarm();
}
if(alarm) {
fadeLED();
}
delay(10);
}
void startAlarm() { alarm = true; tone(PIEZO_OUTPUT_PIN, NOTE_C7); ledBrightness = 0; //inform Android device sntmsg[2] = ALARM_ON; sendAlarmStateMessage(); }
void stopAlarm() { alarm = false; //turn off piezo buzzer noTone(PIEZO_OUTPUT_PIN); //turn off LED digitalWrite(LED_OUTPUT_PIN, LOW); //inform Android device sntmsg[2] = ALARM_OFF; sendAlarmStateMessage(); }
void sendAlarmStateMessage() { if (acc.isConnected()) { acc.write(sntmsg, 3); } }
void fadeLED() { analogWrite(LED_OUTPUT_PIN, ledBrightness); //increase or decrease brightness ledBrightness = ledBrightness + fadeSteps; //change fade direction when reaching max or min of analog values if (ledBrightness < 0 || ledBrightness > 255) { fadeSteps = -fadeSteps ; } }`
您可以看到,红外光栅的模拟引脚定义已经取代了倾斜开关引脚定义。
#define IR_LIGHT_BARRIER_INPUT_PIN A0
下一个新定义是红外光栅上电压变化的阈值。当红外探测器暴露在红外发射器下时,测得的电压输出非常低。读取的 ADC 值通常在低位两位数范围内。一旦 IR 曝光中断,电压输出会显著增加。现在,读取的 ADC 值通常在接近最大 ADC 值 1023 的范围内。介于 0 和 1023 之间的值是触发警报的理想阈值。如果您希望您的警报触发器仅对红外照明的微小变化做出更快的响应,您应该降低阈值。不过,值 511 是一个好的开始。
#define IR_LIGHT_BARRIER_THRESHOLD 511
要在引脚 A0 上存储 IR 光栅的读取 ADC 值,只需使用一个整数变量。
int irLightBarrierValue;
剩下的代码非常简单,在项目 12 中已经很熟悉了。在环路方法中,您需要做的唯一一件新事情是读取 IR 光栅模拟输入引脚上的 ADC 值,并检查它是否超过预定义的阈值。如果有,并且之前没有触发警报,您可以启动警报程序。
irLightBarrierValue = analogRead(IR_LIGHT_BARRIER_INPUT_PIN); if((irLightBarrierValue > IR_LIGHT_BARRIER_THRESHOLD) && !alarm) { startAlarm(); }
这并不难,是吗?让我们来看看在你的报警系统的 Android 软件方面你必须做些什么。
Android 应用
一旦 Android 应用接收到表示警报已经发生的数据消息,它将通过视觉方式通知用户该警报,并在应用的外部存储目录中另外写入一个日志文件。为了能够识别可能触发警报的入侵者,如果设备有内置摄像头,Android 应用将利用 Android camera API 来拍照。如果前置摄像头存在,它将是首选摄像头。如果设备只有一个后置摄像头,将使用这个摄像头。
为了在最后一个项目中提供一个更好的概述,我将清单分开来单独讨论。
变量和生命周期方法
在我进入细节之前,请看一下清单 10-7 。
清单 10-7。【项目 13:ProjectThirteenActivity.java(第一部分)
`package project.thirteen.adk;
import …;
public class ProjectThirteenActivity extends Activity {
…
private PendingIntent photoTakenIntent;
private PendingIntent logFileWrittenIntent; private static final byte COMMAND_ALARM = 0x9;
private static final byte ALARM_TYPE_IR_LIGHT_BARRIER = 0x2;
private static final byte ALARM_OFF = 0x0;
private static final byte ALARM_ON = 0x1;
private static final String PHOTO_TAKEN_ACTION = "PHOTO_TAKEN"; private static final String LOG_FILE_WRITTEN_ACTION = "LOG_FILE_WRITTEN";
private PackageManager packageManager; private boolean hasFrontCamera; private boolean hasBackCamera;
private Camera camera; private SurfaceView surfaceView;
private TextView alarmTextView; private TextView photoTakenTextView; private TextView logTextView; private LinearLayout linearLayout; private FrameLayout frameLayout;
/** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
mUsbManager = UsbManager.getInstance(this); mPermissionIntent = PendingIntent.getBroadcast(this, 0, new Intent( ACTION_USB_PERMISSION), 0); photoTakenIntent = PendingIntent.getBroadcast(this, 0, new Intent( PHOTO_TAKEN_ACTION), 0); logFileWrittenIntent = PendingIntent.getBroadcast(this, 0, new Intent( LOG_FILE_WRITTEN_ACTION), 0); IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); filter.addAction(PHOTO_TAKEN_ACTION); filter.addAction(LOG_FILE_WRITTEN_ACTION); registerReceiver(broadcastReceiver, filter);
packageManager = getPackageManager(); hasFrontCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); hasBackCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA);
setContentView(R.layout.main);
linearLayout = (LinearLayout) findViewById(R.id.linear_layout);
frameLayout = (FrameLayout) findViewById(R.id.camera_preview);
alarmTextView = (TextView) findViewById(R.id.alarm_text);
photoTakenTextView = (TextView) findViewById(R.id.photo_taken_text);
logTextView = (TextView) findViewById(R.id.log_text);
} /**
- Called when the activity is resumed from its paused state and immediately
- after onCreate(). */ @Override public void onResume() { super.onResume();
camera = getCamera(); dummySurfaceView = new CameraPreview(this, camera); frameLayout.addView(dummySurfaceView);
… }
/** Called when the activity is paused by the system. */ @Override public void onPause() { super.onPause(); closeAccessory(); if(camera != null) { camera.release(); camera = null; frameLayout.removeAllViews(); } }
/**
- Called when the activity is no longer needed prior to being removed from
- the activity stack. */ @Override public void onDestroy() { super.onDestroy(); unregisterReceiver(broadcastReceiver); }
…`
ProjectThirteenActivity的第一部分显示了最终项目需要调整的初始化和生命周期方法。让我们快速浏览一下变量声明和定义。您可以看到,您再次使用PendingIntent s 用于通知目的。您将在日志文件写入事件和相机拍照事件中使用它们。
private PendingIntent photoTakenIntent; private PendingIntent logFileWrittenIntent;
接下来,您会看到与 Arduino 草图中使用的相同的警报类型字节标识符,用于将 IR 光栅识别为警报的触发源。
private static final byte ALARM_TYPE_IR_LIGHT_BARRIER = 0x2;
您还必须定义一个新的动作常量来标识稍后照片事件的广播。
private static final String PHOTO_TAKEN_ACTION = "PHOTO_TAKEN";
在这个项目中再次使用PackageManager来确定设备是否有前置摄像头和后置摄像头。
private PackageManager packageManager; private boolean hasFrontCamera; private boolean hasBackCamera;
您还将持有对设备摄像头的引用,因为您需要调用Camera对象本身的某些生命周期方法来拍照。SurfaceView是一个特殊的View元素,它会在你拍照前显示当前的相机预览。
private Camera camera; private SurfaceView surfaceView;
您可能还注意到有两个新的 UI 元素。一个是TextView,显示一段文字,表明照片已经被拍摄。第二个是一个FrameLayout View容器。这种容器用于将多个View叠加在一起以达到叠加效果。
private TextView photoTakenTextView; private FrameLayout frameLayout;
现在让我们看看在ProjectThirteenActivity的生命周期方法中你必须做什么。在onCreate方法中,你可以进行通常的初始化。同样,您必须为照片事件定义新的Pendingintent,并在IntentFilter注册广播动作。
photoTakenIntent = PendingIntent.getBroadcast(this, 0, new Intent(PHOTO_TAKEN_ACTION), 0); filter.addAction(PHOTO_TAKEN_ACTION);
再次使用PackageManager来检查设备特性。只是这次你要检查设备上的前置摄像头和后置摄像头。
hasFrontCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FRONT); hasBackCamera = packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA);
最后一步是通常的 UI 初始化。
setContentView(R.layout.main); linearLayout = (LinearLayout) findViewById(R.id.linear_layout); frameLayout = (FrameLayout) findViewById(R.id.camera_preview); alarmTextView = (TextView) findViewById(R.id.alarm_text); photoTakenTextView = (TextView) findViewById(R.id.photo_taken_text); logTextView = (TextView) findViewById(R.id.log_text);
这些是在创建Activity时必需的步骤。当应用暂停和恢复时,您还必须注意某些事情。当应用恢复运行时,您必须获取设备摄像头的引用。您还需要准备一个类型为SurfaceView的预览View元素,这样设备就可以呈现当前的摄像机预览并显示给用户。这个预览SurfaceView然后被添加到你的FrameLayout容器中显示。关于SurfaceView的实现细节,以及如何获得实际的摄像机参考,将在后面显示。
camera = getCamera(); dummySurfaceView = new CameraPreview(this, camera); frameLayout.addView(dummySurfaceView);
分别需要在应用暂停时释放资源。文档指出,您应该释放相机本身的句柄,以便其他应用能够使用相机。此外,您应该从FrameLayout中移除SurfaceView,这样当应用再次恢复时,容器中只存在一个新创建的SurfaceView。
if(camera != null) { camera.release(); camera = null; frameLayout.removeAllViews(); }
这就是生命周期方法。
XML 资源定义
您看到您需要再次定义一个新的布局和一些新的文本,如清单 10-8 所示。
***清单 10-8。*项目 13: main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/linear_layout" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:gravity="center" android:background="#FFFFFF"> **<FrameLayout android:id="@+id/camera_preview"** **android:layout_width="fill_parent"** **android:layout_height="fill_parent"** **android:layout_weight="1"/>** <TextView android:id="@+id/alarm_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000000" android:text="@string/alarm_reset_message"/> **<TextView android:id="@+id/photo_taken_text"** **android:layout_width="wrap_content"** **android:layout_height="wrap_content"** **android:textColor="#00FF00"/>** <TextView android:id="@+id/log_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#00FF00"/> </LinearLayout>
参考文本在strings.xml文件中定义,如清单 10-9 所示。
***清单 10-9。*项目 13: strings.xml
<?xml version="1.0" encoding="utf-8"?> <resources> **<string name="app_name">ProjectThirteen</string>** <string name="alarm_message">%1$s triggered an alarm!</string> <string name="alarm_reset_message">Alarm system is reset and active!</string> **<string name="alarm_type_ir_light_barrier">IR Light Barrier</string>** **<string name="photo_taken_message">Photo has been taken.</string>** <string name="log_written_message">Log has been written.</string> </resources>
BroadcastReceiver 和 Runnable 实现
现在让我们看看处理通信部分的BroadcastReceiver和Runnable实现(清单 10-10 )。
清单 10-10。【项目 13:ProjectThirteenActivity.java(第二部分)
`…
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (ACTION_USB_PERMISSION.equals(action)) { synchronized (this) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { openAccessory(accessory); } else { Log.d(TAG, "permission denied for accessory " + accessory); } mPermissionRequestPending = false; } } else if (UsbManager.ACTION_USB_ACCESSORY_DETACHED.equals(action)) { UsbAccessory accessory = UsbManager.getAccessory(intent); if (accessory != null && accessory.equals(mAccessory)) { closeAccessory(); } } else if (PHOTO_TAKEN_ACTION.equals(action)) { photoTakenTextView.setText(R.string.photo_taken_message); } else if (LOG_FILE_WRITTEN_ACTION.equals(action)) { logTextView.setText(R.string.log_written_message); } } };
private void openAccessory(UsbAccessory accessory) {
… }
private void closeAccessory() { … }
Runnable commRunnable = new Runnable() {
@Override public void run() { int ret = 0; byte[] buffer = new byte[3];
while (ret >= 0) { try { ret = mInputStream.read(buffer); } catch (IOException e) { Log.e(TAG, "IOException", e); break; }
switch (buffer[0]) { case COMMAND_ALARM:
if (buffer[1] == ALARM_TYPE_IR_LIGHT_BARRIER) { final byte alarmState = buffer[2]; final String alarmMessage = getString(R.string.alarm_message, getString(R.string.alarm_type_ir_light_barrier)); runOnUiThread(new Runnable() {
@Override
public void run() {
if(alarmState == ALARM_ON) {
linearLayout.setBackgroundColor(Color.RED);
alarmTextView.setText(alarmMessage);
} else if(alarmState == ALARM_OFF) {
linearLayout.setBackgroundColor(Color.WHITE);
alarmTextView.setText(R.string.alarm_reset_message);
photoTakenTextView.setText("");
logTextView.setText("");
}
}
});
if(alarmState == ALARM_ON) {
takePhoto();
writeToLogFile(new StringBuilder(alarmMessage).append(" - ")
.append(new Date()).toString());
} else if(alarmState == ALARM_OFF){
camera.startPreview();
}
} break;
default: Log.d(TAG, "unknown msg: " + buffer[0]); break; } } } };
…`
一旦接收到相应的广播,只需增强BroadcastReceiver也对照片事件做出反应。您将更新photoTakenTextView以向用户显示已经拍摄了一张照片。
else if (PHOTO_TAKEN_ACTION.equals(action)) { photoTakenTextView.setText(R.string.photo_taken_message); }
Runnable实现评估接收到的消息。确定当前报警状态并设置报警消息后,您可以在runOnUiThread方法中相应地更新 UI 元素。
if(alarmState == ALARM_ON) { linearLayout.setBackgroundColor(Color.RED); alarmTextView.setText(alarmMessage); } else if(alarmState == ALARM_OFF) { linearLayout.setBackgroundColor(Color.WHITE); alarmTextView.setText(R.string.alarm_reset_message); photoTakenTextView.setText(""); logTextView.setText(""); }
在 UI 线程之外,您继续执行拍照和写入文件系统的额外任务。
if(alarmState == ALARM_ON) { takePhoto(); writeToLogFile(new StringBuilder(alarmMessage).append(" - ") .append(new Date()).toString()); } else if(alarmState == ALARM_OFF){ camera.startPreview(); }
这些方法调用不应该在 UI 线程上进行,因为它们处理可能阻塞 UI 本身的 IO 操作。在出现警报的情况下,拍照和写日志文件的实现将在下面的清单中显示。重置警报时,您还必须重置相机的生命周期,并开始新的相机图片预览。注意,在拍照之前必须调用startPreview方法。否则,您的应用将会崩溃。
使用相机
现在让我们看看新的 Android 应用真正有趣的部分:如何用设备的集成摄像头拍照(清单 10-11 )。
清单 10-11。【项目 13:ProjectThirteenActivity.java(第三部分)
`…
private Camera getCamera(){ Camera camera = null; try { if(hasFrontCamera) { int frontCameraId = getFrontCameraId(); if(frontCameraId != -1) { camera = Camera.open(frontCameraId); } } if((camera == null) && hasBackCamera) { camera = Camera.open(); } } catch (Exception e){ Log.d(TAG, "Camera could not be initialized.", e); } return camera; }
private int getFrontCameraId() { int cameraId = -1; int numberOfCameras = Camera.getNumberOfCameras(); for (int i = 0; i < numberOfCameras; i++) { CameraInfo cameraInfo = new CameraInfo(); Camera.getCameraInfo(i, cameraInfo); if (CameraInfo.CAMERA_FACING_FRONT == cameraInfo.facing) { cameraId = i; break; } } return cameraId; }
private void takePhoto() { if(camera != null) { camera.takePicture(null, null, pictureTakenHandler); } }
private PictureCallback pictureTakenHandler = new PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) { writePictureDataToFile(data);
}
};
…`
getCamera方法展示了两种获取设备摄像头引用的方法。Camera类提供了两个静态方法来获取引用。这里显示的第一个方法是open方法,它使用一个int参数通过 id 获取特定的摄像机。
camera = Camera.open(frontCameraId)
第二个 open 方法不带参数,返回设备的默认摄像机引用。这通常是后置摄像头。
camera = Camera.open();
不幸的是,要确定前置摄像头的 id,您必须检查设备提供的每个摄像头,并检查其方向以找到正确的摄像头,如getFrontCameraId方法所示。
takePhoto方法显示了如何指示相机拍照。为此,您调用了camera对象上的takePicture方法。takePicture方法有三个参数。这些参数是回调接口,提供了图片拍摄过程生命周期的挂钩。第一个是类型为ShutterCallback的接口,它在照相机捕捉到图片时被调用。第二个参数是PictureCallback接口,一旦相机准备好未压缩的原始图片数据,就会调用该接口。我只提供最后一个参数,也是一个PictureCallback,一旦当前图片的 jpeg 数据处理完毕,就会调用这个参数。
camera.takePicture(null, null, pictureTakenHandler);
PictureCallback接口的实现相当容易。你只需要实现onPictureTaken方法。
`private PictureCallback pictureTakenHandler = new PictureCallback() {
@Override public void onPictureTaken(byte[] data, Camera camera) { writePictureDataToFile(data); } };`
回调方法以字节数组的形式提供经过处理的 jpeg 数据,这些数据可以写入文件系统上的图片文件。
文件系统操作
文件系统操作如清单 10-12 中的所示。
清单 10-12。【项目 13:ProjectThirteenActivity.java(第四部分)
`…
private void writeToLogFile(String logMessage) {
File logFile = getFile("ProjectThirteenLog.txt"); if(logFile != null) {
BufferedWriter bufferedWriter = null;
try {
bufferedWriter = new BufferedWriter(new FileWriter(logFile, true));
bufferedWriter.write(logMessage);
bufferedWriter.newLine();
Log.d(TAG, "Written message to file: " + logFile.toURI());
logFileWrittenIntent.send();
} catch (IOException e) {
Log.d(TAG, "Could not write to Log File.", e);
} catch (CanceledException e) {
Log.d(TAG, "LogFileWrittenIntent was cancelled.", e);
} finally {
if(bufferedWriter != null) {
try {
bufferedWriter.close();
} catch (IOException e) {
Log.d(TAG, "Could not close Log File.", e);
}
}
}
}
}
private void writePictureDataToFile(byte[] data) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
String currentDateAndTime = dateFormat.format(new Date());
File pictureFile = getFile(currentDateAndTime + ".jpg");
if(pictureFile != null) {
BufferedOutputStream bufferedOutputStream = null;
try {
bufferedOutputStream = new BufferedOutputStream( new FileOutputStream(pictureFile));
bufferedOutputStream.write(data);
Log.d(TAG, "Written picture data to file: " + pictureFile.toURI());
photoTakenIntent.send();
} catch (IOException e) {
Log.d(TAG, "Could not write to Picture File.", e);
} catch (CanceledException e) {
Log.d(TAG, "photoTakenIntent was cancelled.", e);
} finally {
if(bufferedOutputStream != null) {
try {
bufferedOutputStream.close();
} catch (IOException e) {
Log.d(TAG, "Could not close Picture File.", e);
}
}
}
}
} private File getFile(String fileName) {
File file = new File(getExternalDir(), fileName);
if(!file.exists()) {
try {
file.createNewFile();
} catch (IOException e) {
Log.d(TAG, "File could not be created.", e);
}
}
return file;
}
private File getExternalDir() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return getExternalFilesDir(null); } else { return null; } } }`
获取或创建指定的File对象的任务已经被提取到它自己的方法中,称为getFile,以便在写入日志文件或图片文件时可以重用。写日志文件已经在之前的项目中描述过了,所以我将只关注writePictureDataToFile方法,它处理将图片数据写到应用的外部存储目录中的文件。
第一步是创建一个文件来写入数据。使用当前日期和时间作为文件名是一个好主意,这样您就可以很快看到照片是何时拍摄的。
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); String currentDateAndTime = dateFormat.format(new Date()); File pictureFile = getFile(currentDateAndTime + ".jpg");
SimpleDateFormat类是一个工具类,用于将日期表示格式化为特定的形式。假设您的当前日期是 2012 年 12 月 23 日,您的时间是凌晨 1 点。格式化的String表示将是 2012-12-23-01-00-00。现在你只需要添加文件类型 jpeg 的文件结尾并创建你的File对象。
创建的File被提供给由BufferedOutputStream包裹的FileOutputStream,以将图片数据写入File。如果一切顺利,就可以发送描述照片已经拍摄并保存的广播。
bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(pictureFile)); bufferedOutputStream.write(data); Log.d(TAG, "Written picture data to file: " + pictureFile.toURI()); photoTakenIntent.send();
Activity的编码到此为止。
SurfaceView 实现
请记住,您仍然需要实现一个类型为SurfaceView的类,以便可以在您的应用中呈现相机预览。看看清单 10-13 中的,它显示了扩展了SurfaceView类的CameraPreview类。
清单 10-13。项目 13:CameraPreview.java
`package project.thirteen.adk;
import java.io.IOException;
import android.content.Context; import android.hardware.Camera; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView;
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private static final String TAG = CameraPreview.class.getSimpleName(); private SurfaceHolder mHolder; private Camera mCamera;
public CameraPreview(Context context, Camera camera) { super(context); mCamera = camera;
// Add a SurfaceHolder.Callback so we get notified when the // underlying surface is created. mHolder = getHolder(); mHolder.addCallback(this); // deprecated setting, but required on Android versions prior to 3.0 mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); }
public void surfaceCreated(SurfaceHolder holder) { try { mCamera.setPreviewDisplay(holder); mCamera.setDisplayOrientation(90); mCamera.startPreview(); } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } }
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // not implemented }
@Override
public void surfaceDestroyed(SurfaceHolder holder) { // not implemented
}
}`
CameraPreview类只有两个字段:对设备摄像头的引用和一个所谓的SurfaceHolder。SurfaceHolder是一个SurfaceView显示表面的接口,它将显示相机拍摄的预览图片。
private SurfaceHolder mHolder; private Camera mCamera;
在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
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" **package="project.thirteen.adk"** android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10" /> <uses-feature android:name="android.hardware.usb.accessory" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> **<uses-permission android:name="android.permission.CAMERA" />** `
<activity android:name=".ProjectThirteenActivity"
android:label="@string/app_name" android:screenOrientation="portrait">
`
最终结果
现在,您已经为最终的项目测试运行做好了一切准备。将 Arduino 草图上传到 ADK 板上,在您的 Android 设备上部署您的 Android 应用,并查看您新的支持摄像头的报警系统(图 10-17 )。
***图 10-17。*项目 13:最终结果
总结
在这最后一章中,你建立了你自己的警报系统,包括你在整本书中了解的一些部分。你建造了两个版本的警报系统,一个由倾斜开关触发,另一个由自建的红外光栅触发。报警系统借助压电蜂鸣器和红色 LED 发出声音和视觉反馈。一个连接的 Android 设备通过提供发送通知短信或拍摄可能的入侵者的照片的可能性,增强了警报系统。您学习了如何以编程方式发送这些 SMS 消息,以及如何使用相机 API 来指示相机拍照。通过将警报事件保存到日志文件中,您还了解了在 Android 中保存数据的一种方法。