65.9K
CodeProject 正在变化。 阅读更多。
Home

Arduino WiFi 连接的天气站(带 Android 用户界面)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (83投票s)

2016 年 2 月 17 日

CPOL

33分钟阅读

viewsIcon

224805

downloadIcon

2664

在 Arduino 板的帮助下,构建一个便宜的、30 欧元以下的天气站传感器节点,并使用 WiFi 连接通过任何 Android 设备进行控制。

有关代码和文档的最新版本,请在 github 上查看项目。

本教程以及其他有趣的在线 Web 开发教程和书籍由 web-engineering.info 发布。

其他相关文章

构建 WiFi 连接的天气站

在本教程中,我们将展示如何借助 Arduino Pro Mini(或替代的 Nano、UNO 或 MEGA2560)、ESP8266 WiFi 模块、DHT22 温湿度传感器以及 Android 设备(智能手机、平板电脑、手表等)来构建一个 WiFi 连接的天气站,用于配置天气站和观察传感器数据。此项目的总成本(不含 Android 设备)低于 30 欧元。

我们项目的设计目标是:

  • 简单性:解决方案必须简单易于构建,即使是初学者也能胜任;
  • 可用性:可以通过 WiFi 读取温度(范围 [-30, 50]°C)和湿度(范围 [10 - 100]%)值,并使用 Android 设备(智能手机、平板电脑、手表等)进行可视化;
  • 低成本:目标是 30 欧元或以下(不含 Android 设备);
  • 可维护性:当需要时,必须能够添加新的传感器和功能,并且在出现问题时我们应该能够进行修复。

人们可以购买具有类似功能的设备,但价格范围从大约 75 欧元起。此外,这些设备不容易修改,它们大多数使用 SMD(表面贴装器件)和集成 IC,因此可能的改进或维修困难或不可能。

此外,由于它的易于实现且不花费任何成本(除了几个 MCU 周期和几分钟的编程),我们可以轻松添加一些其他功能:

  • 用于 5V 电源的电压表 - 这相当容易,因为 ATmega 328(以及 ATmega 168 和其他一些)MCU 附带内置电压表(电压传感器),如果未校准,精度相对较低,但可以提供有价值的信息,无需额外成本;
  • Arduino MCU 的可用 RAM 量 - 主要用于调试目的,它提供了有关后续改进的有用信息。与电压表一样,无需额外的硬件,并且相对于 MCU 使用,它只花费几个 MCU 周期;
  • 计算平均温度和湿度 - 这是在软件中完成的,为天气站用户提供了有用的信息。

免责声明:处理带电有危险。本项目使用 5V 和 3.3V 电压,在任何环境条件下都对人体安全。但是,低电压来自连接到市电的电源适配器,因此我们强烈建议您采取安全预防措施,并使用值得信赖的品牌作为电源。对于任何造成的损坏,我们概不负责!请自行承担风险,并/或寻求电子工程师的帮助。

硬件配置

项目所需的硬件组件如下所示。这些组件大多从 eBay 和 Amazon 购买,列出的价格包括德国境内的邮费(有些也包括欧盟境内)。如果您愿意等待两到五周才能收到商品,您也可以从中国在线商店(AliExpress、eBay 等)购买,价格是我们显示的的一半还少。

硬件组件 估算价格 描述
Arduino Pro Mini 3 - 5 欧元 我们使用的是克隆版,但功能与原版设备相同。虽然原版 Arduino Pro Mini 的质量更高,但价格也更高(两到四倍)。
ESP8266-02 WiFi 模块 6 - 8 欧元 这些模块大多有 4Mb (512KB) 的闪存,允许使用 1.1.0(2015 年 6 月发布)之前的 AT 固件版本。现在有新的模块可用,SPI 闪存 IC 已更新为 8Mb (1MB),允许它们使用最新的 AT 固件,该固件提供了更多和改进的 AT 命令。我们强烈建议使用具有 1MB 闪存和最新 AT 固件的 ESP8266 模块。
逻辑电平转换器模块 1 - 3 欧元 它用于将 Arduino 板和 ESP8266 WiFi 模块之间的 UART 通信所需的 5V 转换为 3.3V。这些设备中的一些还支持 2.5V 和 1.8V 电平。
DHT22 温湿度传感器 4 - 6 欧元 如果您不需要负温度且其较低的精度对您来说可以接受,则可以将其替换为 DHT11 传感器模块。它在一个封装中包含一个温度和相对空气湿度传感器,并使用 1-Wire 数字自定义接口进行数据通信。
LM317 降压线性稳压器 每件约 0.5 欧元,10 件套约 2 - 3 欧元 它需要两个额外的电阻(或一个电阻和一个电位器)和两个额外的电容,总成本约为 1 欧元。我们使用它将电压降低到 3.3V,但它可以调整到 1.25 到 37V 之间的值。该器件在适当冷却的情况下能够提供最大 1.5A 的电流(在我们的情况下不需要)。
一个红色/绿色双色 LED,或两个独立的红色和绿色 LED 每件约 0.5 欧元,10 件套约 2 欧元 共阴极(在我们例子中是 GND)LED 是双色 LED 的一个好选择。只要它们需要小于 20-25mA 并且正向电压小于 5V(由 Arduino I/O 引脚提供),就可以使用任何尺寸和颜色的两个独立 LED。
5V@1A 稳压电源适配器 3-5 欧元 允许稳压电源适配器有 ±5% 的变化。请注意,不建议使用廉价低质量的电源,因为它们通常调节性差,并且通常超出 ±5% 的误差范围,因此可能会损坏其他组件,甚至危及您的生命。可以使用更高安培值的电源适配器,但这些通常更贵。此外,您还可以使用 USB 端口为我们的设备供电,该端口能够提供 500mA 或更高的电流(某些笔记本电脑的 USB 端口不合适)。

