DIY 蓝牙低功耗温湿度传感器
使用 ESP32 和温湿度传感器配置一个蓝牙低功耗服务器,提供远程监控。
引言
ESP32 平台为托管蓝牙低功耗 (BLE) 应用提供了一个廉价且小巧的平台。本文介绍如何将低成本的温湿度传感器与 ESP32 平台结合,实现一个 BLE 服务器设备,该设备可提供温度、湿度和日期/时间读数。
ESP32 平台提供了一个小巧、可用、便捷且低成本的平台,用于托管 BLE 应用。它以多种外形尺寸提供,成本低于 10 美元,只需在亚马逊上搜索 ESP32 即可看到大量列表。
此特定应用程序仅使用 ESP32 的一个 GPIO 引脚,因此几乎所有可用平台都可以正常工作。
背景
本例中使用的 ESP32 平台是 LOLIN32 - 在亚马逊上售价低于 10 美元。该平台具有内置电池接口的优势,可实现电池供电和电池充电。
该应用程序将实现一个蓝牙低功耗 (BLE) 服务器 - 一个 BLE 端点设备,用于提供读写数据。在这种情况下,BLE 服务器将允许读取温度、湿度和日期时间。
远程 BLE 客户端可以连接到此设备并读取值。客户端还可以写入日期时间字段以设置 BLE 服务器中的日历和时钟。
只需稍加努力,此应用程序就可以修改 - 用另一个传感器或控制器替换温度和湿度传感器,以提供对其他量或设备的测量和/或控制。
硬件架构
一个 DHT-11 传感器通过三根线连接到 ESP32。
在上图的照片中,您会看到红色线连接到“+”(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”按钮。
在广告列表的列表中,您应该会看到您在此应用程序中创建的 BLE 设备。我在配置文件中将 BLE 名称设置为“DIY TempHumidity Sensor”。您将在上面的屏幕截图中看到它以黄色突出显示。
接下来,我们点击“DIY TempHumiditySensor”,这会带我们到一个可以连接的屏幕。此屏幕显示了从我们的 BLE 设备收到的广告数据包中包含的所有数据。
接下来,我们按“interrogate”按钮,这将使 BT Inspector 连接到设备,扫描其服务列表,并扫描每个服务以获取其特性列表。您将看到发现了一个服务,其中包含三个特性。
最后,我们可以点击单个服务,这会带我们到一个可以读取和/或写入特性的屏幕。
如您所见,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