#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
如果觉得我的文章对你有用,请随意赞赏