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

DIY 蓝牙低功耗温湿度传感器

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (2投票s)

2024年2月26日

CPOL

14分钟阅读

viewsIcon

4058

使用 ESP32 和温湿度传感器配置一个蓝牙低功耗服务器,提供远程监控。

引言

ESP32 平台为托管蓝牙低功耗 (BLE) 应用提供了一个廉价且小巧的平台。本文介绍如何将低成本的温湿度传感器与 ESP32 平台结合,实现一个 BLE 服务器设备,该设备可提供温度、湿度和日期/时间读数。

ESP32 平台提供了一个小巧、可用、便捷且低成本的平台,用于托管 BLE 应用。它以多种外形尺寸提供,成本低于 10 美元,只需在亚马逊上搜索 ESP32 即可看到大量列表。

此特定应用程序仅使用 ESP32 的一个 GPIO 引脚,因此几乎所有可用平台都可以正常工作。

背景

本例中使用的 ESP32 平台是 LOLIN32 - 在亚马逊上售价低于 10 美元。该平台具有内置电池接口的优势,可实现电池供电和电池充电。

LOLIN 32 ESP32 development board

该应用程序将实现一个蓝牙低功耗 (BLE) 服务器 - 一个 BLE 端点设备,用于提供读写数据。在这种情况下,BLE 服务器将允许读取温度、湿度和日期时间。

远程 BLE 客户端可以连接到此设备并读取值。客户端还可以写入日期时间字段以设置 BLE 服务器中的日历和时钟。

只需稍加努力,此应用程序就可以修改 - 用另一个传感器或控制器替换温度和湿度传感器,以提供对其他量或设备的测量和/或控制。

硬件架构

一个 DHT-11 传感器通过三根线连接到 ESP32。

Front of the DHT11 sensor with red, yellow, brown wires connected.

在上图的照片中,您会看到红色线连接到“+”(vcc),棕色线连接到“-”(gnd),黄色线连接到“out”(数据)。

这三根线连接到 ESP32 如下:

  • “+”或“vcc” - 红色 - ESP32 3.3V 电源
  • “-”或“gnd” - 棕色 - ESP32 GND(接地)
  • “out” - 黄色 - ESP32 GPIO4

传感器本身使用一根线进行通信。这似乎是被称为“1-Wire”或“MicroLAN”的协议。此 链接描述了它的工作原理。这是一个特定于 DHT-11 传感器 的教程。基本上,这是一种低速通信协议,主要优点是只需要一根线。

DHT11 传感器通常处于低功耗睡眠模式。当 ESP32 向其发送“启动”信号时,DHT11 会唤醒,进行测量,并将数据发回。然后 DHT11 会自动再次进入睡眠模式。

DHT11 传感器是一种低成本传感器,可提供有限范围和精度的温度和湿度测量(0-50°C,精度为 2%,相对湿度为 20-80%,精度为 5%)。

较新型号 DHT22 传感器具有改进的范围和精度。(湿度 0-100%,精度 2-5%,温度 -40 至 80°C,精度 ±0.5°C)

此 ESP32 应用程序支持这两种传感器。通过配置文件中的条目选择传感器类型。

软件架构

软件架构由两个主要子组件组成。

  • 蓝牙低功耗 (BLE) 驱动程序 - ESP32 SDK 内置软件
  • DHT 传感器驱动程序 - 我们使用的是 Adafruit 稍作修改的驱动程序(添加了一个函数,允许在创建对象后更新传感器类型)

蓝牙低功耗 (BLE)

ESP32 SDK 提供实现 BLE 设备的软件。BLE 由使用 BLE 协议通过 2.4 GHz 无线电频率进行通信的本地网络组成(链接)。从软件角度来看,我们需要处理的 BLE 主要功能是:

  • 端点类型(客户端/服务器)
  • 服务和特性

本质上,BLE 服务器允许其他设备连接到它,并可能允许其他设备读写特性。当另一个设备读取特性时,它可能是一个“提供”给连接客户端的测量值,例如温度。

当另一个设备写入特性时,它可能是一个“提供”给连接客户端的操作,例如打开或关闭继电器以打开或关闭灯。

完整的规格说明大约有 100 厘米厚的打印纸,所以我们不深入讨论!

特性可以附加一个描述符 - 一个描述该特性的字符串。例如,表示测量温度的特性可能有一个描述符“temperature, F”。

在 BLE 中,一个或多个特性被分组到一个“服务”下。单个 BLE 服务器可能有一个或多个“服务”。因此,BLE 服务器提供的服务列表就像是服务器的顶层目录。特性就像是每个服务下的二级目录。