天气站硬件设计

有一些应用程序可用于设计电子电路图,但大多数情况下,初学者难以理解。我们选择使用 Fritzing,它可以绘制漂亮的、面向面包板的设计,还可以绘制原理图和 PCB 布局。它还提供了一个与 Arduino Software 集成的内置环境,允许您编写 Arduino 代码、编译它并将其部署到 Arduino 板上。面包板上的硬件原型如图 1 所示。

图 1:天气站硬件的完整面包板设计。

硬件分为几个模块:

  • 电源:Arduino Pro Mini 板需要 5-12V(由 5V@1A 电源适配器提供),但 ESP8266 WiFi 模块需要 3.3V 稳压电压,我们通过使用降压线性稳压器获得。
  • 传感器节点控制器:Arduino Pro Mini 板代表控制器板,它运行此项目的所有逻辑;
  • WiFi 通信模块:我们使用了廉价易用的 ESP8266 模块进行传感器节点和 Android 设备之间的 WiFi 通信;
  • 传感器:温度和湿度值来自 DHT22 传感器;
  • 逻辑电平电压转换器:允许 Arduino 板(使用 5V 信号)和 ESP8266 模块(需要 3.3V 信号)之间进行安全的串行/UART 通信;
  • WiFi 状态 LED:使用一个共阴极绿色/红色双色 LED 来直观地指示 WiFi 状态。

注意:我们知道,许多面包板在两个电源轨的 GND 线之间没有默认连接,因此您需要添加一根连接线将两条 GND 线连接起来,如图 1 所示。此外,有些面包板的每个电源轨都分成两半,如果是这种情况,您需要使用跳线。

对于面包板原理图,红色线用于 +5V,橙色线用于 3.3V,黑色线用于 GND 连接。对于通信线,我们使用绿色和蓝色线用于 RX/TX 线,黄色线用于 DHT22 传感器的数据引脚。在图 2 中,我们展示了我们天气站传感器节点的完整电子原理图。

图 2:天气站硬件的完整原理图设计。

ESP8266 3.3V 电源

我们的传感器节点由一个稳压的 5V@1A 电源适配器供电。Arduino Pro Mini 板和大多数组件可以直接使用电源适配器提供的 5V 电压轨。通过使用 LM317T IC(一种可调降压线性稳压器)实现降至 ESP8266 WiFi 模块所需的 3.3V 稳压。它只需要几个额外的组件:两个电阻和两个电容。在适当冷却的情况下,它能够提供高达 1.5A 的电流。然而,在我们的示例中不需要冷却,平均电流低于 250mA(仅在 ESP8266 模块传输数据时达到 500mA,但仅在非常短的时间内)。图 3 显示了在面包板上构建的 3.3V 电源,图 4 显示了相应的电子原理图。

图 3:面包板上的 3.3V 电源。
图 4:3.3V 电源的原理图。

R1 和 R2 电阻的值根据以下规则选择:

  • R1 的值必须在 LM317 IC 数据手册规定的 100 - 1000 欧姆范围内;
  • R1 和 R2 都是标准电阻值,因此在任何电子商店都很容易买到;
  • Vout ~= 3.3 时,验证 LM317 IC 数据手册提供的公式 Vout = 1.25 ( 1 + R2 / R1)

对于 R1 = 330 欧姆R2 = 560 欧姆,实际输出电压为 Vout ~= 3.37V,因此最大 ±5% 的误差在规格范围内。C1 是一个 0.1µF (100nF) 的陶瓷电容器,用于过滤高频尖峰并防止 LM317T IC 内部振荡。C2 是一个 1µF 的电解电容器(可以使用高达约 1000µF 的值),其目的是平滑输出电压(3.3V 线)。C2 的设计电压必须高于 3.3V,并且务必正确安装电容器的极性。如果使用不当,例如反接或超出其规格的电压,电解电容器非常容易发生小爆炸。

注意:可以使用 1K 欧姆电位器代替固定值 R2 电阻。这样,如果(并且当)需要更精确的输出电压时,可以获得更精确的输出电压。这样您也可以为 R1 使用较低的值,而 240 欧姆是一个不错的选择,正如 LM317 数据手册所推荐的那样。

通过逻辑电平转换器的串行通信

Arduino Pro Mini 板由 5V 电源供电,因此可以在 UART RX 和 TX 通信线上使用 5V。另一方面,ESP8266 WiFi 模块使用 3.3V 电源,并且对于 VCC、RX、TX 或其任何其他可用 I/O 线不是 5V 耐受的。因此,Arduino 板的 RX/TX 线与 ESP8266 模块的 RX/TX 线之间不能直接连接,忽略此警告可能会损坏 ESP8266 模块。这种情况的一个简单解决方案是使用现成的逻辑转换器模块。它提供 3.3V 线(有些还支持 1.8V、2.5V 和 2.8V)以及 5V 通信线。请注意,此类模块在每条线上只能提供非常低的电流(几毫安),因此只能用于数据通信,而不能用作电压调节器。我们使用一个四通道模块,因此它提供四条低压(3.3V)线和四条相应的高压(5V)线。两条线用于 UART 通信,另外两条线用于分别将 ESP8266 模块的 CH_PD 和 RESET 线连接到 Arduino 板的引脚 2 和 3。

