feat: add ue_scene websocket integration
This commit is contained in:
352
docs/ue-ndisplay-websocket-protocol.md
Normal file
352
docs/ue-ndisplay-websocket-protocol.md
Normal file
@@ -0,0 +1,352 @@
|
||||
# UE nDisplay WebSocket 协议草案
|
||||
|
||||
## 目的
|
||||
|
||||
本协议定义后端到 UE `nDisplay` 集群运行时之间的场景同步契约。
|
||||
|
||||
设计目标:
|
||||
|
||||
- 由 UE 主节点单点接入后端
|
||||
- 使用全量快照重建完整场景
|
||||
- 使用增量更新降低同步成本
|
||||
- 支持控制帧和显示配置
|
||||
- 支持序列号、重放和重同步
|
||||
|
||||
## 适用范围
|
||||
|
||||
该协议建立在通用 WebSocket 协议之上,参考 [websocket_protocol.md](/D:/work/planet/planet/websocket_protocol.md:1)。
|
||||
|
||||
新增的核心内容是 `ue_scene` 频道。
|
||||
|
||||
## 连接模型
|
||||
|
||||
### 客户端
|
||||
|
||||
- UE 主节点连接 `ws://<backend-host>:8000/ws?token=<access_token>`
|
||||
- UE 渲染节点不直接连接后端场景通道
|
||||
|
||||
### 订阅
|
||||
|
||||
UE 主节点建议订阅:
|
||||
|
||||
- `ue_scene`
|
||||
- `alerts`
|
||||
- `dashboard`
|
||||
|
||||
## 协议规则
|
||||
|
||||
- 每次场景更新都带单调递增的 `sequence`
|
||||
- 增量更新必须包含 `base_sequence`
|
||||
- UE 如果发现序列断裂,应先发 `sync_request`
|
||||
- 服务端优先返回可重放的增量历史;无法重放时返回全量快照
|
||||
- 后端始终发送地理坐标,UE 本地负责转成球体世界坐标
|
||||
- 控制消息与场景消息分离
|
||||
|
||||
## 消息类型
|
||||
|
||||
### `connection_established`
|
||||
|
||||
服务端 -> 客户端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "connection_established",
|
||||
"timestamp": "2026-04-17T02:30:00.000Z",
|
||||
"data": {
|
||||
"connection_id": "conn_primary_node_01",
|
||||
"server_version": "0.19.0",
|
||||
"heartbeat_interval": 30,
|
||||
"supported_channels": [
|
||||
"ue_scene",
|
||||
"alerts",
|
||||
"dashboard",
|
||||
"datasource_tasks"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `subscribe`
|
||||
|
||||
客户端 -> 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "subscribe",
|
||||
"timestamp": "2026-04-17T02:30:01.000Z",
|
||||
"data": {
|
||||
"channels": ["ue_scene", "alerts"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `sync_request`
|
||||
|
||||
客户端 -> 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "sync_request",
|
||||
"timestamp": "2026-04-17T02:30:05.000Z",
|
||||
"data": {
|
||||
"channel": "ue_scene",
|
||||
"reason": "initial_connect",
|
||||
"last_sequence": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`reason` 常见值:
|
||||
|
||||
- `initial_connect`
|
||||
- `sequence_gap`
|
||||
- `manual_resync`
|
||||
- `profile_changed`
|
||||
|
||||
### `data_frame` 全量快照
|
||||
|
||||
服务端 -> 客户端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "data_frame",
|
||||
"channel": "ue_scene",
|
||||
"timestamp": "2026-04-17T02:30:06.000Z",
|
||||
"data": {
|
||||
"update_type": "full",
|
||||
"sequence": 12,
|
||||
"cluster_time": "2026-04-17T02:30:06.000Z",
|
||||
"display_profile": {
|
||||
"profile_id": "polarized-wall-a",
|
||||
"stereo_mode": "polarized",
|
||||
"screen_width_m": 3.0,
|
||||
"screen_height_m": 2.0,
|
||||
"target_refresh_hz": 120
|
||||
},
|
||||
"camera_state": {
|
||||
"mode": "auto_cruise",
|
||||
"path_id": "global_overview",
|
||||
"fov": 42.0
|
||||
},
|
||||
"payload": {
|
||||
"meta": {
|
||||
"generated_at": "2026-04-17T02:30:06.000Z",
|
||||
"total_records": 20800,
|
||||
"state_hash": 123456789
|
||||
},
|
||||
"layers": {
|
||||
"satellites": {
|
||||
"revision": 111,
|
||||
"items": []
|
||||
},
|
||||
"supercomputers": {
|
||||
"revision": 222,
|
||||
"items": []
|
||||
},
|
||||
"gpu_clusters": {
|
||||
"revision": 333,
|
||||
"items": []
|
||||
},
|
||||
"submarine_cables": {
|
||||
"revision": 444,
|
||||
"items": []
|
||||
},
|
||||
"landing_points": {
|
||||
"revision": 555,
|
||||
"items": []
|
||||
},
|
||||
"alerts": {
|
||||
"revision": 0,
|
||||
"items": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `data_frame` 增量更新
|
||||
|
||||
服务端 -> 客户端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "data_frame",
|
||||
"channel": "ue_scene",
|
||||
"timestamp": "2026-04-17T02:31:00.000Z",
|
||||
"data": {
|
||||
"update_type": "incremental",
|
||||
"sequence": 13,
|
||||
"base_sequence": 12,
|
||||
"cluster_time": "2026-04-17T02:31:00.000Z",
|
||||
"meta": {
|
||||
"generated_at": "2026-04-17T02:31:00.000Z",
|
||||
"total_records": 20805,
|
||||
"state_hash": 123456790
|
||||
},
|
||||
"changes": {
|
||||
"gpu_clusters": {
|
||||
"added": [],
|
||||
"updated": [],
|
||||
"removed": []
|
||||
},
|
||||
"submarine_cables": {
|
||||
"added": [],
|
||||
"updated": [],
|
||||
"removed": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `control_frame`
|
||||
|
||||
客户端 -> 服务端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "control_frame",
|
||||
"timestamp": "2026-04-17T02:32:00.000Z",
|
||||
"data": {
|
||||
"target": "ue_scene",
|
||||
"command": "set_camera_mode",
|
||||
"arguments": {
|
||||
"mode": "manual",
|
||||
"camera_id": "operator_cam_1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
常见命令:
|
||||
|
||||
- `set_camera_mode`
|
||||
- `set_camera_path`
|
||||
- `focus_entity`
|
||||
- `set_layer_visibility`
|
||||
- `set_alert_filter`
|
||||
- `set_display_profile`
|
||||
- `pause_auto_cruise`
|
||||
- `resume_auto_cruise`
|
||||
|
||||
### `control_acknowledged`
|
||||
|
||||
服务端 -> 客户端
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "control_acknowledged",
|
||||
"timestamp": "2026-04-17T02:32:00.050Z",
|
||||
"data": {
|
||||
"target": "ue_scene",
|
||||
"command": "set_camera_mode",
|
||||
"accepted": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `heartbeat`
|
||||
|
||||
双向
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "heartbeat",
|
||||
"timestamp": "2026-04-17T02:32:30.000Z",
|
||||
"data": {
|
||||
"action": "ping"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
服务端返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "heartbeat",
|
||||
"timestamp": "2026-04-17T02:32:30.020Z",
|
||||
"data": {
|
||||
"action": "pong"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 场景实体结构
|
||||
|
||||
每个场景实体建议包含:
|
||||
|
||||
- `id`
|
||||
- `entity_type`
|
||||
- `revision`
|
||||
- `geo`
|
||||
- `visual`
|
||||
- `metrics`
|
||||
- `labels`
|
||||
- `status`
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "gpu_clusters:123",
|
||||
"entity_type": "gpu_cluster",
|
||||
"revision": 987654321,
|
||||
"geo": {
|
||||
"lat": 35.9327,
|
||||
"lng": -84.3107,
|
||||
"alt": 0.0
|
||||
},
|
||||
"visual": {
|
||||
"style": "pulse_marker",
|
||||
"size": 1.0,
|
||||
"color": "#FF8C42"
|
||||
},
|
||||
"metrics": {
|
||||
"name": "Frontier"
|
||||
},
|
||||
"labels": {
|
||||
"title": "Frontier",
|
||||
"subtitle": "Oak Ridge, US"
|
||||
},
|
||||
"status": {
|
||||
"health": "normal",
|
||||
"alert_level": "none"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 同步策略
|
||||
|
||||
UE 主节点建议这样处理:
|
||||
|
||||
1. 保存本地最后一次成功应用的 `sequence`
|
||||
2. 收到增量帧时检查 `base_sequence`
|
||||
3. 若 `base_sequence` 不等于本地序列,则立即发 `sync_request`
|
||||
4. 如果服务端能重放缺失增量,则按顺序依次应用
|
||||
5. 如果服务端无法重放,则接收并应用新的全量快照
|
||||
6. 场景更新应在主节点原子提交,再交给集群同步显示
|
||||
|
||||
## 服务端重同步规则
|
||||
|
||||
服务端建议行为:
|
||||
|
||||
- `initial_connect`:直接返回全量快照
|
||||
- `manual_resync`:直接返回全量快照
|
||||
- `profile_changed`:直接返回全量快照
|
||||
- `sequence_gap`:优先尝试从历史缓存中回放增量
|
||||
- 如果历史缓存不完整:回退到全量快照
|
||||
- 如果客户端已是最新序列:返回空变化增量或保持静默
|
||||
|
||||
## 集群职责约束
|
||||
|
||||
- 只有 UE 主节点接收后端场景同步消息
|
||||
- 渲染节点不直接重放后端消息
|
||||
- 服务端表达的是场景意图,而不是每个节点的底层渲染状态
|
||||
|
||||
## 推荐的后端扩展
|
||||
|
||||
- 为 `ue_scene` 增加更多真实可视化层
|
||||
- 将告警/BGP 事件并入统一场景层
|
||||
- 为控制命令增加真正的状态持久化与回执
|
||||
- 为主节点断线重连补状态恢复策略
|
||||
Reference in New Issue
Block a user