智能插座:远程家电控制(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 给继电器控制引脚输出高电平时,继电器内部的电磁铁吸合,接通高压电路;输出低电平时,电磁铁释放,断开高压电路。
电路连接图
-
ESP32 GPIO18 → 继电器控制输入 -
ESP32 GPIO19 → 按钮开关(使用内部上拉,低电平触发) -
ESP32 GPIO21 → LED指示灯(串联220Ω限流电阻) -
继电器公共端(COM) → 220V火线输入 -
继电器常开端(NO) → 220V火线输出到插座 -
继电器常闭端(NC) → 不使用(保持断开)
4. 开发环境搭建(VSCode + ESP-IDF)
-
安装 VSCode 并打开。 -
在扩展商店搜索并安装 Espressif IDF 插件。 -
按 F1输入ESP-IDF: Configure ESP-IDF extension,选择 ADVANCED 模式,下载 ESP-IDF v5.5.3 并配置工具链。 -
创建新项目: ESP-IDF: New Project,选择模板template-app,命名为smart_plug。 -
打开项目后,通过 ESP-IDF: SDK Configuration editor(idf.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)
主要功能模块
-
Wi-Fi 连接管理:使用事件循环自动连接并重连。 -
HTTP 服务器:提供 RESTful API 和网页控制界面。 -
MQTT 客户端:支持与智能家居平台(如 Home Assistant)集成。 -
定时任务:基于 NTP 网络时间的定时开关。 -
状态保存:使用 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”,读者可自行替换。
编译与烧录
-
在 VSCode 中打开项目,按 F1执行ESP-IDF: Set Espressif device target选择你的芯片(如esp32S3)。 -
连接开发板,执行 ESP-IDF: Build, Flash and Monitor或使用快捷键。 -
在串口监视器中查看输出,当显示 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. 调试与测试
测试步骤
-
低压测试:先用 5V 直流负载(如 LED)测试继电器控制逻辑。 -
Wi-Fi 连接:确认 ESP32 能正常连接网络并分配 IP。 -
Web 界面:在浏览器中访问 ESP32 IP,测试所有控制功能。 -
高压测试:在确保安全的前提下,连接实际电器进行测试。
常见问题排查
-
继电器不动作:检查 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。
来自:老才科技学习记录
原创:老才














![表情[chi]-寻找资源网](http://www.seekresource.com/wp-content/themes/zibll/img/smilies/chi.gif)



暂无评论内容