在某些特殊的场合之下,比如没有网络的大兴安岭,克拉玛利,比如不允许上网的某些研究所,比如在飞机上。那么我们能否连接热点,上网做一款线上简易群聊呢
基于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[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;
- }
复制代码
具体的arduino的配置可以看我的上一篇内容
|