对于此应用程序,实现了三个特性:

  • 温度(华氏度)
  • 湿度(百分比)
  • 日期和时间“2023/02/20 12:34:56” - 服务器的内部时钟读数

这三个特性在此应用程序中被归类到一个服务下。

因此,客户端希望读取这些特性的典型操作是:

  • 连接到设备并查询其服务 - 它会找到一个服务
  • 查询该服务下的特性列表 - 它会找到三个特性
  • 读取所需特性的值

BLE 服务和特性还有另一个要讨论的主题。它们使用一个长达 36 位数字的十六进制字符串唯一命名,称为 UUID(通用唯一标识符)。每当需要用唯一名称标记某物时,这些 UUID 都会在各处使用。这些 UUID 可以通过多种方式生成,但一种简单的方法是使用 专用网站

对于 BLE,每个服务和每个特性都必须有自己的 UUID。

事实是,有些服务和特性非常常见,它们被分配了相同的特殊 UUID - 这些是 BLE 术语“GATT”(通用属性配置文件)的一部分。您可以在 此网站 上查找更多信息。对于此 BLE 服务器,我们没有使用 GATT - 尽管可能存在用于温度和湿度的 GATT。随时可以尝试!

BLE 广播

为了连接到设备,客户端必须知道要连接到哪个设备。为了解决这个问题,BLE 定义了一种称为“广播”的功能,其中活动的 BLE 服务器会定期“广播”自身,方法是发送一个特殊的 BLE 无线电数据包。客户端可以监听这些数据包来发现其范围内可能存在的 BLE 服务器。然后,它可以选择其中一个服务器进行连接。

有一个非常方便的应用程序(我认为是 iPhone 和 Android),名为 BT Inspector,可用于说明这一点。它允许手机监听其范围内广播的 BLE 服务器。然后,您可以使用该应用程序尝试连接到服务器并检查其服务和特性。该应用程序还允许读写特性。这是一个惊人的测试设备!

以下是使用 BT Inspector 的一些示例。第一项是扫描广播的 BLE 服务器。按“Scan”按钮。

Shows BT Inspector scanning for advertising BLE servers

在广告列表的列表中,您应该会看到您在此应用程序中创建的 BLE 设备。我在配置文件中将 BLE 名称设置为“DIY TempHumidity Sensor”。您将在上面的屏幕截图中看到它以黄色突出显示。

接下来,我们点击“DIY TempHumiditySensor”,这会带我们到一个可以连接的屏幕。此屏幕显示了从我们的 BLE 设备收到的广告数据包中包含的所有数据。

Screen showing BLE device advertised data

接下来,我们按“interrogate”按钮,这将使 BT Inspector 连接到设备,扫描其服务列表,并扫描每个服务以获取其特性列表。您将看到发现了一个服务,其中包含三个特性。

List of services and characteristics found after connecting to the BLE server

最后,我们可以点击单个服务,这会带我们到一个可以读取和/或写入特性的屏幕。

Screen showing read/write of a characteristic

如您所见,BT Inspector 应用是检查 BLE 设备的有用工具。有些设备不容易连接 - 它们可能需要身份验证和/或某种配对。但对于此应用程序,服务器非常开放 - 客户端可以连接并读取特性。

DHT 传感器

该应用程序使用 DHT-11 传感器,可提供温度和湿度测量。该传感器也很容易获得,在亚马逊上简单搜索就会得到多个结果。该传感器仅需三根线连接到 ESP32 - 有电源(3.3V)、接地(0V)和信号线。

在此应用程序中,传感器上的信号线连接到 ESP32 GPIO4 连接 - 允许软件配置传感器并读取其测量值。

如上图所示,三个传感器连接标记为“-”、“out”和“+”(或 GND、DATA 和 VCC)。ESP32 在运行过程中使用其 GPIO4 线与 DHT11 传感器通信。

Adafruit 的 DHT 驱动程序可以与 DHT-11 或更新、更准确的 DHT-22 传感器接口。所连接传感器的类型可以在配置文件中定义。

驱动程序将允许应用程序初始化与 DHT 传感器的连接,并读取温度和湿度。您只需告诉驱动程序连接的是哪种传感器,以及用于与其通信的 GPIO 引脚。

配置文件

应用程序将在启动时尝试从 ESP32 设备的 SPIFFS 分区读取配置文件。如果您将一个名为 *config.ini* 的文件放在设备 SPIFFS 的顶层文件夹中,它将在启动时被读取以配置应用程序。如果此文件无法读取,将使用这些参数的默认值。这是一个简单的文本文件,有四行。这是一个示例:

SERVERNAME=DIY TempHumidity Sensor
SENSOR=DHT11
UPDATERATE=5
UNITS=F

