/** * Protocol.kt * SkyLink 多平台实时视频传输 - Kotlin 协议定义 * * 这是对应 protocol.h 的 Kotlin 实现版本,用于 Android 端解析 UDP 数据包。 * 使用 ByteBuffer 进行二进制数据的解析和序列化。 * * @author flowzl * @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 }