31、定时、远程、本地控制:一个ESP32自制智能插座的全能体验

31、定时、远程、本地控制:一个ESP32自制智能插座的全能体验

智能插座:远程家电控制(ESP-IDF版)

厌倦了出门后担心忘记关电器?想远程控制家里的设备?今天我们就来打造一个真正的智能插座,让你的手机变成万能遥控器!这不仅是个实用的小工具,更是你物联网技能的完美展示。本教程将使用 ESP-IDF v5.5.3 开发框架,并在 VSCode 中完成编写与调试。

1. 项目简介6

智能插座是智能家居的基础组件,它允许你通过手机或网络远程控制任何插在上面的电器。本项目将教你如何使用 ESP32S3、继电器模块和一些简单的电路,制作一个功能完整的智能插座,支持:

  • 远程开关控制
  • 定时开关功能
  • 用电状态监控(预留接口)
  • 本地物理按键控制

2. 硬件准备

核心组件

  • ESP32S3开发板:作为主控制器,提供 Wi-Fi 连接和处理能力
  • 继电器模块(5V):用于控制高电压电器的开关
  • AC-DC电源模块(5V/1A):为 ESP32 和继电器提供稳定电源
  • 按钮开关:用于本地手动控制
  • LED指示灯:显示当前工作状态
  • 电阻、电容等基础元件

安全警告 ⚠️

重要提醒:本项目涉及220V交流电操作,如果你没有相关经验,请务必在专业人士指导下进行,或者仅使用低压直流负载进行测试。安全永远是第一位的!

3. 电路设计原理

继电器工作原理

继电器是一个电磁开关,它可以用小电流(ESP32的3.3V GPIO)控制大电流(220V交流电)。当 ESP32 给继电器控制引脚输出高电平时,继电器内部的电磁铁吸合,接通高压电路;输出低电平时,电磁铁释放,断开高压电路。

图片[1]-31、定时、远程、本地控制:一个ESP32自制智能插座的全能体验-寻找资源网

电路连接图

  • ESP32 GPIO18 → 继电器控制输入
  • ESP32 GPIO19 → 按钮开关(使用内部上拉,低电平触发)
  • ESP32 GPIO21 → LED指示灯(串联220Ω限流电阻)
  • 继电器公共端(COM) → 220V火线输入
  • 继电器常开端(NO) → 220V火线输出到插座
  • 继电器常闭端(NC) → 不使用(保持断开)

4. 开发环境搭建(VSCode + ESP-IDF)

  1. 安装 VSCode 并打开。
  2. 在扩展商店搜索并安装 Espressif IDF 插件。
  3. F1 输入 ESP-IDF: Configure ESP-IDF extension,选择 ADVANCED 模式,下载 ESP-IDF v5.5.3 并配置工具链。
  4. 创建新项目:ESP-IDF: New Project,选择模板 template-app,命名为 smart_plug
  5. 打开项目后,通过 ESP-IDF: SDK Configuration editoridf.py menuconfig)配置项目:
    • Example Connection Configuration 中设置 Wi-Fi SSID 和密码(也可在代码中硬编码,但建议使用菜单配置)。
    • Component config → LWIP → Enable SNTP 中启用 SNTP 以获取网络时间。
    • Component config → ESP System Settings → SPIFFS configuration 中配置 SPIFFS 大小(如 1MB)。

5. 软件实现(ESP-IDF v5.5.3)

主要功能模块

  1. Wi-Fi 连接管理:使用事件循环自动连接并重连。
  2. HTTP 服务器:提供 RESTful API 和网页控制界面。
  3. MQTT 客户端:支持与智能家居平台(如 Home Assistant)集成。
  4. 定时任务:基于 NTP 网络时间的定时开关。
  5. 状态保存:使用 SPIFFS 文件系统保存继电器状态和定时配置。

完整代码实现

