237 lines
6.2 KiB
Markdown
237 lines
6.2 KiB
Markdown
# 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++ 解析包头
|
||
|
||
```cpp
|
||
// 读取包头
|
||
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 解析包头
|
||
|
||
```kotlin
|
||
val buffer = ByteBuffer.wrap(data)
|
||
buffer.order(ByteOrder.LITTLE_ENDIAN)
|
||
val header = SkyLinkPacketHeader.fromByteArray(data)
|
||
|
||
if (header?.isValid() == true) {
|
||
// 处理有效包
|
||
}
|
||
```
|
||
|
||
### Python 解析包头(参考)
|
||
|
||
```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
|
||
```
|
||
|
||
---
|
||
|