重要的是要注意,Arduino Pro Mini 板的 RX/TX 线必须与 ESP8266 模块的 RX/TX 线交叉连接。这意味着:Arduino 板的 RX 线连接到 ESP8266 模块的 TX 线,Arduino 板的 TX 线连接到 ESP8266 模块的 RX 线。这些连接通过逻辑电平转换器模块完成,如上所述,并在图 1 和图 2 中所示。

ESP8266 模块的 CH_PD(通道断电)和 RESET 引脚需要通过 3.3K 欧姆电阻连接到 VCC(+3.3V)线。通常,可以使用 10K 欧姆电阻,但我们观察到 WiFi 模块会周期性重启(我们测试了其中几个)。通过测试和试错法,我们发现 3.3K 欧姆电阻对我们所有的 ESP8266 模块效果最好。

注意:原理图中显示的是 ESP8266-01 模块,因为它在 Fritzing 组件中可用,但实际上我们在实际电路板上使用的是 ESP8266-02 模块。这应该没有区别,并且通常我们应该能够使用这些模块的任何版本。ESP8266 模块有大约十三个版本,它们在外观、尺寸和可用 I/O 方面有所不同,但都具有相同的 WiFi 功能。

DHT22 传感器

DHT22 传感器可以在 [-40, 80]°C 的范围内进行温度读数,典型精度为 ±0.5°C,分辨率为 0.1 个单位 (°C)。然而,在实际测试中,我们观察到的精度接近 ±1°C。该传感器还可以在 [0, 100]% 的范围内读取湿度值,典型精度为 ±2 个单位 (%),分辨率为 0.1 个单位。在我们的实际测试中,精度约为 ±5%。

DHT22 传感器可以连接到 3.3V 线或 5V 线,我们选择使用 5V 线。传感器的数据引脚(有关更多详细信息,请参阅 数据手册)通过一个 10K 欧姆电阻连接到 5V 电源轨,以避免通信错误(当传感器不通过数据引脚通信时,此线必须保持 HIGH)。传感器数据引脚连接到 Arduino 板的数字引脚 9。

WiFi 状态 LED

在测试过程中,我们发现了解 ESP8266 模块的实际状态很有帮助,尤其因为该模块有时会变得无响应。我们使用了一个双色红/绿 LED,它实际上是一个物理封装中的一个绿色和一个红色 LED,它们共享一个公共阴极(GND)连接。绿色 LED 的阳极通过一个 560 欧姆的电阻连接到 Arduino Pro Mini 板的引脚 8,从而允许大约 5mA 的电流通过 LED。红色 LED 的阳极通过一个 560 欧姆的电阻连接到 Arduino Pro Mini 板的引脚 7,从而允许大约 6mA 的电流通过 LED。

LED 绝不能直接连接在 VCC 和 GND 线之间(或直接连接到 MCU 的 I/O 引脚),除非是特制的 LED,在这种情况下,它们内置了电阻或使用了其他限流设计。而是必须使用串联电阻来限制通过 LED 的电流。使用 欧姆定律,我们可以通过知道 LED 的正向电压(LED 开始导通并发光时的电压)、使用的电源电压和我们希望通过 LED 的电流(控制 LED 的亮度)来计算电阻值。标准红色 LED 的正向电压约为 1.7V,绿色或橙色 LED 的正向电压约为 2V,白色或蓝色 LED 的正向电压约为 2.7V。由于我们只需要 LED 提供视觉指示,它们不必非常亮。在测试我们的双色 LED 后,我们决定红色 LED 需要 6mA,绿色 LED 需要 5mA(绿色光比其他颜色对人眼更可见)。根据欧姆定律,我们有 V = IR,所以 R = V / I。在我们的例子中,V 是电源电压 (+5V) 与 LED 正向电压之差。求解红色 LED 的方程,我们得到 R = (5 - 1.7) / 0.006,得到 R = 550 欧姆。由于 550 欧姆不是标准值,可以使用下一个可用的标准电阻值,即 560 欧姆。作为家庭作业,您可以计算绿色 LED 的值。

注意:如果您没有双色 LED,请使用两个普通 LED。另外,如果您喜欢,可以使用其他 LED 颜色,甚至可以使用不同的电阻值来获得不同的亮度级别(使用欧姆定律找到合适的电阻值)。标准的 3mm 或 5mm LED 通常能承受 20mA 的电流,之后会过热,很有可能烧毁。然而,有些 LED 可以承受更大的电流,但需要特殊的驱动器。另外,请注意,您不应从每个 Arduino 引脚吸取超过 20mA 的电流,否则可能会损坏板。

硬件汇总概览

下表总结了硬件组件的连接方式:
Arduino Pro Mini 引脚 连接到
RX 逻辑电平转换器第一个 RX HV 引脚
TX 逻辑电平转换器第一个 TX HV 引脚
2 逻辑电平转换器第二个 RX HV 引脚
3 逻辑电平转换器第二个 TX HV 引脚
7 双色 LED 的红色阳极,通过 R4(560 欧姆)电阻
8 双色 LED 的绿色阳极,通过 R4(560 欧姆)电阻
9 DHT22 传感器的引脚
VCC 电源的 VCC (+5V) 电压轨
GND 电源的 GND 电压轨
 
逻辑电平转换器引脚 连接到
第一个 TX LV ESP8266 模块的 RX 引脚
第一个 RX LV ESP8266 模块的 TX 引脚
第一个 TX HV Arduino Pro Mini 板的 TX 引脚
第一个 RX HV Arduino Pro Mini 板的 RX 引脚
第二个 TX LV ESP8266 模块的 RESET 引脚
第二个 RX LV ESP8266 模块的 CH_PD 引脚
第二个 TX HV Arduino Pro Mini 板的引脚 3
第二个 RX HV Arduino Pro Mini 板的引脚 2
LV GND 电源的 GND 电压轨
HV GND 电源的 GND 电压轨
LV 3.3V 电压轨,来自 LM317 IC 的输出引脚
HV 电源的 5V 电压轨
 