我们将项目分为多个源文件,但为简洁起见,这里将所有代码放在 main.c 中。实际开发建议按模块拆分。

#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_http_server.h"
#include "mqtt_client.h"
#include "esp_netif.h"
#include "esp_spiffs.h"
#include "driver/gpio.h"
#include "lwip/apps/sntp.h"
#include "cJSON.h"

// 日志标签
staticconstchar *TAG = "SMART_PLUG";

// 引脚定义
#define RELAY_GPIO      18
#define BUTTON_GPIO     19
#define LED_GPIO        21

// Wi-Fi 配置(建议通过 menuconfig 配置)
#define WIFI_SSID       CONFIG_EXAMPLE_WIFI_SSID
#define WIFI_PASS       CONFIG_EXAMPLE_WIFI_PASSWORD

// MQTT 配置
#define MQTT_BROKER_URL "mqtt://broker.hivemq.com:1883"
#define MQTT_TOPIC      "smart_plug/status"

// 全局变量
staticbool relay_state = false;
staticbool button_pressed = false;
staticuint32_t last_button_time = 0;
staticesp_mqtt_client_handle_t mqtt_client = NULL;
statichttpd_handle_t server = NULL;

// 定时任务结构
#define MAX_SCHEDULES 5
typedefstruct {
    uint8_t hour;
    uint8_t minute;
    bool action;   // true=开, false=关
    bool enabled;
} schedule_t;
staticschedule_t schedules[MAX_SCHEDULES];

// 函数声明
staticvoidwifi_init_sta(void);
staticvoidhttp_server_init(void);
staticvoidmqtt_app_start(void);
staticvoidspiffs_init(void);
staticvoidntp_init(void);
staticvoidsave_relay_state(void);
staticvoidload_relay_state(void);
staticvoidpublish_mqtt_status(void);
staticvoidbutton_task(void *pvParameters);
staticvoidschedule_task(void *pvParameters);

/* ------------------- HTTP 服务器处理函数 ------------------- */
staticesp_err_troot_get_handler(httpd_req_t *req)
{
    constchar resp[] = "OK";
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, resp, strlen(resp));
    return ESP_OK;
}

staticesp_err_tstatus_get_handler(httpd_req_t *req)
{
    char json[64];
    snprintf(json, sizeof(json), "{\"state\":%s}", relay_state ? "true" : "false");
    httpd_resp_set_type(req, "application/json");
    httpd_resp_send(req, json, strlen(json));
    return ESP_OK;
}

staticesp_err_ttoggle_get_handler(httpd_req_t *req)
{
    char query[32];
    size_t query_len = httpd_req_get_url_query_len(req);
    if (query_len > 0) {
        httpd_req_get_url_query_str(req, query, sizeof(query));
        char param[8];
        if (httpd_query_key_value(query, "cmd", param, sizeof(param)) == ESP_OK) {
            if (strcmp(param, "on") == 0) {
                relay_state = true;
                gpio_set_level(RELAY_GPIO, 1);
                gpio_set_level(LED_GPIO, 1);
                save_relay_state();
                publish_mqtt_status();
            } elseif (strcmp(param, "off") == 0) {
                relay_state = false;
                gpio_set_level(RELAY_GPIO, 0);
                gpio_set_level(LED_GPIO, 0);
                save_relay_state();
                publish_mqtt_status();
            }
        }
    }
    httpd_resp_sendstr(req, "OK");
    return ESP_OK;
}

staticconsthttpd_uri_t uri_root = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = root_get_handler,
    .user_ctx  = NULL
};

staticconsthttpd_uri_t uri_status = {
    .uri       = "/status",
    .method    = HTTP_GET,
    .handler   = status_get_handler,
    .user_ctx  = NULL
};

staticconsthttpd_uri_t uri_toggle = {
    .uri       = "/toggle",
    .method    = HTTP_GET,
    .handler   = toggle_get_handler,
    .user_ctx  = NULL
};

