weyhao 发表于 2025-9-23 13:54:11

ESP32-C5——断网可用的简单群聊

在某些特殊的场合之下,比如没有网络的大兴安岭,克拉玛利,比如不允许上网的某些研究所,比如在飞机上。那么我们能否连接热点,上网做一款线上简易群聊呢
基于Firebeete试用的ESP32-C5开发板以及其强大的wifi功能,我们基于其作为一个简单的服务器,提供热点连接,然后可以访问群留言聊天功能。

废话不说,结果展示如下

首先需要访问Esp32的热点,然后输入密码12345678,就可以使用啦,默认30秒更新一次,因为esp32能力有限,需要快速更新可以点击更新,多人使用也是可以的,这样可以聊起天来,哈哈哈哈哈,相当于一个简单群聊


手机端也可以使用,代码展示如下:
// 针对ESP32-C5优化的包含部分
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
// 使用ArduinoJson 6.x并进行内存优化
#include <ArduinoJson.h>

/* WiFi设置 */
const char* ssid   = "ESP32-MessageBoard";
const char* password = "12345678";

/* IP地址设置 */
IPAddress local_ip(192,168,1,1);
IPAddress gateway(192,168,1,1);
IPAddress subnet(255,255,255,0);

// 创建Web服务器实例,使用标准HTTP端口80以便于浏览器访问
WebServer msgServer(80);

// 最多存储的留言数量 - 为ESP32-C5内存限制而减少
#define MAX_MESSAGES 30

// 留言结构体 - 优化字符串存储
struct Message {
char author;// 使用固定大小缓冲区替代String
char content;
char timestamp;
};

// 留言数组
Message messages;
int messageCount = 0;

// 函数声明
void loadMessages();
void saveMessages();
String sendMessageBoardHTML();

void setup() {
Serial.begin(115200);

// 初始化SPIFFS,针对ESP32-C5进行错误处理
if(!SPIFFS.begin(true)){
    Serial.println("SPIFFS挂载失败");
    // 尝试替代初始化方法
    if(!SPIFFS.begin(false)) {
      Serial.println("SPIFFS挂载再次失败,继续执行但功能可能受限");
    }
}

// 从文件加载留言
loadMessages();

// 设置WiFi接入点
WiFi.softAP(ssid, password);
WiFi.softAPConfig(local_ip, gateway, subnet);
delay(100);

// 设置Web服务器路由
msgServer.on("/", HTTP_GET, handleRoot);
msgServer.on("/messages", HTTP_GET, handleGetMessages);
msgServer.on("/post", HTTP_POST, handlePostMessage);
msgServer.onNotFound(handleNotFound);

// 启动服务器
msgServer.begin();
Serial.println("留言板HTTP服务器已启动在端口80");
Serial.print("IP地址: ");
Serial.println(WiFi.softAPIP());
}

void loop() {
msgServer.handleClient();
}

// 处理根路径请求
void handleRoot() {
Serial.println("客户端已连接到留言板");
msgServer.send(200, "text/html", sendMessageBoardHTML());
}

void handleNotFound() {
msgServer.send(404, "text/plain", "找不到页面");
}

// 为ESP32-C5内存限制而减小JSON文档大小
void handleGetMessages() {
// 使用StaticJsonDocument以获得更好的内存管理
StaticJsonDocument<3072> doc;
JsonArray array = doc.to<JsonArray>();

for (int i = 0; i < messageCount; i++) {
    JsonObject obj = array.createNestedObject();
    obj["author"] = messages.author;
    obj["content"] = messages.content;
    obj["timestamp"] = messages.timestamp;
}

String response;
serializeJson(doc, response);
msgServer.send(200, "application/json", response);
}