ESP8266 模块引脚 连接到
RX 逻辑转换器模块的第一个 LV TX 引脚
TX 逻辑转换器模块的第一个 LV RX 引脚
RESET 逻辑转换器模块的第二个 LV TX 引脚
CH_PD 逻辑转换器模块的第二个 LV RX 引脚
GND 电源的 GND 电压轨
VCC 3.3V 电压轨,来自 LM317 输出引脚
 
LM317 IC 引脚 连接到
输入 电源的 5V 电压轨
输出 3.3V 电压轨
Adj 输出 3.3V 电压轨通过 R1 和到 GND 电压轨通过 R2

软件配置

Arduino 代码

您可以使用 FritzingArduino IDE 编写、编译和部署 Arduino 代码到 Arduino 板(在某些情况下,这可能更可取,因为它通常开箱即用)。对于我们的项目,需要以下 Arduino 库:

  • Arduino-ESP8266,用于 ESP8266 模块和 Arduino 板之间的 UART 通信。克隆库存储库,将文件夹重命名为 ESP8266,并将其复制到 Arduino 软件安装文件夹的 libraries 子文件夹中。
  • DHTLib,用于与 DHT22 传感器通信。如果您将 DHT11 传感器用作 DHT22 的替代品,此库也支持 DHT11 传感器。克隆库存储库,将其重命名为 DHT,并将其复制到 Arduino 软件安装文件夹的 libraries 子文件夹中。

Arduino 程序(也称为草图)具有以下最小结构:

// 1. constants definition (optional if no constant is needed)
// 2. include headers for used libraries ( optional if no library is used)
// 3. define the global variables (optional if no global variable is required)

// program initialization
void setup() { 
  // write here the setup code.
}

// infinite loop cycle
void loop() { 
  // the code from this method loops as long as the Arduino board is powered.
}              

setup 方法中,我们编写初始化代码,该代码仅在 Arduino 通电或在软件或硬件重置后执行一次。loop 方法包含只要板子有电就会在 Arduino MCU 中循环的代码。

Arduino 引脚配置的常量定义

首先,我们定义代表所用 Arduino 板引脚的常量。强烈建议使用常量而不是在代码中 all over 使用引脚号。这样,如果您决定使用另一个引脚,则可以轻松更改常量值,而不必试图找到使用引脚号的所有位置。

// Arduino pin number used for the communication with DHT22 sensor.
#define DHT22_PIN 9
// pins number for WiFi disabled LED
#define WIFI_DISABLED_LED_PIN 7
// pins number for WiFi enabled/active LED
#define WIFI_ACTIVE_LED_PIN 8
// arduino pin used to connect to the CH_PD (Power Down) WiFi module pin
#define WIFI_CH_PD_PIN 2
// arduino pin used to connect to the RESET WiFi module pin
#define WIFI_RESET_PIN 3

引脚 9 用于与 DHT22 传感器进行数据通信,引脚 7 和 8 用于红色和绿色 LED(WiFi 状态 LED),引脚 2 用于将 WiFi 置于睡眠模式(并唤醒它),引脚 3 用于对 WiFi 模块进行硬件重置,这需要将其拉低(设置为 LOW)至少 200 毫秒。

导入 DHT22 和 ESP8266 模块所需的库

我们需要指定程序使用的库。这通过使用 #include 指令以标准的 C/C++ 样式完成:

#include <dht.h>
#include <ESP8266.h>                        

使用此指令时,<> 会告诉编译器和链接器在 Arduino IDE 安装的 libraries 子文件夹中查找,而使用双引号意味着库文件位于您的 Arduino 草图文件夹中。

定义程序全局变量

与许多(如果不是全部)Arduino 程序一样,我们使用一些全局变量:

// DHT22 sensor controller class
dht dht22;

// ESP8266 WiFi module controller
ESP8266 esp( Serial);

// store the average temperature and humidity 
// values, starting with last system reset
float avgTemperature = 0;
float avgHumidity = 0;

// utility variable used to compute the averate temperature value
unsigned long avgDhtStep = 1;

// data template for sensors
const char SENSORS_DATA_TEMPLATE[] PROGMEM = 
    "{\"temperature\": %s, \"avgTemperature\": %s, \"humidity\": %s, \"avgHumidity\": %s, \"voltage\": %s, \"freeRam\": %d}";

dht22 变量代表控制与 DHT22 传感器通信的库的一个实例。esp 变量代表用于与 WiFi 模块通信的 ESP8266 库的一个实例。作为构造函数的参数,我们提供了 Serial 对象,因此通信是在 Arduino 板的 UART0 端口上进行的。当使用 Arduino Pro Mini(也包括 Nano 或 UNO)板时,这是唯一可用的串行端口。但是,该库旨在与所有 Arduino 板配合使用,其中一些板最多有四个 UART 端口,可通过 SerialSerial1Serial2Serial3 全局对象访问。

由于我们想知道天气站测量的平均温度和湿度值,我们定义了 avgTemperatureavgHumidityavgDhtStep 变量。前两个用于存储平均温度和湿度值,而后一个用于计算读取温度值的次数,以便根据公式计算正确的平均值:avg = (avg * (n - 1) + newValue) / n

SENSORS_DATA_TEMPLATE 变量存储用于与 Android 应用程序通信的模板(JSON 结构)。特殊的 PROGMEM 变量修饰符强制将值存储在闪存而不是 RAM 中,从而释放大约 120 字节的 RAM(约占 ATmega328P MCU 的总 RAM 的 6%,Arduino Pro Mini、Nano 和 UNO 板使用)。