staticvoidhttp_server_init(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;
    if (httpd_start(&server, &config) == ESP_OK) {
        httpd_register_uri_handler(server, &uri_root);
        httpd_register_uri_handler(server, &uri_status);
        httpd_register_uri_handler(server, &uri_toggle);
        ESP_LOGI(TAG, "HTTP server started");
    } else {
        ESP_LOGE(TAG, "Failed to start HTTP server");
    }
}

/* ------------------- MQTT 客户端 ------------------- */
staticvoidmqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;
    switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI(TAG, "MQTT connected");
            publish_mqtt_status();
            break;
        case MQTT_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "MQTT disconnected");
            break;
        default:
            break;
    }
}

staticvoidmqtt_app_start(void)
{
    esp_mqtt_client_config_t mqtt_cfg = {
        .broker.address.uri = MQTT_BROKER_URL,
    };
    mqtt_client = esp_mqtt_client_init(&mqtt_cfg);
    esp_mqtt_client_register_event(mqtt_client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
    esp_mqtt_client_start(mqtt_client);
}

staticvoidpublish_mqtt_status(void)
{
    if (mqtt_client) {
        constchar *payload = relay_state ? "ON" : "OFF";
        esp_mqtt_client_publish(mqtt_client, MQTT_TOPIC, payload, 0, 1, 0);
    }
}

/* ------------------- SPIFFS 状态保存 ------------------- */
staticvoidspiffs_init(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path = "/spiffs",
        .partition_label = NULL,
        .max_files = 5,
        .format_if_mount_failed = true
    };
    esp_err_t ret = esp_vfs_spiffs_register(&conf);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to mount SPIFFS");
    } else {
        ESP_LOGI(TAG, "SPIFFS mounted");
    }
}

staticvoidsave_relay_state(void)
{
    FILE *f = fopen("/spiffs/state.txt", "w");
    if (f) {
        fprintf(f, "%d", relay_state ? 1 : 0);
        fclose(f);
        ESP_LOGI(TAG, "State saved");
    }
}

staticvoidload_relay_state(void)
{
    FILE *f = fopen("/spiffs/state.txt", "r");
    if (f) {
        int val;
        if (fscanf(f, "%d", &val) == 1) {
            relay_state = (val == 1);
            gpio_set_level(RELAY_GPIO, relay_state ? 1 : 0);
            gpio_set_level(LED_GPIO, relay_state ? 1 : 0);
        }
        fclose(f);
    }
}

/* ------------------- Wi-Fi 连接 ------------------- */
staticvoidwifi_event_handler(void *arg, esp_event_base_t event_base,
                               int32_t event_id, void *event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } elseif (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        esp_wifi_connect();
    } elseif (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
        ESP_LOGI(TAG, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
    }
}

staticvoidwifi_init_sta(void)
{
    esp_netif_init();
    esp_event_loop_create_default();
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    esp_wifi_init(&cfg);

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &instance_any_id);
    esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &instance_got_ip);

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_WPA2_PSK,
        },
    };
    esp_wifi_set_mode(WIFI_MODE_STA);
    esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
    esp_wifi_start();

    ESP_LOGI(TAG, "Wi-Fi connecting to %s...", WIFI_SSID);
}

/* ------------------- NTP 时间同步 ------------------- */
staticvoidntp_init(void)
{
    sntp_setoperatingmode(SNTP_OPMODE_POLL);
    sntp_setservername(0, "pool.ntp.org");
    sntp_init();
    ESP_LOGI(TAG, "NTP started");
}

