ADK-Arduino-入门指南-二-

177 阅读1小时+

ADK Arduino 入门指南(二)

原文:Beginning Android ADK with Arduino

协议:CC BY-NC-SA 4.0

四、输入

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

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

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

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

项目 3:读取按钮的状态

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

零件

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

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

images

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

按钮或开关

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

images

***图 4-2。*闭路

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

images

***图 4-3。*带开关的电路

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

images

***图 4-4。*按钮和开关

上拉电阻

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

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

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

images

***图 4-5。*上拉电阻电路

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

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

ADK 董事会

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

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

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

设置

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

images

***图 4-6。*项目 3 设置

软件

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

Arduino 草图

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

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

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

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

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

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

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

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

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

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

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

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

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

currentButtonState = digitalRead(INPUT_PIN);

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

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

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

Android 应用

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

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

`package project.three.adk;

import …; public class ProjectThreeActivity extends Activity {

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

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

private TextView buttonStateTextView;

private Vibrator vibrator; private boolean isVibrating;

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

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

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

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

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

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

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

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

private void closeAccessory() { … }

Runnable commRunnable = new Runnable() {

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

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

switch (buffer[0]) { case COMMAND_BUTTON:

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

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

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

break; } } } };

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

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

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

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

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

private TextView buttonStateTextView;

private Vibrator vibrator; private boolean isVibrating;`

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

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

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

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

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

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

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

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

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

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

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

images

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

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

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

零件

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

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

images

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

ADK 董事会

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

images

图 4-9 。模拟输入引脚

电位计

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

images

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

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

images

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

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

images

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

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

images

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

images

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

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

images

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

设置

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

images

图 4-13 。项目 4 设置

软件

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

Arduino 草图

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

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

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

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

byte sntmsg[6]; int analogPinReading;

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

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

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

**analogPinReading = analogRead(INPUT_PIN);**

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

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

00000000 00000000 00000001 00101100 = 300

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

0b100101100 = 300

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

00101100 = 44

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

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

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

00000000 00000000 00000001 00101100

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

Android 应用

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

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

`package project.four.adk;

import …;

public class ProjectFourActivity extends Activity {

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

private TextView adcValueTextView; private ProgressBar adcValueProgressBar;

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

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

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

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

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

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

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

private void closeAccessory() { … }

Runnable commRunnable = new Runnable() {

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

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

switch (buffer[0]) { case COMMAND_ANALOG:

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

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

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

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

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

private TextView adcValueTextView; private ProgressBar adcValueProgressBar;`

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

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

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

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

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

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

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

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

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

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

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

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

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

0 + 0 + 256 + 44 = 300

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

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

images

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

总结

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

五、声音

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

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

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

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

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

零件

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

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

images

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

ADK 董事会

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

压电蜂鸣器

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

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

images

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

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

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

images

***图 5-3。*压电蜂鸣器

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

设置

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

images

***图 5-4。*项目 5 设置

软件

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

Arduino 草图

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

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

#define COMMAND_ANALOG 0x3 #define TARGET_PIN_2 0x2

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

byte rcvmsg[6];

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

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

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

tone(pin, frequency);

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

tone(pin, frequency, duration);

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

Android 应用

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

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