void handlePostMessage() {
if (msgServer.hasArg("plain")) {
    String message = msgServer.arg("plain");
    StaticJsonDocument<512> doc;
    DeserializationError error = deserializeJson(doc, message);
   
    if (!error) {
      const char* author = doc["author"];
      const char* content = doc["content"];
      
      // 获取当前时间(由于ESP32没有实时时钟,这里使用运行时间)
      unsigned long currentMillis = millis();
      int seconds = currentMillis / 1000;
      int minutes = seconds / 60;
      int hours = minutes / 60;
      
      minutes = minutes % 60;
      seconds = seconds % 60;
      
      char timestamp;
      sprintf(timestamp, "%02d:%02d:%02d", hours, minutes, seconds);
      
      // 添加新留言 - 使用strncpy处理固定缓冲区
      if (messageCount < MAX_MESSAGES) {
      strncpy(messages.author, author, sizeof(messages.author) - 1);
      messages.author.author) - 1] = '\0';
      
      strncpy(messages.content, content, sizeof(messages.content) - 1);
      messages.content.content) - 1] = '\0';
      
      strncpy(messages.timestamp, timestamp, sizeof(messages.timestamp) - 1);
      messages.timestamp.timestamp) - 1] = '\0';
      
      messageCount++;
      } else {
      // 如果留言已满,移除最旧的留言
      for (int i = 0; i < MAX_MESSAGES - 1; i++) {
          memcpy(&messages, &messages, sizeof(Message));
      }
      
      strncpy(messages.author, author, sizeof(messages.author) - 1);
      messages.author.author) - 1] = '\0';
      
      strncpy(messages.content, content, sizeof(messages.content) - 1);
      messages.content.content) - 1] = '\0';
      
      strncpy(messages.timestamp, timestamp, sizeof(messages.timestamp) - 1);
      messages.timestamp.timestamp) - 1] = '\0';
      }
      
      // 保存留言到文件
      saveMessages();
      
      msgServer.send(200, "application/json", "{\"status\":\"success\"}");
    } else {
      msgServer.send(400, "application/json", "{\"status\":\"error\",\"message\":\"无效的JSON格式\"}");
    }
} else {
    msgServer.send(400, "application/json", "{\"status\":\"error\",\"message\":\"没有提供数据\"}");
}
}

// 保存留言到SPIFFS文件系统 - 针对ESP32-C5优化
void saveMessages() {
File file = SPIFFS.open("/messages.json", FILE_WRITE);
if (!file) {
    Serial.println("无法打开文件进行写入");
    return;
}

// 使用更小的JSON文档
StaticJsonDocument<3072> doc;
JsonArray array = doc.to<JsonArray>();

// 分批处理
const int batchSize = 5;
int totalBatches = (messageCount + batchSize - 1) / batchSize;

file.print("[");

for (int batch = 0; batch < totalBatches; batch++) {
    int startIdx = batch * batchSize;
    int endIdx = min(startIdx + batchSize, messageCount);
   
    for (int i = startIdx; i < endIdx; i++) {
      JsonObject obj = array.createNestedObject();
      obj["author"] = messages.author;
      obj["content"] = messages.content;
      obj["timestamp"] = messages.timestamp;
      
      String jsonStr;
      serializeJson(obj, jsonStr);
      file.print(jsonStr);
      
      if (i < messageCount - 1) {
      file.print(",");
      }
      
      // 清除对象以便下次使用
      array.clear();
    }
   
    // 给ESP32一些时间处理
    yield();
}

file.print("]");
file.close();
Serial.println("留言保存完成");
}

// 从SPIFFS文件系统加载留言 - 针对ESP32-C5优化
void loadMessages() {
if (!SPIFFS.exists("/messages.json")) {
    Serial.println("留言文件不存在,将创建新文件");
    return;
}

File file = SPIFFS.open("/messages.json", FILE_READ);
if (!file) {
    Serial.println("无法打开文件进行读取");
    return;
}

// 流式解析以减少内存使用
messageCount = 0;

// 检查文件是否为空或不是有效的JSON
if (file.size() < 2) {
    file.close();
    return;
}

// 使用JsonStreamingParser处理大文件
DynamicJsonDocument doc(512);
DeserializationError error;

// 读取开始括号
char c = file.read();
if (c != '[') {
    Serial.println("文件格式错误");
    file.close();
    return;
}

// 读取每个留言对象
while (file.available() && messageCount < MAX_MESSAGES) {
    // 查找对象的开始
    while (file.available() && file.peek() != '{') {
      file.read();
    }
   
    if (!file.available()) break;
   
    // 读取对象
    String jsonStr = "";
    int braceCount = 0;
    bool inString = false;
    char prevChar = 0;
   
    do {
      c = file.read();
      jsonStr += c;
      
      if (c == '"' && prevChar != '\\') {
      inString = !inString;
      }
      
      if (!inString) {
      if (c == '{') braceCount++;
      if (c == '}') braceCount--;
      }
      
      prevChar = c;
    } while (file.available() && (braceCount > 0 || c != '}'));
   
    // 解析对象
    error = deserializeJson(doc, jsonStr);
   
    if (!error) {
      const char* author = doc["author"];
      const char* content = doc["content"];
      const char* timestamp = doc["timestamp"];
      
      if (author && content && timestamp) {
      strncpy(messages.author, author, sizeof(messages.author) - 1);
      messages.author.author) - 1] = '\0';
      
      strncpy(messages.content, content, sizeof(messages.content) - 1);
      messages.content.content) - 1] = '\0';
      
      strncpy(messages.timestamp, timestamp, sizeof(messages.timestamp) - 1);
      messages.timestamp.timestamp) - 1] = '\0';
      
      messageCount++;
      }
    }
   
    // 清除以便下一个对象
    doc.clear();
   
    // 给ESP32一些时间处理
    yield();
}

