外观
3.6 传感器采集与远程交互
这节要搞懂:读温湿度、拉天气、接收手机的远程命令。 需要基础:3.5
一、读温湿度(AHT20)
这类传感器的用法都是"三步走":
① 发一条命令:"开始测量"
② 等 80 毫秒(它需要时间完成转换)
③ 读回 6 个字节,按公式算出温度和湿度c
#define AHT20_ADDR 0x38
esp_err_t bsp_sensor_read(float *temperature, float *humidity)
{
// ① 发送触发测量命令
uint8_t cmd[3] = { 0xAC, 0x33, 0x00 };
if (i2c_master_transmit(s_sensor_dev, cmd, sizeof(cmd), 1000) != ESP_OK) {
return ESP_FAIL;
}
// ② 等它测量完(手册要求至少 75 毫秒)
vTaskDelay(pdMS_TO_TICKS(80));
// ③ 读回 6 个字节
uint8_t data[6];
if (i2c_master_receive(s_sensor_dev, data, sizeof(data), 1000) != ESP_OK) {
return ESP_FAIL;
}
// ④ 按手册的公式换算(照抄就行)
uint32_t raw_humidity = ((uint32_t)data[1] << 12)
| ((uint32_t)data[2] << 4)
| (data[3] >> 4);
uint32_t raw_temperature = (((uint32_t)data[3] & 0x0F) << 16)
| ((uint32_t)data[4] << 8)
| data[5];
*humidity = raw_humidity * 100.0f / 1048576.0f; // 1048576 = 2^20
*temperature = raw_temperature * 200.0f / 1048576.0f - 50.0f;
return ESP_OK;
}那个 80 毫秒的等待
这就是为什么传感器要放在单独的任务里 —— 它会阻塞 80 毫秒,放在界面任务里会让界面卡顿。
二、传感器任务
c
void sensor_task(void *pvParameters)
{
TickType_t last_wake_time = xTaskGetTickCount();
int fail_count = 0;
while (1) {
float temperature, humidity;
if (bsp_sensor_read(&temperature, &humidity) == ESP_OK) {
fail_count = 0;
// 存进共享数据,主界面循环会自己来读(3.3 讲过 s_data_mutex)
xSemaphoreTake(s_data_mutex, portMAX_DELAY);
s_temperature = temperature;
s_humidity = humidity;
xSemaphoreGive(s_data_mutex);
}
else {
fail_count++;
if (fail_count >= 3) {
ESP_LOGE(TAG, "sensor failed %d times", fail_count);
}
// ⭐ 注意:失败时什么都不做,保留上次的数值
}
// 顺便读光敏、调背光
backlight_auto_adjust();
vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(2000));
}
}读失败时别把界面清零
新手常见错误:读失败就显示 0℃。
结果屏幕上温度一会儿正常一会儿变 0,看着像坏了。
正确做法:读失败就什么都不改,界面保持上次的数值。 连续失败很多次再报警。
三、自动背光要"平滑"
直接把光敏读数换算成亮度,手一晃屏幕就闪,体验很差。
解决办法:每次只朝目标值靠一点点(一阶低通滤波)。
c
void backlight_auto_adjust(void)
{
int raw = 0;
adc_oneshot_read(adc_handle, ADC_CHANNEL, &raw); // 读光敏,0~4095
// 1. 换算成目标亮度:环境越亮,背光越亮
int target = 100 + raw * 900 / 4095;
if (target > 1000) {
target = 1000;
}
// 2. ⭐ 每次只移动差值的 1/8,变化就平滑了
static int current = 500;
current = current + (target - current) / 8;
backlight_set(current);
}分母 8 越大越迟钝,可以自己调着试。
四、远程命令:只管报信,不管业务
这台设备没有按键,"用户操作"来自两个地方:MQTT(远程,走网络)和 蓝牙(近距离直连)。
具体的回调代码在 3.3 已经给过了,这里只强调设计原则,它和按键其实一模一样:
c
// 不管消息从哪来,回调里只做两件事:解析、投队列
// BLE / MQTT 收到 JSON 后,统一交给这个入口
static void on_config_json(const char *json)
{
// 内部解析出认识的字段,最后调 app_config_update()
// 写 NVS + 触发配置变化回调(3.3 讲过)
app_config_update_from_json(json);
}回调完全不知道"设了闹钟之后要存 NVS、要改硬件" —— 那是 app_config_update() 和 config_apply_task 的事(3.3 讲过)。
这样分工,以后加一种新的控制方式(网页、红外遥控),业务代码一行都不用改。
两种通道的分工
| 蓝牙 BLE | MQTT | |
|---|---|---|
| 要不要联网 | ❌ 不用 | ✅ 必须联网 |
| 距离 | 约 10 米 | 任意远 |
| 手机端 | 专用 App / 微信小程序 | MQTT 客户端 App |
| 适合 | 首次配置、没网时兜底 | 日常远程控制 |
为什么两种都要
没网的时候蓝牙还能用 —— 这是它最大的价值。
设备刚拆封、路由器坏了、换了个地方,都能靠蓝牙先把它调起来(顺便还能用蓝牙配网,见 3.8)。
五、什么是 JSON
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,用"键: 值"的方式描述结构化数据。绝大多数 Web API 都用它返回数据。
天气服务器返回给你的大概长这样:
json
{
"now": {
"temp": "28",
"text": "晴"
}
}我们要做的就是从这一坨文本里把 temp 取出来,用 cJSON 库。
六、拉天气
需要先去申请一个免费的天气 API(和风天气、OpenWeather 都有免费额度),拿到一个 key。
c
#include "esp_http_client.h"
#include "cJSON.h"
static char s_response[2048];
static int s_response_len = 0;
// 数据到达时会调用这个函数
static esp_err_t http_event_handler(esp_http_client_event_t *evt)
{
if (evt->event_id == HTTP_EVENT_ON_DATA) {
// ⭐ 一定要判断会不会装不下
if (s_response_len + evt->data_len < sizeof(s_response) - 1) {
memcpy(s_response + s_response_len, evt->data, evt->data_len);
s_response_len += evt->data_len;
s_response[s_response_len] = '\0';
}
}
return ESP_OK;
}
esp_err_t weather_fetch(int *out_temperature)
{
s_response_len = 0;
esp_http_client_config_t config = {
.url = "https://api.example.com/weather?city=hangzhou&key=YOUR_KEY",
.event_handler = http_event_handler,
.timeout_ms = 10000,
.crt_bundle_attach = esp_crt_bundle_attach, // HTTPS 需要
};
esp_http_client_handle_t client = esp_http_client_init(&config);
esp_err_t err = esp_http_client_perform(client);
int status = esp_http_client_get_status_code(client);
esp_http_client_cleanup(client);
if (err != ESP_OK || status != 200) {
ESP_LOGW(TAG, "http failed, status = %d", status);
return ESP_FAIL;
}
// 解析 JSON
cJSON *root = cJSON_Parse(s_response);
if (root == NULL) {
return ESP_FAIL;
}
esp_err_t result = ESP_FAIL;
cJSON *now = cJSON_GetObjectItem(root, "now");
if (cJSON_IsObject(now)) { // ⭐ 每一层都要判断
cJSON *temp = cJSON_GetObjectItem(now, "temp");
if (cJSON_IsString(temp)) {
*out_temperature = atoi(temp->valuestring);
result = ESP_OK;
}
}
cJSON_Delete(root); // ⭐ 一定要释放!
return result;
}解析 JSON 的三条纪律
- 每次取字段都要判断有没有、类型对不对 —— 服务器返回错误格式时不判断会直接崩溃
cJSON_Delete绝对不能忘 —— 每 10 分钟漏一次,几天就把内存耗光- 接收缓冲区要判断会不会溢出 —— 响应比预想的大时会踩坏内存
七、网络任务
c
void weather_task(void *pvParameters)
{
while (1) {
// weather_task 是 WiFi 连上之后才创建的,所以这里不用再判断有没有网
{
int temperature;
if (weather_fetch(&temperature) == ESP_OK) {
xSemaphoreTake(s_data_mutex, portMAX_DELAY);
s_weather_temp = temperature;
xSemaphoreGive(s_data_mutex);
}
}
vTaskDelay(pdMS_TO_TICKS(10 * 60 * 1000)); // 10 分钟拉一次
}
}别拉太频繁
免费的天气 API 都有每天调用次数限制(比如 1000 次)。
10 分钟一次 = 一天 144 次,完全够用。设成 10 秒一次会很快超额被封。
本节要点
- 传感器读取会阻塞,必须放独立任务
- ⭐ 读失败时保留上次数值,别让界面闪
- 自动背光要做平滑滤波
- MQTT / BLE 回调只管报信,业务交给界面任务
- JSON = 键值对格式的数据,用 cJSON 解析
- ⭐ JSON 每层都要判断,
cJSON_Delete不能忘 - 天气 10 分钟拉一次就够
操作过程
目标:屏幕上显示室内温湿度和网上拉的天气,背光会跟着环境明暗自动变化。
第 1 步:先单独验证传感器
在 app_task 里临时加一段,确认传感器本身没问题:
c
float t, h;
if (bsp_sensor_read(&t, &h) == ESP_OK) {
ESP_LOGI(TAG, "sensor ok: %.1f C %.1f %%", t, h);
} else {
ESP_LOGE(TAG, "sensor read failed");
}第 2 步:编译烧录
第 3 步:应该看到什么
串口:
I (3200) main: sensor ok: 26.5 C 62.1 %
I (5500) weather: fetch ok, temp = 28屏幕:室内温湿度、天气都显示出来了。
第 4 步:用手捂住传感器
捂住 10 秒,屏幕上的温度和湿度应该明显上升(体温和呼吸的水汽)。
松开后慢慢降回去。
这一步很重要
它证明的不只是"能读到数",而是整条链路是通的:传感器 → sensor_task → 共享数据 → 主界面刷新。
第 5 步:验证自动背光
用手完全遮住光敏电阻 → 屏幕应该变暗 用手电筒照它 → 屏幕应该变亮
变化应该是平滑的,不是一下子跳过去。
第 6 步:验证天气
天气要联网才有。看串口有没有拉到:
I (12000) weather: fetch ok, temp = 28屏幕上的城市和温度应该和手机天气 App 对得上。
想立刻看到刷新
天气默认 10 分钟才拉一次。调试时可以用蓝牙把间隔改短:
json
{"refresh_minutes":1}测完记得改回 10 —— 免费 API 有每日次数限制。
动手试试
- 打通"传感器 → 队列 → 屏幕",用手捂住传感器看数字变化。
- 实现自动背光,用手电筒照光敏电阻,看背光变化是否平滑。
- 申请一个免费天气 API,把当地温度显示到屏幕上。