`package project.five.adk;

import …;

public class ProjectFiveActivity extends Activity {

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

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

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

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

/**

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

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

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

OnItemSelectedListener onItemSelectedListener = new OnItemSelectedListener() {

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

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

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

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

private void openAccessory(UsbAccessory accessory) { … }

private void closeAccessory() { … }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

notesSpinner.setAdapter(adapter);

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

`OnItemSelectedListener onItemSelectedListener = new OnItemSelectedListener() {

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

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

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

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

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

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

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

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

images

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

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

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

零件

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

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

images

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

ADK 董事会

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

压电蜂鸣器

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

下拉电阻

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

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

images

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

设置

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

images

***图 5-8。*项目 6 设置

软件

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

Arduino 草图

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

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

#define COMMAND_ANALOG 0x3 #define INPUT_PIN_0 0x0

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

byte sntmsg[6];

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

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

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

Android 应用

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

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

`package project.six.adk;

import …;

public class ProjectSixActivity extends Activity {

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

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

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

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

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

/**

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

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

/**

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

private final BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { … } }; private void openAccessory(UsbAccessory accessory) { mFileDescriptor = mUsbManager.openAccessory(accessory); if (mFileDescriptor != null) { mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); Thread thread = new Thread(null, commRunnable, TAG); thread.start(); Log.d(TAG, "accessory opened"); } else { Log.d(TAG, "accessory open fail"); } }

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

Runnable commRunnable = new Runnable() {

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

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

switch (buffer[0]) { case COMMAND_ANALOG:

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

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

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

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

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

images

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

总结

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

六、光强感知

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

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

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

images

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

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

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

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

零件

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

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

images

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

ADK 董事会

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

光敏电阻

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

images

***图 6-2。*光敏电阻

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

images

***图 6-3。*光电效应

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

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

电阻器

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

设置

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

images

***图 6-4。*项目 7 设置

软件

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

Arduino 草图

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

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

#define COMMAND_LIGHT_INTENSITY 0x5 #define INPUT_PIN_0 0x0

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

byte sntmsg[3];

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

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

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

#define COMMAND_LIGHT_INTENSITY 0x5 #define INPUT_PIN_0 0x0

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

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

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

Android 应用

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

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

`package project.seven.adk;

import …;

public class ProjectSevenActivity extends Activity {

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

private TextView lightIntensityTextView; private LayoutParams windowLayoutParams;

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

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

/**

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

/**

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

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

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

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

Runnable commRunnable = new Runnable() {`

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

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

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

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

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

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

private TextView lightIntensityTextView; private LayoutParams windowLayoutParams;`

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

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

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

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

windowLayoutParams = getWindow().getAttributes();

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

windowLayoutParams.screenBrightness = lightIntensityValue / 100.0f;

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

getWindow().setAttributes(windowLayoutParams);

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

images

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

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

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

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

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

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

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

lightSensorEventListener的实现如下:

`SensorEventListener lightSensorEventListener = new SensorEventListener(){

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

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

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

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

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

总结

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

七、温度感应

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

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

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

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

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

零件

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

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

images

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

热敏电阻

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

images

***图 7-2。*热敏电阻

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

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

施泰因哈特-哈特方程

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

images

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

images

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

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

b = 1 / B

c = 0

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

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

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

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

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

images

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

设置

当热敏电阻的电阻变化时,你必须设置一个分压电路来测量电压的变化。分压器的组成取决于您使用的热敏电阻的类型。如果您使用负系数热敏电阻(NTC ),您的基本电路设置如图 7-3 所示。

images

图 7-3。 NTC 热敏电阻分压器

如果你使用正系数热敏电阻(PTC),你需要一个如图图 7-4 所示的电路。

images

图 7-4。 PTC 热敏电阻分压器

在这个项目中,温度上升时,模拟输入引脚上测得的电压会增加,温度下降时,测得的电压会降低。因此,请确保根据您使用的热敏电阻构建您的分压器电路,如上所示。图 7-5 显示了 NTC 热敏电阻的项目设置。

images

***图 7-5。*项目 8 设置

软件

这个项目的 Arduino 草图将使用 Arduino 平台的一些数学函数。您将使用自己编写的方法来表达公式,以计算当前温度。温度值将随后传输到 Android 设备。Android 应用将演示如何在 Android 设备的屏幕上绘制简单的形状和文本,以可视化测量的温度。

Arduino 草图

您将首次在 Arduino 草图中编写自己的自定义方法。自定义方法必须在强制设置和循环方法之外编写。它们可以有返回类型和输入参数。

此外,您将使用 Arduino 平台的一些数学函数。您将需要logexp函数来应用施泰因哈特-哈特方程计算温度。计算出的温度值需要进行比特移位,以便正确传输到 Android 设备。看一下完整的清单 7-1;我描述一下上市后的细节。

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

`#include <Max3421e.h> #include <Usb.h> #include <AndroidAccessory.h>

#define COMMAND_TEMPERATURE 0x4 #define INPUT_PIN_0 0x0 //----- //change those values according to your thermistor's datasheet long r0 = 4700; long beta = 3980; //-----

double ?? = 298.15; long additional_resistor = 10000; float v_in = 5.0; double r_inf; double currentThermistorResistance;

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

byte sntmsg[6];

void setup() { Serial.begin(19200); acc.powerOn(); sntmsg[0] = COMMAND_TEMPERATURE; sntmsg[1] = INPUT_PIN_0; r_inf = r0 * (exp((-beta) / ??)); }

void loop() { if (acc.isConnected()) { int currentADCValue = analogRead(INPUT_PIN_0); float voltageMeasured = getCurrentVoltage(currentADCValue); double currentThermistorResistance = getCurrentThermistorResistance(voltageMeasured); double currentTemperatureInDegrees = getCurrentTemperatureInDegrees(currentThermistorResistance);

// multiply the float value by 10 to retain one value behind the decimal point before // converting to an integer for better value transmission int convertedValue = currentTemperatureInDegrees * 10;

sntmsg[2] = (byte) (convertedValue >> 24); sntmsg[3] = (byte) (convertedValue >> 16); sntmsg[4] = (byte) (convertedValue >> 8); sntmsg[5] = (byte) convertedValue; acc.write(sntmsg, 6); delay(100); } }

// "reverse ADC calculation" float getCurrentVoltage(int currentADCValue) { return v_in * currentADCValue / 1024; }

// rearranged voltage divider formula for thermistor resistance calculation double getCurrentThermistorResistance(float voltageMeasured) { return ((v_in * additional_resistor) - (voltageMeasured * additional_resistor)) / voltageMeasured; }

//Steinhart-Hart B equation for temperature calculation double getCurrentTemperatureInDegrees(double currentThermistorResistance) { return (beta / log(currentThermistorResistance / r_inf)) - 273.15; }`

让我们看看草图顶部定义的变量。您在这里看到的第一个变量是数据协议的定义。为了确认传输了温度数据,选择了字节常数COMMAND_TEMPERATURE 0x4。用于测量的模拟输入引脚被定义为INPUT_PIN_0 0x0

现在已经定义了特定于数据表的值:

long r0 = 4700; long beta = 3980;

我在这个项目中使用了一个 4.7kω的热敏电阻,这意味着热敏电阻在 25 摄氏度时的电阻( R0 )为 4.7kω。这就是为什么r0被定义为 4700。在我的例子中,热敏电阻的数据表只定义了 B 值,即 3980。查看热敏电阻数据表,必要时调整这些值。

接下来,您将看到用于计算目的的常量值的一些定义:

double ?? = 298.15; long additional_resistor = 10000; float v_in = 5.0;

你需要 25 摄氏度时的开尔文温度( ?? )来计算 R∞ 。此外,还需要分压器电路中的第二个电阻值(10k)和输入电压来计算热敏电阻的电流电阻。

计算当前温度时,施泰因哈特-哈特方程的 B 参数变量中需要最后两个变量。

现在让我们看看程序流中发生了什么。在设置方法中,您将计算 R∞ 的值,因为它只需要在开始时计算一次。

r_inf = r0 * (exp((-beta) / ??));

循环方法中的重复步骤可描述如下:

  1. 读取当前 ADC 值。
  2. 根据 ADC 值计算引脚上的实际电压。
  3. 计算当前热敏电阻的电阻。
  4. 计算当前温度。
  5. 将温度转换为整数以便于传输。
  6. 传输数据。

现在让我们来看看单个步骤的详细描述。

analogRead方法返回当前读取的 ADC 值。您将使用它来计算施加于模拟输入引脚的实际电压。为此,您可以使用自己编写的自定义方法:

float getCurrentVoltage(int currentADCValue) { return v_in * currentADCValue / 1024; }

getCurrentVoltage方法将currentADCValue作为输入参数,并将计算出的电压作为float返回。由于 Arduino 平台将 0V 到 5V 的电压范围映射为 1024 个值,所以你只需将currentADCValue乘以 5.0V,再除以 1024,就可以计算出当前的电压。

现在你已经有了测量的电压,你可以用自己编写的方法getCurrentThermistorResistance计算热敏电阻的实际电阻。

double getCurrentThermistorResistance(float voltageMeasured) { return ((v_in * additional_resistor) - (voltageMeasured * additional_resistor)) / voltageMeasured; }

getCurrentThermistorResistance方法将测得的电压作为输入参数,计算电阻,并将其作为double返回。

最后可以进行最重要的计算。你用自己写的方法getCurrentTemperatureInDegrees来计算温度。

double getCurrentTemperatureInDegrees(double currentThermistorResistance) { return (beta / log(currentThermistorResistance / r_inf)) - 273.15; }

该方法将当前热敏电阻的电阻作为输入参数。它使用施泰因哈特-哈特方程的 B 参数变量来计算当前温度,单位为开尔文。要把它转换成摄氏度,你必须减去 273.15。该方法以摄氏度为单位返回当前温度作为double。这里使用的 Arduino log函数是上面公式中使用的自然对数函数 ln。

在将数据传输到 Android 设备之前,剩下的最后一步是转换温度值,以便于传输。例如,您可能已经计算出 22.52 摄氏度的双精度值。因为您只传输字节,所以您必须将值转换成非浮点数。小数点后有一个数字的精度就足够了,因此转换就像将该值乘以 10 一样简单,从而得到 225。

int convertedValue = currentTemperatureInDegrees * 10;

在乘法过程中,小数点向右移动一位。由于乘法运算也会将值转换为非浮点数,因此小数点后的数字用于在删除前一个数字之前向上或向下舍入。因此,值 22.52 将变成 225,值 22.56 将变成 226。

现在您有了一个整数值,您需要再次使用移位技术将它转换成一个四字节数组。

sntmsg[2] = (byte) (convertedValue >> 24); sntmsg[3] = (byte) (convertedValue >> 16); sntmsg[4] = (byte) (convertedValue >> 8); sntmsg[5] = (byte) convertedValue; acc.write(sntmsg, 6);

Arduino 部分到此为止,让我们来看看 Android 应用。

Android 应用

正如您已经知道的,Android 应用的第一步是建立与 ADK 板的通信,读取传输的数据并将其转换回原来的整数值。完成后,你可以通过在设备屏幕上绘制 2D 图形来可视化当前温度。这个应用将向你展示如何使用一些 2D 图形类和方法在屏幕的画布上绘制简单的形状。清单 7-2 显示了当前项目活动的一个片段,重点是新的和重要的部分。

清单 7-2。项目八:ProjectEightActivity.java

`package project.eight.adk;

import …;

public class ProjectEightActivity extends Activity {

private static final byte COMMAND_TEMPERATURE = 0x4; private static final byte TARGET_PIN = 0x0;

private TemperatureView temperatureView;

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

setContentView(R.layout.main); temperatureView = (TemperatureView) findViewById(R.id.temperature_view); }

/**

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

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

/**

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

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

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

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

Runnable commRunnable = new Runnable() {

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

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

switch (buffer[0]) { case COMMAND_TEMPERATURE: if (buffer[1] == TARGET_PIN) { final float temperatureValue = (((buffer[2] & 0xFF) << 24) + ((buffer[3] & 0xFF) << 16) + ((buffer[4] & 0xFF) << 8) + (buffer[5] & 0xFF)) / 10; runOnUiThread(new Runnable() {

@Override public void run() { temperatureView.setCurrentTemperature(temperatureValue); } }); } break;

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

首先看看变量的定义。前两个消息字节必须与 Arduino 草图中定义的字节相匹配,因此您可以按如下方式定义它们:

private static final byte COMMAND_TEMPERATURE = 0x4; private static final byte TARGET_PIN = 0x0;

然后你可以看到另一个类型为TemperatureView的变量。

private TemperatureView temperatureView;

TemperatureView顾名思义,是扩展安卓系统View类的自编自定义View。我们很快就会看到这个类,但是首先让我们继续 activity 类的剩余代码。

在读取接收到的消息后,您必须将字节数组转换回它原来的整数值。您只需反转 Arduino 部分中完成的位移来获得整数值。此外,您需要将整数除以 10,以获得您最初计算的浮点值。

`final float temperatureValue = (((buffer[2] & 0xFF) << 24)

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

收到的值 225 现在将被转换为 22.5。

最后要做的事情是将值传递给TemperatureView,这样您就可以在它的画布上绘制温度可视化。

`runOnUiThread(new Runnable() {

@Override public void run() { temperatureView.setCurrentTemperature(temperatureValue); } });`

请记住,您应该只在 UI 线程上更新 UI 元素。您必须在runOnUIThread方法中设置TemperatureView的温度值,因为它会在以后重新绘制时使自己失效。

2D 绘图是在TemperatureView类中实现的,所以先看看完整的清单 7-3 。

清单 7-3。项目八:TemperatureView.java

`package project.eight.adk;

import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.RectF; import android.util.AttributeSet; import android.view.View;

public class TemperatureView extends View { private float currentTemperature; private Paint textPaint = new Paint(); private Paint thermometerPaint = new Paint(); private RectF thermometerOval = new RectF(); private RectF thermometerRect = new RectF();

private int availableWidth; private int availableHeight;

private final float deviceDensity;

private int ovalLeftBorder; private int ovalTopBorder; private int ovalRightBorder; private int ovalBottomBorder;

private int rectLeftBorder; private int rectTopBorder; private int rectRightBorder; private int rectBottomBorder;

public TemperatureView(Context context, AttributeSet attrs) { super(context, attrs); textPaint.setColor(Color.BLACK); thermometerPaint.setColor(Color.RED); deviceDensity = getResources().getDisplayMetrics().density; TypedArray attributeArray = context.obtainStyledAttributes(attrs, R.styleable.temperature_view_attributes); int textSize = attributeArray.getInt( R.styleable.temperature_view_attributes_textSize, 18); textSize = (int) (textSize * deviceDensity + 0.5f); textPaint.setTextSize(textSize); }

@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); availableWidth = getMeasuredWidth(); availableHeight = getMeasuredHeight();

ovalLeftBorder = (availableWidth / 2) - (availableWidth / 10); ovalTopBorder = availableHeight - (availableHeight / 10) - (availableWidth / 5); ovalRightBorder = (availableWidth / 2) + (availableWidth / 10); ovalBottomBorder = availableHeight - (availableHeight / 10); //setup oval with its position centered horizontally and at the bottom of the screen thermometerOval.set(ovalLeftBorder, ovalTopBorder, ovalRightBorder, ovalBottomBorder);

rectLeftBorder = (availableWidth / 2) - (availableWidth / 15); rectRightBorder = (availableWidth / 2) + (availableWidth / 15); rectBottomBorder = ovalBottomBorder - ((ovalBottomBorder - ovalTopBorder) / 2); }

public void setCurrentTemperature(float currentTemperature) { this.currentTemperature = currentTemperature; //only draw a thermometer in the range of -50 to 50 degrees celsius float thermometerRectTop = currentTemperature + 50; if(thermometerRectTop < 0) { thermometerRectTop = 0; } else if(thermometerRectTop > 100){ thermometerRectTop = 100; } rectTopBorder = (int) (rectBottomBorder - (thermometerRectTop * (availableHeight / 140))); //update rect borders thermometerRect.set(rectLeftBorder, rectTopBorder, rectRightBorder, rectBottomBorder); invalidate(); }

@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); //draw shapes canvas.drawOval(thermometerOval, thermometerPaint); canvas.drawRect(thermometerRect, thermometerPaint); //draw text in the upper left corner canvas.drawText(getContext().getString( R.string.temperature_value, currentTemperature), availableWidth / 10, availableHeight / 10, textPaint); } }`

先看一下变量。如前所述,currentTemperature变量将由包含TemperatureView的活动设置。

private float currentTemperature;

接下来你可以看到两个Paint参考。一个Paint对象定义了颜色、大小、笔划宽度等等。当你在绘制图形或文本时,你可以为相应的方法调用提供一个Paint对象来优化绘制结果。您将使用两个 Paint 对象,一个用于文本可视化,另一个用于稍后将绘制的形状。

private Paint textPaint = new Paint(); private Paint thermometerPaint = new Paint();

RectF对象可以理解为用于定义形状边界的边界框。

private RectF thermometerOval = new RectF(); private RectF thermometerRect = new RectF();

你将画一个温度计,所以你将画两个不同的形状:一个椭圆形的底部和一个矩形的温度条(见图 7-6 )。

images

图 7-6。 2D 形状创建温度计(椭圆形+长方形=温度计)

接下来的两个变量将包含View的宽度和高度。它们被用来计算你将画温度计的位置。

private int availableWidth; private int availableHeight;

为了能够根据设备的屏幕属性调整文本大小,您必须确定您的屏幕密度(稍后会详细介绍)。

private final float deviceDensity;

您将绘制的描绘温度计的 2D 图形具有定义的边界。这些边界需要动态计算,以适应任何屏幕尺寸,因此您也可以将它们保存在全局变量中。

`private int ovalLeftBorder; private int ovalTopBorder; private int ovalRightBorder; private int ovalBottomBorder;

private int rectLeftBorder; private int rectTopBorder; private int rectRightBorder; private int rectBottomBorder;`

变量就是这样。现在我们将看看方法的实现,从TemperatureView的构造函数开始。

public TemperatureView(Context context, AttributeSet attrs) { super(context, attrs); textPaint.setColor(Color.BLACK); thermometerPaint.setColor(Color.RED); deviceDensity = getResources().getDisplayMetrics().density; TypedArray attributeArray = context.obtainStyledAttributes(attrs, R.styleable.temperature_view_attributes); int textSize = attributeArray.getInt( R.styleable.temperature_view_attributes_textSize, 18); textSize = (int) (textSize * deviceDensity + 0.5f); textPaint.setTextSize(textSize); }

如果您想将自定义的View嵌入到一个 XML 布局文件中,您需要实现一个构造函数,它不仅接受一个Context对象,还接受一个AttributeSet。一旦View充气,这些将由系统设置。AttributeSet包含您可以进行的 XML 定义,比如宽度和高度,甚至自定义属性。您还需要调用父View的构造函数来正确设置属性。该构造函数也用于设置Paint对象。这只需要一次,所以你可以在这里设置颜色和文本大小。

当定义文本大小时,你必须考虑到设备有不同的屏幕属性。它们可以有从小到特大的不同尺寸,每种尺寸也可以有不同的密度,从低密度到超高密度。尺寸描述了以屏幕对角线测量的实际物理尺寸。密度描述了定义的物理区域中的像素数量,通常表示为每英寸点数(dpi)。如果您要为文本定义一个固定的像素大小,它将在具有相同大小的设备之间显示非常不同。在低密度的设备上,它可以呈现得非常大,而相同大小的其他设备会呈现得非常小,因为它们具有更高的密度。

images 注意要了解更多关于屏幕尺寸和密度的信息,请访问位于[developer.android.com/guide/practices/screens_support.html](http://developer.android.com/guide/practices/screens_support.html)的 Android 开发者指南。

要解决这个问题,你需要做几件事。首先,您必须确定设备的密度,以计算您需要设置的实际像素大小,以便文本在不同设备上看起来一致。

deviceDensity = getResources().getDisplayMetrics().density;

现在你有了密度,你只需要文本的相对大小来计算实际的像素大小。当编写自己的View元素时,您也可以为该视图定义自定义属性。在这个例子中,您将为TemperatureView定义属性textSize。为了做到这一点,您必须创建一个新文件,定义所有的定制属性TemperatureView可以有。在res/values中创建一个名为attributes.xml的 XML 文件。文件的名字没有限制,你可以选择随便叫;只要确保它以.xml结尾。在这个 XML 文件中,你必须定义如清单 7-4 所示的属性。

***清单 7-4。*项目 8: attributes.xml

<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="temperature_view_attributes"> <attr name="textSize" format="integer"/> </declare-styleable> </resources>

接下来,您需要将TemperatureView添加到布局中,并设置它的textSize属性。如果您在 XML 布局文件中使用您自己的定制视图,您必须用它们的完全限定类名来定义它们,即它们的包名加上它们的类名。这个项目的main.xml布局文件看起来像清单 7-5 中的。

***清单 7-5。*项目 8: main.xml

<?xml version="1.0" encoding="utf-8"?> <project.eight.adk.TemperatureView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:temperatureview="http://schemas.android.com/apk/res/project.eight.adk" android:id="@+id/custom_view" android:layout_width="fill_parent" android:layout_height="fill_parent" android:background="#FFFFFF" temperatureview:textSize=”18”> </project.eight.adk.TemperatureView

因为您不仅添加了系统属性,还添加了自己的属性,所以除了标准的系统名称空间之外,您还必须定义自己的名称空间。

xmlns:temperatureview="http://schemas.android.com/apk/res/project.eight.adk"

要定义您自己的名称空间,您需要指定一个名称,就像这里用temperatureview所做的那样,并添加模式位置。您之前添加的定制属性的模式位置是[schemas.android.com/apk/res/project.eight.adk](http://schemas.android.com/apk/res/project.eight.adk)。模式位置的最后一个重要部分反映了您的包结构。一旦添加了模式,就可以通过添加名称空间名称作为前缀来定义定制属性textSize

temperatureview:textSize=”18”>

现在您已经成功配置了自定义属性textSize。让我们看看如何在初始化TemperatureView时访问它的值。

TypedArray attributeArray = context.obtainStyledAttributes(attrs, R.styleable.temperature_view_attributes); int textSize = attributeArray.getInt(R.styleable.temperature_view_attributes_textSize, 18);

首先,您必须获得对一个TypedArray对象的引用,该对象包含给定的可样式化属性集的所有属性。为此,您调用当前上下文对象上的obtainStyledAttributes方法。这个方法有两个参数,当前视图的AttributeSet和您感兴趣的 styleable 属性集。在返回的TypedArray中你会找到你的textSize属性。要访问它,您可以在TypedArray上调用类型特定的 getter 方法,并提供您感兴趣的属性名称,如果找不到该属性,还可以提供一个默认值。

最后,您有了定义好的文本大小,可以用来计算设备密度所需的实际像素大小。

textSize = (int) (textSize * deviceDensity + 0.5f);

对于TemperatureView的构造函数就是这样。接下来是onMeasure方法。

`@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); availableWidth = getMeasuredWidth(); availableHeight = getMeasuredHeight();

ovalLeftBorder = (availableWidth / 2) - (availableWidth / 10); ovalTopBorder = availableHeight - (availableHeight / 10) - (availableWidth / 5); ovalRightBorder = (availableWidth / 2) + (availableWidth / 10); ovalBottomBorder = availableHeight - (availableHeight / 10); //setup oval with its position centered horizontally and at the bottom of the screen thermometerOval.set(ovalLeftBorder, ovalTopBorder, ovalRightBorder, ovalBottomBorder);

rectLeftBorder = (availableWidth / 2) - (availableWidth / 15); rectRightBorder = (availableWidth / 2) + (availableWidth / 15); rectBottomBorder = ovalBottomBorder - ((ovalBottomBorder - ovalTopBorder) / 2); }`

onMeasure方法继承自View系统类。系统调用它来计算显示View所需的尺寸。它在这里被覆盖以获得View的当前宽度和高度,以便以后可以以适当的比例绘制形状。注意,同样调用父类的onMeasure方法是很重要的,否则系统将抛出一个IllegalStateException。一旦你有了宽度和高度,你就可以定义椭圆形的边界框了,因为它以后不会改变。您还可以计算矩形四个边框中的三个。唯一依赖于当前温度的边界是顶部边界,因此将在以后进行计算。边界的计算定义了在每个设备上看起来成比例的形状。

为了更新测量的温度值以便可视化,您编写了一个名为setCurrentTemperature的 setter 方法,它将当前温度作为一个参数。setCurrentTemperature方法不仅仅是一个简单的变量设置器。它还用于更新温度计栏矩形的边界框,并使视图无效,以便重新绘制视图。

public void setCurrentTemperature(float currentTemperature) { this.currentTemperature = currentTemperature; //only draw a thermometer in the range of -50 to 50 degrees celsius float thermometerRectTop = currentTemperature + 50; if(thermometerRectTop < 0) { thermometerRectTop = 0; } else if(thermometerRectTop > 100){ thermometerRectTop = 100; } rectTopBorder = (int) (rectBottomBorder - (thermometerRectTop * (availableHeight / 140))); //update rect borders thermometerRect.set(rectLeftBorder, rectTopBorder, rectRightBorder, rectBottomBorder); invalidate(); }

更新矩形的边界后,你需要使TemperatureView无效。从TemperatureView的超类View继承而来的invalidate方法告诉系统这个特定的视图元素是无效的,需要重新绘制。

最后一种方法是负责 2D 图形绘制的实际方法。每次需要更新时,在一个View上调用onDraw方法。你可以像在setCurrentTemperature方法中一样,通过调用invalidate方法告诉系统它需要重画。让我们来看看它的实现。

`@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas);

//draw shapes canvas.drawOval(thermometerOval, thermometerPaint); canvas.drawRect(thermometerRect, thermometerPaint);

//draw text in the upper left corner canvas.drawText(getContext().getString( R.string.temperature_value, currentTemperature), availableWidth / 10, availableHeight / 10, textPaint); }`

当系统调用onDraw方法时,它提供一个与View关联的Canvas对象。Canvas对象用于在其表面上绘图。draw 方法调用的顺序很重要。你可以把它想象成现实生活中的画布,你可以在上面一层一层地画。在这里你可以看到,首先一个椭圆形及其预定义的RectFPaint对象被绘制。接下来,绘制象征温度计条的矩形。最后通过定义要绘制的文本来绘制文本可视化,其坐标和原点是左上角及其关联的Paint对象。编码部分到此为止。

算了这么多,终于到了看你自建温度计是否管用的时候了。部署您的应用,如果您用指尖加热热敏电阻,应该会看到温度升高。最终结果应该看起来像图 7-7 。

images

***图 7-7。*项目 8:最终结果

总结

在这一章中,你学会了如何制作自己的温度计。您还学习了热敏电阻的基本知识,以及如何借助施泰因哈特-哈特方程计算环境温度。出于可视化的目的,您编写了自己的自定义 UI 元素。您还使用 2D 图形绘制了一个虚拟温度计以及当前测量温度的文本表示。