file.close();
Serial.println("留言加载完成,共 " + String(messageCount) + " 条");
}

// 生成留言板HTML页面
String sendMessageBoardHTML() {
String ptr = "<!DOCTYPE html>\n";
ptr += "<html lang='zh'>\n";
ptr += "<head>\n";
ptr += "<meta charset='UTF-8'>\n";
ptr += "<meta name='viewport' content='width=device-width, initial-scale=1.0'>\n";
ptr += "<title>ESP32在线群聊</title>\n";
ptr += "<style>\n";
ptr += "    body {\n";
ptr += "      font-family: Arial, sans-serif;\n";
ptr += "      max-width: 800px;\n";
ptr += "      margin: 0 auto;\n";
ptr += "      padding: 20px;\n";
ptr += "      background-color: #f5f5f5;\n";
ptr += "    }\n";
ptr += "    h1 {\n";
ptr += "      color: #333;\n";
ptr += "      text-align: center;\n";
ptr += "      margin-bottom: 30px;\n";
ptr += "    }\n";
ptr += "    .message-form {\n";
ptr += "      background-color: white;\n";
ptr += "      padding: 20px;\n";
ptr += "      border-radius: 8px;\n";
ptr += "      box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n";
ptr += "      margin-bottom: 30px;\n";
ptr += "    }\n";
ptr += "    .form-group {\n";
ptr += "      margin-bottom: 15px;\n";
ptr += "    }\n";
ptr += "    label {\n";
ptr += "      display: block;\n";
ptr += "      margin-bottom: 5px;\n";
ptr += "      font-weight: bold;\n";
ptr += "    }\n";
ptr += "    input, textarea {\n";
ptr += "      width: 100%;\n";
ptr += "      padding: 10px;\n";
ptr += "      border: 1px solid #ddd;\n";
ptr += "      border-radius: 4px;\n";
ptr += "      box-sizing: border-box;\n";
ptr += "    }\n";
ptr += "    textarea {\n";
ptr += "      height: 100px;\n";
ptr += "      resize: vertical;\n";
ptr += "    }\n";
ptr += "    button {\n";
ptr += "      background-color: #4CAF50;\n";
ptr += "      color: white;\n";
ptr += "      border: none;\n";
ptr += "      padding: 10px 15px;\n";
ptr += "      border-radius: 4px;\n";
ptr += "      cursor: pointer;\n";
ptr += "      font-size: 16px;\n";
ptr += "    }\n";
ptr += "    button:hover {\n";
ptr += "      background-color: #45a049;\n";
ptr += "    }\n";
ptr += "    .message-list {\n";
ptr += "      background-color: white;\n";
ptr += "      border-radius: 8px;\n";
ptr += "      box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n";
ptr += "      overflow: hidden;\n";
ptr += "    }\n";
ptr += "    .message-item {\n";
ptr += "      padding: 15px 20px;\n";
ptr += "      border-bottom: 1px solid #eee;\n";
ptr += "    }\n";
ptr += "    .message-item:last-child {\n";
ptr += "      border-bottom: none;\n";
ptr += "    }\n";
ptr += "    .message-header {\n";
ptr += "      display: flex;\n";
ptr += "      justify-content: space-between;\n";
ptr += "      margin-bottom: 10px;\n";
ptr += "      font-size: 14px;\n";
ptr += "      color: #666;\n";
ptr += "    }\n";
ptr += "    .message-author {\n";
ptr += "      font-weight: bold;\n";
ptr += "      color: #333;\n";
ptr += "    }\n";
ptr += "    .message-time {\n";
ptr += "      color: #999;\n";
ptr += "    }\n";
ptr += "    .message-content {\n";
ptr += "      line-height: 1.5;\n";
ptr += "    }\n";
ptr += "    .refresh-btn {\n";
ptr += "      background-color: #2196F3;\n";
ptr += "      margin-bottom: 15px;\n";
ptr += "    }\n";
ptr += "    .refresh-btn:hover {\n";
ptr += "      background-color: #0b7dda;\n";
ptr += "    }\n";
ptr += "    .status {\n";
ptr += "      text-align: center;\n";
ptr += "      padding: 10px;\n";
ptr += "      margin-top: 10px;\n";
ptr += "      border-radius: 4px;\n";
ptr += "      display: none;\n";
ptr += "    }\n";
ptr += "    .status.success {\n";
ptr += "      background-color: #dff0d8;\n";
ptr += "      color: #3c763d;\n";
ptr += "    }\n";
ptr += "    .status.error {\n";
ptr += "      background-color: #f2dede;\n";
ptr += "      color: #a94442;\n";
ptr += "    }\n";
ptr += "    @media (max-width: 600px) {\n";
ptr += "      body {\n";
ptr += "      padding: 10px;\n";
ptr += "      }\n";
ptr += "      .message-form, .message-list {\n";
ptr += "      border-radius: 0;\n";
ptr += "      }\n";
ptr += "    }\n";
ptr += "</style>\n";
ptr += "</head>\n";
ptr += "<body>\n";
ptr += "<h1>ESP32在线群聊</h1>\n";

ptr += "<div class='message-form'>\n";
ptr += "    <div class='form-group'>\n";
ptr += "      <label for='author'>您的名字:</label>\n";
ptr += "      <input type='text' id='author' name='author' required>\n";
ptr += "    </div>\n";
ptr += "    <div class='form-group'>\n";
ptr += "      <label for='content'>群聊内容:</label>\n";
ptr += "      <textarea id='content' name='content' required></textarea>\n";
ptr += "    </div>\n";
ptr += "    <button type='button' id='submit-btn'>发布</button>\n";
ptr += "    <div id='status' class='status'></div>\n";
ptr += "</div>\n";

ptr += "<button type='button' id='refresh-btn' class='refresh-btn'>刷新</button>\n";

ptr += "<div id='message-list' class='message-list'>\n";
ptr += "    <div class='message-item' style='text-align:center;color:#999;'>加载中...</div>\n";
ptr += "</div>\n";

ptr += "<script>\n";
ptr += "    // DOM元素\n";
ptr += "    const authorInput = document.getElementById('author');\n";
ptr += "    const contentInput = document.getElementById('content');\n";
ptr += "    const submitBtn = document.getElementById('submit-btn');\n";
ptr += "    const refreshBtn = document.getElementById('refresh-btn');\n";
ptr += "    const messageList = document.getElementById('message-list');\n";
ptr += "    const statusDiv = document.getElementById('status');\n";

ptr += "    // 保存用户名到本地存储\n";
ptr += "    if (localStorage.getItem('author')) {\n";
ptr += "      authorInput.value = localStorage.getItem('author');\n";
ptr += "    }\n";

ptr += "    // 加载留言\n";
ptr += "    function loadMessages() {\n";
ptr += "      fetch('/messages')\n";
ptr += "      .then(response => response.json())\n";
ptr += "      .then(data => {\n";
ptr += "          messageList.innerHTML = '';\n";
ptr += "          \n";
ptr += "          if (data.length === 0) {\n";
ptr += "            messageList.innerHTML = '<div class=\"message-item\" style=\"text-align:center;color:#999;\">暂无留言</div>';\n";
ptr += "            return;\n";
ptr += "          }\n";
ptr += "          \n";
ptr += "          // 按时间倒序排列留言\n";
ptr += "          data.reverse().forEach(message => {\n";
ptr += "            const messageItem = document.createElement('div');\n";
ptr += "            messageItem.className = 'message-item';\n";
ptr += "            \n";
ptr += "            const messageHeader = document.createElement('div');\n";
ptr += "            messageHeader.className = 'message-header';\n";
ptr += "            \n";
ptr += "            const messageAuthor = document.createElement('span');\n";
ptr += "            messageAuthor.className = 'message-author';\n";
ptr += "            messageAuthor.textContent = message.author;\n";
ptr += "            \n";
ptr += "            const messageTime = document.createElement('span');\n";
ptr += "            messageTime.className = 'message-time';\n";
ptr += "            messageTime.textContent = message.timestamp;\n";
ptr += "            \n";
ptr += "            messageHeader.appendChild(messageAuthor);\n";
ptr += "            messageHeader.appendChild(messageTime);\n";
ptr += "            \n";
ptr += "            const messageContent = document.createElement('div');\n";
ptr += "            messageContent.className = 'message-content';\n";
ptr += "            messageContent.textContent = message.content;\n";
ptr += "            \n";
ptr += "            messageItem.appendChild(messageHeader);\n";
ptr += "            messageItem.appendChild(messageContent);\n";
ptr += "            \n";
ptr += "            messageList.appendChild(messageItem);\n";
ptr += "          });\n";
ptr += "      })\n";
ptr += "      .catch(error => {\n";
ptr += "          console.error('获取留言失败:', error);\n";
ptr += "          messageList.innerHTML = '<div class=\"message-item\" style=\"text-align:center;color:#999;\">获取留言失败</div>';\n";
ptr += "      });\n";
ptr += "    }\n";

ptr += "    // 提交新留言\n";
ptr += "    function submitMessage() {\n";
ptr += "      const author = authorInput.value.trim();\n";
ptr += "      const content = contentInput.value.trim();\n";
ptr += "      \n";
ptr += "      if (!author) {\n";
ptr += "      showStatus('请输入您的名字', 'error');\n";
ptr += "      return;\n";
ptr += "      }\n";
ptr += "      \n";
ptr += "      if (!content) {\n";
ptr += "      showStatus('请输入消息内容', 'error');\n";
ptr += "      return;\n";
ptr += "      }\n";
ptr += "      \n";
ptr += "      // 保存用户名到本地存储\n";
ptr += "      localStorage.setItem('author', author);\n";
ptr += "      \n";
ptr += "      // 禁用提交按钮\n";
ptr += "      submitBtn.disabled = true;\n";
ptr += "      \n";
ptr += "      fetch('/post', {\n";
ptr += "      method: 'POST',\n";
ptr += "      headers: {\n";
ptr += "          'Content-Type': 'application/json',\n";
ptr += "      },\n";
ptr += "      body: JSON.stringify({ author, content })\n";
ptr += "      })\n";
ptr += "      .then(response => response.json())\n";
ptr += "      .then(data => {\n";
ptr += "      if (data.status === 'success') {\n";
ptr += "          showStatus('留言发布成功!', 'success');\n";
ptr += "          contentInput.value = '';\n";
ptr += "          loadMessages();\n";
ptr += "      } else {\n";
ptr += "          showStatus('留言发布失败: ' + (data.message || '未知错误'), 'error');\n";
ptr += "      }\n";
ptr += "      })\n";
ptr += "      .catch(error => {\n";
ptr += "      console.error('提交留言失败:', error);\n";
ptr += "      showStatus('提交留言失败,请稍后再试', 'error');\n";
ptr += "      })\n";
ptr += "      .finally(() => {\n";
ptr += "      submitBtn.disabled = false;\n";
ptr += "      });\n";
ptr += "    }\n";

ptr += "    // 显示状态消息\n";
ptr += "    function showStatus(message, type) {\n";
ptr += "      statusDiv.textContent = message;\n";
ptr += "      statusDiv.className = 'status ' + type;\n";
ptr += "      statusDiv.style.display = 'block';\n";
ptr += "      \n";
ptr += "      setTimeout(() => {\n";
ptr += "      statusDiv.style.display = 'none';\n";
ptr += "      }, 3000);\n";
ptr += "    }\n";

ptr += "    // 事件***\n";
ptr += "    submitBtn.addEventListener('click', submitMessage);\n";
ptr += "    refreshBtn.addEventListener('click', loadMessages);\n";
ptr += "    \n";
ptr += "    // 回车键提交\n";
ptr += "    contentInput.addEventListener('keypress', function(e) {\n";
ptr += "      if (e.key === 'Enter' && !e.shiftKey) {\n";
ptr += "      e.preventDefault();\n";
ptr += "      submitMessage();\n";
ptr += "      }\n";
ptr += "    });\n";

ptr += "    // 初始加载留言\n";
ptr += "    loadMessages();\n";

ptr += "    // 定时刷新留言(每30秒)\n";
ptr += "    setInterval(loadMessages, 30000);\n";
ptr += "</script>\n";
ptr += "</body>\n";
ptr += "</html>\n";

return ptr;
}具体的arduino的配置可以看我的上一篇内容


页: [1]
查看完整版本: ESP32-C5——断网可用的简单群聊