This commit is contained in:
flowerstonezl
2026-01-19 17:08:49 +08:00
commit 056289625c
1486 changed files with 110768 additions and 0 deletions

294
protocol/Protocol.kt Normal file
View File

@@ -0,0 +1,294 @@
/**
* Protocol.kt
* SkyLink 多平台实时视频传输 - Kotlin 协议定义
*
* 这是对应 protocol.h 的 Kotlin 实现版本,用于 Android 端解析 UDP 数据包。
* 使用 ByteBuffer 进行二进制数据的解析和序列化。
*
* @author SkyLink Team
* @date 2026-01-19
*/
package com.skylink.protocol
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* SkyLink 协议版本号
*/
const val SKYLINK_VERSION = "1.0"
/**
* 数据包魔数
*/
const val SKYLINK_MAGIC: Short = 0xAA55.toShort()
/**
* UDP 最大包大小 (MTU - IP - UDP)
*/
const val MAX_UDP_PACKET_SIZE = 1472
/**
* 最大载荷大小
*/
const val MAX_PAYLOAD_SIZE = MAX_UDP_PACKET_SIZE - PacketHeaderSize
/**
* 包头大小 (字节)
*/
const val PacketHeaderSize = 50
/**
* UDP 接收缓冲区大小
*/
const val RX_BUFFER_SIZE = 65536
/**
* 最大分片数
*/
const val MAX_CHUNKS = 1000
/**
* 帧超时时间 (毫秒)
*/
const val FRAME_TIMEOUT_MS = 5000L
/**
* JPEG 图像格式 ID
*/
const val IMAGE_FORMAT_JPEG: Byte = 0x01
/**
* JPEG 最大质量
*/
const val JPEG_MAX_QUALITY = 100
/**
* JPEG 最小质量
*/
const val JPEG_MIN_QUALITY = 10
// ============================================================================
// 数据类定义
// ============================================================================
/**
* SkyLink 数据包头结构
*
* 完整映射 C++ protocol.h 的 PacketHeader
* - Magic (2 bytes)
* - FrameID (4 bytes)
* - TotalChunks (2 bytes)
* - ChunkIndex (2 bytes)
* - DataLen (2 bytes)
* - Timestamp (8 bytes)
* - Latitude (8 bytes)
* - Longitude (8 bytes)
* - Altitude (8 bytes)
* - CRC16 (2 bytes)
* - Reserve (4 bytes)
* 总计: 50 字节
*/
data class SkyLinkPacketHeader(
val magic: Short, // 0xAA55
val frameId: Int, // 帧ID
val totalChunks: Short, // 总分片数
val chunkIndex: Short, // 当前分片索引
val dataLen: Short, // 载荷长度
val timestamp: Double, // 时间戳
val lat: Double, // 纬度
val lon: Double, // 经度
val alt: Double, // 海拔
val crc16: Short, // 校验码
val reserve: Int // 预留字段
) {
companion object {
/**
* 从 ByteArray 中解析包头
* @param data 字节数组
* @return 解析成功返回 SkyLinkPacketHeader否则返回 null
*/
fun fromByteArray(data: ByteArray): SkyLinkPacketHeader? {
if (data.size < PacketHeaderSize) {
return null
}
val buffer = ByteBuffer.wrap(data, 0, PacketHeaderSize)
buffer.order(ByteOrder.LITTLE_ENDIAN) // ARM 平台使用小端序
return try {
val magic = buffer.short
val frameId = buffer.int
val totalChunks = buffer.short
val chunkIndex = buffer.short
val dataLen = buffer.short
val timestamp = buffer.double
val lat = buffer.double
val lon = buffer.double
val alt = buffer.double
val crc16 = buffer.short
val reserve = buffer.int
SkyLinkPacketHeader(
magic = magic,
frameId = frameId,
totalChunks = totalChunks,
chunkIndex = chunkIndex,
dataLen = dataLen,
timestamp = timestamp,
lat = lat,
lon = lon,
alt = alt,
crc16 = crc16,
reserve = reserve
)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将包头序列化为 ByteArray
*/
fun toByteArray(header: SkyLinkPacketHeader): ByteArray {
val buffer = ByteBuffer.allocate(PacketHeaderSize)
buffer.order(ByteOrder.LITTLE_ENDIAN)
buffer.putShort(header.magic)
buffer.putInt(header.frameId)
buffer.putShort(header.totalChunks)
buffer.putShort(header.chunkIndex)
buffer.putShort(header.dataLen)
buffer.putDouble(header.timestamp)
buffer.putDouble(header.lat)
buffer.putDouble(header.lon)
buffer.putDouble(header.alt)
buffer.putShort(header.crc16)
buffer.putInt(header.reserve)
return buffer.array()
}
}
/**
* 验证包头魔数是否有效
*/
fun isValid(): Boolean {
return magic == SKYLINK_MAGIC
}
/**
* 获取此包头对应的载荷部分
*/
fun getPayloadSize(): Int {
return dataLen.toInt()
}
}
/**
* 完整的 SkyLink 数据包
*/
data class SkyLinkPacket(
val header: SkyLinkPacketHeader,
val payload: ByteArray // 实际的图像数据
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as SkyLinkPacket
if (header != other.header) return false
if (!payload.contentEquals(other.payload)) return false
return true
}
override fun hashCode(): Int {
var result = header.hashCode()
result = 31 * result + payload.contentHashCode()
return result
}
/**
* 获取总的包大小
*/
fun getTotalSize(): Int {
return PacketHeaderSize + payload.size
}
}
/**
* 图像帧的组装结果
*/
data class DecodedFrame(
val frameId: Int,
val imageData: ByteArray, // JPEG 图像数据
val timestamp: Double,
val lat: Double,
val lon: Double,
val alt: Double,
val receivedTime: Long = System.currentTimeMillis() // 接收时刻
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DecodedFrame
if (frameId != other.frameId) return false
if (!imageData.contentEquals(other.imageData)) return false
if (timestamp != other.timestamp) return false
if (lat != other.lat) return false
if (lon != other.lon) return false
if (alt != other.alt) return false
if (receivedTime != other.receivedTime) return false
return true
}
override fun hashCode(): Int {
var result = frameId
result = 31 * result + imageData.contentHashCode()
result = 31 * result + timestamp.hashCode()
result = 31 * result + lat.hashCode()
result = 31 * result + lon.hashCode()
result = 31 * result + alt.hashCode()
result = 31 * result + receivedTime.hashCode()
return result
}
}
// ============================================================================
// 辅助函数
// ============================================================================
/**
* 验证 CRC16 校验 (CCITT 多项式)
*/
fun calculateCrc16(data: ByteArray): Short {
var crc: Int = 0xFFFF
for (byte in data) {
crc = crc xor ((byte.toInt() and 0xFF) shl 8)
for (j in 0 until 8) {
crc = if (crc and 0x8000 != 0) {
((crc shl 1) xor 0x1021) and 0xFFFF
} else {
(crc shl 1) and 0xFFFF
}
}
}
return crc.toShort()
}
/**
* 验证包头合法性
*/
fun isValidSkyLinkPacket(header: SkyLinkPacketHeader): Boolean {
return header.isValid() &&
header.chunkIndex < header.totalChunks &&
header.dataLen > 0 &&
header.dataLen <= MAX_PAYLOAD_SIZE
}