初始化 ESP8266 WiFi 模块

ESP8266 模块通过 UART 协议与 Arduino MCU 通信。建议对 WiFi 模块进行硬件重置,以确保在开始 UART 通信之前模块处于正确状态。

void setupWiFi() {
  // STEP 1:
  // Set pins used for WiFi status LEDs as OUTPUT.
  pinMode( WIFI_ACTIVE_LED_PIN, OUTPUT);
  pinMode( WIFI_DISABLED_LED_PIN, OUTPUT);

  // STEP 2:
  // Arduino pin connected to ESP8266 CH_PD pin is set to OUTPUT.
  // To keep the module active, this pin must be kept HIGH.
  pinMode( WIFI_CH_PD_PIN, OUTPUT);
  digitalWrite( WIFI_CH_PD_PIN, HIGH);
  // Arduino pin connected to ESP8266 RESET pin is set to OUTPUT.
  // To avoid random resents, we keep this HIGH.
  pinMode( WIFI_RESET_PIN, OUTPUT);
  digitalWrite( WIFI_RESET_PIN, HIGH);

  // STEP 3:
  // perform a hardware reset (ESP8266 RESET pin goes LOW)
  digitalWrite( WIFI_RESET_PIN, LOW);
  delay( 200);
  // allow ESP8266 module to boot
  digitalWrite( WIFI_RESET_PIN, HIGH);

  // STEP 4:
  // baud 115200, communication with ESP8266 module
  Serial.begin( 115200);

  // STEP 5:
  // wait for the ESP8266 module to start, after the forced hardware reset.
  // We check the wifi state once a second, until the ESP8266 WiFi module responds.
  while( !checkWiFi()) {
    delay( 1000);
  };

  // STEP 6:
  // start UDP connection - wait on all ports
  esp.atCipstartUdp();
} 

WiFi 模块相关的初始化需要以下步骤:

  1. 用于 WiFi 状态 LED 的 Arduino 引脚(即由 WIFI_ACTIVE_LED_PINWIFI_DISABLED_LED_PIN 常量定义的引脚)设置为输出。
  2. 用于控制 ESP8266 模块的 CH_PD 和 RESET 线(即由 WIFI_CH_PD_PINWIFI_RESET_PIN 常量定义的引脚)设置为 OUTPUT,以便我们可以根据情况将其设置为 LOW 或 HIGH。这两个引脚在正常运行期间需要保持 HIGH。
  3. 通过拉低 WiFi 模块的 RESET 引脚(设置为 LOW 约 200 毫秒)执行硬件重置。
  4. 以 115200 波特率启动 UART/串行通信。
  5. 等待 WiFi 模块启动,这需要两秒钟或更长时间。
  6. 启动 UDP 通信,并在所有端口上等待传入数据。我们可以只使用一个特定端口,但我们希望保持灵活性。

UDP 通信用于 Android 设备和我们的天气站传感器节点之间的 WiFi 数据传输。下面显示了用于检查 WiFi 模块是否处于活动状态(通过 UART 线通信)的 checkWiFi 方法。

boolean checkWiFi() {
  if( esp.at() == ESP8266::Error::NONE) {
    digitalWrite( WIFI_DISABLED_LED_PIN, LOW);
    digitalWrite( WIFI_ACTIVE_LED_PIN, HIGH);
    return true;
  } else { 
    digitalWrite( WIFI_ACTIVE_LED_PIN, LOW);
    digitalWrite( WIFI_DISABLED_LED_PIN, HIGH);
    return false;
  }
}

此方法如果 ESP8266 模块响应 AT 命令,则返回 true,否则返回 falseAT 命令用于检查模块是否处于活动状态,它并不代表模块的实际命令。此外,checkWiFi 方法会启用(或禁用)红色/绿色 LED,提供当前 WiFi 状态的视觉指示。

由于 WiFi 设置只需要在硬件通电时运行一次,因此我们在 Arduino 特定的 setup 方法中调用 setupWiFi 模块。

void setup() {
  // setup WiFi - ESP8266 module
  setupWiFi();
  // add other code here...
}

从 DHT22 温湿度传感器读取数据

DHT22 传感器为我们的传感器节点提供温度和湿度值。它的刷新率为 0.5Hz,这意味着我们不能比每两秒钟读取一次传感器数据。读取数据相当容易,因为所有的硬编码都隐藏在 DHTLib 库中。我们编写了一个方法来获取这些值并计算温度和湿度的平均值。

void updateDHTData() {
  if ( dht22.read22( DHT22_PIN) == DHTLIB_OK) {
    avgTemperature = ( avgTemperature * (avgDhtStep - 1) + dht22.temperature) / (float)avgDhtStep;
    avgHumidity = ( avgHumidity * (avgDhtStep - 1) + dht22.humidity) / (float)avgDhtStep;
  }
}

最新的温度和湿度值可以通过读取 dht22 对象的 temperaturehumidity 属性来获取。使用 read22 方法可以在使用 DHT22 传感器时执行读取,但如果使用 DHT11 传感器,则可以使用 read11 来获得相同的效果。当读取成功时,该方法返回 DHTLIB_OK,如果遇到问题则返回各种错误代码。出于简化原因,我们忽略了可能的错误,但在后续教程中,我们还将讨论如何解决这些可能的问。

使用秘密的内置 Arduino 电压表读取 5V 电源电压值

