#include <WiFiManager.h> #include <WiFi.h> #include <TFT_eSPI.h> #include <PubSubClient.h> #include "esp_system.h" #include <Arduino.h> #include <TJpg_Decoder.h> #include <HTTPClient.h> #include <src/CommonUtils.h> TFT_eSPI tft = TFT_eSPI(); // 创建TFT对象 WiFiManager wifiManager; unsigned long buttonPressTime = 0; bool isConnected = false; // 正在连接标记 bool resetTriggered = false; #define BOOT_BUTTON_PIN 0 // 显示状态常量 #define STATUS_WIFI_CONNECTING 0 #define STATUS_WIFI_CONNECTED 1 #define STATUS_WIFI_DISCONNECTED 2 #define STATUS_MQTT_CONNECTING 3 #define STATUS_MQTT_CONNECTED 4 #define STATUS_DOWNLOADING 5 #define STATUS_DISPLAYING_IMAGE 6 #define STATUS_PLAYING_MJPEG 7 int currentStatus = STATUS_WIFI_CONNECTING; // MQTT 配置 const char* mqttServer = "xxxx"; const int mqttPort = 1883; const char* mqttUser = "xxxx"; // 无需认证留空 const char* mqttPassword = "xxxx"; const char* clientId = "arduino_client_1232"; // 客户端ID需唯一 WiFiClient espClient; PubSubClient client(espClient); bool isVideo = false; // 内存缓冲区 uint8_t* jpegBuffer = NULL; // JPEG原始数据缓冲区 (PSRAM) size_t jpegSize = 0; // JPEG图片大小 uint8_t* mjpegBuffer = NULL; // MJPEG文件缓冲区 (PSRAM) size_t mjpegSize = 0; // MJPEG文件大小 size_t mjpegPosition = 0; // 当前MJPEG文件位置 // 帧缓冲区 uint8_t* frameBuffer = NULL; // 单帧数据缓冲区 size_t frameSize = 0; // 当前帧大小 // 流缓冲区 uint8_t* streamBuffer = NULL; // 流数据缓冲区 size_t streamBufferSize = 0; // 流缓冲区大小 size_t streamBufferPos = 0; // 流缓冲区位置 // 定义常量以提高可读性和可维护性 #define BUFFER_SIZE 4096 // 增大缓冲区大小 #define DOWNLOAD_TIMEOUT 15000 // 下载超时时间(毫秒) #define DATA_TIMEOUT 5000 // 数据超时时间(毫秒) #define PROGRESS_INTERVAL 500 // 进度显示间隔(毫秒) // 帧处理相关 #define MAX_FRAME_SIZE (1024 * 1024) // 最大帧大小1MB #define STREAM_BUFFER_SIZE (16 * 1024) // 流缓冲区大小16KB #define FRAME_DELAY 40 // 帧延迟(毫秒)，影响播放速度 // 默认MJPEG URL const char* defaultMjpegUrl = "https://chenhuan-esp.oss-cn-beijing.aliyuncs.com/output.avi"; // 当前播放状态 bool isPlayingMjpeg = false; unsigned long lastFrameTime = 0; unsigned long frameCount = 0; unsigned long mjpegStartTime = 0; // TJpgDec解码回调函数 - 用于将解码后的块数据绘制到屏幕 bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) { // 检查块是否在屏幕范围内 if (y >= TFT_HEIGHT) return 0; // 绘制到TFT屏幕 tft.pushImage(x, y, w, h, bitmap); return 1; // 返回1继续解码，返回0终止解码 } // 向屏幕写入状态信息 void writeToScreen(const char* message, bool clearScreen = true) { if (clearScreen) { tft.fillScreen(TFT_BLACK); tft.setCursor(0, 0); } tft.printf(message); } // 更新屏幕状态显示 void updateStatusDisplay() { tft.fillScreen(TFT_BLACK); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.setTextSize(1); // 显示基本信息 tft.print("ESP32 MJPEG Player - "); switch (currentStatus) { case STATUS_WIFI_CONNECTING: tft.println("WiFi Connecting"); break; case STATUS_WIFI_CONNECTED: tft.println("WiFi Connected"); break; case STATUS_WIFI_DISCONNECTED: tft.println("WiFi Disconnected"); break; case STATUS_MQTT_CONNECTING: tft.println("MQTT Connecting"); break; case STATUS_MQTT_CONNECTED: tft.println("MQTT Connected"); break; case STATUS_DOWNLOADING: tft.println("Downloading..."); break; case STATUS_DISPLAYING_IMAGE: tft.println("Displaying Image"); break; case STATUS_PLAYING_MJPEG: tft.println("Playing MJPEG"); break; } // 显示WiFi信息 tft.setCursor(10, 30); if (WiFi.status() == WL_CONNECTED) { tft.print("WiFi: "); tft.print(WiFi.SSID()); tft.print(" ("); tft.print(WiFi.RSSI()); tft.println(" dBm)"); } else { tft.println("WiFi: Not Connected"); } // 显示MQTT信息 tft.setCursor(10, 50); if (client.connected()) { tft.println("MQTT: Connected"); } else { tft.println("MQTT: Not Connected"); } // 显示MJPEG播放信息 if (isPlayingMjpeg) { tft.setCursor(10, 70); float fps = frameCount / ((millis() - mjpegStartTime) / 1000.0); tft.print("FPS: "); tft.print(fps, 1); tft.print(" Frame: "); tft.print(frameCount); } } // 解码并显示JPEG图片 bool decodeAndDisplayJpeg(uint8_t* buffer, size_t size) { if (!buffer || size == 0) { Serial.println("错误：JPEG缓冲区为空"); return false; } Serial.println("开始解码JPEG图片..."); // 注册TJpgDec输出回调函数 TJpgDec.setCallback(tft_output); // 调用TJpgDec解码函数 bool result = TJpgDec.drawJpg(0, 0, buffer, size); if (result) { Serial.println("JPEG图片显示成功"); } return result; } // 检查WiFi状态并更新 void checkWifiStatus() { // 按钮检测（保持原功能） if (digitalRead(BOOT_BUTTON_PIN) == LOW && !resetTriggered) { delay(50); if (digitalRead(BOOT_BUTTON_PIN) == LOW) { buttonPressTime = millis(); resetTriggered = true; while (digitalRead(BOOT_BUTTON_PIN) == LOW) { if (millis() - buttonPressTime > 3000) { Serial.println("\n重置WiFi配置..."); writeToScreen("Resetting WiFi configuration..."); wifiManager.resetSettings(); delay(2000); ESP.restart(); } } resetTriggered = false; } } // WiFi状态监测 if (WiFi.status() != WL_CONNECTED && isConnected) { isConnected = false; currentStatus = STATUS_WIFI_DISCONNECTED; Serial.println("WiFi连接丢失，尝试重连..."); writeToScreen("WiFi connection lost, trying to reconnect..."); } else if (WiFi.status() == WL_CONNECTED && !isConnected) { isConnected = true; currentStatus = STATUS_WIFI_CONNECTED; Serial.println("WiFi已连接"); writeToScreen("WiFi is connected"); } } // 重连MQTT服务器 void reconnectMQTT() { if (WiFi.status() != WL_CONNECTED) return; currentStatus = STATUS_MQTT_CONNECTING; updateStatusDisplay(); while (!client.connected()) { Serial.print("尝试连接MQTT服务器..."); if (client.connect(clientId, mqttUser, mqttPassword)) { Serial.println("连接成功"); currentStatus = STATUS_MQTT_CONNECTED; updateStatusDisplay(); // 订阅主题 char topic[100]; sprintf(topic, "%s/%s", clientId, "downloadImages"); client.subscribe(topic); sprintf(topic, "%s/%s", clientId, "downloadVideos"); client.subscribe(topic); sprintf(topic, "%s/%s", clientId, "playMjpeg"); client.subscribe(topic); } else { Serial.print("连接失败，状态码="); Serial.print(client.state()); Serial.println(" 重试中..."); delay(2000); } } } // 核心下载函数（下载到PSRAM） bool downloadToPSRAM(const String& url, uint8_t*& buffer, size_t& size, bool showProgress = true) { // 初始化HTTP客户端 HTTPClient http; http.setReuse(true); // 启用连接复用 http.begin(url); // 设置超时和重试 http.setTimeout(DOWNLOAD_TIMEOUT); http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // 执行请求 int httpCode = http.GET(); if (httpCode != HTTP_CODE_OK) { Serial.printf("HTTP错误: %d\n", httpCode); http.end(); return false; } // 获取数据流和内容长度 WiFiClient* stream = http.getStreamPtr(); size_t contentLength = http.getSize(); size_t totalSize = 0; // 确保PSRAM缓冲区已分配（如果未分配则分配） if (!buffer) { buffer = (uint8_t*)ps_malloc(contentLength); if (!buffer) { Serial.println("PSRAM分配失败"); http.end(); return false; } size = contentLength; } else if (contentLength > size) { // 如果已有缓冲区不够大，则重新分配 free(buffer); buffer = (uint8_t*)ps_malloc(contentLength); if (!buffer) { Serial.println("PSRAM重新分配失败"); http.end(); return false; } size = contentLength; } unsigned long timeoutMark = millis(); unsigned long lastProgressTime = 0; // 更新状态显示 currentStatus = STATUS_DOWNLOADING; updateStatusDisplay(); // 核心下载循环 while (http.connected() && (totalSize < contentLength || contentLength == 0)) { size_t available = stream->available(); if (available > 0) { // 计算本次读取大小 size_t readSize = min(available, contentLength - totalSize); // 读取数据到PSRAM缓冲区 size_t readBytes = stream->readBytes(buffer + totalSize, readSize); if (readBytes > 0) { totalSize += readBytes; // 重置超时计数器 timeoutMark = millis(); // 定期显示进度 if (showProgress && millis() - lastProgressTime > PROGRESS_INTERVAL) { if (contentLength > 0) { Serial.printf("进度: %.1f%% (%zu/%zu bytes)\r", (totalSize * 100.0) / contentLength, totalSize, contentLength); } else { Serial.printf("已下载: %zu bytes\r", totalSize); } // 更新进度显示 tft.setCursor(10, 90); tft.fillRect(10, 90, 220, 20, TFT_BLACK); if (contentLength > 0) { tft.printf("Downloading: %.1f%%", (totalSize * 100.0) / contentLength); } else { tft.printf("Downloading: %zu bytes", totalSize); } lastProgressTime = millis(); } } } else if (millis() - timeoutMark > DATA_TIMEOUT) { // 数据超时 Serial.println("\n下载超时"); break; } } // 关闭资源 http.end(); // 最终校验 if (contentLength > 0 && totalSize != contentLength) { Serial.printf("\n下载不完整 (%zu/%zu bytes)\n", totalSize, contentLength); // 释放不完整数据的缓冲区 if (buffer) { free(buffer); buffer = NULL; size = 0; } return false; } if (showProgress) { Serial.println("\n下载成功"); } // 更新实际大小 size = totalSize; return true; } // 查找MJPEG中的下一帧 bool findNextFrame() { if (!mjpegBuffer || mjpegPosition >= mjpegSize) { return false; } // JPEG帧以0xFF 0xD8开始，以0xFF 0xD9结束 const uint8_t JPEG_START[] = {0xFF, 0xD8}; const uint8_t JPEG_END[] = {0xFF, 0xD9}; // 寻找下一帧的开始 size_t frameStart = mjpegPosition; while (frameStart < mjpegSize - 1) { if (mjpegBuffer[frameStart] == JPEG_START[0] && mjpegBuffer[frameStart + 1] == JPEG_START[1]) { break; } frameStart++; } if (frameStart >= mjpegSize - 1) { // 没有找到更多帧 return false; } // 寻找帧的结束 size_t frameEnd = frameStart + 2; while (frameEnd < mjpegSize - 1) { if (mjpegBuffer[frameEnd] == JPEG_END[0] && mjpegBuffer[frameEnd + 1] == JPEG_END[1]) { frameEnd += 2; // 包括结束标记 break; } frameEnd++; } if (frameEnd >= mjpegSize) { // 不完整的帧 return false; } // 设置帧数据和大小 if (frameBuffer) { free(frameBuffer); } frameSize = frameEnd - frameStart; frameBuffer = (uint8_t*)ps_malloc(frameSize); if (!frameBuffer) { Serial.println("无法分配帧缓冲区"); return false; } memcpy(frameBuffer, mjpegBuffer + frameStart, frameSize); // 更新当前位置为下一帧开始 mjpegPosition = frameEnd; return true; } // 播放MJPEG文件 void playMjpeg() { if (!mjpegBuffer || mjpegSize == 0) { Serial.println("没有MJPEG数据可播放"); return; } Serial.println("开始播放MJPEG..."); isPlayingMjpeg = true; currentStatus = STATUS_PLAYING_MJPEG; frameCount = 0; mjpegStartTime = millis(); // 重置位置以开始播放 mjpegPosition = 0; // 主播放循环 while (isPlayingMjpeg && findNextFrame()) { // 检查WiFi连接 if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi断开，暂停MJPEG播放"); isPlayingMjpeg = false; break; } // 处理MQTT消息 client.loop(); // 计算帧延迟 unsigned long currentTime = millis(); if (currentTime - lastFrameTime < FRAME_DELAY) { delay(FRAME_DELAY - (currentTime - lastFrameTime)); } lastFrameTime = millis(); // 更新状态显示 if (frameCount % 10 == 0) { //updateStatusDisplay(); } // 显示当前帧 decodeAndDisplayJpeg(frameBuffer, frameSize); frameCount++; // 释放帧缓冲区 if (frameBuffer) { free(frameBuffer); frameBuffer = NULL; } } // 释放资源 if (frameBuffer) { free(frameBuffer); frameBuffer = NULL; } isPlayingMjpeg = false; Serial.printf("MJPEG播放完成，共播放 %lu 帧\n", frameCount); } // 消息回调函数（收到订阅消息时触发） void callback(char* topic, byte* payload, unsigned int length) { Serial.print("收到消息 ["); Serial.print(topic); Serial.print("]: "); String message = ""; for (unsigned int i = 0; i < length; i++) { message += (char)payload[i]; } Serial.println(message); // 处理接收到的消息 if (CommonUtils::endsWith(topic, "downloadImages")) { // 下载并显示图片 currentStatus = STATUS_DOWNLOADING; updateStatusDisplay(); bool downloadSuccess = downloadToPSRAM(message, jpegBuffer, jpegSize, true); if (downloadSuccess) { currentStatus = STATUS_DISPLAYING_IMAGE; updateStatusDisplay(); // 解码并显示JPEG图片 decodeAndDisplayJpeg(jpegBuffer, jpegSize); } else { tft.fillScreen(TFT_RED); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("Download Failed"); } isVideo = false; } else if (CommonUtils::endsWith(topic, "downloadVideos")) { // 下载视频（可能是MJPEG格式） currentStatus = STATUS_DOWNLOADING; updateStatusDisplay(); bool downloadSuccess = downloadToPSRAM(message, mjpegBuffer, mjpegSize, true); if (downloadSuccess) { Serial.println("视频下载成功，使用playMjpeg命令播放"); tft.fillScreen(TFT_GREEN); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("Video Downloaded"); tft.setCursor(10, 30); tft.println("Use playMjpeg to play"); } else { tft.fillScreen(TFT_RED); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("Download Failed"); } isVideo = true; } else if (CommonUtils::endsWith(topic, "playMjpeg")) { // 播放MJPEG if (message == "default" || message == "") { // 播放默认MJPEG if (!mjpegBuffer || mjpegSize == 0) { // 如果没有下载过，先下载 currentStatus = STATUS_DOWNLOADING; updateStatusDisplay(); bool downloadSuccess = downloadToPSRAM(defaultMjpegUrl, mjpegBuffer, mjpegSize, true); if (!downloadSuccess) { tft.fillScreen(TFT_RED); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("Download Failed"); return; } } // 播放MJPEG playMjpeg(); } else { // 从指定URL下载并播放MJPEG currentStatus = STATUS_DOWNLOADING; updateStatusDisplay(); bool downloadSuccess = downloadToPSRAM(message, mjpegBuffer, mjpegSize, true); if (downloadSuccess) { // 播放MJPEG playMjpeg(); } else { tft.fillScreen(TFT_RED); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("Download Failed"); } } isVideo = true; } } void setup() { Serial.begin(115200); delay(1000); // 等待串口连接稳定 pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP); // 初始化显示屏 tft.init(); tft.setRotation(0); tft.setSwapBytes(true); tft.fillScreen(TFT_BLUE); // 控制背光 (如果需要) pinMode(TFT_BL, OUTPUT); digitalWrite(TFT_BL, HIGH); // 点亮背光 // 显示初始信息 tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.setTextSize(2); tft.println("ESP32 MJPEG Player"); // 显示PSRAM信息 uint32_t psramSize = ESP.getPsramSize(); uint32_t freePsram = ESP.getFreePsram(); Serial.printf("PSRAM 可用: %u 字节\n", psramSize); Serial.printf("PSRAM 空闲: %u 字节\n", freePsram); tft.setTextSize(1); tft.setCursor(10, 40); tft.printf("PSRAM: %u KB 空闲\n", freePsram / 1024); // 连接WiFi Serial.println("开始连接WiFi..."); tft.setCursor(10, 60); tft.print("Connecting to WiFi..."); if (!wifiManager.autoConnect("ESP32-Config")) { Serial.println("进入配置模式"); tft.fillScreen(TFT_RED); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("WiFi Config Mode"); tft.setCursor(10, 30); tft.println("Connect to ESP32-Config"); tft.setCursor(10, 50); tft.println("and open 192.168.4.1"); while (1); // 停止执行 } // WiFi连接成功 isConnected = true; currentStatus = STATUS_WIFI_CONNECTED; Serial.println("WiFi已连接"); tft.fillScreen(TFT_GREEN); tft.setCursor(10, 10); tft.setTextColor(TFT_WHITE); tft.println("WiFi Connected"); tft.setCursor(10, 30); tft.print("IP: "); tft.println(WiFi.localIP()); delay(2000); // 初始化MQTT client.setServer(mqttServer, mqttPort); client.setCallback(callback); // 设置消息回调函数 // 分配内存缓冲区 jpegBuffer = (uint8_t*)ps_malloc(1024 * 1024); // 1MB JPEG缓冲区 if (!jpegBuffer) { Serial.println("JPEG缓冲区分配失败!"); } else { Serial.println("JPEG缓冲区分配成功"); } mjpegBuffer = (uint8_t*)ps_malloc(1024 * 1024 * 4); // 4MB MJPEG缓冲区 if (!mjpegBuffer) { Serial.println("MJPEG缓冲区分配失败!"); } else { Serial.println("MJPEG缓冲区分配成功"); } // 初始化TJpgDec TJpgDec.setJpgScale(1); // 缩放比例 TJpgDec.setCallback(tft_output); // 设置输出回调函数 // 显示初始状态 updateStatusDisplay(); Serial.println("程序初始化完成"); } void loop() { // 检查WiFi状态 checkWifiStatus(); // 保持MQTT连接 if (isConnected && !client.connected()) { reconnectMQTT(); } // 处理MQTT消息 if (client.connected()) { client.loop(); } // 显示状态更新 static unsigned long lastStatusUpdate = 0; if (millis() - lastStatusUpdate > 5000) { //updateStatusDisplay(); lastStatusUpdate = millis(); } // 如果正在播放MJPEG，则继续播放 if (!isPlayingMjpeg&&frameCount!=0&&isVideo==true) { playMjpeg(); } delay(10); // 短暂延迟，避免CPU占用过高 }
[scode type="yellow"]雷达传感器也遇到了问题最终原因查明仍然是需要接入5v电压的问题[/scode]#include <SPI.h> #include <SD.h> #include <SoftwareSerial.h> #include <HMMD_Radar.h> // 自定义SPI引脚定义 #define SD_CS_PIN 10 // 片选引脚 #define SD_MOSI_PIN 2 // 主输出从输入 #define SD_MISO_PIN 1 // 主输入从输出 #define SD_SCK_PIN 3 // 时钟引脚 // 定义UART和引脚 #define RADAR_RX_PIN 20 #define RADAR_TX_PIN 21 SoftwareSerial mySerial(RADAR_TX_PIN, RADAR_RX_PIN); File dataFile; const char* filename = "/data.txt"; // 存储数据的文件名 void setup() { Serial.begin(115200); Serial.println("Initializing SD card..."); // 初始化自定义SPI引脚 SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN); // 初始化SD卡 if (!SD.begin(SD_CS_PIN, SPI, 1000000)) { Serial.println("SD初始化失败"); return; } else { Serial.println("SD初始化成功"); } // 检查文件是否存在，如果不存在则创建并写入标题行 if (!SD.exists(filename)) { dataFile = SD.open(filename, FILE_WRITE); if (dataFile) { dataFile.println("Timestamp,Data"); // 添加标题行 dataFile.close(); Serial.println("Created new file with headers."); } else { Serial.println("Error creating file!"); } } Serial1.begin(115200, SERIAL_8N1, RADAR_TX_PIN, RADAR_RX_PIN); // UART1 初始化 } void loop() { if (Serial1.available()) { String line = Serial1.readStringUntil('\n'); // 按行读取 UART1 数据 line.trim(); // 去除多余的 \r 或空格 if (line.length() > 0) { Serial.println("接收到： " + line); // 将数据写入SD卡 dataFile = SD.open(filename, FILE_APPEND); if (dataFile) { // 添加时间戳（以毫秒为单位） dataFile.print(millis()); dataFile.print(","); dataFile.println(line); dataFile.close(); Serial.println("数据已写入SD卡"); } else { Serial.println("错误：无法打开文件进行写入！"); } } } delay(10); }
#include <SPIFFS.h> #include <Arduino.h> #include <TFT_eSPI.h> #include "src/WIFILedManager.h" #include <TJpg_Decoder.h> #define BOOT_BUTTON_PIN 0 WIFILedManager ledManager(48, 1); TFT_eSPI tft = TFT_eSPI(); void listSPIFFSFiles() { File root = SPIFFS.open("/"); if (!root) { Serial.println("无法打开根目录"); return; } Serial.println("SPIFFS 文件列表:"); File file = root.openNextFile(); while (file) { Serial.printf(" %s (%d Bytes)\n", file.name(), file.size()); file = root.openNextFile(); } root.close(); } // TFT渲染回调函数（必须实现） bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) { tft.pushImage(x, y, w, h, bitmap); // 将解码后的像素块推送到屏幕 return true; // 返回true表示渲染成功 } void setup() { Serial.begin(115200); Serial.println("开始启动"); Serial.println("初始化按钮引脚..."); pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP); Serial.println("初始化屏幕..."); tft.init(); tft.setRotation(1); tft.fillScreen(TFT_BLACK); digitalWrite(TFT_BL, HIGH); Serial.println("屏幕初始化完成"); Serial.println("初始化 LED 管理器..."); ledManager.begin(); Serial.println("挂载 SPIFFS..."); if (!SPIFFS.begin()) { Serial.println("SPIFFS Mount Failed"); return; } Serial.println("SPIFFS 挂载成功"); listSPIFFSFiles(); bool exist = SPIFFS.exists("/test.jpg"); if (exist) { Serial.println("The file exists!"); } else { Serial.println("No such file found."); } // 设置TJpgDec TJpgDec.setJpgScale(1); // 缩放比例（1:1） TJpgDec.setSwapBytes(true); // 交换颜色字节序（适配TFT_eSPI） TJpgDec.setCallback(tft_output); // 绑定渲染回调函数 // 显示图片（全屏居中） TJpgDec.drawFsJpg(0, 0, "/test.jpg"); } void loop() { static unsigned long pressStart = 0; static bool resetFlag = false; // 按钮检测 if (digitalRead(BOOT_BUTTON_PIN) == LOW) { if (!resetFlag) { pressStart = millis(); resetFlag = true; } if (millis() - pressStart > 3000) { ledManager.resetSettings(); } } else { resetFlag = false; } // 更新网络状态 ledManager.update(); delay(100); }
#include <WiFiManager.h> #include <WiFi.h> #include <Adafruit_NeoPixel.h> #include <Ticker.h> #define BOOT_BUTTON_PIN 0 // BOOT按钮对应GPIO0 // 定义WS2812 LED参数 #define LED_PIN 48 #define LED_COUNT 1 Adafruit_NeoPixel strip = Adafruit_NeoPixel(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800); // LED状态控制 Ticker ledTicker; bool isConnected = false; const uint8_t BRIGHTNESS = 12; // 整体亮度 const uint32_t CONNECTED_COLOR = strip.Color(0, 255, 0); // 绿色 const uint32_t CONNECTING_COLOR = strip.Color(255, 255, 0); // 黄色 unsigned long buttonPressTime = 0; bool resetTriggered = false; void updateLed() { static bool ledState = false; if (!isConnected) { uint32_t color = ledState ? CONNECTING_COLOR : 0; for(int i=0; i<LED_COUNT; i++) { strip.setPixelColor(i, color); } strip.show(); ledState = !ledState; } } void setup() { Serial.begin(115200); pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP); // 初始化LED strip.begin(); strip.setBrightness(BRIGHTNESS); strip.show(); // 启动连接状态闪烁 ledTicker.attach(0.5, updateLed); WiFiManager wifiManager; wifiManager.setTimeout(180); if (!wifiManager.autoConnect("ESP32-Config")) { Serial.println("配网失败，重启设备"); ledTicker.detach(); delay(5000); ESP.restart(); } // 连接成功处理 ledTicker.detach(); isConnected = true; for(int i=0; i<LED_COUNT; i++) { strip.setPixelColor(i, CONNECTED_COLOR); } strip.show(); Serial.println("\nWiFi已连接！"); Serial.print("IP地址: "); Serial.println(WiFi.localIP()); } void loop() { // 按钮检测（保持原功能） if (digitalRead(BOOT_BUTTON_PIN) == LOW && !resetTriggered) { delay(50); if (digitalRead(BOOT_BUTTON_PIN) == LOW) { buttonPressTime = millis(); resetTriggered = true; while (digitalRead(BOOT_BUTTON_PIN) == LOW) { if (millis() - buttonPressTime > 3000) { Serial.println("\n重置WiFi配置..."); WiFiManager wifiManager; wifiManager.resetSettings(); // 重置LED状态 ledTicker.detach(); for(int i=0; i<LED_COUNT; i++) { strip.setPixelColor(i, 0); } strip.show(); delay(2000); ESP.restart(); } } resetTriggered = false; } } // WiFi状态监测 if (WiFi.status() != WL_CONNECTED && isConnected) { isConnected = false; ledTicker.attach(0.5, updateLed); Serial.println("WiFi连接丢失，尝试重连..."); } else if (WiFi.status() == WL_CONNECTED && !isConnected) { isConnected = true; ledTicker.detach(); for(int i=0; i<LED_COUNT; i++) { strip.setPixelColor(i, CONNECTED_COLOR); } strip.show(); } delay(100); }
#define BOOT_BUTTON_PIN 0 // BOOT按钮对应GPIO0 #define BOOT_LED 97 // BOOT WS2812 LED 使用GPIO时为97，固定白色，不可调节颜色 #define LED_PIN 48 // BOOT WS2812 LED 设置为可调节led灯时，可调节颜色 用Adafruit_NeoPixel触发
/** * wm_strings_en.h * engligh strings for * WiFiManager, a library for the ESP8266/Arduino platform * for configuration of WiFi credentials using a Captive Portal * * @author Creator tzapu * @author tablatronix * @version 0.0.0 * @license MIT */ #ifndef _WM_STRINGS_EN_H_ #define _WM_STRINGS_EN_H_ #ifndef WIFI_MANAGER_OVERRIDE_STRINGS // !!! ABOVE WILL NOT WORK if you define in your sketch, must be build flag, if anyone one knows how to order includes to be able to do this it would be neat.. I have seen it done.. // strings files must include a consts file! #include "wm_consts_en.h" // include constants, tokens, routes const char WM_LANGUAGE[] PROGMEM = "en-US"; // i18n lang code const char HTTP_HEAD_START[] PROGMEM = " " " " " " " " " " " {v} "; const char HTTP_SCRIPT[] PROGMEM = " "; // @todo add button states, disable on click , show ack , spinner etc const char HTTP_HEAD_END[] PROGMEM = " "; // {c} = _bodyclass // example of embedded logo, base64 encoded inline, No styling here // const char HTTP_ROOT_MAIN[] PROGMEM = " {v} WiFiManager "; const char HTTP_ROOT_MAIN[] PROGMEM = " {t} {v} "; const char * const HTTP_PORTAL_MENU[] PROGMEM = { " 配置WiFi \n", " 配置WiFi（不扫描） \n", " 系统信息 \n", " 参数设置 \n", " 关闭门户 \n", " 重启设备 \n", " 退出配置 \n", " 清除配置 \n", " 固件升级 \n", " " }; // const char HTTP_PORTAL_OPTIONS[] PROGMEM = strcat(HTTP_PORTAL_MENU[0] , HTTP_PORTAL_MENU[3] , HTTP_PORTAL_MENU[7]); const char HTTP_PORTAL_OPTIONS[] PROGMEM = ""; const char HTTP_ITEM_QI[] PROGMEM = " "; const char HTTP_ITEM_QP[] PROGMEM = " {r}% "; const char HTTP_ITEM[] PROGMEM = " {v}{qi}{qp} "; const char HTTP_FORM_START[] PROGMEM = " "; const char HTTP_FORM_WIFI[] PROGMEM = " 网络名称 密码 显示密码 "; const char HTTP_FORM_WIFI_END[] PROGMEM = ""; const char HTTP_FORM_STATIC_HEAD[] PROGMEM = " "; const char HTTP_FORM_END[] PROGMEM = " 保存设置 "; const char HTTP_FORM_LABEL[] PROGMEM = " {t} "; const char HTTP_FORM_PARAM_HEAD[] PROGMEM = " "; const char HTTP_FORM_PARAM[] PROGMEM = " \n"; const char HTTP_SCAN_LINK[] PROGMEM = " 重新扫描 "; const char HTTP_SAVED[] PROGMEM = " 凭证已保存 正在尝试连接网络 若连接失败请重新接入AP "; const char HTTP_PARAMSAVED[] PROGMEM = " 参数已保存 "; const char HTTP_END[] PROGMEM = " "; const char HTTP_ERASEBTN[] PROGMEM = " 清除WiFi配置 "; const char HTTP_UPDATEBTN[] PROGMEM = " 升级固件 "; const char HTTP_BACKBTN[] PROGMEM = " 返回首页 "; const char HTTP_STATUS_ON[] PROGMEM = " 已连接至 {v} IP地址 {i} "; const char HTTP_STATUS_OFF[] PROGMEM = " 未连接至 {v}{r} "; const char HTTP_STATUS_OFFPW[] PROGMEM = " 认证失败"; const char HTTP_STATUS_OFFNOAP[] PROGMEM = " 找不到AP"; const char HTTP_STATUS_OFFFAIL[] PROGMEM = " 连接失败"; const char HTTP_STATUS_NONE[] PROGMEM = " 未设置AP "; const char HTTP_BR[] PROGMEM = " "; const char HTTP_STYLE[] PROGMEM = " "; #ifndef WM_NOHELP const char HTTP_HELP[] PROGMEM = " 可用页面 " " " " 页面 功能 " " / " " 主菜单 " " /wifi " " 显示WiFi扫描结果并进入配置（/0wifi不扫描） " " /wifisave " " 保存WiFi配置信息 " " /param " " 参数设置页面 " " /info " " 系统信息页面 " " /u " " OTA在线升级 " " /close " " 关闭弹窗（配置门户仍运行） " " /exit " " 退出配置门户 " " /restart " " 重启设备 " " /erase " " 清除WiFi配置并重启 " " " " 项目主页 https://github.com/tzapu/WiFiManager."; #else const char HTTP_HELP[] PROGMEM = ""; #endif const char HTTP_UPDATE[] PROGMEM = "上传新固件 开始升级 * 在Captive Portal中可能无效，请直接访问 http://192.168.4.1"; const char HTTP_UPDATE_FAIL[] PROGMEM = " 升级失败！ 请重启设备后重试 "; const char HTTP_UPDATE_SUCCESS[] PROGMEM = " 升级成功 设备即将重启... "; #ifdef WM_JSTEST const char HTTP_JS[] PROGMEM = " "; #endif // Info html // @todo remove html elements from progmem, repetetive strings #ifdef ESP32 const char HTTP_INFO_esphead[] PROGMEM = " ESP32芯片 "; const char HTTP_INFO_chiprev[] PROGMEM = " 芯片版本 {1} "; const char HTTP_INFO_lastreset[] PROGMEM = " 上次复位原因 CPU0: {1} CPU1: {2} "; const char HTTP_INFO_aphost[] PROGMEM = " 接入点主机名 {1} "; const char HTTP_INFO_psrsize[] PROGMEM = " PSRAM容量 {1} 字节 "; const char HTTP_INFO_temp[] PROGMEM = " 温度 {1} °C / {2} °F "; const char HTTP_INFO_hall[] PROGMEM = " 霍尔传感器 {1} "; #else const char HTTP_INFO_esphead[] PROGMEM = " ESP8266芯片 "; const char HTTP_INFO_fchipid[] PROGMEM = " 闪存芯片ID {1} "; const char HTTP_INFO_corever[] PROGMEM = " 核心版本 {1} "; const char HTTP_INFO_bootver[] PROGMEM = " 引导版本 {1} "; const char HTTP_INFO_lastreset[] PROGMEM = " 上次复位原因 {1} "; const char HTTP_INFO_flashsize[] PROGMEM = " 实际闪存容量 {1} 字节 "; #endif const char HTTP_INFO_memsmeter[] PROGMEM = " "; const char HTTP_INFO_memsketch[] PROGMEM = " 内存 - 固件大小 已用 / 总容量 {1} / {2}"; const char HTTP_INFO_freeheap[] PROGMEM = " 内存 - 空闲堆 剩余 {1} 字节 "; const char HTTP_INFO_wifihead[] PROGMEM = " 无线网络 "; const char HTTP_INFO_uptime[] PROGMEM = " 运行时间 {1} 分 {2} 秒 "; const char HTTP_INFO_chipid[] PROGMEM = " 芯片ID {1} "; const char HTTP_INFO_idesize[] PROGMEM = " 闪存容量 {1} 字节 "; const char HTTP_INFO_sdkver[] PROGMEM = " SDK版本 {1} "; const char HTTP_INFO_cpufreq[] PROGMEM = " CPU频率 {1}MHz "; const char HTTP_INFO_apip[] PROGMEM = " 接入点IP {1} "; const char HTTP_INFO_apmac[] PROGMEM = " 接入点MAC {1} "; const char HTTP_INFO_apssid[] PROGMEM = " 接入点SSID {1} "; const char HTTP_INFO_apbssid[] PROGMEM = " BSSID {1} "; const char HTTP_INFO_stassid[] PROGMEM = " 站点SSID {1} "; const char HTTP_INFO_staip[] PROGMEM = " 站点IP {1} "; const char HTTP_INFO_stagw[] PROGMEM = " 站点网关 {1} "; const char HTTP_INFO_stasub[] PROGMEM = " 站点子网 {1} "; const char HTTP_INFO_dnss[] PROGMEM = " DNS服务器 {1} "; const char HTTP_INFO_host[] PROGMEM = " 主机名 {1} "; const char HTTP_INFO_stamac[] PROGMEM = " 站点MAC {1} "; const char HTTP_INFO_conx[] PROGMEM = " 连接状态 {1} "; const char HTTP_INFO_autoconx[] PROGMEM = " 自动连接 {1} "; const char HTTP_INFO_aboutver[] PROGMEM = " WiFi管理器版本 {1} "; const char HTTP_INFO_aboutarduino[] PROGMEM = " Arduino版本 {1} "; const char HTTP_INFO_aboutsdk[] PROGMEM = " ESP-SDK/IDF版本 {1} "; const char HTTP_INFO_aboutdate[] PROGMEM = " 构建日期 {1} "; const char S_brand[] PROGMEM = "WiFiManager"; const char S_debugPrefix[] PROGMEM = "*wm:"; const char S_y[] PROGMEM = "Yes"; const char S_n[] PROGMEM = "No"; const char S_enable[] PROGMEM = "Enabled"; const char S_disable[] PROGMEM = "Disabled"; const char S_GET[] PROGMEM = "GET"; const char S_POST[] PROGMEM = "POST"; const char S_NA[] PROGMEM = "Unknown"; const char S_passph[] PROGMEM = "********"; const char S_titlewifisaved[] PROGMEM = "Credentials saved"; const char S_titlewifisettings[] PROGMEM = "Settings saved"; const char S_titlewifi[] PROGMEM = "Config ESP"; const char S_titleinfo[] PROGMEM = "Info"; const char S_titleparam[] PROGMEM = "Setup"; const char S_titleparamsaved[] PROGMEM = "Setup saved"; const char S_titleexit[] PROGMEM = "Exit"; const char S_titlereset[] PROGMEM = "Reset"; const char S_titleerase[] PROGMEM = "Erase"; const char S_titleclose[] PROGMEM = "Close"; const char S_options[] PROGMEM = "options"; const char S_nonetworks[] PROGMEM = "No networks found. Refresh to scan again."; const char S_staticip[] PROGMEM = "Static IP"; const char S_staticgw[] PROGMEM = "Static gateway"; const char S_staticdns[] PROGMEM = "Static DNS"; const char S_subnet[] PROGMEM = "Subnet"; const char S_exiting[] PROGMEM = "Exiting"; const char S_resetting[] PROGMEM = "Module will reset in a few seconds."; const char S_closing[] PROGMEM = "You can close the page, portal will continue to run"; const char S_error[] PROGMEM = "An error occured"; const char S_notfound[] PROGMEM = "File not found\n\n"; const char S_uri[] PROGMEM = "URI: "; const char S_method[] PROGMEM = "\nMethod: "; const char S_args[] PROGMEM = "\nArguments: "; const char S_parampre[] PROGMEM = "param_"; // debug strings const char D_HR[] PROGMEM = "--------------------"; // softap ssid default prefix #ifdef ESP8266 const char S_ssidpre[] PROGMEM = "ESP"; #elif defined(ESP32) const char S_ssidpre[] PROGMEM = "ESP32"; #else const char S_ssidpre[] PROGMEM = "WM"; #endif // END WIFI_MANAGER_OVERRIDE_STRINGS #endif #endif
#include <stdio.h> #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_wifi.h" #include "esp_event.h" #include "esp_log.h" #include "nvs_flash.h" #include "esp_netif.h" #include "esp_http_server.h" // 配置参数 #define EXAMPLE_ESP_WIFI_SSID "ESP321_Config" // 热点名称 #define EXAMPLE_ESP_WIFI_PASS "12345678" // 热点密码 #define EXAMPLE_ESP_WIFI_CHANNEL 6 // 热点信道 #define MAX_STA_CONN 4 // 最大连接数 #define CONFIG_EXAMPLE_WEB_MOUNT_POINT "/www" // Web挂载点 static const char *TAG = "wifi_config"; // 宏定义 #define MIN(a, b) ((a) < (b) ? (a) : (b)) // 函数前置声明 static void wifi_init_softap(void); static void wifi_stop_ap(void); static void wifi_connect_sta(const char *ssid, const char *password); static bool check_saved_wifi_config(void); static void start_webserver(void); static void stop_webserver(void); static void save_wifi_config(const char *ssid, const char *password); static void url_decode(char *str); static httpd_handle_t server = NULL; /* 配置页面的HTML */ static const char* CONFIG_HTML = " " " " " " " " " " " WiFi Configuration " " " " " " " " " " WiFi Configuration " " " " WiFi SSID: " " " " WiFi Password: " " " " Connect " " " " " " " " "; /* 成功页面的HTML */ static const char* SUCCESS_HTML = " " " " " " " " " Configuration Successful " " " " " " " " Configuration Successful! " " ESP32 is now connecting to your WiFi network. " " " " "; /* 处理根路径请求 */ static esp_err_t root_get_handler(httpd_req_t *req) { httpd_resp_set_type(req, "text/html"); return httpd_resp_send(req, CONFIG_HTML, strlen(CONFIG_HTML)); } /* URL解码 */ static void url_decode(char *str) { char *p = str; char decoded; while (*p) { if (*p == '%') { sscanf(p + 1, "%2hhx", &decoded); *str++ = decoded; p += 3; } else if (*p == '+') { *str++ = ' '; p++; } else { *str++ = *p++; } } *str = '\0'; } /* 处理配置提交 */ static esp_err_t configure_post_handler(httpd_req_t *req) { char buf[100]; int ret, remaining = req->content_len; char ssid[32] = {0}; char password[64] = {0}; while (remaining > 0) { if ((ret = httpd_req_recv(req, buf, MIN(remaining, sizeof(buf)))) <= 0) { if (ret == HTTPD_SOCK_ERR_TIMEOUT) { continue; } return ESP_FAIL; } remaining -= ret; // 简单的表单解析 char *p = buf; while (*p) { if (strncmp(p, "ssid=", 5) == 0) { p += 5; char *end = strchr(p, '&'); if (end) { strncpy(ssid, p, MIN(end - p, sizeof(ssid) - 1)); } else { strncpy(ssid, p, sizeof(ssid) - 1); } url_decode(ssid); } else if (strncmp(p, "password=", 9) == 0) { p += 9; char *end = strchr(p, '&'); if (end) { strncpy(password, p, MIN(end - p, sizeof(password) - 1)); } else { strncpy(password, p, sizeof(password) - 1); } url_decode(password); } p++; } } ESP_LOGI(TAG, "Received WiFi configuration: SSID=%s, Password=%s", ssid, password); // 保存配置到NVS save_wifi_config(ssid, password); // 返回成功页面 httpd_resp_set_type(req, "text/html"); httpd_resp_send(req, SUCCESS_HTML, strlen(SUCCESS_HTML)); // 延迟后关闭服务器并连接WiFi vTaskDelay(2000 / portTICK_PERIOD_MS); stop_webserver(); wifi_stop_ap(); wifi_connect_sta(ssid, password); return ESP_OK; } /* 保存WiFi配置到NVS */ static void save_wifi_config(const char *ssid, const char *password) { nvs_handle_t nvs; esp_err_t err; err = nvs_open("storage", NVS_READWRITE, &nvs); if (err != ESP_OK) { ESP_LOGE(TAG, "Error opening NVS: %s", esp_err_to_name(err)); return; } err = nvs_set_str(nvs, "ssid", ssid); if (err != ESP_OK) { ESP_LOGE(TAG, "Error saving SSID: %s", esp_err_to_name(err)); } err = nvs_set_str(nvs, "password", password); if (err != ESP_OK) { ESP_LOGE(TAG, "Error saving password: %s", esp_err_to_name(err)); } err = nvs_commit(nvs); if (err != ESP_OK) { ESP_LOGE(TAG, "Error committing NVS: %s", esp_err_to_name(err)); } nvs_close(nvs); } /* 启动HTTP服务器 */ static void start_webserver(void) { httpd_config_t config = HTTPD_DEFAULT_CONFIG(); config.server_port = 80; ESP_LOGI(TAG, "Starting HTTP server on port: %d", config.server_port); if (httpd_start(&server, &config) == ESP_OK) { httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler, .user_ctx = NULL }; httpd_uri_t configure = { .uri = "/configure", .method = HTTP_POST, .handler = configure_post_handler, .user_ctx = NULL }; httpd_register_uri_handler(server, &root); httpd_register_uri_handler(server, &configure); } } /* 停止HTTP服务器 */ static void stop_webserver(void) { if (server) { httpd_stop(server); server = NULL; } } /* 初始化并启动WiFi热点 */ static void wifi_init_softap(void) { esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_ap(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); wifi_config_t wifi_config = { .ap = { .ssid = EXAMPLE_ESP_WIFI_SSID, .ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID), .channel = EXAMPLE_ESP_WIFI_CHANNEL, .password = EXAMPLE_ESP_WIFI_PASS, .max_connection = MAX_STA_CONN, .authmode = WIFI_AUTH_WPA_WPA2_PSK }, }; if (strlen(EXAMPLE_ESP_WIFI_PASS) == 0) { wifi_config.ap.authmode = WIFI_AUTH_OPEN; } ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, "WiFi AP started. SSID: %s Password: %s Channel: %d", EXAMPLE_ESP_WIFI_SSID, EXAMPLE_ESP_WIFI_PASS, EXAMPLE_ESP_WIFI_CHANNEL); } /* 停止WiFi热点 */ static void wifi_stop_ap(void) { ESP_ERROR_CHECK(esp_wifi_stop()); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL)); ESP_LOGI(TAG, "WiFi AP stopped"); } /* 连接到指定的WiFi网络 */ static void wifi_connect_sta(const char *ssid, const char *password) { esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta(); assert(sta_netif); wifi_config_t wifi_config = { .sta = { .threshold.authmode = WIFI_AUTH_WPA2_PSK, }, }; strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1); strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_connect()); ESP_LOGI(TAG, "Connecting to WiFi SSID: %s", ssid); } /* 检查是否已有保存的WiFi配置 */ static bool check_saved_wifi_config(void) { nvs_handle_t nvs; esp_err_t err; char ssid[32] = {0}; char password[64] = {0}; size_t required_size; err = nvs_open("storage", NVS_READONLY, &nvs); if (err != ESP_OK) { return false; } // 获取SSID长度 err = nvs_get_str(nvs, "ssid", NULL, &required_size); if (err != ESP_OK || required_size == 0) { nvs_close(nvs); return false; } // 获取SSID err = nvs_get_str(nvs, "ssid", ssid, &required_size); if (err != ESP_OK) { nvs_close(nvs); return false; } // 获取密码 nvs_get_str(nvs, "password", password, &required_size); nvs_close(nvs); if (strlen(ssid) > 0) { ESP_LOGI(TAG, "Found saved WiFi config: SSID=%s", ssid); // 初始化WiFi esp_netif_init(); esp_event_loop_create_default(); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); wifi_connect_sta(ssid, password); return true; } return false; } void app_main(void) { // 初始化NVS esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // 清除保存的WiFi配置 nvs_handle_t nvs; ret = nvs_open("storage", NVS_READWRITE, &nvs); if (ret == ESP_OK) { nvs_erase_key(nvs, "ssid"); nvs_erase_key(nvs, "password"); nvs_commit(nvs); nvs_close(nvs); } // 检查是否有保存的WiFi配置 if (!check_saved_wifi_config()) { // 没有保存的配置，启动热点和Web服务器 wifi_init_softap(); start_webserver(); } }