144
protocol/protocol.h Normal file
View File

@@ -0,0 +1,144 @@
/**
* @file protocol.h
* @brief SkyLink 多平台实时视频传输 - 公共协议定义
*
* 这个文件定义了机载端ROS 2、PC 端Qt和移动端Android之间的通信协议。
* 必须保持三端的字节对齐和字段顺序一致。
*
* @author SkyLink Team
* @date 2026-01-19
*/
#pragma once
#include <cstdint>
#include <cstring>
/**
* @brief 强制1字节对齐避免编译器补齐导致的大小差异
*/
#pragma pack(push, 1)
/**
* @struct PacketHeader
* @brief UDP 数据包头部结构
*
* 总大小: 50 字节
* - 报头识别: 2 字节 (Magic)
* - 帧标识: 4 字节 (FrameID)
* - 分片信息: 4 字节 (TotalChunks + ChunkIndex)
* - 数据长度: 2 字节 (DataLen)
* - 时间戳: 8 字节 (Timestamp)
* - GPS 位置: 24 字节 (Lat, Lon, Alt)
* - 预留: 6 字节 (Reserve 用于将来扩展)
*/
struct PacketHeader {
// ============ 包头识别 ============
uint16_t magic; ///< 魔数 0xAA55 用于识别合法数据包
// ============ 帧标识 ============
uint32_t frame_id; ///< 唯一的帧编号,从 0 开始递增,用于帧同步
// ============ 分片信息 ============
uint16_t total_chunks; ///< 该帧总共被分成多少个 UDP 包
uint16_t chunk_index; ///< 当前数据包是第几个分片 (0-based)
// ============ 载荷信息 ============
uint16_t data_len; ///< 当前包的有效载荷长度(单位:字节)
// ============ 时间戳 ============
double timestamp; ///< GPS 时间戳 (秒数Unix 时间 或 ROS 时间)
// ============ GPS 位置数据 ============
double lat; ///< 纬度 (WGS84 坐标系)
double lon; ///< 经度 (WGS84 坐标系)
double alt; ///< 海拔高度 (米)
// ============ 校验与预留 ============
uint16_t crc16; ///< CRC16 校验码 (可选,当前未使用)
uint32_t reserve; ///< 保留字段,用于将来的扩展
/**
* @brief 获取包头大小(字节数)
* @return 包头固定大小50 字节
*/
static constexpr uint16_t size() {
return sizeof(PacketHeader);
}
};
/**
* @brief 恢复默认的对齐方式
*/
#pragma pack(pop)
// ============================================================================
// 协议常量定义
// ============================================================================
/** @brief 数据包魔数,用于识别合法的 SkyLink 数据包 */
constexpr uint16_t SKYLINK_MAGIC = 0xAA55;
/** @brief 最大的单个 UDP 包大小 (包含报头) - MTU 1500 字节 */
constexpr uint16_t MAX_UDP_PACKET_SIZE = 1472; // 1500 - 20 (IP) - 8 (UDP)
/** @brief 最大的载荷大小 */
constexpr uint16_t MAX_PAYLOAD_SIZE = MAX_UDP_PACKET_SIZE - PacketHeader::size();
/** @brief UDP 接收缓冲区大小 */
constexpr uint32_t RX_BUFFER_SIZE = 65536;
/** @brief 最大支持的分片数 */
constexpr uint16_t MAX_CHUNKS = 1000;
/** @brief 帧超时时间(毫秒),超过此时间的分片将被丢弃 */
constexpr uint32_t FRAME_TIMEOUT_MS = 5000;
// ============================================================================
// 图像编码相关常量
// ============================================================================
/** @brief JPEG 图像格式 ID */
constexpr uint8_t IMAGE_FORMAT_JPEG = 0x01;
/** @brief JPEG 最大压缩质量 */
constexpr uint8_t JPEG_MAX_QUALITY = 100;
/** @brief JPEG 最小压缩质量 */
constexpr uint8_t JPEG_MIN_QUALITY = 10;
// ============================================================================
// 辅助函数
// ============================================================================
/**
* @brief 验证包头魔数是否合法
* @param header 待验证的包头结构
* @return 如果魔数为 0xAA55 返回 true否则返回 false
*/
inline bool isValidPacket(const PacketHeader& header) {
return header.magic == SKYLINK_MAGIC;
}
/**
* @brief 计算 CRC16 校验码 (可选)
* @param data 数据指针
* @param len 数据长度
* @return 16 位 CRC 校验结果
*/
inline uint16_t crc16_ccitt(const uint8_t* data, uint16_t len) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc = crc << 1;
}
crc &= 0xFFFF;
}
}
return crc;
}