跳转到内容

MQTT 图像传输

MQTT 图像传输

本节详细介绍如何通过 MQTT 高效传输 ESP32-CAM 拍摄的图片。学习完成后,您将能够:

  • 配置 MQTT 参数以支持大文件传输
  • 实现可靠的大文件 MQTT 传输
  • 处理传输失败和重试
  • 监控传输性能和成功率
┌─────────────────────────────────────────────────────────────┐
│ 图片传输流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [ESP32-CAM] │
│ │ │
│ 1. 拍照 → 获取 JPEG 缓冲区 │
│ 2. 检查缓冲区大小 │
│ 3. 是否需要分块? │
│ ├── 否: 直接发布二进制 │
│ └── 是: 分块发送 + 序号 │
│ │ │
│ ▼ │
│ [Mosquitto Broker] │
│ │ │
│ ▼ │
│ [Node-RED] │
│ 1. 接收图片数据 │
│ 2. 重组/验证完整性 │
│ 3. 保存/显示/推送 │
│ │
└─────────────────────────────────────────────────────────────┘
platformio.ini
// build_flags =
// -DMQTT_MAX_PACKET_SIZE=60000
// -DMQTT_KEEPALIVE=120
// 设置 MQTT 客户端缓冲
void setupMQTT() {
client.setBufferSize(60000); // 60KB 缓冲
client.setServer(mqtt_server, mqtt_port);
client.setCallback(callback);
}
mosquitto.conf
# 允许的最大包大小 (默认 256MB)
max_packet_size 100000000
# 消息持久化 (防止消息丢失)
persistence true
persistence_location /mosquitto/data/
# 增加 QoS 1/2 的消息重试间隔
max_queued_messages 1000
// 直接发布二进制图片数据
void sendPhotoBinary() {
camera_fb_t* fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed!");
client.publish("esp32cam/status", "error:capture");
return;
}
// 检查缓冲区是否足够
if (fb->len > 60000) {
Serial.printf("Photo too large: %zu bytes, use chunked mode\n", fb->len);
sendPhotoChunked(fb);
return;
}
// 发布二进制数据
client.publish("esp32cam/photo",
fb->buf, // 数据指针
fb->len, // 数据长度
false); // retain
Serial.printf("Binary photo sent: %zu bytes\n", fb->len);
// 发布元数据到状态 Topic
String meta = "{\"size\":" + String(fb->len) +
",\"width\":640,\"height\":480}";
client.publish("esp32cam/photo_meta", meta.c_str());
esp_camera_fb_return(fb);
}
[MQTT In: esp32cam/photo] ──→ [Switch: QoS] ──→ [Write File: 保存]
└──→ [Image Output: 显示]
// 大图片分块传输
void sendPhotoChunked(camera_fb_t* fb) {
int chunkSize = 40000; // 每块 40KB (留余量)
int totalChunks = (fb->len + chunkSize - 1) / chunkSize;
String photoId = String(millis());
Serial.printf("Sending %d chunks, total %zu bytes\n",
totalChunks, fb->len);
for (int i = 0; i < totalChunks; i++) {
int offset = i * chunkSize;
int size = min(chunkSize, (int)(fb->len - offset));
// 构建分块消息 (JSON)
String chunk = "{\"id\":\"" + photoId + "\",";
chunk += "\"seq\":" + String(i) + ",";
chunk += "\"total\":" + String(totalChunks) + ",";
chunk += "\"size\":" + String(fb->len) + "}";
// 分块数据发布到不同的 Topic
String dataTopic = "esp32cam/photo_chunk/" + String(i);
client.publish(dataTopic.c_str(),
fb->buf + offset, size, false);
delay(50); // 块间延迟,避免填满缓冲
}
// 发送完成信号
String done = "{\"id\":\"" + photoId + "\",\"status\":\"complete\"}";
client.publish("esp32cam/photo_status", done.c_str());
Serial.println("All chunks sent");
}
// Function: 重组分块图片
// 接收各个分块 Topic 的消息并重组
var chunkData = msg.payload;
var topic = msg.topic;
// 提取块序号: esp32cam/photo_chunk/0 → 0
var chunkIndex = parseInt(topic.split('/').pop());
// 使用 Flow Context 缓存块数据
var buffer = flow.get("photoBuffer") || {};
var photoMeta = flow.get("photoMeta") || {};
// 首次接收,初始化
if (!buffer.total) {
buffer = {
chunks: [],
total: 0,
received: 0,
size: 0,
startTime: Date.now()
};
}
// 存储块数据
buffer.chunks[chunkIndex] = chunkData;
buffer.received++;
buffer.size += chunkData.length;
flow.set("photoBuffer", buffer);
// 检查是否接收完成
// 需要知道总块数... 这里简化处理
// 实际实现应通过元数据 Topic 获取总块数
if (buffer.received >= buffer.total) {
// 合并所有块
var fullImage = Buffer.concat(buffer.chunks);
msg.payload = fullImage;
msg.filename = "/data/photos/photo_" + Date.now() + ".jpg";
// 清除缓存
flow.set("photoBuffer", null);
return msg; // 传递给 Write File 节点
}
return null; // 等待更多块
// Node-RED: 发送接收确认
// ESP32 发布图片后,Node-RED 回复确认
// Function: 发送确认消息
msg.topic = "esp32cam/ack";
msg.payload = JSON.stringify({
id: msg.payload.photo_id || "",
status: "received",
size: msg.payload.length || 0,
timestamp: Date.now()
});
return msg;
// ESP32: 等待确认,超时重传
void sendPhotoWithAck() {
for (int retry = 0; retry < 3; retry++) {
sendPhotoBinary();
// 等待确认 (非阻塞方式)
unsigned long timeout = millis() + 5000; // 5秒超时
while (millis() < timeout) {
client.loop();
if (photoAcknowledged) {
Serial.println("Photo acknowledged by server");
return;
}
}
Serial.printf("Retry %d: no acknowledgment\n", retry + 1);
}
Serial.println("Photo send failed after 3 retries");
client.publish("esp32cam/status", "error:send_failed");
}
// Node-RED: 监控图片传输性能
// Function: 记录传输统计
var stats = context.get("photoStats") || {
total: 0,
success: 0,
failed: 0,
totalBytes: 0,
avgTime: 0
};
stats.total++;
stats.totalBytes += msg.payload.length || 0;
// 记录传输时间 (假设 ESP32 在消息中附带时间戳)
var sentTime = msg.payload.timestamp || 0;
if (sentTime > 0) {
var transferTime = Date.now() - sentTime;
stats.avgTime = (stats.avgTime * (stats.total - 1) + transferTime) / stats.total;
}
context.set("photoStats", stats);
// 每小时重置计数器
// 通过 Inject 节点定时报告
Terminal window
# 监控图片传输
mosquitto_sub -t "esp32cam/photo" -C 1 > received_photo.jpg
ls -la received_photo.jpg
# 检查传输统计
mosquitto_sub -t "esp32cam/status" -v
# 测试大图片分块
mosquitto_pub -t "esp32cam/command" -m "take_photo_hires"

