| 在某些特殊的场合之下,比如没有网络的大兴安岭,克拉玛利,比如不允许上网的某些研究所,比如在飞机上。那么我们能否连接热点,上网做一款线上简易群聊呢 基于Firebeete试用的ESP32-C5开发板以及其强大的wifi功能,我们基于其作为一个简单的服务器,提供热点连接,然后可以访问群留言聊天功能。
 
  废话不说,结果展示如下
 
  首先需要访问Esp32的热点,然后输入密码12345678,就可以使用啦,默认30秒更新一次,因为esp32能力有限,需要快速更新可以点击更新,多人使用也是可以的,这样可以聊起天来,哈哈哈哈哈,相当于一个简单群聊
 
 
 手机端也可以使用,代码展示如下:
 
 具体的arduino的配置可以看我的上一篇内容复制代码// 针对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[32];  // 使用固定大小缓冲区替代String
  char content[256];
  char timestamp[16];
};
// 留言数组
Message messages[MAX_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[i].author;
    obj["content"] = messages[i].content;
    obj["timestamp"] = messages[i].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[16];
      sprintf(timestamp, "%02d:%02d:%02d", hours, minutes, seconds);
      
      // 添加新留言 - 使用strncpy处理固定缓冲区
      if (messageCount < MAX_MESSAGES) {
        strncpy(messages[messageCount].author, author, sizeof(messages[messageCount].author) - 1);
        messages[messageCount].author[sizeof(messages[messageCount].author) - 1] = '\0';
        
        strncpy(messages[messageCount].content, content, sizeof(messages[messageCount].content) - 1);
        messages[messageCount].content[sizeof(messages[messageCount].content) - 1] = '\0';
        
        strncpy(messages[messageCount].timestamp, timestamp, sizeof(messages[messageCount].timestamp) - 1);
        messages[messageCount].timestamp[sizeof(messages[messageCount].timestamp) - 1] = '\0';
        
        messageCount++;
      } else {
        // 如果留言已满,移除最旧的留言
        for (int i = 0; i < MAX_MESSAGES - 1; i++) {
          memcpy(&messages[i], &messages[i + 1], sizeof(Message));
        }
        
        strncpy(messages[MAX_MESSAGES - 1].author, author, sizeof(messages[MAX_MESSAGES - 1].author) - 1);
        messages[MAX_MESSAGES - 1].author[sizeof(messages[MAX_MESSAGES - 1].author) - 1] = '\0';
        
        strncpy(messages[MAX_MESSAGES - 1].content, content, sizeof(messages[MAX_MESSAGES - 1].content) - 1);
        messages[MAX_MESSAGES - 1].content[sizeof(messages[MAX_MESSAGES - 1].content) - 1] = '\0';
        
        strncpy(messages[MAX_MESSAGES - 1].timestamp, timestamp, sizeof(messages[MAX_MESSAGES - 1].timestamp) - 1);
        messages[MAX_MESSAGES - 1].timestamp[sizeof(messages[MAX_MESSAGES - 1].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[i].author;
      obj["content"] = messages[i].content;
      obj["timestamp"] = messages[i].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[messageCount].author, author, sizeof(messages[messageCount].author) - 1);
        messages[messageCount].author[sizeof(messages[messageCount].author) - 1] = '\0';
        
        strncpy(messages[messageCount].content, content, sizeof(messages[messageCount].content) - 1);
        messages[messageCount].content[sizeof(messages[messageCount].content) - 1] = '\0';
        
        strncpy(messages[messageCount].timestamp, timestamp, sizeof(messages[messageCount].timestamp) - 1);
        messages[messageCount].timestamp[sizeof(messages[messageCount].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;
}
 
 
 
 |