这是每行控制的内容:

  • SERVERNAME - 此行确定 BLE 服务器将广播的名称。如果您有多个设备,可以在每个 BLE 服务器上更改此名称,以便轻松识别特定设备。
  • SENSOR - 此行确定连接的温度/湿度传感器的类型。其值应为 DHT11 或 DHT22。
  • UPDATERATE - 此行确定 BLE 服务器更新温度和湿度特性的频率。这是每次更新之间的秒数。应介于 2 到 60 之间。
  • UNITS - 此行确定温度测量的单位 - 应为 C 或 F。

设置任务

设置任务是应用程序在 ESP32 上启动时执行的任务。这些任务初始化各种硬件和软件组件。此代码放置在 Arduino IDE 的 `setup()` 函数中。下面将看到 setup 函数的代码。

请注意,有一个“舒适”LED 会在特定时间闪烁。在设置任务期间,舒适 LED 会亮起。在操作任务期间,当读取传感器并更新特性时,舒适 LED 会亮起。

这些是设置任务的步骤:

  • 设置串行端口以允许输出诊断消息。通常,没有什么连接到此处,所以这些消息会被忽略。但如果您连接到 Arduino IDE 中的设备,您可以转到“Tools”|“Serial Monitor”来查看此端口输出的内容。首先,它将输出一个登录消息,表明应用程序正在启动。
  • 打开舒适 LED。
  • 将实时时钟初始化为固定日期 2024 年 1 月 1 日 00:00:00。
  • 从 SPIFFS 读取配置文件 - 根据此文件设置参数,或将其设置为默认值。
  • 初始化温度/湿度传感器的驱动程序。
  • 使用设备名称、服务和特性初始化 BLE 设备,并初始化客户端连接、断开连接或写入特性的回调。
  • 启动 BLE 设备并开始广播。将 BLE MAC 地址打印到诊断端口。
  • 为操作任务初始化一些变量。
  • 关闭舒适 LED。

请注意,需要相当多的代码才能将 BLE 设备配置、初始化并投入运行。它需要使用设备名称进行初始化,创建服务器,创建服务,创建所有特性,将特性添加到服务,最后启动广播过程。

//--------------------------------------------------------------------
// Setup tasks - called one time on power up
void setup() 
{
  char tmpbuf[32];

  //-- start up serial port for diagnostics
  Serial.begin(115200);
  Serial.println(SIGNON);

  //-- initialize signal LED to blink when in operation
  pinMode(LED,OUTPUT);
  digitalWrite(LED,LEDON);

  //-- initialize the RTC
  rtc.setTime(0,0,0,1,1,2024);

  //-- mount SPIFFS and read config file
  bleServerName[0] = '\0'; // "my diy temp sensor name"
  sensorIsDHT11 = true; // SENSOR=DHT11 or DHT12
  updateRateSec = 10; // read every 10 seconds

  if(!SPIFFS.begin(true))
  {
    print("An Error has occurred while mounting SPIFFS");
  }
  else
  {
    // read config file
    readKey(CONFIGFN,"SERVERNAME=",bleServerName,63);
    readKey(CONFIGFN,"SENSOR=",tmpbuf,15);
    sensorIsDHT11 = (strcmp(tmpbuf, "DHT11") == 0);
    readKey(CONFIGFN,"UPDATERATE=",tmpbuf,15); // update rate in seconds
    int x = atoi(tmpbuf);
    if (x < 2) x = 2;
    if (x > 60) x = 60;
    updateRateSec = x;
    tempIsF = true;
    readKey(CONFIGFN,"UNITS=",tmpbuf,15); // UNITS=C or UNITS=F, 
                                          // F is default temperature unit
    char c = tmpbuf[0];
    if ((c=='C') || (c == 'c')) tempIsF = false;

    readKey(BACKUPFN,"TIME=",tmpbuf,31);
    setRtcTime(tmpbuf);
  }

  // default server name if we can't read it from the configuration file
  if (bleServerName[0] == '\0')   strcpy(bleServerName,"DIY Temp Humidity Sensor");

  Serial.print("BTLE Name: "); Serial.println(bleServerName);

  //-- initialize the DHT - 11 sensor (or DHT-22)
  dht.setType(sensorIsDHT11 ? DHT11 : DHT22);
  dht.begin();  

  //-- Create the BLE Device
  // We need to initialize the device with a name
  BLEDevice::init(bleServerName);

  // Then we need to create the BLE Server
  pServer = BLEDevice::createServer();
  pServer->setCallbacks(new MyServerCallbacks());

  // Then we need to create the BLE Service that will hold characteristics
  dhtService = pServer->createService(SERVICE_UUID);

  // Then we need to create the Characteristics and Create a BLE Descriptor of each one
  // Temperature
  dhtService->addCharacteristic(&dhtTemperatureFahrenheitCharacteristics);
  dhtTemperatureFahrenheitDescriptor.setValue("DHT temperature Fahrenheit");
  dhtTemperatureFahrenheitCharacteristics.addDescriptor
                 (&dhtTemperatureFahrenheitDescriptor);

  // Humidity
  dhtService->addCharacteristic(&dhtHumidityCharacteristics);
  dhtHumidityDescriptor.setValue("DHT humidity");
  dhtHumidityCharacteristics.addDescriptor(new BLE2902());

  // Time
  // time is writable, so there's a call back for when a client writes the time
  dhtTimeCharacteristics.setCallbacks(new CharacteristicCallBack()); 
  dhtService->addCharacteristic(&dhtTimeCharacteristics);
  dhtTimeDescriptor.setValue("Date and Time yyyy/mo/da hr:mn:ss");
  dhtTimeCharacteristics.addDescriptor(new BLE2902());
  
  // Now we can start the service running
  dhtService->start();

  // Finally, let's start advertising that this server is alive
  BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
  pAdvertising->addServiceUUID(SERVICE_UUID);
  pServer->getAdvertising()->start();
  std::string myaddr = BLEDevice::getAddress().toString();
  Serial.print("BLE: Advertising and awaiting a client connection on ");
  Serial.println((char*)myaddr.c_str());
  

  //-- initialization for the app  
  lastSecond = rtc.getSecond();
  lastHr = rtc.getHour();
  secondCtr = 0;

  // LED has been on during initialization
  digitalWrite(LED,LEDOFF);
}