一些 ATmega MCU,如 ATmega328/168(p) 具有内置电压传感器,可以通过代码访问。该传感器不是很精确(精度在 ±10% 范围内)。它使用这些 MCU 的内置 1.1V 电压参考(一些其他 ATmega MCU 也有 2.56V 的内部电压参考)。以下代码允许读取 AVcc 线电压,默认情况下它连接到 Arduino 板的 VCC 线。

float getVcc() {
  long result;
  // read 1.1V reference against AVcc
  ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
  // wait for Vref to settle
  delay(2); 
  // convert
  ADCSRA |= _BV(ADSC); 
  while (bit_is_set(ADCSRA,ADSC));
  result = ADCL;
  result |= ADCH<<8;
  // Back-calculate AVcc in mV
  result = 1103700L / result; 
  // return response in volts
  return result / 1000.0;
}

虽然这看起来很复杂,但它实际上使用了一些 MCU 寄存器和位操作来读取 VCC 值与内置的 1.1V 电压参考进行比较,该参考一旦校准就具有相当好的稳定性。通过使用高质量的万用表和稳定的电压源,可以在需要时“软件校准”单个 MCU 的内部电压参考。

计算可用 RAM 量

注意:关于 Arduino MCU 内存使用优化的完整指南以及获取可用 RAM 量详细信息的说明,请参阅我们的博客文章 优化 Arduino 内存使用

我们想收集的最后一块数据是我们的 Arduino MCU 上可用的 RAM 量。这可以通过调用 ESP8266 库中的 getFreeMCUMemory 方法来实现。它返回一个整数,表示调用该方法时 MCU 未使用的 RAM 字节数。

与 Android 设备进行 WiFi 通信

我们收集的所有传感器数据都需要发送到 Android 设备。朝着这个方向的第一步是监听来自 Android 设备的数据请求(定期发起数据请求)。为此,我们使用 loop 方法并等待传入数据。

void loop() {
  char data[10] = {0}, *ipdData = data;
  // Incomming data from ESP8266 module
  // Lengt must be greater than 7 bytes ("+IPD,n:")
  if ( Serial.available() > 7 && esp.ipd( ipdData) == ESP8266::Error::NONE) {
    // process the request
    processRequest( ipdData);
  }
  // a small delay before checking againd for WiFi incomming data
  delay( 25);
}

ipd 方法是 ESP8266 库的一部分,用于拆分通过 WiFi 接收的数据,并仅保留重要部分。WiFi 模块以以下格式发送数据:+IPD,n,id:ipdData,其中 nipdDatalengthid 是通信链接 ID(0 到 4 之间的整数),ipdData 表示相关数据字节。ipd 方法的第一个参数是引用参数,指向接收到的数据字节的指针。此方法有额外的可选参数,提供有关数据字节数和连接 ID 的信息。此外,名为 processRequest 的方法用于解码数据并执行所需的操作。

由于我们期望数据格式为 +IPD,n:(链接 ID 不使用),因此在收到至少 7 个字节后才处理数据是有意义的。此外,对于这个简单的项目,我们只期望一个数据字节,代表数据更新请求。在这个项目的后续版本中,我们希望支持更多命令,因此我们使用这种通用形式。同样,出于同样的原因,我们定义了一个枚举来定义可用命令列表:

enum class Command {
  GET_ALL_SENSORS_DATA = 97
};

我们通过 ESP8266 模块的 TX 线(即我们 Arduino 板的 RX 线)接收到的数据是:+IPD,1:a。97(Command::GET_ALL_SENSORS_DATA 枚举字面量)的 ASCII 表示是字符 a

下面显示了 processRequest 方法的代码:

void processRequest( char *data) {
  char progmemData[150] = {0};
  char *pData = progmemData;
  // first char represents the command
  char cmd = *(data); 
  switch ( (Command)cmd) {
    case Command::GET_ALL_SENSORS_DATA:
      createSensorsDataFromTemplate( pData);
      esp.atCipsend( pData);
    break;
    default:
      // nothing to do ...
    break;
  }
}

它的主要目的是解码收到的命令并以所需数据进行响应。如前所述,我们只有一个命令,因此也只有一个操作案例,但它将被扩展,所以即使是这种情况,我们也使用通用结构。相关代码涉及调用 createSensorsDataFromTemplate 方法。它使用基于 JSON 的数据模板,用真实数据替换占位符,并通过调用 ESP8266 库的 atCipsend 方法将响应发送到 Android 设备。

void createSensorsDataFromTemplate( char *&data) {
  char buffTemp[7] = {0}, buffAvgTemp[7] = {0}, buffAvgHum[7] = {0},
    buffHum[7] = {0}, buffVcc[5] = {0}, tmpl[140] = {0};
  char *pTmpl = tmpl;
  uint8_t templateLen = -1;
  // read template from PROGMEM
  getPMData( SENSORS_DATA_TEMPLATE, pTmpl, templateLen);
  // create data string from template by replacing
  // parameters with their actual values from sensors
  sprintf( data, pTmpl, 
    dtostrf( dht22.temperature, 6, 1, buffTemp),
    dtostrf( avgTemperature, 6, 1, buffAvgTemp),
    dtostrf( dht22.humidity, 6, 1, buffHum), 
    dtostrf( avgHumidity, 6, 1, buffAvgHum),
    dtostrf( getVcc(), 4, 2, buffVcc),
    getFreeMCUMemory());
}

使用 getPMData 实用方法(也是 ESP8266 库的一部分),从闪存中读取数据模板字符串。使用标准的 sprintf 方法用真实值替换参数。虽然对于功能齐全的 C/C++ 环境,您会使用 sprintf%x.yf 语法处理浮点数,但这在 Arduino 代码中不起作用。相反,我们使用 dtostrf 来格式化温度和湿度值(我们希望小数点后只有一位)。

