init
This commit is contained in:
294
protocol/Protocol.kt
Normal file
294
protocol/Protocol.kt
Normal 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
144
protocol/protocol.h
Normal 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user