回调任务

ESP32 BLE 驱动程序使用回调机制来通知用户代码发生的某些事件。对于此应用程序,我们想知道连接和断开连接(当客户端连接或断开与服务器的连接时)。我们还想知道对时间/日期特性的写入 - 因为这是某个客户端试图让我们知道正确日期和时间的尝试。

为了实现这一点,我们需要根据 BLE 驱动程序提供的模板定义一些回调例程,如下所示:

首先,声明一个类实例来处理 `onConnect` 和 `onDisconnect` 事件。这只是设置或清除一个标志,让其余代码知道是否有客户端连接。

对于断开连接,我们还想重新开始广播 - 因为当客户端连接时,广播会自动禁用。所以如果我们断开连接时不重新启动广播,那么其他客户端将永远无法再次看到我们的 BLE 设备。

//--------------------------------------------------------------------
// BLE: Callbacks for onConnect and onDisconnect
class MyServerCallbacks: public BLEServerCallbacks {
  void onConnect(BLEServer* pServer) {
    deviceConnected = true;
  };
  void onDisconnect(BLEServer* pServer) {
    deviceConnected = false;
    pServer->startAdvertising();
  }
};

对于写入特性,我们声明一个类实例来处理时间日期特性上的 `onWrite` 事件。它看起来像这样:

char setTimeData[32] = "";
//--------------------------------------------------------------------
// callback for when time characteristic is written by some client
//
class CharacteristicCallBack : public BLECharacteristicCallbacks
{
public:
  void onWrite(BLECharacteristic *characteristic_) override
  {
    // client writes a date-time string to set the BLE device clock
    // Expect it to be exactly this format "2024/02/23 12:34:56"
    std::string ttag = characteristic_->getValue();
    strncpy(setTimeData, (char*)ttag.c_str(),31);
    // copy it to a special setting variable setTimeData
    // next 1 second task interval it will be picked up
    // and used to set the RTC
    //Serial.print("Time was written: ");
    //Serial.println((char*)ttag.c_str());
  }
};

您可以看到,此处理程序所做的就是将新值复制到变量 `setTimeData`。我们将在操作任务中看到新的数据已存入此处,并运行一些代码尝试使用写入的值来设置 RTC 到所需的日期/时间。

操作任务

操作任务在 Arduino IDE 的 `loop()` 函数中实现。在 `setup()` 函数调用之后,只要 ESP32 处于通电运行状态,就会不断调用此函数。

在这里,我们处理操作任务。在这种情况下,我们有一些任务需要每秒执行一次,有些任务需要每小时执行一次。我们通过观察 RTC 来触发这些任务,以查看时钟的秒或小时值何时发生变化。

