Skip to content

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 讲过)。

这样分工,以后加一种新的控制方式(网页、红外遥控),业务代码一行都不用改。

两种通道的分工 ​

蓝牙 BLEMQTT
要不要联网❌ 不用✅ 必须联网
距离约 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 的三条纪律

  1. 每次取字段都要判断有没有、类型对不对 —— 服务器返回错误格式时不判断会直接崩溃
  2. cJSON_Delete 绝对不能忘 —— 每 10 分钟漏一次,几天就把内存耗光
  3. 接收缓冲区要判断会不会溢出 —— 响应比预想的大时会踩坏内存

七、网络任务 ​

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 有每日次数限制。

动手试试 ​

  1. 打通"传感器 → 队列 → 屏幕",用手捂住传感器看数字变化。
  2. 实现自动背光,用手电筒照光敏电阻,看背光变化是否平滑。
  3. 申请一个免费天气 API,把当地温度显示到屏幕上。