/* ------------------- 按钮处理任务 ------------------- */
staticvoidbutton_task(void *pvParameters)
{
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_INPUT,
        .pin_bit_mask = (1ULL << BUTTON_GPIO),
        .pull_up_en = 1,
    };
    gpio_config(&io_conf);

    while (1) {
        int level = gpio_get_level(BUTTON_GPIO);
        if (level == 0) {  // 低电平有效(按下)
            if (!button_pressed) {
                button_pressed = true;
                last_button_time = xTaskGetTickCount() * portTICK_PERIOD_MS;
            } else {
                // 消抖延时
                if ((xTaskGetTickCount() * portTICK_PERIOD_MS - last_button_time) > 50) {
                    // 切换继电器
                    relay_state = !relay_state;
                    gpio_set_level(RELAY_GPIO, relay_state ? 1 : 0);
                    gpio_set_level(LED_GPIO, relay_state ? 1 : 0);
                    save_relay_state();
                    publish_mqtt_status();
                    ESP_LOGI(TAG, "Button toggled, state=%d", relay_state);
                    button_pressed = false; // 简单处理,防止连续触发
                }
            }
        } else {
            button_pressed = false;
        }
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

/* ------------------- 定时任务检查 ------------------- */
staticvoidschedule_task(void *pvParameters)
{
    // 等待时间同步
    time_t now = 0;
    struct tm timeinfo = {0};
    int retry = 0;
    constint retry_count = 20;
    while (sntp_get_sync_status() == SNTP_SYNC_STATUS_RESET && ++retry < retry_count) {
        vTaskDelay(2000 / portTICK_PERIOD_MS);
    }
    time(&now);
    localtime_r(&now, &timeinfo);
    ESP_LOGI(TAG, "Time synced: %02d:%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(60000)); // 每分钟检查一次
        time(&now);
        localtime_r(&now, &timeinfo);
        int hour = timeinfo.tm_hour;
        int minute = timeinfo.tm_min;

        for (int i = 0; i < MAX_SCHEDULES; i++) {
            if (schedules[i].enabled &&
                schedules[i].hour == hour &&
                schedules[i].minute == minute) {
                relay_state = schedules[i].action;
                gpio_set_level(RELAY_GPIO, relay_state ? 1 : 0);
                gpio_set_level(LED_GPIO, relay_state ? 1 : 0);
                save_relay_state();
                publish_mqtt_status();
                ESP_LOGI(TAG, "Schedule triggered: %s", relay_state ? "ON" : "OFF");
            }
        }
    }
}

/* ------------------- 主程序入口 ------------------- */
voidapp_main(void)
{
    // 初始化 NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 初始化 GPIO
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_DISABLE,
        .mode = GPIO_MODE_OUTPUT,
        .pin_bit_mask = (1ULL << RELAY_GPIO) | (1ULL << LED_GPIO),
    };
    gpio_config(&io_conf);
    gpio_set_level(RELAY_GPIO, 0);
    gpio_set_level(LED_GPIO, 0);

    // 挂载 SPIFFS 并加载状态
    spiffs_init();
    load_relay_state();

    // 连接 Wi-Fi
    wifi_init_sta();

    // 启动 HTTP 服务器
    http_server_init();

    // 启动 MQTT
    mqtt_app_start();

    // 启动 NTP
    ntp_init();

    // 创建按钮处理任务
    xTaskCreate(button_task, "button_task", 2048, NULL, 10, NULL);

    // 初始化定时任务(这里简单示例,实际可从文件加载)
    for (int i = 0; i < MAX_SCHEDULES; i++) {
        schedules[i].enabled = false;
    }
    // 创建定时检查任务
    xTaskCreate(schedule_task, "schedule_task", 3072, NULL, 5, NULL);
}

代码说明

  • Wi-Fi 连接:使用 esp_wifi 和事件循环自动连接,断开后自动重连。
  • HTTP 服务器:基于 esp_http_server 组件,提供了 /(可替换为完整 HTML 页面)、/status 和 /toggle 接口。
  • MQTT:使用 mqtt_client 连接公共 Broker,发布继电器状态。
  • SPIFFS:保存继电器状态到文件系统,断电后恢复。
  • 按钮处理:单独任务轮询 GPIO,带简单消抖。
  • 定时任务:等待 NTP 同步后,每分钟检查一次定时表。
  • HTML 页面:代码中暂未包含完整 HTML,你可以将原文章中的 HTML 字符串嵌入到 root_get_handler 中,使用 httpd_resp_send 发送。为避免过长,这里仅返回 “OK”,读者可自行替换。