编程 Arduino 板

重要:为了能够编译和构建此项目的代码,需要最新版本的 Android 软件(v1.6.6+)。其中一个原因是它使用了 C++11 特定的构造,例如 enum class,而旧版本的 Arduino 软件不支持 C++11。虽然也可以用较旧的 Arduino 软件版本完成,但这需要更改某些配置文件,因此可能会产生额外的问题。

我们需要通过使用 Arduino IDE 中的 Tools > Board 选择列表来选择正确的 Arduino 板。如果使用了本教程中讨论的 Arduino Pro Mini 板,我们必须选择 Arduino Pro or Pro Mini。此外,还需要选择用于编程 Arduino 板的通信端口(COM 端口),可在 Tools > Port 菜单下找到。最后,单击左上角的箭头按钮开始编译和部署过程。

注意: Arduino Pro Mini 板不像 Arduino Nano、UNO、MEGA2560 和其他板那样具有内置的自动复位功能。这意味着我们需要在 Arduino IDE 显示上传中时手动按下板上的复位按钮。可能需要几次尝试才能掌握按下按钮的正确时机,但必须在 IDE 显示上传中后不久按下。此外,在 Arduino 编程期间必须断开 ESP8266 的 TX 线,否则操作会失败。由于每次代码更新都需要这样做,因此可以使用跳线或开关来简化此任务。

Android 代码

使用 Android 应用程序来观察天气站传感器节点数据。对于我们的 Android 应用程序的开发,使用了 IntelliJ IDEA Ultimate。此 IDE 的免费社区版本也可用,可用于我们的项目。作为替代方案,您可以使用 Android Studion

Android 安全配置

使用您喜欢的 IDE 创建一个空的 Android 应用程序后,您需要做的第一件事就是编辑应用程序的安全设置。这些可以在 AndroidManifest.xml 文件中找到。由于我们需要使用 WiFi 通信,因此需要添加以下两个参数:

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>

此外,我们希望在应用程序运行时禁用 Android 设备的自动锁定功能,这需要设置以下权限:

<uses-permission android:name="android.permission.WAKE_LOCK" />

相同的 AndroidManifest.xml 文件定义了运行此应用程序所需的 Android OS 版本。我们使用 Android 4.3.1 (API 18)、4.4.2 (API 19) 和 5.0.1 (API 21) 对此应用程序进行了测试。因此,可以使用以下参数将最低要求版本设置为 API 18 是安全的:

<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="21"/>

虽然此应用程序可能在 4.3.1 (API 18) 以下的 Android OS 上运行,但我们在 Android 2.3.3 (API 10) 上的测试失败了。

创建 Android 用户界面

可以通过 UI 编辑器创建 Android 应用程序用户界面,或者如果您已经熟悉 Android,可以直接编辑布局文件。在我们的例子中,该文件名为 main.xml,位于 res/layout 文件夹下。

我们使用 ScrollView 容器作为用户界面父级,以支持小屏幕设备和较低物理分辨率。作为布局模板,我们使用 TableLayout,它包含两列:标签和值加上测量单位。

<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:fillViewport="true">
  <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="fill_parent"
               android:layout_height="fill_parent"
               android:id="@+id/main">
    <TableRow android:layout_width="fill_parent"
              android:layout_height="wrap_content">
      <!-- table row content -->
    </TableRow>
    <!-- more table rows... -->
  </TableLayout>
</ScrollView>

例如,用于显示当前温度值的行如下所示:

<TableRow android:layout_width="fill_parent"
          android:layout_height="wrap_content">
  <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="  Temperature:   "
            android:id="@+id/temperatureLabel"/>
  <TextView android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:text="N/A°C"
            android:id="@+id/temperatureValueTextView"/>
</TableRow>

每个 UI 元素都有一个 android:id 属性,其值是唯一的,用于从 Android Java 代码访问 UI 元素。结果用户界面如图 5 所示。

图 5:Android 应用程序用户界面。

编写 Android Java 代码

我们使用一个 Android activity 来实现我们的类。这是创建 Android 应用程序的最简单方法,特别是如果您是第一个 Android 应用程序。

public class MainActivity extends Activity {
  // ...here come all the properties and methods...
}

对于与天气站传感器节点进行 UDP 通信,我们使用 Java DatagramSocket,并将其初始化为端口 1024(也可以使用其他端口,从 1025 开始)。此代码应在尝试发送任何 UDP 数据包到网络之前执行。在此场景中,我们每 10 秒请求一次传感器数据。如果您愿意,可以随意修改为其他值。

public class MainActivity extends Activity {
  int udpPort = 1025;
  DatagramSocket socket;
  
  // other properties....  
  
