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]