2026-01-19 17:08:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @file GroundReceiver.h
|
|
|
|
|
|
* @brief SkyLink Qt 地面站 - UDP 接收和数据重组头文件
|
|
|
|
|
|
*
|
|
|
|
|
|
* 在单独的线程中运行,负责:
|
|
|
|
|
|
* 1. 监听 UDP 广播
|
|
|
|
|
|
* 2. 接收分片数据包
|
|
|
|
|
|
* 3. 重组完整帧
|
|
|
|
|
|
* 4. 解码 JPEG 图像
|
|
|
|
|
|
*
|
2026-01-20 10:41:22 +08:00
|
|
|
|
* @author flowzl
|
2026-01-19 17:08:49 +08:00
|
|
|
|
* @date 2026-01-19
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
|
|
#include <QObject>
|
|
|
|
|
|
#include <QUdpSocket>
|
|
|
|
|
|
#include <QThread>
|
|
|
|
|
|
#include <QImage>
|
|
|
|
|
|
#include <QMap>
|
2026-01-21 13:44:29 +08:00
|
|
|
|
#include <QDebug>
|
|
|
|
|
|
#include <QString>
|
2026-01-19 17:08:49 +08:00
|
|
|
|
#include <map>
|
|
|
|
|
|
#include <vector>
|
|
|
|
|
|
#include <memory>
|
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
|
#include <mutex>
|
|
|
|
|
|
|
2026-01-21 13:44:29 +08:00
|
|
|
|
// OpenCV for YUYV conversion
|
|
|
|
|
|
#include <opencv2/opencv.hpp>
|
|
|
|
|
|
#include <opencv2/imgproc.hpp>
|
|
|
|
|
|
|
2026-01-19 17:08:49 +08:00
|
|
|
|
#include "protocol.h"
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @struct FrameBuffer
|
|
|
|
|
|
* @brief 帧缓冲区,用于重组分片数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
struct FrameBuffer {
|
|
|
|
|
|
uint32_t frame_id;
|
|
|
|
|
|
uint16_t total_chunks;
|
|
|
|
|
|
std::map<uint16_t, std::vector<uint8_t>> chunks; // chunk_index -> 数据
|
|
|
|
|
|
double timestamp;
|
|
|
|
|
|
double lat, lon, alt;
|
|
|
|
|
|
uint64_t receive_time_ms;
|
2026-02-02 17:06:20 +08:00
|
|
|
|
uint32_t photo_number = 0; // 照片编号(从reserve字段提取)
|
2026-01-19 17:08:49 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 检查是否所有分片都已接收
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool is_complete() const {
|
|
|
|
|
|
if (total_chunks == 0) return false;
|
|
|
|
|
|
for (uint16_t i = 0; i < total_chunks; i++) {
|
|
|
|
|
|
if (chunks.find(i) == chunks.end()) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 将所有分片合并为一个字节数组
|
2026-01-21 13:44:29 +08:00
|
|
|
|
* @return 合并后的数据,如果分片不完整返回空vector
|
2026-01-19 17:08:49 +08:00
|
|
|
|
*/
|
|
|
|
|
|
std::vector<uint8_t> merge() const {
|
|
|
|
|
|
std::vector<uint8_t> result;
|
2026-01-21 13:44:29 +08:00
|
|
|
|
// 预分配空间以提高性能
|
|
|
|
|
|
size_t estimated_size = chunks.size() * 500; // 估算每个分片约500字节
|
|
|
|
|
|
result.reserve(estimated_size);
|
|
|
|
|
|
|
|
|
|
|
|
// 调试:检查分片顺序
|
|
|
|
|
|
QString chunk_indices;
|
|
|
|
|
|
for (const auto& pair : chunks) {
|
|
|
|
|
|
chunk_indices += QString::number(pair.first) + " ";
|
|
|
|
|
|
}
|
|
|
|
|
|
qDebug() << "帧" << frame_id << "合并前,分片索引:" << chunk_indices;
|
|
|
|
|
|
|
2026-01-19 17:08:49 +08:00
|
|
|
|
for (uint16_t i = 0; i < total_chunks; i++) {
|
|
|
|
|
|
auto it = chunks.find(i);
|
|
|
|
|
|
if (it != chunks.end()) {
|
2026-01-21 13:44:29 +08:00
|
|
|
|
// 调试:如果是第一个分片,打印前几个字节
|
|
|
|
|
|
if (i == 0 && !it->second.empty()) {
|
|
|
|
|
|
QString hex_preview;
|
|
|
|
|
|
for (size_t j = 0; j < std::min((size_t)8, it->second.size()); j++) {
|
|
|
|
|
|
hex_preview += QString::number(it->second[j], 16).rightJustified(2, '0') + " ";
|
|
|
|
|
|
}
|
|
|
|
|
|
qDebug() << "帧" << frame_id << "合并时,分片0前8字节:" << hex_preview;
|
|
|
|
|
|
}
|
2026-01-19 17:08:49 +08:00
|
|
|
|
result.insert(result.end(), it->second.begin(), it->second.end());
|
2026-01-21 13:44:29 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 如果缺少分片,返回空vector表示不完整
|
|
|
|
|
|
qWarning() << "帧" << frame_id << "合并时缺少分片" << i;
|
|
|
|
|
|
return std::vector<uint8_t>();
|
2026-01-19 17:08:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @class GroundReceiver
|
|
|
|
|
|
* @brief UDP 接收和数据重组引擎
|
|
|
|
|
|
*/
|
|
|
|
|
|
class GroundReceiver : public QObject {
|
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 构造函数
|
|
|
|
|
|
* @param parent 父对象
|
|
|
|
|
|
* @param port UDP 监听端口 (默认 9999)
|
|
|
|
|
|
*/
|
|
|
|
|
|
explicit GroundReceiver(QObject* parent = nullptr, uint16_t port = 9999);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 析构函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
~GroundReceiver();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 启动接收器
|
|
|
|
|
|
*/
|
|
|
|
|
|
void start();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 停止接收器
|
|
|
|
|
|
*/
|
|
|
|
|
|
void stop();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 获取是否正在运行
|
|
|
|
|
|
*/
|
|
|
|
|
|
bool is_running() const;
|
|
|
|
|
|
|
|
|
|
|
|
signals:
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 完整帧解码完成信号
|
|
|
|
|
|
* @param img 解码后的 QImage
|
|
|
|
|
|
* @param lat 纬度
|
|
|
|
|
|
* @param lon 经度
|
|
|
|
|
|
* @param alt 海拔
|
2026-02-02 17:06:20 +08:00
|
|
|
|
* @param photo_number 照片编号
|
2026-01-19 17:08:49 +08:00
|
|
|
|
*/
|
2026-02-02 17:06:20 +08:00
|
|
|
|
void frameDecoded(const QImage& img, double lat, double lon, double alt, uint32_t photo_number);
|
2026-01-19 17:08:49 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 统计信息更新信号
|
|
|
|
|
|
* @param fps 每秒帧数
|
|
|
|
|
|
* @param kbs 每秒 KB 数
|
|
|
|
|
|
*/
|
|
|
|
|
|
void statsUpdated(int fps, int kbs);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 错误信号
|
|
|
|
|
|
* @param error_msg 错误信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
void error(const QString& error_msg);
|
|
|
|
|
|
|
|
|
|
|
|
private slots:
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief UDP 有数据可读时的槽函数
|
|
|
|
|
|
*/
|
|
|
|
|
|
void on_udp_read_ready();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 处理 UDP 错误
|
|
|
|
|
|
*/
|
|
|
|
|
|
void on_udp_error();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 定期清理超时的帧
|
|
|
|
|
|
*/
|
|
|
|
|
|
void on_cleanup_timeout();
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
// UDP 套接字和网络
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
uint16_t listen_port_;
|
|
|
|
|
|
std::unique_ptr<QUdpSocket> udp_socket_;
|
|
|
|
|
|
QThread worker_thread_;
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
// 帧缓冲区管理
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
std::map<uint32_t, FrameBuffer> frame_buffers_; // frame_id -> FrameBuffer
|
|
|
|
|
|
std::mutex frame_buffers_mutex_;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t last_complete_frame_id_;
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
// 统计信息
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
struct {
|
|
|
|
|
|
uint32_t total_packets = 0; ///< 总接收包数
|
|
|
|
|
|
uint32_t total_bytes = 0; ///< 总接收字节数
|
|
|
|
|
|
uint32_t total_frames = 0; ///< 完整帧数
|
|
|
|
|
|
uint32_t dropped_frames = 0; ///< 丢弃的帧数
|
|
|
|
|
|
uint32_t frames_this_second = 0; ///< 本秒帧数
|
|
|
|
|
|
uint32_t bytes_this_second = 0; ///< 本秒字节数
|
|
|
|
|
|
uint64_t last_stats_time = 0; ///< 上次统计时刻
|
|
|
|
|
|
} stats_;
|
|
|
|
|
|
|
|
|
|
|
|
std::mutex stats_mutex_;
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
// 私有方法
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 处理接收到的 UDP 数据包
|
|
|
|
|
|
*/
|
|
|
|
|
|
void process_packet(const uint8_t* data, size_t len);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 处理完整的帧
|
|
|
|
|
|
*/
|
|
|
|
|
|
void handle_complete_frame(const FrameBuffer& frame);
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 从 JPEG 数据解码为 QImage
|
|
|
|
|
|
*/
|
|
|
|
|
|
QImage decode_jpeg(const std::vector<uint8_t>& data);
|
|
|
|
|
|
|
2026-01-21 13:44:29 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 从 YUYV 数据转换为 QImage
|
|
|
|
|
|
* @param yuyv_data YUYV格式的原始数据
|
|
|
|
|
|
* @param width 图像宽度
|
|
|
|
|
|
* @param height 图像高度
|
|
|
|
|
|
* @return 转换后的QImage
|
|
|
|
|
|
*/
|
|
|
|
|
|
QImage decode_yuyv(const std::vector<uint8_t>& yuyv_data, int width, int height);
|
|
|
|
|
|
|
2026-01-19 17:08:49 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* @brief 清理超时的帧缓冲区
|
|
|
|
|
|
*/
|
|
|
|
|
|
void cleanup_timeout_frames();
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* @brief 更新和报告统计信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
void update_stats(uint32_t packet_size);
|
|
|
|
|
|
};
|