  @Override
  public void onCreate( Bundle savedInstanceState) {
    // here are some other initializations
    (new Thread(new Runnable() {
      @Override
      public void run() {
        try {
          socket = new DatagramSocket(udpPort);
          while (true) {
            if (appInBackground) continue;
            try {
              sendUdpData( Commands.GET_ALL_SENSORS_DATA, null);
              Thread.sleep( 10000);
            } catch (Exception e) {
              e.printStackTrace();
            }
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    })).start();
  }
}

使用匿名的 Java 线程请求周期性数据更新。通过使用 Thread.sleep 静态方法获得 10 秒延迟,并通过 sendUdpData 方法执行数据更新请求。

void sendUdpData( Commands cmd, byte[] params) {
  try {
    final DatagramPacket packet;
    int paramsLength = ( params != null ? params.length : 0);
    byte data[] = new byte[paramsLength + 1];
    byte command[] = new byte[1];
    command[0] = cmd.getValue();
    System.arraycopy( command, 0, data, 0, command.length);
    if ( params != null) System.arraycopy(params, 0, data, 1, params.length);
    packet = new DatagramPacket( data, data.length, getBroadcastAddress(), udpPort);
    socket.send( packet);
  } catch( IOException e){
    e.printStackTrace();
  }
}

sendUdpData 方法接受两个参数:要发送的命令及其可选参数。请记住,对于 Arduino 代码,我们使用枚举来定义所有可用命令(嗯,目前只有一个命令,但以后会改变)。现在,Android 应用程序也是如此:使用 Java 枚举,我们定义 GET_ALL_SENSORS_DATA 命令,其命令代码相同,为 97(这是 Arduino 应用程序期望的)。

enum Commands {
  GET_ALL_SENSORS_DATA ( (byte)97);
  private final byte id;
  Commands( byte id) { this.id = id; }
  public byte getValue() { return id; }
}

此外,创建一个 DatagramPacket,并将命令(并在可能的情况下,命令参数)作为字节数组提供(如 DatagramPacket 构造函数所要求的)。然后将 UDP 数据包发送到天气站传感器节点。作为响应,传感器节点提供一个 JSON 对象,其中包含更新用户界面所需的传感器数据。由于 UDP 通信是异步的(我们不知道请求到达传感器节点需要多长时间,也不知道响应何时收到),因此使用一个线程来持续监听传入的 UDP 数据包。

public void onCreate( Bundle savedInstanceState) {
  (new Thread(new Runnable() {
    @Override
    public void run() {
      while (true) {
        DatagramPacket udpPacket = receiveUdpData( udpPort);
        if (udpPacket == null) continue;
        String udpPacketData =  new String( udpPacket.getData());
        try {
          JSONObject jsonObj = new JSONObject(udpPacketData);
          updateUserInterface( jsonObj);
        } catch ( JSONException e) {
          e.printStackTrace();
        } 
      }
    }
  })).start();
}
DatagramPacket receiveUdpData( int port) {
  try {
    byte[] data  = new byte[1024];
    DatagramPacket packet = new DatagramPacket( data, data.length);
    if ( socket == null) return null;
    socket.receive(packet);
    return packet;
  } catch( IOException e){
    e.printStackTrace();
    return null;
  }
}                

接收到的 UDP 数据(字节流)被转换为 JSON 对象并传递给 updateUserInterface 方法,该方法负责提取传感器值并将其显示在用户界面中。我们只显示处理温度值的代码,但湿度、电压和其他值也适用(请参阅完整的源代码)。

void updateUserInterface( final JSONObject jsonObj) {
  try {
    final double temperature = jsonObj.getDouble("temperature");
    temperatureValueTextView.post(new Runnable() {
      public void run() {
        temperatureValueTextView.setText(String.valueOf(temperature) + "°C");
      }
    });
  } catch (JSONException e) {
    e.printStackTrace();
  }
}

由于 Android API 的限制,UI 组件对象只能由创建它的线程修改。在我们的例子中,主应用程序线程创建 GUI 组件对象,而 updateUserInterface 由监听 UDP 数据的线程调用。在这种情况下,可以使用 post 方法,从而能够更新用户界面组件中的传感器值。通过使用相应的实现类(例如 TextView)并调用 findViewById 方法来获取对用户界面组件的引用,如下所示。

public class MainActivity extends Activity {
  // some other properties...

  TextView sensorsDataReceivedTimeTextView;
            
  public void onCreate( Bundle savedInstanceState) {
    sensorsDataReceivedTimeTextView = (TextView) findViewById(R.id.sensorsDataReceivedTimeTextView);
  }
  // some other methods...
}

我们的应用程序有一个特殊要求,即在应用程序运行时提供 Android 设备的“禁用自动休眠功能”。这意味着,只要应用程序处于活动状态,设备屏幕就会保持亮起。为了实现此行为,我们使用 Activity 超类继承的 window 对象的 addFlags 方法,并提供相应的参数。在我们的例子中,这些参数定义为 WindowManager.LayoutParams 枚举的字面量。

window.addFlags( WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON); 
window.addFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 

请记住,这需要通过如本教程前面所示编辑 AndroidManifest.xml 文件来启用 android.permission.WAKE_LOCK 权限。

进一步改进

我们可以通过考虑以下改进来扩展我们的项目:

  • 使用 GSM 模块(约 15 欧元)进行短信警报。如果某些测量参数不在预设限制内,我们可以收到手机短信警报。例如,如果温度降至 0°C 以下,我们想收到短信,因为这可能表明家庭供暖系统有问题。
  • 通过考虑各种问题情况来改进代码:
    • WiFi 模块无响应 - 定期检查模块并在需要时执行硬件重置。
    • DHT22 传感器错误 - 提供健壮的代码,在温度和湿度传感器不稳定或损坏时发出警报。
    • 使用 Arduino EEPROM 内存存储配置参数。
  • 改进硬件设计,使其能够由电池供电。这包括将我们用于 3.3V 线路的线性稳压器替换为更节能的(基于开关模式的)。
  • 允许使用太阳能为天气站节点的电池充电(特别是对于用于户外的、直接阳光下的节点)。
  • 添加土壤湿度传感器,提供关于何时浇灌植物的指示。
  • 添加光强度传感器,提供一种更好的方式来创建与昼夜相关的温度统计数据。尽管这可以在 Android 软件中完成,但它需要您的手机大部分时间连接到节点,而通常情况并非如此。
  • 为我们的节点添加实时时钟 (RTC),进一步改进关于测量环境特性的各种统计数据。

敬请关注!所有这些改进将在我们后续的教程中进行讨论。

© . All rights reserved.