正如您将在下面的代码中看到的,我们不断读取 rtc 秒值,当它改变时,我们会执行两个任务:

  • 检查是否有数据被写入到时间日期 - 通过客户端写入时间日期特性。如果是,我们将尝试根据客户端写入的内容设置 rtc。我们期望此数据格式为 yyyy/mm/dd hh:mm:ss。例如,如果客户端写入字符串“2024/02/18 12:34:56”,我们将设置 RTC。如果他们写入其他内容,例如“HELLO WORLD”,我们将忽略它。
  • 检查是否是读取传感器的时机。每隔几秒钟(从配置文件中读取 `UPDATERATE`),ESP32 将从 DHT 传感器读取温度和湿度值,并相应地更新特性。在此操作期间,舒适 LED 会亮起,所以如果您在此时查看电路板,您会看到这个 LED 发出短暂的光芒。

还有一个每小时一次的任务 - 每小时一次,我们读取 RTC 并将其值保存在 SPIFFS 顶层文件夹中的另一个文件 * /backup.dat* 中。

这样做是为了让我们知道 BLE 服务器上次运行的“近似”时间(精确到小时)。当我们启动并运行 `setup()` 代码时,RTC 被初始化为 `2024/01/01 00:00:00`。如果存在备份文件,它将被读取,RTC 时间将被更新为该时间。因此,如果 ESP32 意外断电或因某种原因重置,当它启动时,RTC 将被重置为上次激活时的小时。

在下面的 `loop()` 函数的底部,您将看到代码 - 每小时一次 - 每当 RTC 的小时改变时(xx:00:00),将时间写入此备份文件。

char tmpbuf[32];
//--------------------------------------------------------------------
// Called forever after setup() completes
void loop() 
{
  //-- detect when each second ticks by on the rtc
  int sec = rtc.getSecond();
  if (sec != lastSecond)
  {
    lastSecond = sec;
    secondCtr++;

    // inside this if() we do things every 1 second    
    // -- time characteristic handling
    if (strlen(setTimeData) > 0)
    {
      // if a client wrote a time, we set our internal clock to that time/date
      setRtcTime(setTimeData);
      setTimeData[0] = '\0';
      backupTimeValue();
    }
    else
    {
      // otherwise we update the characteristic value with current time/date
      String ttag = rtc.getTime("%Y/%m/%d %H:%M:%S");
      dhtTimeCharacteristics.setValue((char*)ttag.c_str());
    }

    // -- temperature and humidity characteristic handling
    if ((secondCtr % updateRateSec) == 0) // every 5 seconds or so
    {
      digitalWrite(LED,LEDON);
      // Read temperature as Fahrenheit 
      dht.setType(sensorIsDHT11 ? DHT11 : DHT22);
      float tempF = dht.readTemperature(tempIsF);
      // Read humidity
      float hum = dht.readHumidity();
      
      // Update temperature
      tmpbuf[0] = '\0';
      sprintf(tmpbuf,"%.1f", tempF);
      //dtostrf(tempF, 6, 2, tmpbuf);
      //Set temperature Characteristic value and notify connected client
      dhtTemperatureFahrenheitCharacteristics.setValue(tmpbuf);
      dhtTemperatureFahrenheitCharacteristics.notify();
      Serial.print("Temperature Fahrenheit: ");
      Serial.print(tmpbuf);
      Serial.print(" ºF");
      
      // update humidity characteristic
      tmpbuf[0] = '\0';
      sprintf(tmpbuf,"%.0f",hum);
      //dtostrf(hum, 6, 2, tmpbuf);
      //Set humidity Characteristic value and notify connected client
      dhtHumidityCharacteristics.setValue(tmpbuf);
      dhtHumidityCharacteristics.notify();   
      Serial.print(" - Humidity: ");
      Serial.print(tmpbuf);
      Serial.println(" %");

      // LED is on during 1 second processing
      // 1 second processing has completed, so turn it off
      digitalWrite(LED,LEDOFF);
    }
  }
  // Hourly tasks
  int hr = rtc.getHour();
  if (hr != lastHr)
  {
    lastHr = hr;
    backupTimeValue(); // once per hour, save current time
    // on reset, if there's a saved time value, use that as
    // the startup time
  }
}

以上就是 BLE 服务器应用程序的全部内容。您可以在 GitHub 上找到完整的代码。

后续思考

为 ESP32 开发这个应用程序很有趣。我似乎学到了很多关于蓝牙低功耗的知识。然而,考虑到完整规范大约有 100 厘米的打印材料,还有很多东西需要学习!

我认为使用此代码来实现具有不同功能的 BLE 服务器会相对直接 - 例如调暗灯光、控制继电器、读取照明、GPS 服务器等。

玩得开心!

历史

  • 版本 1.0,2024 年 2 月 25 日,Deangi
© . All rights reserved.