#include <ESP8266WiFi.h> // ESP8266 WiFi功能库
#include <PubSubClient.h> // MQTT客户端库
#include <EEPROM.h> // EEPROM存储库
#include <ESP8266WebServer.h> // Web服务器库
// -------------------------- 基础配置参数 --------------------------
const int EEPROM_SIZE = 512; // EEPROM存储大小
const int FIRMWARE_VERSION = 0x07; // 固件版本号(每次修改代码递增)
const char* AP_SSID = "==="; // 配网热点名称
const char* AP_PASS = "==="; // 配网热点密码
const IPAddress AP_IP(192, 168, 1, 234); // AP固定IP
// -------------------------- 巴法云配置(每个引脚独立主题) --------------------------
const char* MQTT_SERVER = "bemfa.com"; // 巴法云服务器
const int MQTT_PORT = 9501; // 巴法云端口
const char* CLIENT_ID = "==="; // 替换为你的私钥
// 每个引脚的专属MQTT主题(需替换为你自己的巴法云主题)
const char* TOPIC_D0 = "ceshi002_d0";
const char* TOPIC_D1 = "ceshi002_d1";
const char* TOPIC_D2 = "ceshi002_d2";
const char* TOPIC_D3 = "ceshi002_d3";
const char* TOPIC_D4 = "ceshi002_d4";
// -------------------------- 网络与指令参数 --------------------------
const unsigned long RECONNECT_DELAY = 10000; // 重连间隔(10秒)
const unsigned long CMD_DEBOUNCE_TIME = 1000; // 指令防抖
const int WIFI_RETRY_LIMIT = 2; // 最大重试次数
const unsigned long CONFIG_HOLD_TIME = 3000; // 按键长按时间(3秒)
// -------------------------- 引脚1:D0(GPIO16)配置与状态 --------------------------
const int PIN_D0 = 16;
bool state_D0 = HIGH; // 初始状态:关闭(HIGH=关闭,LOW=开启)
unsigned long lastCmd_D0 = 0; // 最后指令时间(防抖)
// -------------------------- 引脚2:D1(GPIO5)配置与状态 --------------------------
const int PIN_D1 = 5;
bool state_D1 = HIGH;
unsigned long lastCmd_D1 = 0;
// -------------------------- 引脚3:D2(GPIO4)配置与状态 --------------------------
const int PIN_D2 = 4;
bool state_D2 = HIGH;
unsigned long lastCmd_D2 = 0;
// -------------------------- 引脚4:D3(GPIO0)配置与状态 --------------------------
const int PIN_D3 = 0;
bool state_D3 = HIGH;
unsigned long lastCmd_D3 = 0;
// -------------------------- 引脚5:D4(GPIO2)配置与状态 --------------------------
const int PIN_D4 = 2;
bool state_D4 = HIGH;
unsigned long lastCmd_D4 = 0;
// -------------------------- 全局变量 --------------------------
WiFiClient espClient; // WiFi客户端
PubSubClient client(espClient); // MQTT客户端
ESP8266WebServer server(80); // Web服务器
String wifiSSID, wifiPassword; // WiFi账号密码
unsigned long lastConnectTime = 0; // 最后连接时间
bool isWiFiConnected = false; // WiFi连接状态
int wifiRetryCount = 0; // 重试计数器
const int CONFIG_PIN = 13; // 配置重置按键(GPIO13)
unsigned long configPressStart = 0; // 按键按下时间
bool configButtonPressed = false; // 按键状态
// -------------------------- HTML页面模板(多引脚控制页面) --------------------------
const char PIN_CONTROL_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#2196F3">
<title>MaopaiHome </title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background-color: #f8f9fa; min-height: 100vh; padding: 20px; }
.container { max-width: 400px; margin: 20px auto; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); padding: 25px 20px; }
.header { text-align: center; margin-bottom: 30px; }
.title { font-size: 22px; font-weight: 600; color: #333; }
/* 单个引脚控制模块 */
.pin-module { margin-bottom: 25px; padding-bottom: 20px; border-bottom: 1px solid #f0f0f0; }
.pin-name { font-size: 18px; font-weight: 500; color: #333; margin-bottom: 15px; }
.led-indicator { display: flex; justify-content: center; align-items: center; margin-bottom: 15px; }
.led-light {
width: 60px; height: 60px; border-radius: 50%; border: 2px solid #eee;
transition: all 0.3s ease; display: flex; align-items: center; justify-content: center;
}
.led-light.on { background-color: #4CAF50; box-shadow: 0 0 15px rgba(76, 175, 80, 0.4); border-color: #c8e6c9; }
.led-light.off { background-color: #f5f5f5; box-shadow: 0 0 5px rgba(0,0,0,0.05); border-color: #eee; }
.led-text { font-size: 12px; color: white; font-weight: 500; }
.led-light.off .led-text { color: #999; }
/* 控制按钮 */
.control-buttons { display: flex; gap: 15px; }
.control-btn {
flex: 1; height: 45px; border-radius: 8px; border: none;
font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s;
}
.on-btn { background-color: #4CAF50; color: white; }
.on-btn:active { background-color: #388E3C; }
.off-btn { background-color: #f44336; color: white; }
.off-btn:active { background-color: #D32F2F; }
/* 状态信息与底部 */
.status-bar {
background-color: #f5f5f5; border-radius: 8px;
padding: 12px 15px; font-size: 13px; color: #666; margin-bottom: 20px;
}
.bottom-bar { display: flex; justify-content: space-between; }
.action-link { font-size: 13px; color: #2196F3; text-decoration: none; padding: 5px 0; }
.action-link:hover { text-decoration: underline; }
/* 加载动画 */
.loader {
width: 18px; height: 18px; border: 2px solid rgba(255,255,255,0.5);
border-radius: 50%; border-top-color: white; animation: spin 1s linear infinite;
display: none; margin-left: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading .loader { display: inline-block; }
.loading .btn-text { display: none; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="title">MaopaiHome</div>
</div>
<!-- 状态信息 -->
<div class="status-bar">
<div class="status-item"><span class="status-label">WiFi状态:</span> {wifi_status}</div>
<div class="status-item"><span class="status-label">最后更新:</span> {update_time}</div>
</div>
<!-- D0 控制模块 -->
<div class="pin-module">
<div class="pin-name">D0 引脚</div>
<div class="led-indicator">
<div id="led-d0" class="led-light {d0_class}">
<div class="led-text">{d0_text}</div>
</div>
</div>
<div class="control-buttons">
<button class="control-btn on-btn" onclick="controlPin('d0', 'on')">
<span class="btn-text">开启</span>
<div class="loader"></div>
</button>
<button class="control-btn off-btn" onclick="controlPin('d0', 'off')">
<span class="btn-text">关闭</span>
<div class="loader"></div>
</button>
</div>
</div>
<!-- D1 控制模块 -->
<div class="pin-module">
<div class="pin-name">D1 引脚</div>
<div class="led-indicator">
<div id="led-d1" class="led-light {d1_class}">
<div class="led-text">{d1_text}</div>
</div>
</div>
<div class="control-buttons">
<button class="control-btn on-btn" onclick="controlPin('d1', 'on')">
<span class="btn-text">开启</span>
<div class="loader"></div>
</button>
<button class="control-btn off-btn" onclick="controlPin('d1', 'off')">
<span class="btn-text">关闭</span>
<div class="loader"></div>
</button>
</div>
</div>
<!-- D2 控制模块 -->
<div class="pin-module">
<div class="pin-name">D2 引脚</div>
<div class="led-indicator">
<div id="led-d2" class="led-light {d2_class}">
<div class="led-text">{d2_text}</div>
</div>
</div>
<div class="control-buttons">
<button class="control-btn on-btn" onclick="controlPin('d2', 'on')">
<span class="btn-text">开启</span>
<div class="loader"></div>
</button>
<button class="control-btn off-btn" onclick="controlPin('d2', 'off')">
<span class="btn-text">关闭</span>
<div class="loader"></div>
</button>
</div>
</div>
<!-- D3 控制模块 -->
<div class="pin-module">
<div class="pin-name">D3 引脚</div>
<div class="led-indicator">
<div id="led-d3" class="led-light {d3_class}">
<div class="led-text">{d3_text}</div>
</div>
</div>
<div class="control-buttons">
<button class="control-btn on-btn" onclick="controlPin('d3', 'on')">
<span class="btn-text">开启</span>
<div class="loader"></div>
</button>
<button class="control-btn off-btn" onclick="controlPin('d3', 'off')">
<span class="btn-text">关闭</span>
<div class="loader"></div>
</button>
</div>
</div>
<!-- D4 控制模块 -->
<div class="pin-module">
<div class="pin-name">D4 引脚</div>
<div class="led-indicator">
<div id="led-d4" class="led-light {d4_class}">
<div class="led-text">{d4_text}</div>
</div>
</div>
<div class="control-buttons">
<button class="control-btn on-btn" onclick="controlPin('d4', 'on')">
<span class="btn-text">开启</span>
<div class="loader"></div>
</button>
<button class="control-btn off-btn" onclick="controlPin('d4', 'off')">
<span class="btn-text">关闭</span>
<div class="loader"></div>
</button>
</div>
</div>
<!-- 底部操作 -->
<div class="bottom-bar">
<a href="/wifi-config" class="action-link">WiFi配置</a>
<a href="javascript:location.reload()" class="action-link">刷新状态</a>
</div>
</div>
<script>
// 控制指定引脚
function controlPin(pin, cmd) {
// 重置所有按钮加载状态
document.querySelectorAll('.control-btn').forEach(btn => btn.classList.remove('loading'));
// 查找当前操作的按钮并设置加载状态
const buttons = document.querySelectorAll('.control-btn');
let targetBtn = null;
for (let i = 0; i < buttons.length; i++) {
if (buttons[i].getAttribute('onclick') === `controlPin('${pin}', '${cmd}')`) {
targetBtn = buttons[i];
break;
}
}
if (targetBtn) targetBtn.classList.add('loading');
// 发送请求
fetch('/control?pin=' + pin + '&cmd=' + cmd)
.then(response => {
if (!response.ok) throw new Error('网络响应不正常');
return response.text();
})
.then(data => {
if (targetBtn) targetBtn.classList.remove('loading');
setTimeout(updateAllStatus, 200);
})
.catch(error => {
console.error('控制失败:', error);
if (targetBtn) targetBtn.classList.remove('loading');
alert('操作失败: ' + error.message);
});
}
// 更新所有引脚状态
function updateAllStatus() {
fetch('/get-status')
.then(response => {
if (!response.ok) throw new Error('获取状态失败');
return response.json();
})
.then(data => {
// 更新WiFi和时间状态
document.querySelector('.status-bar .status-item:nth-child(1)').innerHTML =
'<span class="status-label">WiFi状态:</span> ' + data.wifi;
document.querySelector('.status-bar .status-item:nth-child(2)').innerHTML =
'<span class="status-label">最后更新:</span> ' + data.time;
// 更新各引脚状态
updatePinStatus('d0', data.d0);
updatePinStatus('d1', data.d1);
updatePinStatus('d2', data.d2);
updatePinStatus('d3', data.d3);
updatePinStatus('d4', data.d4);
})
.catch(error => {
console.error('更新状态失败:', error);
});
}
// 更新单个引脚状态
function updatePinStatus(pin, status) {
const ledElement = document.getElementById('led-' + pin);
if (ledElement) {
ledElement.className = 'led-light ' + status.class;
const textElement = ledElement.querySelector('.led-text');
if (textElement) textElement.textContent = status.text;
}
}
// 页面加载完成后初始化
window.addEventListener('DOMContentLoaded', function() {
updateAllStatus();
// 每3秒自动更新一次状态
setInterval(updateAllStatus, 3000);
});
</script>
</body>
</html>
)rawliteral";
// -------------------------- HTML页面模板(WiFi配置/成功页面) --------------------------
const char WIFI_CONFIG_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="theme-color" content="#4CAF50">
<title>MaopaiWiFi配置</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background-color: #f8f9fa; min-height: 100vh; padding: 20px; }
.container { max-width: 400px; margin: 30px auto; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); padding: 25px 20px; }
.header { text-align: center; margin-bottom: 25px; }
.title { font-size: 20px; font-weight: 600; color: #333; }
.form-group { margin-bottom: 20px; }
label { display: block; font-size: 14px; color: #666; margin-bottom: 6px; }
input { width: 100%; height: 45px; padding: 0 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; }
input:focus { outline: none; border-color: #4CAF50; }
.btn-group { display: flex; gap: 12px; margin-bottom: 20px; }
.submit-btn { flex: 1; height: 48px; background-color: #4CAF50; color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
.submit-btn:active { background-color: #388E3C; }
.clear-btn { flex: 1; height: 48px; background-color: #f44336; color: white; border: none; border-radius: 6px; font-size: 16px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; }
.clear-btn:active { background-color: #D32F2F; }
.back-link { display: block; text-align: center; margin-top: 15px; font-size: 14px; color: #2196F3; text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.footer { text-align: center; margin-top: 20px; font-size: 12px; color: #999; }
</style>
</head>
<body>
<div class="container">
<div class="header"><div class="title">WiFi网络配置</div></div>
<form method="POST" action="/save-wifi">
<div class="form-group">
<label for="ssid">WiFi名称</label>
<input type="text" id="ssid" name="ssid" required placeholder="请输入WiFi名称">
</div>
<div class="form-group">
<label for="pass">WiFi密码</label>
<input type="password" id="pass" name="pass" placeholder="无密码则留空">
</div>
<div class="btn-group">
<button type="submit" class="submit-btn">提交配置</button>
<button type="button" class="clear-btn" onclick="confirmClear()">清除配置</button>
</div>
<a href="/" class="back-link">返回控制页</a>
</form>
</div>
<div class="footer">重置热点名: MaopaiJD-Peiwang | 重置访问: 192.168.1.234</div>
<script>
function confirmClear() {
if (confirm("确定要清除所有配置吗?\n清除后设备将重启并恢复初始状态!")) {
fetch('/clear-eeprom')
.then(response => {
if (!response.ok) throw new Error('清除失败');
return response.text();
})
.then(() => alert("配置清除成功,设备即将重启..."))
.catch(() => alert("清除失败,请重试!"));
}
}
</script>
</body>
</html>
)rawliteral";
const char CONFIG_SUCCESS_HTML[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>配置成功</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
body { background-color: #f8f9fa; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.container { max-width: 350px; width: 100%; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); padding: 25px 20px; text-align: center; }
.success-icon { width: 60px; height: 60px; margin: 0 auto 20px; background-color: #4CAF50; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; font-size: 30px; }
h1 { font-size: 18px; color: #333; margin-bottom: 10px; }
p { font-size: 14px; color: #666; margin-bottom: 25px; }
.back-btn { display: inline-block; padding: 12px 30px; background-color: #2196F3; color: white; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; }
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✓</div>
<h1>WiFi配置成功</h1>
<p>设备已尝试连接到指定WiFi网络</p>
<a href="/" class="back-btn">返回控制页</a>
</div>
</body>
</html>
)rawliteral";
// -------------------------- 工具函数 --------------------------
void clearEEPROM() {
EEPROM.begin(EEPROM_SIZE);
Serial.println("===== 开始彻底清除EEPROM所有数据 =====");
for (int i = 0; i < EEPROM_SIZE; i++) {
EEPROM.write(i, 0xFF);
delay(1);
}
EEPROM.commit();
for (int i = 0; i < EEPROM_SIZE; i++) {
EEPROM.write(i, 0x00);
delay(1);
}
EEPROM.commit();
EEPROM.end();
Serial.println("===== EEPROM所有数据已彻底清除 =====");
}
void saveWiFiConfig(String ssid, String pass) {
EEPROM.begin(EEPROM_SIZE);
for (int i = 0; i < EEPROM_SIZE; i++) {
EEPROM.write(i, 0);
}
for (int i = 0; i < ssid.length() && i < 128; i++) {
EEPROM.write(i, ssid[i]);
}
int passStart = 129;
for (int i = 0; i < pass.length() && i < 256; i++) {
EEPROM.write(passStart + i, pass[i]);
}
EEPROM.write(EEPROM_SIZE - 1, FIRMWARE_VERSION);
EEPROM.commit();
EEPROM.end();
}
bool loadWiFiConfig() {
EEPROM.begin(EEPROM_SIZE);
wifiSSID = "";
wifiPassword = "";
for (int i = 0; i < 128; i++) {
char c = EEPROM.read(i);
if (c == 0) break;
wifiSSID += c;
}
int passStart = 129;
for (int i = 0; i < 256; i++) {
char c = EEPROM.read(passStart + i);
if (c == 0) break;
wifiPassword += c;
}
EEPROM.end();
return wifiSSID.length() > 0;
}
bool isNewFirmwareFirstRun() {
EEPROM.begin(EEPROM_SIZE);
byte storedVersion = EEPROM.read(EEPROM_SIZE - 1);
EEPROM.end();
return storedVersion != FIRMWARE_VERSION;
}
void initAPMode() {
WiFi.disconnect(true);
delay(1000);
WiFi.mode(WIFI_AP_STA);
WiFi.softAPConfig(AP_IP, AP_IP, IPAddress(255, 255, 255, 0));
bool apStarted = WiFi.softAP(AP_SSID, AP_PASS);
if (apStarted) {
Serial.println("\n===== AP模式已启动 =====");
Serial.print("热点名称: "); Serial.println(AP_SSID);
Serial.print("热点密码: "); Serial.println(AP_PASS);
Serial.print("访问地址: "); Serial.println(WiFi.softAPIP().toString());
Serial.println("========================");
} else {
Serial.println("AP模式启动失败,将重试...");
delay(2000);
initAPMode();
}
}
String getCurrentTime() {
unsigned long sec = millis() / 1000;
unsigned long min = sec / 60;
unsigned long hour = min / 60;
sec %= 60;
min %= 60;
hour %= 24;
String timeStr = (hour < 10 ? "0" : "") + String(hour) + ":" +
(min < 10 ? "0" : "") + String(min) + ":" +
(sec < 10 ? "0" : "") + String(sec);
return timeStr;
}
// -------------------------- 核心控制函数(每个引脚独立函数) --------------------------
/**
* 控制D0引脚
* @param targetState 目标状态(LOW=开启,HIGH=关闭)
* @param isFromMqtt 是否来自MQTT指令
*/
void controlD0(bool targetState, bool isFromMqtt = false) {
if (millis() - lastCmd_D0 < CMD_DEBOUNCE_TIME || targetState == state_D0) {
return;
}
lastCmd_D0 = millis();
digitalWrite(PIN_D0, targetState);
state_D0 = targetState;
Serial.println("D0 状态切换为: " + String(targetState == LOW ? "开" : "关"));
// MQTT同步
if (!isFromMqtt && client.connected()) {
client.publish(TOPIC_D0, targetState == LOW ? "on" : "off");
}
}
/**
* 控制D1引脚
*/
void controlD1(bool targetState, bool isFromMqtt = false) {
if (millis() - lastCmd_D1 < CMD_DEBOUNCE_TIME || targetState == state_D1) {
return;
}
lastCmd_D1 = millis();
digitalWrite(PIN_D1, targetState);
state_D1 = targetState;
Serial.println("D1 状态切换为: " + String(targetState == LOW ? "开" : "关"));
if (!isFromMqtt && client.connected()) {
client.publish(TOPIC_D1, targetState == LOW ? "on" : "off");
}
}
/**
* 控制D2引脚
*/
void controlD2(bool targetState, bool isFromMqtt = false) {
if (millis() - lastCmd_D2 < CMD_DEBOUNCE_TIME || targetState == state_D2) {
return;
}
lastCmd_D2 = millis();
digitalWrite(PIN_D2, targetState);
state_D2 = targetState;
Serial.println("D2 状态切换为: " + String(targetState == LOW ? "开" : "关"));
if (!isFromMqtt && client.connected()) {
client.publish(TOPIC_D2, targetState == LOW ? "on" : "off");
}
}
/**
* 控制D3引脚
*/
void controlD3(bool targetState, bool isFromMqtt = false) {
if (millis() - lastCmd_D3 < CMD_DEBOUNCE_TIME || targetState == state_D3) {
return;
}
lastCmd_D3 = millis();
digitalWrite(PIN_D3, targetState);
state_D3 = targetState;
Serial.println("D3 状态切换为: " + String(targetState == LOW ? "开" : "关"));
if (!isFromMqtt && client.connected()) {
client.publish(TOPIC_D3, targetState == LOW ? "on" : "off");
}
}
/**
* 控制D4引脚
*/
void controlD4(bool targetState, bool isFromMqtt = false) {
if (millis() - lastCmd_D4 < CMD_DEBOUNCE_TIME || targetState == state_D4) {
return;
}
lastCmd_D4 = millis();
digitalWrite(PIN_D4, targetState);
state_D4 = targetState;
Serial.println("D4 状态切换为: " + String(targetState == LOW ? "开" : "关"));
if (!isFromMqtt && client.connected()) {
client.publish(TOPIC_D4, targetState == LOW ? "on" : "off");
}
}
// -------------------------- Web服务器与MQTT(核心功能) --------------------------
/**
* 生成多引脚控制页面HTML
*/
String generatePinControlPage() {
String html = String(PIN_CONTROL_HTML);
String wifiStatus = isWiFiConnected ? "已连接: " + WiFi.SSID() : "未连接";
String currentTime = getCurrentTime();
// 替换D0状态
html.replace("{d0_text}", state_D0 == LOW ? "已开启" : "已关闭");
html.replace("{d0_class}", state_D0 == LOW ? "on" : "off");
// 替换D1状态
html.replace("{d1_text}", state_D1 == LOW ? "已开启" : "已关闭");
html.replace("{d1_class}", state_D1 == LOW ? "on" : "off");
// 替换D2状态
html.replace("{d2_text}", state_D2 == LOW ? "已开启" : "已关闭");
html.replace("{d2_class}", state_D2 == LOW ? "on" : "off");
// 替换D3状态
html.replace("{d3_text}", state_D3 == LOW ? "已开启" : "已关闭");
html.replace("{d3_class}", state_D3 == LOW ? "on" : "off");
// 替换D4状态
html.replace("{d4_text}", state_D4 == LOW ? "已开启" : "已关闭");
html.replace("{d4_class}", state_D4 == LOW ? "on" : "off");
// 替换WiFi和时间
html.replace("{wifi_status}", wifiStatus);
html.replace("{update_time}", currentTime);
return html;
}
/**
* MQTT消息回调(匹配每个引脚的主题)
*/
void mqttCallback(char* topic, byte* payload, unsigned int length) {
String topicStr = String(topic);
String msg = "";
for (int i = 0; i < length; i++) {
msg += (char)payload[i];
}
msg.trim();
Serial.print("收到MQTT指令 [");
Serial.print(topicStr);
Serial.print("]: ");
Serial.println(msg);
// 匹配D0主题
if (topicStr.equals(TOPIC_D0)) {
if (msg.equalsIgnoreCase("on")) {
controlD0(LOW, true);
} else if (msg.equalsIgnoreCase("off")) {
controlD0(HIGH, true);
}
}
// 匹配D1主题
else if (topicStr.equals(TOPIC_D1)) {
if (msg.equalsIgnoreCase("on")) {
controlD1(LOW, true);
} else if (msg.equalsIgnoreCase("off")) {
controlD1(HIGH, true);
}
}
// 匹配D2主题
else if (topicStr.equals(TOPIC_D2)) {
if (msg.equalsIgnoreCase("on")) {
controlD2(LOW, true);
} else if (msg.equalsIgnoreCase("off")) {
controlD2(HIGH, true);
}
}
// 匹配D3主题
else if (topicStr.equals(TOPIC_D3)) {
if (msg.equalsIgnoreCase("on")) {
controlD3(LOW, true);
} else if (msg.equalsIgnoreCase("off")) {
controlD3(HIGH, true);
}
}
// 匹配D4主题
else if (topicStr.equals(TOPIC_D4)) {
if (msg.equalsIgnoreCase("on")) {
controlD4(LOW, true);
} else if (msg.equalsIgnoreCase("off")) {
controlD4(HIGH, true);
}
}
}
/**
* 连接MQTT服务器(订阅所有引脚主题)
*/
bool connectMQTT() {
client.setServer(MQTT_SERVER, MQTT_PORT);
client.setCallback(mqttCallback);
if (client.connect(CLIENT_ID)) {
// 订阅每个引脚的主题
client.subscribe(TOPIC_D0);
client.subscribe(TOPIC_D1);
client.subscribe(TOPIC_D2);
client.subscribe(TOPIC_D3);
client.subscribe(TOPIC_D4);
Serial.println("MQTT连接成功,已订阅所有引脚主题");
// 同步初始状态到巴法云
client.publish(TOPIC_D0, state_D0 == LOW ? "on" : "off");
client.publish(TOPIC_D1, state_D1 == LOW ? "on" : "off");
client.publish(TOPIC_D2, state_D2 == LOW ? "on" : "off");
client.publish(TOPIC_D3, state_D3 == LOW ? "on" : "off");
client.publish(TOPIC_D4, state_D4 == LOW ? "on" : "off");
return true;
} else {
Serial.print("MQTT连接失败,错误代码: ");
Serial.println(client.state());
return false;
}
}
/**
* 初始化Web服务器路由
*/
void initWebServer() {
// 根路由:多引脚控制页面
server.on("/", []() {
server.send(200, "text/html", generatePinControlPage());
});
// WiFi配置页面
server.on("/wifi-config", []() {
server.send_P(200, "text/html", WIFI_CONFIG_HTML);
});
// 保存WiFi配置
server.on("/save-wifi", []() {
if (server.hasArg("ssid")) {
String ssid = server.arg("ssid");
String pass = server.arg("pass");
saveWiFiConfig(ssid, pass);
server.send_P(200, "text/html", CONFIG_SUCCESS_HTML);
delay(2000);
ESP.restart();
} else {
server.send(400, "text/html", "<meta name='viewport' content='width=device-width'><body style='text-align:center;color:red;margin-top:50px;'>配置失败,请填写WiFi名称</body>");
}
});
// 清除EEPROM
server.on("/clear-eeprom", []() {
clearEEPROM();
server.send(200, "text/plain", "EEPROM cleared successfully");
delay(2000);
ESP.restart();
});
// 核心路由:控制指定引脚
server.on("/control", []() {
if (server.hasArg("pin") && server.hasArg("cmd")) {
String pin = server.arg("pin");
String cmd = server.arg("cmd");
bool targetState = (cmd == "on") ? LOW : HIGH;
// 根据引脚名执行控制
if (pin == "d0") controlD0(targetState);
else if (pin == "d1") controlD1(targetState);
else if (pin == "d2") controlD2(targetState);
else if (pin == "d3") controlD3(targetState);
else if (pin == "d4") controlD4(targetState);
else {
server.send(400, "text/plain", "error: invalid pin");
return;
}
server.send(200, "text/plain", "success");
} else {
server.send(400, "text/plain", "error: missing pin or cmd");
}
});
// 核心路由:获取所有引脚状态(JSON)
server.on("/get-status", []() {
String json = "{"
"\"wifi\":\"" + (isWiFiConnected ? "已连接: " + WiFi.SSID() : "未连接") + "\","
"\"time\":\"" + getCurrentTime() + "\","
"\"d0\":{\"text\":\"" + (state_D0 == LOW ? "已开启" : "已关闭") + "\",\"class\":\"" + (state_D0 == LOW ? "on" : "off") + "\"},"
"\"d1\":{\"text\":\"" + (state_D1 == LOW ? "已开启" : "已关闭") + "\",\"class\":\"" + (state_D1 == LOW ? "on" : "off") + "\"},"
"\"d2\":{\"text\":\"" + (state_D2 == LOW ? "已开启" : "已关闭") + "\",\"class\":\"" + (state_D2 == LOW ? "on" : "off") + "\"},"
"\"d3\":{\"text\":\"" + (state_D3 == LOW ? "已开启" : "已关闭") + "\",\"class\":\"" + (state_D3 == LOW ? "on" : "off") + "\"},"
"\"d4\":{\"text\":\"" + (state_D4 == LOW ? "已开启" : "已关闭") + "\",\"class\":\"" + (state_D4 == LOW ? "on" : "off") + "\"}"
"}";
server.send(200, "application/json", json);
});
// 处理404错误
server.onNotFound([]() {
server.send(404, "text/plain", "Not found");
});
server.begin();
Serial.println("Web服务器已启动");
}
// -------------------------- 配置按键检查 --------------------------
void checkConfigButton() {
if (digitalRead(CONFIG_PIN) == LOW) {
if (!configButtonPressed) {
configButtonPressed = true;
configPressStart = millis();
Serial.println("配置按键已按下,长按3秒将重置配置...");
} else {
if (millis() - configPressStart >= CONFIG_HOLD_TIME) {
Serial.println("长按时间达标,开始重置配置...");
// 闪烁D4作为提示
for (int i = 0; i < 5; i++) {
digitalWrite(PIN_D4, LOW); delay(200);
digitalWrite(PIN_D4, HIGH); delay(200);
}
clearEEPROM();
delay(2000);
ESP.restart();
}
}
} else {
configButtonPressed = false;
}
}
// -------------------------- 初始化与主循环 --------------------------
void setup() {
Serial.begin(115200);
delay(100); // 确保串口初始化完成
// 初始化所有引脚为输出模式并设置初始状态
pinMode(PIN_D0, OUTPUT);
pinMode(PIN_D1, OUTPUT);
pinMode(PIN_D2, OUTPUT);
pinMode(PIN_D3, OUTPUT);
pinMode(PIN_D4, OUTPUT);
// 设置初始状态(关闭)
digitalWrite(PIN_D0, HIGH);
digitalWrite(PIN_D1, HIGH);
digitalWrite(PIN_D2, HIGH);
digitalWrite(PIN_D3, HIGH);
digitalWrite(PIN_D4, HIGH);
// 初始化配置按键
pinMode(CONFIG_PIN, INPUT_PULLUP);
// 新固件首次运行处理
if (isNewFirmwareFirstRun()) {
Serial.println("检测到新固件首次运行,执行EEPROM彻底清除...");
// 闪烁D4作为提示
for (int i = 0; i < 3; i++) {
digitalWrite(PIN_D4, LOW); delay(500);
digitalWrite(PIN_D4, HIGH); delay(500);
}
clearEEPROM();
// 写入当前固件版本
EEPROM.begin(EEPROM_SIZE);
EEPROM.write(EEPROM_SIZE - 1, FIRMWARE_VERSION);
EEPROM.commit();
EEPROM.end();
Serial.println("新固件初始化完成,准备进入配网模式");
}
// 启动AP模式
initAPMode();
// 加载WiFi配置
bool hasConfig = loadWiFiConfig();
if (hasConfig) {
connectWiFi();
} else {
Serial.println("无WiFi配置,请通过网页设置");
isWiFiConnected = false;
}
// 初始化Web服务器
initWebServer();
// 连接MQTT
if (isWiFiConnected) {
connectMQTT();
lastConnectTime = millis();
}
}
bool connectWiFi() {
if (wifiSSID.length() == 0) return false;
Serial.print("连接WiFi: ");
Serial.println(wifiSSID);
WiFi.mode(WIFI_STA);
WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
// 等待连接,超时时间10秒
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(500);
Serial.print(".");
// 连接过程中闪烁D4作为提示
digitalWrite(PIN_D4, !digitalRead(PIN_D4));
}
// 恢复D4初始状态
digitalWrite(PIN_D4, state_D4);
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi连接成功,IP: " + WiFi.localIP().toString());
isWiFiConnected = true;
wifiRetryCount = 0;
return true;
} else {
Serial.println("\nWiFi连接失败");
isWiFiConnected = false;
wifiRetryCount++;
return false;
}
}
void loop() {
// 处理Web请求
server.handleClient();
// 检查配置按键
checkConfigButton();
// WiFi重连逻辑
if (WiFi.status() != WL_CONNECTED) {
isWiFiConnected = false;
// 如果有配置且达到最大重试次数,清除配置并重启AP
if (wifiSSID.length() > 0 && wifiRetryCount >= WIFI_RETRY_LIMIT) {
Serial.println("已达到最大重试次数,清除WiFi配置并重启AP...");
WiFi.mode(WIFI_AP);
delay(1000);
initAPMode();
clearEEPROM();
delay(2000);
ESP.restart();
}
// 定时重连
if (millis() - lastConnectTime > RECONNECT_DELAY) {
Serial.println("尝试重新连接WiFi...");
if (wifiSSID.length() > 0) { // 只有有配置时才尝试重连
connectWiFi();
}
lastConnectTime = millis();
}
} else {
isWiFiConnected = true;
}
// MQTT重连逻辑
if (isWiFiConnected && !client.connected()) {
Serial.println("尝试重新连接MQTT...");
if (!connectMQTT()) {
delay(5000);
}
}
// 处理MQTT消息
if (client.connected()) {
client.loop();
}
delay(10);
}