Q1: MQTT 传输图片的最大大小限制?

Section titled “Q1: MQTT 传输图片的最大大小限制?”

理论最大 256MB (Mosquitto 默认),但实际受限于 ESP32 内存 (60KB Max) 和网络稳定性。建议单张图片不超过 50KB (VGA JPEG)。

是的,MQTT QoS 1/2 在客户端断开时无法保证投递。建议 ESP32 在拍照后暂存到 SD 卡,网络恢复后补传。

VGA JPEG (~30KB) 单张传输约 1-3 秒。100 张约 2-5 分钟,取决于 Wi-Fi 质量和分块策略。

推荐做法:

  • 限制单张图片大小在 50KB 以内
  • 大图片使用分块传输 (每块 ≤ 40KB)
  • 实现接收确认和超时重传机制
  • 监控传输成功率

避免做法:

  • 单条 MQTT 消息超过 60KB (PubSubClient 限制)
  • 无确认机制盲目发送
  • 传输过程中不检查 MQTT 连接状态
  • 忽略分块传输的块序号验证
  1. 二进制传输直接发送 JPEG 缓冲,效率最高
  2. 分块传输解决大图片单次发送内存不足
  3. QoS 1 确保图片消息至少投递一次
  4. 确认机制验证图片接收完整性
  5. 重试机制处理网络不稳定导致的传输失败