这个案例比单独 MQTT 更接近真实设备:一个 ESP8266 控制继电器,读取温湿度,通过 MQTT 上报状态,接收远程开关命令,同时保留本地按钮。网络断了以后,按钮仍然能控制继电器。

目标功能

功能 说明
温湿度上报 每 10 秒发布一次
继电器控制 支持 MQTT 命令和本地按钮
状态保持 每次控制后发布当前状态
安全限制 继电器每次最多连续打开 30 分钟
离线兜底 MQTT 不在线也能用按钮控制
设备遗嘱 异常断线后 Broker 标记设备离线

接线

模块 ESP8266
DHT DATA D5
继电器 IN D2
按钮一端 D6
按钮另一端 GND
DHT VCC、继电器 VCC 3V3 或独立 5V,按模块规格
GND 共地

很多继电器模块是低电平触发,如果你的模块相反,改 RELAY_ON 和 RELAY_OFF。

MQTT 消息

上报状态

Topic:

1
device/esp001/state

Payload:

1
2
3
4
5
6
7
8
{
"relay": 1,
"source": "mqtt",
"temperature": 26.4,
"humidity": 61.2,
"rssi": -48,
"uptime": 120
}

下发命令

Topic:

1
device/esp001/cmd

Payload:

1
{"relay":1}
1
{"relay":0}

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <DHT.h>

const char* WIFI_SSID = "your-wifi";
const char* WIFI_PASS = "your-password";

const char* MQTT_HOST = "192.168.1.10";
const uint16_t MQTT_PORT = 1883;

const char* DEVICE_ID = "esp001";

const int DHT_PIN = D5;
const int RELAY_PIN = D2;
const int BUTTON_PIN = D6;

const int RELAY_ON = LOW;
const int RELAY_OFF = HIGH;

const unsigned long REPORT_INTERVAL = 10000;
const unsigned long MAX_ON_TIME = 30UL * 60UL * 1000UL;
const unsigned long DEBOUNCE_MS = 40;

#define DHT_TYPE DHT11

WiFiClient net;
PubSubClient mqtt(net);
DHT dht(DHT_PIN, DHT_TYPE);

bool relayOn = false;
bool lastButtonLevel = HIGH;
bool stableButtonLevel = HIGH;
unsigned long buttonChangedAt = 0;
unsigned long relayOnAt = 0;
unsigned long lastReportAt = 0;

String topicState() {
return String("device/") + DEVICE_ID + "/state";
}

String topicCmd() {
return String("device/") + DEVICE_ID + "/cmd";
}

String topicOnline() {
return String("device/") + DEVICE_ID + "/online";
}

void setRelay(bool on, const char* source) {
relayOn = on;
digitalWrite(RELAY_PIN, on ? RELAY_ON : RELAY_OFF);

if (on) {
relayOnAt = millis();
} else {
relayOnAt = 0;
}

Serial.print("relay ");
Serial.print(on ? "on" : "off");
Serial.print(" by ");
Serial.println(source);
}

void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) {
return;
}

WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASS);

unsigned long started = millis();
while (WiFi.status() != WL_CONNECTED && millis() - started < 15000) {
delay(500);
Serial.print(".");
}

Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.print("wifi ip=");
Serial.println(WiFi.localIP());
} else {
Serial.println("wifi timeout, local button still works");
}
}

void publishState(const char* source) {
if (!mqtt.connected()) {
return;
}

float humidity = dht.readHumidity();
float temperature = dht.readTemperature();

StaticJsonDocument<256> doc;
doc["relay"] = relayOn ? 1 : 0;
doc["source"] = source;
doc["rssi"] = WiFi.RSSI();
doc["uptime"] = millis() / 1000;

if (!isnan(temperature)) {
doc["temperature"] = temperature;
}
if (!isnan(humidity)) {
doc["humidity"] = humidity;
}

char payload[256];
size_t len = serializeJson(doc, payload);
mqtt.publish(topicState().c_str(), payload, len, true);

Serial.print("state ");
Serial.println(payload);
}

void onMqttMessage(char* topic, byte* payload, unsigned int length) {
StaticJsonDocument<128> doc;
DeserializationError err = deserializeJson(doc, payload, length);
if (err) {
Serial.print("cmd json error ");
Serial.println(err.c_str());
return;
}

if (doc.containsKey("relay")) {
bool on = doc["relay"].as<int>() == 1;
setRelay(on, "mqtt");
publishState("mqtt");
}
}

void connectMQTT() {
if (WiFi.status() != WL_CONNECTED || mqtt.connected()) {
return;
}

mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMqttMessage);

String clientId = String("esp8266-") + DEVICE_ID + "-" + String(ESP.getChipId(), HEX);
bool ok = mqtt.connect(clientId.c_str(), topicOnline().c_str(), 1, true, "0");

if (!ok) {
Serial.print("mqtt failed rc=");
Serial.println(mqtt.state());
return;
}

mqtt.publish(topicOnline().c_str(), "1", true);
mqtt.subscribe(topicCmd().c_str());
publishState("boot");
}

void readButton() {
bool level = digitalRead(BUTTON_PIN);

if (level != lastButtonLevel) {
lastButtonLevel = level;
buttonChangedAt = millis();
}

if (millis() - buttonChangedAt > DEBOUNCE_MS && level != stableButtonLevel) {
stableButtonLevel = level;

if (stableButtonLevel == LOW) {
setRelay(!relayOn, "button");
publishState("button");
}
}
}

void checkSafety() {
if (relayOn && relayOnAt > 0 && millis() - relayOnAt > MAX_ON_TIME) {
setRelay(false, "safety");
publishState("safety");
}
}

void setup() {
Serial.begin(115200);
pinMode(RELAY_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);

digitalWrite(RELAY_PIN, RELAY_OFF);
dht.begin();

connectWiFi();
connectMQTT();
}

void loop() {
readButton();
checkSafety();

if (WiFi.status() != WL_CONNECTED) {
connectWiFi();
}

if (!mqtt.connected()) {
connectMQTT();
}

if (mqtt.connected()) {
mqtt.loop();
}

unsigned long now = millis();
if (now - lastReportAt > REPORT_INTERVAL) {
lastReportAt = now;
publishState("report");
}
}

命令测试

订阅状态:

1
mosquitto_sub -h 192.168.1.10 -t 'device/esp001/#' -v

打开继电器:

1
mosquitto_pub -h 192.168.1.10 -t 'device/esp001/cmd' -m '{"relay":1}'

关闭继电器:

1
mosquitto_pub -h 192.168.1.10 -t 'device/esp001/cmd' -m '{"relay":0}'

看在线状态:

1
mosquitto_sub -h 192.168.1.10 -t 'device/esp001/online' -v

异常断电后,Broker 会收到:

1
device/esp001/online 0

继电器安全注意

如果继电器控制的是真实电器,建议至少做到:

  • 强弱电隔离,接线盒封闭。
  • 继电器额定电流大于负载电流。
  • 高功率设备不要用便宜小继电器硬扛。
  • 程序里加最大开启时间,避免网络异常导致长期开启。
  • 关键设备使用物理急停或机械开关兜底。

可继续扩展

这个案例可以继续加:

  • OTA 升级。
  • NTP 校时。
  • MQTT 用户名密码。
  • TLS。
  • Home Assistant discovery。
  • Web 配网。
  • EEPROM 或 LittleFS 保存配置。

真实项目里,最先补的通常是配置保存和 OTA,不然每次改 Wi-Fi 或 Broker 都要重新烧录。

站内搜索

没有找到内容!