编译与烧录

  1. 在 VSCode 中打开项目,按 F1 执行 ESP-IDF: Set Espressif device target 选择你的芯片(如 esp32S3)。
  2. 连接开发板,执行 ESP-IDF: Build, Flash and Monitor 或使用快捷键。
  3. 在串口监视器中查看输出,当显示 Got IP: xxx 后,即可通过浏览器访问 ESP32 的 IP 地址。

6. 安全注意事项

电气安全

  • 绝缘处理:所有高压部分必须用绝缘材料完全包裹
  • 外壳选择:使用阻燃材料的外壳,确保用户无法接触到内部电路
  • 过载保护:考虑添加保险丝或过流保护
  • 接地处理:确保设备正确接地

网络安全

  • 密码保护:Web 界面应添加基本的身份验证(ESP-IDF 支持 HTTP Basic Auth)
  • HTTPS 支持:生产环境中可使用 esp_tls 组件启用 HTTPS
  • 固件更新:支持 OTA 升级(ESP-IDF 提供 esp_https_ota

7. 功能扩展

基础版本完成后,你可以考虑以下扩展:

  • 电量统计:添加电流/电压传感器(如 HLW8032),通过 ESP32 读取并上传。
  • 语音控制:集成 Alexa 或 Google Assistant,通过 MQTT 桥接。
  • 场景联动:与其他智能设备联动(如温度过高时自动关闭),可通过 MQTT 订阅实现。
  • 能耗分析:记录历史用电数据,保存到 SPIFFS 或发送到云端。

8. 调试与测试

测试步骤

  1. 低压测试:先用 5V 直流负载(如 LED)测试继电器控制逻辑。
  2. Wi-Fi 连接:确认 ESP32 能正常连接网络并分配 IP。
  3. Web 界面:在浏览器中访问 ESP32 IP,测试所有控制功能。
  4. 高压测试:在确保安全的前提下,连接实际电器进行测试。

常见问题排查

  • 继电器不动作:检查 GPIO 电平是否正常,继电器模块供电是否足够。
  • Wi-Fi 连接失败:确认 SSID 和密码正确,信号强度足够;可打开 Wi-Fi 日志调试。
  • 网页无法访问:检查防火墙设置,确认 IP 地址无误;可在代码中添加打印输出确认服务器启动。
  • 状态不同步:检查 SPIFFS 挂载是否成功,文件读写权限。
  • 时间不同步:确保网络通畅,可增加 NTP 重试次数。

9. 总结

恭喜你!你已经使用 ESP-IDF v5.5.3 成功制作了一个功能完整的智能插座。这个项目不仅实用,还涵盖了 ESP32 开发的多个重要方面:GPIO 控制、Wi-Fi 连接、HTTP 服务器、MQTT 通信、文件系统、定时任务和 FreeRTOS 任务管理。相比 Arduino 框架,ESP-IDF 提供了更精细的控制和更高的性能,适合构建更复杂的物联网设备。

记住:虽然这个项目很有趣,但涉及高压电时一定要谨慎。如果你打算长期使用,建议购买经过认证的商用智能插座,它们在安全性和可靠性方面都有更好的保障。

下期预告:我们将学习如何制作一个智能调光灯,通过 PWM 技术实现平滑的亮度调节,让你的房间灯光更加温馨舒适!💡

本教程代码基于 ESP-IDF v5.5.3。

来自:老才科技学习记录

原创:老才

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
相关推荐
评论 抢沙发

请登录后发表评论

    暂无评论内容