Files
flyLink/protocol/Protocol.kt

295 lines
7.7 KiB
Kotlin
Raw Normal View History

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