wifi配网模块

在这里插入图片描述

// ESP32-C3 SuperMini MQTT透传固件
// 功能:WiFi配网 + MQTT透传 + 串口通信 + Web监控
// 作者:Auto AI Assistant
// 日期:2026-03-15

#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

// ============== MQTT配置 ==============
#define MQTT_PORT_DEFAULT  1883
#define MQTT_RECONNECT_DELAY 5000
#define MQTT_KEEPALIVE     60

// ============== Web配置 ==============
#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;

// ============== NVS存储 ==============
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();
}

// ============== WiFi管理 ==============
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();
  }
}

// ============== WiFi扫描(同步,仅在AP模式配网时使用)==============
void doWiFiScan(bool force) {
  static unsigned long lastScan = 0;
  static int cachedCount = 0;
  // 30秒缓存
  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();
    // 恢复纯AP
    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) + " 个网络");
  }
  // 构建 JSON 并通过 WebSocket 推送
  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);
}

// ============== MQTT管理 ==============
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()));
  }
}

// ============== Web页面 ==============
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}
/* Tab */
.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}
/* WiFi列表 */
.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 */
#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();

// ── WebSocket ──
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();
}

// ── Tab ──
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'));
}

// ── WiFi扫描(通过WebSocket接收结果)──
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";

// ============== Web路由处理 ==============
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() {
  // 删除现有客户端,下次loop自动重连
  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);
}

// ============== WebSocket ==============
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+"}");
  }
}

// ============== Setup / Loop ==============
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);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值