Files
flyLink/PROTOCOL.md
2026-02-02 17:06:20 +08:00

6.2 KiB
Raw Permalink Blame History

SkyLink 协议规范文档

概述

SkyLink 是一个多平台实时视频传输系统的通信协议设计用于在机载端、PC 地面站和移动端之间进行高效的实时视频传输。

📋 协议版本

  • 版本: 1.0
  • 最后更新: 2026-01-19
  • 维护者: flowzl

🔧 数据包结构

UDP 包头 (PacketHeader)

总大小: 50 字节
┌─────────┬──────────┬────────┬──────────┬────────┬──────────┬──────┬──────┬──────┬────────┬─────────┐
│ Magic   │ FrameID  │ Chunks │ ChunkIdx │ DataLen│Timestamp │ Lat  │ Lon  │ Alt  │ CRC16  │ Reserve │
│ 2 bytes │ 4 bytes  │ 2 byte │ 2 bytes  │ 2bytes │ 8 bytes  │ 8by  │ 8by  │ 8by  │ 2bytes │ 4 bytes │
└─────────┴──────────┴────────┴──────────┴────────┴──────────┴──────┴──────┴──────┴────────┴─────────┘

字段详解

字段 大小 类型 说明
Magic 2 uint16 魔数 0xAA55,用于识别合法数据包
FrameID 4 uint32 唯一的帧编号,从 0 开始递增
TotalChunks 2 uint16 该帧总共被分成多少个 UDP 包
ChunkIndex 2 uint16 当前包是第几个分片 (0-based)
DataLen 2 uint16 当前包的有效载荷长度(单位:字节)
Timestamp 8 double GPS 时间戳或系统时间(秒数)
Latitude 8 double 纬度 (WGS84 坐标系)
Longitude 8 double 经度 (WGS84 坐标系)
Altitude 8 double 海拔高度 (单位:米)
CRC16 2 uint16 CRC16 校验码(可选,当前未使用)
Reserve 4 uint32 保留字段,用于将来扩展

📦 完整数据包结构

┌─────────────────────────────┐
│   UDP Package Header (50B)  │  <- PacketHeader 结构
├─────────────────────────────┤
│   Payload (0 ~ 1422 bytes)  │  <- JPEG 图像数据片段
└─────────────────────────────┘
   Total: 50 ~ 1472 bytes

最大包大小

  • 以太网 MTU: 1500 字节
  • IP 头: 20 字节
  • UDP 头: 8 字节
  • 可用容量: 1500 - 20 - 8 = 1472 字节
  • PacketHeader: 50 字节
  • 最大载荷: 1472 - 50 = 1422 字节

🔄 帧重组流程

发送端 (ROS 2 机载端)

1. 采集图像
   ↓
2. 编码为 JPEG (质量 1-100)
   ↓
3. 计算所需分片数
   Total = ceil(JPEG_SIZE / 1422)
   ↓
4. 逐片段创建 UDP 包头
   - magic = 0xAA55
   - frame_id = 帧计数器
   - total_chunks = 计算出的总片数
   - chunk_index = 0, 1, 2, ...
   - data_len = 当前片段大小
   - timestamp = 当前时间戳
   - lat, lon, alt = 当前 GPS 位置
   ↓
5. 发送 UDP 广播包
   通过 UDP/IP 广播到网络

接收端 (PC 地面站 / Android 移动端)

1. 监听 UDP 端口 9999
   ↓
2. 接收数据包
   ↓
3. 检查魔数是否为 0xAA55
   ✗ 否 → 丢弃该包
    是 → 继续
   ↓
4. 根据 frame_id 找到对应的帧缓冲区
   如果不存在 → 创建新的缓冲区
   ↓
5. 将当前分片存储到 chunks[chunk_index]
   ↓
6. 检查是否所有分片都已接收
   if (chunks.count() == total_chunks)
       将所有分片合并为完整 JPEG 数据
       解码 JPEG 为图像
       显示到界面
       发出 frameDecoded() 信号
   ↓
7. 清理超时未完成的帧
   (超过 5 秒)

⏱️ 时序示例

假设一个 480x360 分辨率的图像JPEG 压缩后 50 KB

Frame#0:
  TotalChunks = ceil(50000 / 1422) = 36 个分片
  
  Packet 0: magic=0xAA55, frame_id=0, chunk_index=0/36, data_len=1422
  Packet 1: magic=0xAA55, frame_id=0, chunk_index=1/36, data_len=1422
  ...
  Packet 35: magic=0xAA55, frame_id=0, chunk_index=35/36, data_len=340
  
  -> 接收器接收到全部 36 个分片后,合并为 50KB JPEG
  -> 解码并显示图像

Frame#1:
  frame_id=1, 重复上述过程

📊 带宽计算

示例配置

  • 分辨率: 640x480
  • JPEG 质量: 70
  • 帧率: 30 FPS
  • 压缩后大小: ~40 KB/帧

带宽需求

总体数据量 = (JPEG_SIZE + Header_Overhead) × FPS
          = (40000 + 50×36) × 30
          ≈ 1.2 MB/s
          ≈ 9.6 Mbps

网络适应性

网络环境 推荐分辨率 推荐帧率 推荐质量
WiFi 5G (100+ Mbps) 1920x1080 30 85
WiFi 2.4G (50 Mbps) 1280x720 20 70
4G (10 Mbps) 640x480 15 50
局域网 (1000 Mbps) 4K 60 95

🔒 安全性考虑

当前缺陷

  • 无加密UDP 明文传输)
  • 无身份验证
  • 无完整性校验CRC16 未启用)

改进建议(后续版本)

  1. 加密: 添加 AES-128-CBC 加密层
  2. 认证: HMAC-SHA256 签名
  3. 校验: 启用 CRC16 检验
  4. 防重放: 添加序列号和时间戳验证

💾 编程实现参考

C++ 解析包头

// 读取包头
PacketHeader header;
socket.read((uint8_t*)&header, sizeof(PacketHeader));

// 验证
if (header.magic != 0xAA55) {
    // 无效包
    return;
}

// 读取载荷
std::vector<uint8_t> payload(header.data_len);
socket.read(payload.data(), header.data_len);

Kotlin 解析包头

val buffer = ByteBuffer.wrap(data)
buffer.order(ByteOrder.LITTLE_ENDIAN)
val header = SkyLinkPacketHeader.fromByteArray(data)

if (header?.isValid() == true) {
    // 处理有效包
}

Python 解析包头(参考)

import struct

# 解析包头
fmt = '<HHHHQQQQHDI'  # little-endian 格式字符串
magic, frame_id, total_chunks, chunk_idx, data_len, \
timestamp, lat, lon, alt, crc16, reserve = \
struct.unpack(fmt, header_bytes[:50])

if magic == 0xAA55:
    # 有效包
    pass