
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>
#include <ESPmDNS.h>
#define MQTT_MAX_PACKET_SIZE 1024
#include <PubSubClient.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include <vector>
#define FIRMWARE_VERSION "1.4.0"
#define FIRMWARE_DATE "2026-03-15"
#define UART_BAUDRATE 115200
#define BUFFER_SIZE 1024
#define PACKET_SIZE 512
#define MAX_WIFI_NETWORKS 20
#define MQTT_PORT_DEFAULT 1883
#define MQTT_RECONNECT_DELAY 5000
#define MQTT_KEEPALIVE 60
#define WEB_PORT 80
#define WS_PORT 81
#define STATUS_INTERVAL 3000
class RingBuffer {
uint8_t buf[BUFFER_SIZE];
uint16_t head = 0, tail = 0, cnt = 0;
public:
uint16_t available() { return cnt; }
bool isFull() { return cnt >= BUFFER_SIZE; }
bool isEmpty() { return cnt == 0; }
uint16_t getFreeSpace(){ return BUFFER_SIZE - cnt; }
bool write(uint8_t d) {
if (isFull()) return false;
buf[head] = d;
head = (head + 1) % BUFFER_SIZE;
cnt++;
return true;
}
bool read(uint8_t *d) {
if (isEmpty()) return false;
*d = buf[tail];
tail = (tail + 1) % BUFFER_SIZE;
cnt--;
return true;
}
void clear() { head = tail = cnt = 0; }
};
RingBuffer uartBuffer;
struct LogEntry { String time; String dir; String hex; size_t len; };
std::vector<LogEntry> logEntries;
Preferences preferences;
WiFiClient wifiClient;
WiFiClientSecure wifiClientSecure;
PubSubClient *mqttClient = nullptr;
WebServer webServer(WEB_PORT);
WebSocketsServer webSocket(WS_PORT);
struct Config {
char wifiSSID[32];
char wifiPassword[64];
char mqttServer[64];
int mqttPort;
char mqttUser[32];
char mqttPassword[32];
char mqttBaseTopic[64];
bool mqttUseTLS;
bool useStaticIP;
IPAddress staticIP, gateway, subnet;
} cfg;
struct Status {
bool wifiConnected = false;
bool mqttConnected = false;
bool configMode = false;
String ipAddress;
String macAddress;
unsigned long uartRxBytes = 0;
unsigned long uartTxBytes = 0;
unsigned long mqttRxBytes = 0;
unsigned long mqttTxBytes = 0;
unsigned long uartPackets = 0;
unsigned long mqttPackets = 0;
} st;
String baseTopic, txTopic, rxTopic;
unsigned long lastStatusUpdate = 0;
unsigned long lastMQTTReconnect = 0;
bool factoryResetPending = false;
void saveConfig() {
preferences.begin("cfg", false);
preferences.putString("ssid", cfg.wifiSSID);
preferences.putString("pass", cfg.wifiPassword);
preferences.putString("mqttSrv", cfg.mqttServer);
preferences.putInt ("mqttPort",cfg.mqttPort);
preferences.putString("mqttUsr", cfg.mqttUser);
preferences.putString("mqttPwd", cfg.mqttPassword);
preferences.putString("mqttTpc", cfg.mqttBaseTopic);
preferences.putBool ("tls", cfg.mqttUseTLS);
preferences.putBool ("staticIP",cfg.useStaticIP);
preferences.putUInt ("sIP", cfg.staticIP);
preferences.putUInt ("gw", cfg.gateway);
preferences.putUInt ("sn", cfg.subnet);
preferences.end();
Serial.println("[NVS] 配置已保存");
}
void loadConfig() {
preferences.begin("cfg", true);
strncpy(cfg.wifiSSID, preferences.getString("ssid", "").c_str(), 31); cfg.wifiSSID[31] = '\0';
strncpy(cfg.wifiPassword, preferences.getString("pass", "").c_str(), 63); cfg.wifiPassword[63] = '\0';
strncpy(cfg.mqttServer, preferences.getString("mqttSrv", "").c_str(), 63); cfg.mqttServer[63] = '\0';
cfg.mqttPort = preferences.getInt ("mqttPort", MQTT_PORT_DEFAULT);
strncpy(cfg.mqttUser, preferences.getString("mqttUsr", "").c_str(), 31); cfg.mqttUser[31] = '\0';
strncpy(cfg.mqttPassword, preferences.getString("mqttPwd", "").c_str(), 31); cfg.mqttPassword[31] = '\0';
strncpy(cfg.mqttBaseTopic, preferences.getString("mqttTpc", "esp32").c_str(),63); cfg.mqttBaseTopic[63] = '\0';
cfg.mqttUseTLS = preferences.getBool ("tls", false);
cfg.useStaticIP = preferences.getBool ("staticIP", false);
cfg.staticIP = preferences.getUInt ("sIP", 0);
cfg.gateway = preferences.getUInt ("gw", 0);
cfg.subnet = preferences.getUInt ("sn", 0);
preferences.end();
Serial.println("[NVS] 配置已加载");
}
void factoryReset() {
Serial.println("[SYS] 工厂重置...");
preferences.begin("cfg", false);
preferences.clear();
preferences.end();
ESP.restart();
}
String currentAPSSID;
void startAPMode() {
st.configMode = true;
String mac = WiFi.macAddress();
currentAPSSID = "ESP32-" + mac.substring(9,11) + mac.substring(12,14) + mac.substring(15,17);
WiFi.softAPConfig(IPAddress(192,168,4,1), IPAddress(192,168,4,1), IPAddress(255,255,255,0));
WiFi.softAP(currentAPSSID.c_str(), "12345678");
st.ipAddress = WiFi.softAPIP().toString();
Serial.println("[AP] " + currentAPSSID + " IP: " + st.ipAddress);
if (MDNS.begin("esp32-c3-cfg")) {
MDNS.addService("http", "tcp", 80);
Serial.println("[mDNS] http://esp32-c3-cfg.local");
}
}
void connectWiFi() {
if (cfg.wifiSSID[0] == '\0') { startAPMode(); return; }
Serial.println("[WiFi] 连接: " + String(cfg.wifiSSID));
WiFi.mode(WIFI_STA);
if (cfg.useStaticIP) WiFi.config(cfg.staticIP, cfg.gateway, cfg.subnet);
WiFi.begin(cfg.wifiSSID, cfg.wifiPassword);
for (int i = 0; i < 20 && WiFi.status() != WL_CONNECTED; i++) { delay(500); Serial.print("."); }
if (WiFi.status() == WL_CONNECTED) {
st.wifiConnected = true;
st.configMode = false;
st.ipAddress = WiFi.localIP().toString();
Serial.println("\n[WiFi] 已连接: " + st.ipAddress);
if (MDNS.begin("esp32-c3")) {
MDNS.addService("http","tcp",80);
MDNS.addService("ws","tcp",81);
}
} else {
st.wifiConnected = false;
Serial.println("\n[WiFi] 连接失败,进入AP模式");
startAPMode();
}
}
void doWiFiScan(bool force) {
static unsigned long lastScan = 0;
static int cachedCount = 0;
if (!force && millis() - lastScan < 30000 && cachedCount > 0) {
Serial.println("[Scan] 使用缓存");
} else {
Serial.println("[Scan] 开始扫描...");
WiFiMode_t m = WiFi.getMode();
bool hasAP = (m == WIFI_AP || m == WIFI_AP_STA);
if (hasAP) {
WiFi.mode(WIFI_AP_STA);
delay(300);
WiFi.softAPConfig(IPAddress(192,168,4,1),IPAddress(192,168,4,1),IPAddress(255,255,255,0));
WiFi.softAP(currentAPSSID.c_str(), "12345678");
} else {
WiFi.mode(WIFI_STA);
delay(300);
}
WiFi.scanDelete();
cachedCount = WiFi.scanNetworks();
lastScan = millis();
if (hasAP) {
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(IPAddress(192,168,4,1),IPAddress(192,168,4,1),IPAddress(255,255,255,0));
WiFi.softAP(currentAPSSID.c_str(), "12345678");
}
Serial.println("[Scan] 发现 " + String(cachedCount) + " 个网络");
}
String json = "{\"type\":\"wifiList\",\"networks\":[";
int cnt = min(cachedCount, MAX_WIFI_NETWORKS);
for (int i = 0; i < cnt; i++) {
String ssid = WiFi.SSID(i);
ssid.replace("\"","\\\"");
if (i) json += ",";
json += "{\"ssid\":\"" + ssid + "\",\"rssi\":" + String(WiFi.RSSI(i)) +
",\"open\":" + (WiFi.encryptionType(i)==WIFI_AUTH_OPEN?"true":"false") + "}";
}
json += "]}";
webSocket.broadcastTXT(json);
}
String bytesToHex(const uint8_t *data, size_t len) {
String s;
char b[3];
for (size_t i = 0; i < len; i++) { sprintf(b, "%02X", data[i]); s += b; }
return s;
}
void addLog(const char* dir, const String& hex, size_t len) {
if (logEntries.size() >= 200) logEntries.erase(logEntries.begin());
logEntries.push_back({String(millis()), String(dir), hex, len});
}
void mqttCallback(char* topic, byte* payload, unsigned int length) {
st.mqttRxBytes += length;
st.mqttPackets++;
String hex = bytesToHex(payload, length);
addLog("rx", hex, length);
if (webSocket.connectedClients()) {
webSocket.broadcastTXT("{\"type\":\"data\",\"dir\":\"rx\",\"hex\":\"" + hex + "\",\"len\":" + length + "}");
}
Serial.write(payload, length);
st.uartTxBytes += length;
}
void generateTopics() {
String mac = WiFi.macAddress(); mac.replace(":", "");
baseTopic = String(cfg.mqttBaseTopic) + "/" + mac;
txTopic = baseTopic + "/tx";
rxTopic = baseTopic + "/rx";
Serial.println("[MQTT] base=" + baseTopic);
}
void connectMQTT() {
if (cfg.mqttServer[0] == '\0' || !st.wifiConnected) return;
if (mqttClient && millis() - lastMQTTReconnect < MQTT_RECONNECT_DELAY) return;
lastMQTTReconnect = millis();
if (!mqttClient) {
if (cfg.mqttUseTLS) { wifiClientSecure.setInsecure(); mqttClient = new PubSubClient(wifiClientSecure); }
else { mqttClient = new PubSubClient(wifiClient); }
mqttClient->setServer(cfg.mqttServer, cfg.mqttPort);
mqttClient->setCallback(mqttCallback);
mqttClient->setKeepAlive(MQTT_KEEPALIVE);
mqttClient->setSocketTimeout(15);
}
String cid = "ESP32-" + WiFi.macAddress(); cid.replace(":", "");
Serial.println("[MQTT] 连接 " + String(cfg.mqttServer) + ":" + cfg.mqttPort);
if (mqttClient->connect(cid.c_str(),
cfg.mqttUser[0] ? cfg.mqttUser : nullptr,
cfg.mqttPassword[0] ? cfg.mqttPassword : nullptr)) {
st.mqttConnected = true;
mqttClient->subscribe(rxTopic.c_str());
mqttClient->publish(baseTopic.c_str(), "online", true);
Serial.println("[MQTT] 已连接,订阅: " + rxTopic);
} else {
st.mqttConnected = false;
Serial.println("[MQTT] 连接失败,状态: " + String(mqttClient->state()));
}
}
const char index_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">
<title>ESP32-C3 MQTT</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,sans-serif;background:#111;color:#ddd;font-size:14px}
.wrap{max-width:960px;margin:0 auto;padding:12px}
.topbar{background:#1a1a2e;border-radius:8px;padding:12px 16px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px}
.topbar h1{font-size:1.1rem;color:#7c9ef5}
.meta{font-size:12px;color:#888;display:flex;gap:16px;flex-wrap:wrap}
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:12px}
@media(max-width:600px){.cards{grid-template-columns:repeat(2,1fr)}}
.card{background:#1a1a2e;border-radius:8px;padding:12px;text-align:center;border:1px solid #2a2a4e}
.card h4{font-size:11px;color:#888;margin-bottom:6px;text-transform:uppercase}
.card .val{font-size:1.3rem;font-weight:700}
.ok{color:#00cc88}.err{color:#ff6b6b}
.main{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px}
@media(max-width:700px){.main{grid-template-columns:1fr}}
.panel{background:#1a1a2e;border-radius:8px;overflow:hidden;border:1px solid #2a2a4e}
.ph{background:#16213e;padding:10px 14px;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #2a2a4e}
.ph h3{font-size:.9rem;color:#aaa}
.pb{padding:12px}
.log{background:#0d0d1a;border-radius:6px;padding:10px;font-family:monospace;font-size:12px;height:320px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#444 transparent}
.log::-webkit-scrollbar{width:4px}.log::-webkit-scrollbar-thumb{background:#444;border-radius:2px}
.le{padding:4px 0;border-bottom:1px solid #1a1a2e;display:flex;gap:8px;align-items:baseline}
.le:last-child{border:none}
.lt{color:#555;min-width:80px;font-size:11px}
.tx{color:#7c9ef5}.rx{color:#00cc88}
.lv{color:#bbb;word-break:break-all}
.tabs{display:flex;gap:4px;margin-bottom:10px}
.tb{flex:1;padding:6px;background:#16213e;border:none;border-radius:5px;color:#888;cursor:pointer;font-size:12px;transition:.2s}
.tb.on{background:#7c9ef5;color:#fff}
.fg{margin-bottom:10px}
label{display:block;font-size:12px;color:#888;margin-bottom:4px}
input[type=text],input[type=password],input[type=number]{width:100%;padding:8px 10px;background:#0d0d1a;border:1px solid #2a2a4e;border-radius:5px;color:#ddd;font-size:13px}
input:focus{outline:none;border-color:#7c9ef5}
input::placeholder{color:#444}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.chk{display:flex;align-items:center;gap:8px;cursor:pointer}
.chk input{width:16px;height:16px;accent-color:#7c9ef5;cursor:pointer;flex-shrink:0}
.wlist{max-height:200px;overflow-y:auto;border:1px solid #2a2a4e;border-radius:5px;margin-bottom:10px;scrollbar-width:thin;scrollbar-color:#444 transparent}
.wi{padding:8px 12px;border-bottom:1px solid #1f1f35;cursor:pointer;display:flex;justify-content:space-between;align-items:center;transition:.15s}
.wi:hover{background:#16213e}.wi:last-child{border:none}
.wn{font-size:13px}.ws{font-size:11px;color:#666}
.sbar{display:inline-block;width:40px;height:4px;background:#333;border-radius:2px;overflow:hidden;margin-left:6px;vertical-align:middle}
.sf{height:100%;background:#7c9ef5;border-radius:2px}
.btn{padding:7px 14px;border:none;border-radius:5px;cursor:pointer;font-size:12px;font-weight:600;transition:.2s}
.btn-p{background:#7c9ef5;color:#fff}.btn-p:hover{background:#5a80e0}
.btn-s{background:#2a2a4e;color:#bbb;border:1px solid #3a3a5e}.btn-s:hover{background:#3a3a5e}
.btn-d{background:#c0392b;color:#fff}.btn-d:hover{background:#e74c3c}
.btn-full{width:100%;padding:9px;margin-top:8px}
.bgroup{display:flex;gap:6px;flex-wrap:wrap}
.bbar{height:8px;background:#0d0d1a;border-radius:4px;overflow:hidden;margin:4px 0 2px}
.bf{height:100%;background:#7c9ef5;border-radius:4px;transition:width .4s}
.bt{display:flex;justify-content:space-between;font-size:11px;color:#555}
#toast{position:fixed;top:16px;left:50%;transform:translateX(-50%);padding:8px 18px;border-radius:6px;font-size:13px;color:#fff;z-index:9999;display:none;animation:fi .3s}
@keyframes fi{from{opacity:0;top:8px}to{opacity:1;top:16px}}
#apbadge{display:none;background:#c0392b;padding:4px 10px;border-radius:12px;font-size:12px;color:#fff}
</style>
</head>
<body>
<div class="wrap">
<div class="topbar">
<div>
<h1>ESP32-C3 MQTT透传</h1>
<div class="meta">
<span>版本: <b id="fwVer">-</b></span>
<span>MAC: <b id="mac">-</b></span>
<span>IP: <b id="ip" style="color:#00cc88">-</b></span>
<span>运行: <b id="uptime">-</b></span>
</div>
</div>
<div id="apbadge">⚡ 配网模式</div>
</div>
<div class="cards">
<div class="card" onclick="doReconnectWiFi()" style="cursor:pointer" title="点击重连">
<h4>📶 WiFi</h4>
<div class="val err" id="wifiSt">断开</div>
</div>
<div class="card" onclick="doReconnectMQTT()" style="cursor:pointer" title="点击重连">
<h4>📡 MQTT</h4>
<div class="val err" id="mqttSt">断开</div>
</div>
<div class="card">
<h4>📥 UART收</h4>
<div class="val" id="uartRx">0B</div>
<div style="font-size:11px;color:#666"><span id="uartPkts">0</span>包</div>
</div>
<div class="card">
<h4>📤 MQTT收</h4>
<div class="val" id="mqttRx">0B</div>
<div style="font-size:11px;color:#666"><span id="mqttPkts">0</span>包</div>
</div>
</div>
<div class="main">
<!-- 数据监控 -->
<div class="panel">
<div class="ph">
<h3>数据监控</h3>
<button class="btn btn-s" onclick="clearLog()">清空</button>
</div>
<div class="pb">
<div class="tabs" id="logTabs">
<button class="tb on" onclick="switchLog('all',0)">全部</button>
<button class="tb" onclick="switchLog('tx',1)">发送</button>
<button class="tb" onclick="switchLog('rx',2)">接收</button>
</div>
<div class="log" id="dataLog"></div>
</div>
</div>
<!-- 配置 -->
<div class="panel">
<div class="ph">
<h3>配置</h3>
<div class="bgroup">
<button class="btn btn-p" onclick="scanWiFi()">扫描WiFi</button>
<button class="btn btn-d" onclick="doFactoryReset()">重置</button>
</div>
</div>
<div class="pb">
<div class="tabs" id="cfgTabs">
<button class="tb on" onclick="switchCfg('wifi',0)">WiFi</button>
<button class="tb" onclick="switchCfg('mqtt',1)">MQTT</button>
</div>
<!-- WiFi配置 -->
<div id="tabWifi">
<div class="wlist" id="wifiList">
<div style="padding:16px;text-align:center;color:#555">点击"扫描WiFi"查找网络</div>
</div>
<div class="fg"><label>SSID</label><input type="text" id="wSSID" placeholder="WiFi名称"></div>
<div class="fg"><label>密码</label><input type="password" id="wPass" placeholder="WiFi密码"></div>
<div class="fg">
<label class="chk"><input type="checkbox" id="useStatic" onchange="toggleStatic(this.checked)"> 使用静态IP</label>
</div>
<div id="staticCfg" style="display:none">
<div class="row2">
<div class="fg"><label>IP地址</label><input type="text" id="sIP" placeholder="192.168.1.100"></div>
<div class="fg"><label>网关</label><input type="text" id="sGW" placeholder="192.168.1.1"></div>
</div>
<div class="fg"><label>子网掩码</label><input type="text" id="sSN" placeholder="255.255.255.0"></div>
</div>
</div>
<!-- MQTT配置 -->
<div id="tabMqtt" style="display:none">
<div class="fg"><label>服务器地址</label><input type="text" id="mSrv" placeholder="broker.hivemq.com"></div>
<div class="row2">
<div class="fg"><label>端口</label><input type="number" id="mPort" value="1883"></div>
<div class="fg" style="padding-top:20px"><label class="chk"><input type="checkbox" id="mTLS"> TLS加密</label></div>
</div>
<div class="row2">
<div class="fg"><label>用户名</label><input type="text" id="mUser" placeholder="可选"></div>
<div class="fg"><label>密码</label><input type="password" id="mPwd" placeholder="可选"></div>
</div>
<div class="fg"><label>基础主题</label><input type="text" id="mTopic" value="esp32"></div>
</div>
<button class="btn btn-p btn-full" onclick="saveConfig()">保存并重启</button>
</div>
</div>
</div>
<!-- 缓冲区 -->
<div class="panel">
<div class="ph"><h3>缓冲区</h3></div>
<div class="pb" style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div>
<label>UART缓冲</label>
<div class="bbar"><div class="bf" id="uartBF" style="width:0"></div></div>
<div class="bt"><span id="uartBU">0</span><span>/ 1024 B</span></div>
</div>
<div>
<label>MQTT缓冲</label>
<div class="bbar"><div class="bf" id="mqttBF" style="width:0"></div></div>
<div class="bt"><span id="mqttBU">0</span><span>/ 1024 B</span></div>
</div>
</div>
</div>
</div>
<div id="toast"></div>
<script>
let ws, logData=[], logFilter='all', t0=Date.now();
function wsConnect(){
ws=new WebSocket('ws://'+location.hostname+':81');
ws.onmessage=e=>{try{onMsg(JSON.parse(e.data))}catch(_){}};
ws.onclose=()=>setTimeout(wsConnect,3000);
}
function onMsg(d){
if(d.type==='status') updStatus(d);
else if(d.type==='data') addLog(d.dir,d.hex,d.len);
else if(d.type==='wifiList') updWifi(d.networks);
}
function updStatus(d){
set('wifiSt', d.wifiConnected?'已连接':'断开', d.wifiConnected?'ok':'err');
set('mqttSt', d.mqttConnected?'已连接':'断开', d.mqttConnected?'ok':'err');
setText('ip', d.ipAddress||'-');
setText('mac', d.macAddress||'-');
setText('fwVer', d.fwVer||'-');
setText('uartRx', fmtB(d.uartRxBytes));
setText('mqttRx', fmtB(d.mqttRxBytes));
setText('uartPkts', d.uartPackets);
setText('mqttPkts', d.mqttPackets);
setW('uartBF', d.uartBufPct); setText('uartBU', d.uartBufUsed);
setW('mqttBF', d.mqttBufPct||0); setText('mqttBU', d.mqttBufUsed||0);
document.getElementById('apbadge').style.display = d.configMode?'block':'none';
}
function set(id,txt,cls){const el=document.getElementById(id);el.textContent=txt;el.className='val '+cls}
function setText(id,v){document.getElementById(id).textContent=v}
function setW(id,p){document.getElementById(id).style.width=(p||0)+'%'}
function hex2txt(h){let s='';for(let i=0;i<h.length;i+=2){const b=parseInt(h.substr(i,2),16);s+=(b>=32&&b<=126)?String.fromCharCode(b):'.'}return s}
function addLog(dir,hex,len){
logData.push({t:new Date().toLocaleTimeString(),dir,hex,len,txt:hex2txt(hex)});
if(logData.length>100) logData.shift();
renderLog();
}
function renderLog(){
const el=document.getElementById('dataLog');
const rows=(logFilter==='all'?logData:logData.filter(e=>e.dir===logFilter));
el.innerHTML=rows.map(e=>`<div class="le"><span class="lt">${e.t}</span><span class="${e.dir}">[${e.dir.toUpperCase()}]</span><span class="lv">${e.txt} <span style="color:#555">(${e.len}B)</span></span></div>`).join('');
el.scrollTop=el.scrollHeight;
}
function clearLog(){logData=[];renderLog()}
function switchLog(f,i){
logFilter=f;
document.querySelectorAll('#logTabs .tb').forEach((b,j)=>b.classList.toggle('on',j===i));
renderLog();
}
function switchCfg(t,i){
document.getElementById('tabWifi').style.display=t==='wifi'?'block':'none';
document.getElementById('tabMqtt').style.display=t==='mqtt'?'block':'none';
document.querySelectorAll('#cfgTabs .tb').forEach((b,j)=>b.classList.toggle('on',j===i));
}
function toggleStatic(v){document.getElementById('staticCfg').style.display=v?'block':'none'}
function loadConfig(){
fetch('/getconfig').then(r=>r.json()).then(d=>{
g('wSSID').value=d.wifiSSID||'';
g('wPass').value=d.wifiPassword||'';
g('useStatic').checked=!!d.useStaticIP;
toggleStatic(!!d.useStaticIP);
g('sIP').value=d.staticIP||'';
g('sGW').value=d.gateway||'';
g('sSN').value=d.subnet||'';
g('mSrv').value=d.mqttServer||'';
g('mPort').value=d.mqttPort||1883;
g('mUser').value=d.mqttUser||'';
g('mPwd').value=d.mqttPassword||'';
g('mTopic').value=d.mqttBaseTopic||'esp32';
g('mTLS').checked=!!d.mqttUseTLS;
}).catch(_=>{});
}
function saveConfig(){
const body={
wifiSSID:g('wSSID').value, wifiPassword:g('wPass').value,
mqttServer:g('mSrv').value, mqttPort:parseInt(g('mPort').value)||1883,
mqttUser:g('mUser').value, mqttPassword:g('mPwd').value,
mqttBaseTopic:g('mTopic').value, mqttUseTLS:g('mTLS').checked,
useStaticIP:g('useStatic').checked,
staticIP:g('sIP').value, gateway:g('sGW').value, subnet:g('sSN').value
};
fetch('/saveconfig',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})
.then(r=>r.json()).then(d=>{if(d.success)toast('配置已保存,正在重启...','ok');else toast('保存失败','err')})
.catch(_=>toast('保存失败','err'));
}
function scanWiFi(){
document.getElementById('wifiList').innerHTML='<div style="padding:16px;text-align:center;color:#555">扫描中...</div>';
fetch('/scanwifi').catch(_=>{});
}
function updWifi(nets){
if(!nets||!nets.length){
document.getElementById('wifiList').innerHTML='<div style="padding:16px;text-align:center;color:#555">未发现网络</div>';
return;
}
document.getElementById('wifiList').innerHTML=nets.map(n=>`
<div class="wi" onclick="g('wSSID').value='${n.ssid.replace(/'/g,"\\'")}'" >
<span class="wn">${n.ssid}${n.open?' 🔓':''}</span>
<span class="ws">${n.rssi}dBm<span class="sbar"><span class="sf" style="width:${Math.min(100,Math.max(0,(n.rssi+100)*2))}%"></span></span></span>
</div>`).join('');
}
function doReconnectWiFi(){ if(confirm('重新连接WiFi?(设备将重启)')) fetch('/reconnectwifi').catch(_=>{}); }
function doReconnectMQTT(){ fetch('/reconnectmqtt').then(_=>toast('MQTT重连中...','ok')).catch(_=>{}); }
function doFactoryReset(){ if(confirm('确定工厂重置?所有配置将清除!')) fetch('/factoryreset').catch(_=>{}); }
function g(id){return document.getElementById(id)}
function fmtB(b){if(!b)return'0B';if(b<1024)return b+'B';if(b<1048576)return(b/1024).toFixed(1)+'KB';return(b/1048576).toFixed(1)+'MB'}
function toast(msg,type){const el=document.getElementById('toast');el.textContent=msg;el.style.background=type==='ok'?'#2ecc71':type==='err'?'#e74c3c':'#7c9ef5';el.style.display='block';setTimeout(()=>el.style.display='none',3000)}
function updUptime(){const s=Math.floor((Date.now()-t0)/1000);const h=Math.floor(s/3600),m=Math.floor(s%3600/60),ss=s%60;setText('uptime',[h,m,ss].map(v=>String(v).padStart(2,'0')).join(':'))}
wsConnect();
loadConfig();
setInterval(updUptime,1000);
</script>
</body>
</html>
)rawliteral";
void handleRoot() { webServer.send(200, "text/html", index_html); }
void handleScanWiFi() {
bool force = (webServer.arg("force") == "1");
Serial.println("[HTTP] /scanwifi force=" + String(force));
doWiFiScan(force);
webServer.send(200, "application/json", "{\"ok\":true}");
}
void handleSaveConfig() {
if (webServer.method() != HTTP_POST) { webServer.send(405, "text/plain", "Method Not Allowed"); return; }
DynamicJsonDocument doc(1024);
deserializeJson(doc, webServer.arg("plain"));
strncpy(cfg.wifiSSID, doc["wifiSSID"] |"", 31); cfg.wifiSSID[31]='\0';
strncpy(cfg.wifiPassword, doc["wifiPassword"] |"", 63); cfg.wifiPassword[63]='\0';
strncpy(cfg.mqttServer, doc["mqttServer"] |"", 63); cfg.mqttServer[63]='\0';
cfg.mqttPort = doc["mqttPort"] | MQTT_PORT_DEFAULT;
strncpy(cfg.mqttUser, doc["mqttUser"] |"", 31); cfg.mqttUser[31]='\0';
strncpy(cfg.mqttPassword, doc["mqttPassword"] |"", 31); cfg.mqttPassword[31]='\0';
strncpy(cfg.mqttBaseTopic, doc["mqttBaseTopic"] |"esp32",63); cfg.mqttBaseTopic[63]='\0';
cfg.mqttUseTLS = doc["mqttUseTLS"] | false;
cfg.useStaticIP = doc["useStaticIP"] | false;
if (cfg.useStaticIP) {
String ip = doc["staticIP"]|"", gw = doc["gateway"]|"", sn = doc["subnet"]|"";
if (!ip.isEmpty()) cfg.staticIP.fromString(ip);
if (!gw.isEmpty()) cfg.gateway.fromString(gw);
if (!sn.isEmpty()) cfg.subnet.fromString(sn);
}
saveConfig();
webServer.send(200, "application/json", "{\"success\":true}");
delay(800);
ESP.restart();
}
void handleGetConfig() {
String j="{";
j+="\"wifiSSID\":\"" +String(cfg.wifiSSID)+"\",";
j+="\"wifiPassword\":\"" +String(cfg.wifiPassword)+"\",";
j+="\"mqttServer\":\"" +String(cfg.mqttServer)+"\",";
j+="\"mqttPort\":" +String(cfg.mqttPort)+",";
j+="\"mqttUser\":\"" +String(cfg.mqttUser)+"\",";
j+="\"mqttPassword\":\"" +String(cfg.mqttPassword)+"\",";
j+="\"mqttBaseTopic\":\"" +String(cfg.mqttBaseTopic)+"\",";
j+="\"mqttUseTLS\":" +String(cfg.mqttUseTLS?"true":"false")+",";
j+="\"useStaticIP\":" +String(cfg.useStaticIP?"true":"false")+",";
j+="\"staticIP\":\"" +cfg.staticIP.toString()+"\",";
j+="\"gateway\":\"" +cfg.gateway.toString()+"\",";
j+="\"subnet\":\"" +cfg.subnet.toString()+"\"";
j+="}";
webServer.send(200, "application/json", j);
}
void handleFactoryReset() {
factoryResetPending = true;
webServer.send(200, "text/plain", "ok");
}
void handleReconnectWiFi() {
webServer.send(200, "text/plain", "ok");
delay(500);
ESP.restart();
}
void handleReconnectMQTT() {
if (mqttClient) {
mqttClient->disconnect();
delete mqttClient;
mqttClient = nullptr;
}
st.mqttConnected = false;
lastMQTTReconnect = 0;
webServer.send(200, "text/plain", "ok");
}
void handleExportLogs() {
String csv = "时间,方向,长度,十六进制\n";
for (auto& e : logEntries)
csv += e.time + "," + e.dir + "," + String(e.len) + "," + e.hex + "\n";
webServer.sendHeader("Content-Disposition","attachment; filename=logs.csv");
webServer.send(200, "text/csv", csv);
}
void handleCaptivePortal() {
String uri = webServer.uri();
String host = webServer.hostHeader();
if (uri=="/connecttest.txt"||uri=="/hotspot-detect.html"||uri=="/generate_204"||
uri=="/fwlink"||uri.startsWith("/ncsi")||uri.startsWith("/mobile/status")){
webServer.sendHeader("Location","http://192.168.4.1/");
webServer.send(302,"text/plain","");
return;
}
if (!host.startsWith("192.168.4.1") && !host.endsWith(".local")) {
webServer.sendHeader("Location","http://192.168.4.1/");
webServer.send(302,"text/plain","");
return;
}
webServer.sendHeader("Location","http://192.168.4.1/");
webServer.send(302,"text/plain","");
}
void setupWebServer() {
webServer.on("/", handleRoot);
webServer.on("/scanwifi", handleScanWiFi);
webServer.on("/saveconfig", handleSaveConfig);
webServer.on("/getconfig", handleGetConfig);
webServer.on("/factoryreset", handleFactoryReset);
webServer.on("/reconnectwifi", handleReconnectWiFi);
webServer.on("/reconnectmqtt", handleReconnectMQTT);
webServer.on("/exportlogs", handleExportLogs);
webServer.onNotFound(handleCaptivePortal);
webServer.begin();
Serial.println("[Web] http://" + st.ipAddress);
}
void wsEvent(uint8_t num, WStype_t type, uint8_t *payload, size_t length) {
if (type == WStype_CONNECTED) Serial.printf("[WS] #%u 已连接\n", num);
if (type == WStype_DISCONNECTED) Serial.printf("[WS] #%u 断开\n", num);
}
void sendStatus() {
if (!webSocket.connectedClients()) return;
String j="{";
j+="\"type\":\"status\",";
j+="\"wifiConnected\":" +String(st.wifiConnected?"true":"false")+",";
j+="\"mqttConnected\":" +String(st.mqttConnected?"true":"false")+",";
j+="\"configMode\":" +String(st.configMode?"true":"false")+",";
j+="\"ipAddress\":\"" +st.ipAddress+"\",";
j+="\"macAddress\":\"" +st.macAddress+"\",";
j+="\"fwVer\":\"" +String(FIRMWARE_VERSION)+"\",";
j+="\"uartRxBytes\":" +String(st.uartRxBytes)+",";
j+="\"mqttRxBytes\":" +String(st.mqttRxBytes)+",";
j+="\"uartPackets\":" +String(st.uartPackets)+",";
j+="\"mqttPackets\":" +String(st.mqttPackets)+",";
j+="\"uartBufUsed\":" +String(uartBuffer.available())+",";
j+="\"uartBufPct\":" +String(uartBuffer.available()*100/BUFFER_SIZE)+",";
j+="\"mqttBufUsed\":0,\"mqttBufPct\":0";
j+="}";
webSocket.broadcastTXT(j);
}
void handleSerial() {
while (Serial.available()) {
uint8_t b = Serial.read();
if (uartBuffer.write(b)) st.uartRxBytes++;
}
}
void processUART() {
if (uartBuffer.isEmpty()) return;
uint8_t data[PACKET_SIZE];
size_t len = 0;
while (len < PACKET_SIZE && uartBuffer.read(&data[len])) len++;
if (!len) return;
st.uartPackets++;
String hex = bytesToHex(data, len);
addLog("tx", hex, len);
if (mqttClient && mqttClient->connected()) {
mqttClient->publish(txTopic.c_str(), data, len);
st.mqttTxBytes += len;
st.mqttPackets++;
}
if (webSocket.connectedClients()) {
webSocket.broadcastTXT("{\"type\":\"data\",\"dir\":\"tx\",\"hex\":\""+hex+"\",\"len\":"+len+"}");
}
}
void setup() {
Serial.begin(UART_BAUDRATE);
delay(100);
Serial.println("\n=== ESP32-C3 MQTT透传 v" + String(FIRMWARE_VERSION) + " ===\n");
st.macAddress = WiFi.macAddress();
loadConfig();
connectWiFi();
generateTopics();
setupWebServer();
webSocket.begin();
webSocket.onEvent(wsEvent);
connectMQTT();
Serial.println("[SYS] 初始化完成 http://" + st.ipAddress);
}
void loop() {
webServer.handleClient();
webSocket.loop();
if (mqttClient) {
if (mqttClient->connected()) {
mqttClient->loop();
st.mqttConnected = true;
} else {
st.mqttConnected = false;
connectMQTT();
}
}
handleSerial();
processUART();
if (millis() - lastStatusUpdate >= STATUS_INTERVAL) {
lastStatusUpdate = millis();
sendStatus();
}
if (factoryResetPending) factoryReset();
delay(5);
}