- 项目介绍+设计思路
网上常见的智能家居相关项目都是在低电压下工作,而对于市电控制的资料相对较少。因此在这次项目中我打算好好研究一下继电器的设计与使用,毕竟关乎安全。计划通过ESP32C3制作一个220V继电器模块,实现接入家庭物联网中枢home assistant并控制家用电器。
整个项目一共由三个部分构成,上位机由Linux开发板组成,在上面搭建docker环境,并运行homeassistant容器,作为整个家庭物联网中心。
第二部分是MQTT通信。目前homeassistant支持的通信协议非常多,这次选择MQTT,可以实现更大的兼容性,方便在不同平台上进行移植。
第三部分是控制节点。也就是我们的ESP32-C3继电器。这次项目代码使用Arduino进行编写,Arduino的库非常庞大,可以很方便的快速实现功能。
- 原理图解释
项目所有的PCB相关文件,都是使用KiCAD进行的绘制。这个原理图基本属于一个ESP32-C3最小系统,仅包含了能让系统运作的最基本部分。除了上面已经讲过的供电部分之外,需要注意的就是要额外添加两个按钮,一个是boot引脚,另一个是RESET引脚,来帮助我们进行固件的烧录。不添加这两个按钮的话,烧录会变得十分麻烦。还有一点就是,记得每一部分电路的供电处都要添加旁路电容,以确保电路工作稳定。
接下来我们看看继电器部分的设计:
继电器部分用了一块单独的PCB,与ESP32-C3部分分开。因为市电220V电压比较高,距离过近可能会出现爬电,造成烧坏设备及触电的风险。在这里我是用一个NPN三极管来控制继电器的开合,并加入一个发光二极管来作为继电器工作状态的指示灯。在这个设计中,最关键的元件是D1这个二极管,由于继电器控制部分的内部是一个线圈,所以它在通断时会产生较高的尖峰电压,我们需要通过续流二极管来消除这个尖峰,否则电压一旦过大,很容易击穿我们接在下面的三极管。
- 方案中可能用到的规定厂商元器件介绍
乐鑫公司的ESP32-C3是一个低功耗的MCU,内置了WIFI和BLE,可以很方便的搭建物联网应用。最关键的是,价格十分低廉,各位创客同学可以尽情尝试。
方案中使用ESP32-S3-MINI模组,使用模组可以大大简化外围电路的设计,同时手工焊接模组难度也比焊接芯片要小,在该项目中是理想的选择。
- PCB绘制打板介绍及遇到的问题和解决方法
根据原理图画好的PCB如下:
新鲜出炉的PCB是这样的:
可以看到由于是喷锡处理的焊盘,焊盘表面并不平整。这对于ESP32-C3模块的焊接增加了一定的难度,因为模块焊盘全部都在下面,并没有额外露出的部分,焊接必须得一次成型,没有烙铁修补的机会。我焊接的方法是先给焊盘上锡,尽量均匀让锡面等高,然后再把模块放上去,热风吹焊。
- 关键代码及说明
上位机中dockers环境的部署,Home Assistant和Mosquito服务器的部署由于和本次项目相关性较小,网上也有很多资料,在这里就不再赘述了。如果未来有机会我会单独开贴讲解(又是一个大坑)。在这个项目中我们主要来看一下MQTT客户端的代码:
项目中用到的库并不多,只有这四个。
#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
首先是连接网络,连接MQTT服务器,并配置好回调函数。这一部分只是一系列的定义及配置,比较简单:
void wifi_loop()
{
if (WiFi.status() != WL_CONNECTED)
{
WiFi.begin(ssid.c_str(), password.c_str());
while (WiFi.status() != WL_CONNECTED)
{
yield();
}
}
}
// MQTT client connect
boolean mqtt_loop()
{
if (WiFi.status() != WL_CONNECTED)
{
return false;
}
if (!client.connected())
{
// init the MQTT setup
client.setServer(MQTT_SERVER_IP, MQTT_SERVER_PORT);
client.setCallback(mqtt_callback);
// Attempt to connect
const String availability_topic = String(MQTT_USER) + "/" + String(MQTT_CLIENT_ID) + "/availability";
if (client.connect(MQTT_CLIENT_ID, MQTT_USER, MQTT_PASSWORD, availability_topic.c_str(), 1, true, "offline", true))
{
boolean isOK = client.publish(availability_topic.c_str(), "online", true);
discover_0();
subscribe_0();
}
else
{
delay(1000);
return false;
}
}
client.loop(); // update message fromm MQTT
return true;
}
为了实现可以让homeassistant可以自动识别我们的终端设备,首先需要publish相应的配置信息给服务器:
void discover_0()
{
// setup
pinMode(DEVICE_0_PIN, OUTPUT);
digitalWrite(DEVICE_0_PIN, LOW);
const String components = DEVICE_0_COMPONENT;
const String name = DEVICE_0_NAME;
const String config_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/config";
const String availability_topic = String(MQTT_USER) + "/" + String(MQTT_CLIENT_ID) + "/availability";
const String command_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/set";
const String state_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/state";
StaticJsonDocument<512> doc;
doc["name"] = String(MQTT_CLIENT_ID) + "_" + name;
doc["unique_id"] = String(MQTT_CLIENT_ID) + "_" + name;
doc["command_topic"] = command_topic;
doc["state_topic"] = state_topic;
doc["availability_topic"] = availability_topic;
doc["schema"] = "json";
doc["optimistic"] = false;
doc["retain"] = false;
doc["qos"] = 0;
doc["brightness"] = true;
unsigned char buffer[512];
unsigned int n = serializeJson(doc, buffer);
client.setBufferSize(512);
boolean isOK = client.publish(config_topic.c_str(), buffer, n, false);
}
之后就是定义MQTT的订阅及发布功能,并将对应功能放在CALLBACK中,这样当ESP32-C3接收到服务器的信息后,就会执行相应操作,并返回执行结果。
void subscribe_0()
{
const String components = DEVICE_0_COMPONENT;
const String name = DEVICE_0_NAME;
const String command_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/set";
client.subscribe(command_topic.c_str(), 0);
}
void publish_0()
{
const String components = DEVICE_0_COMPONENT;
const String name = DEVICE_0_NAME;
const String state_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/state";
StaticJsonDocument<128> doc;
if (DEVICE_0_TGT_VALUE)
{
doc["state"] = DEVICE_ON;
doc["brightness"] = DEVICE_0_SET_VALUE;
}
else
{
doc["state"] = DEVICE_OFF;
}
unsigned char buffer[128];
unsigned int n = serializeJson(doc, buffer);
boolean isOK = client.publish(state_topic.c_str(), buffer, n, false);
}
void process_0(const String topic_p, const String payload_p)
{
const String components = DEVICE_0_COMPONENT;
const String name = DEVICE_0_NAME;
const String command_topic = String(MQTT_USER) + "/" + components + "/" + String(MQTT_CLIENT_ID) + "_" + name + "/set";
if (command_topic == topic_p)
{
StaticJsonDocument<128> doc;
deserializeJson(doc, payload_p);
if (doc["state"] == DEVICE_OFF)
{
DEVICE_0_TGT_VALUE = 0;
}
else
{
if (!DEVICE_0_SET_VALUE)
{
DEVICE_0_TGT_VALUE = 255;
DEVICE_0_SET_VALUE = DEVICE_0_TGT_VALUE;
}
else if (doc.containsKey("brightness"))
{
DEVICE_0_TGT_VALUE = doc["brightness"];
DEVICE_0_SET_VALUE = DEVICE_0_TGT_VALUE;
}
else
{
DEVICE_0_TGT_VALUE = DEVICE_0_SET_VALUE;
}
}
publish_0();
}
}
void execute_0()
{
analogWrite(DEVICE_0_PIN, DEVICE_0_TGT_VALUE);
}
void mqtt_callback(char *p_topic, byte *p_payload, unsigned int p_length)
{
// concat the payload into a string
const String topic = p_topic;
p_payload[p_length] = '\0'; // Null terminator used to terminate the char array
const String payload = (char *)p_payload;
process_0(topic, payload);
}
完整代码在附件中,大家可以自行下载尝试。
- 功能展示及说明
焊接好的成品如下:
可以看到上面部分元件没有焊接。因为这些元件设计时是为了符合设计规范,但实际使用时并不需要。上图中的电阻位置和PCB文件中的略有差异,是因为我不小心在打板的时候搞错了启动引脚,因此实物通过飞线解决问题。上传的PCB已经修复了这个问题,可以直接使用。
下面是独立的继电器模块:
背面在所有的高压引脚处都增加了爬电槽,防止在潮湿环境中出现爬电短路。同时高压区还增加了警示标志:
两个部分合体后,是长这个样子。高压区与控制区隔离,确保安全:
找个螺口灯座,再找根电源插座线,接到继电器里,整个成品就完成了。这里由于我只是做个示范,接线处并没有做绝缘处理。实际上这样操作非常危险,220V短路跟放鞭炮一样,能把一大片都炸的焦黑,大家可千万不要模仿。
- 对本大赛的心得体会
这次比赛让我有机会(动力)可以自己尝试一下从头设计一个日常生活中可能会有用的小电器,非常有意义,希望活动越办越好。