#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占用过高
}
最后修改:2025 年 06 月 24 日 09 : 23 PM
© 允许规范转载