feat(win): T-WIN-003 WebSocket 关键点接收服务

This commit is contained in:
2026-09-14 20:04:54 +08:00
parent 787f1c0cd8
commit 2e539c7fa6
2 changed files with 619 additions and 0 deletions
+295
View File
@@ -0,0 +1,295 @@
extends TestCase
## T-WIN-003:WebSocket 关键点接收服务
##
## 这是真实的 socket 端到端测试:在本机起 PoseServer,
## 用 WebSocketPeer 作为客户端连入,走完整的握手 → 二进制帧 → 控制帧流程。
const PoseServer = preload("res://src/net/pose_server.gd")
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
const TEST_PORT := 18765
const SUBPROTOCOL := "opendance.pose.v1"
var _server: PoseServer = null
var _client: WebSocketPeer = null
var _received: Array = []
var _controls: Array = []
var _connected := 0
func _tree() -> SceneTree:
return Engine.get_main_loop() as SceneTree
## 推进若干帧,让服务端与客户端都有机会 poll
func _pump(frames: int = 1) -> void:
for i in frames:
await _tree().process_frame
func _setup() -> void:
_received.clear()
_controls.clear()
_connected = 0
_server = PoseServer.new()
_server.subprotocol = SUBPROTOCOL
_tree().root.add_child(_server)
_server.frame_received.connect(func(frame): _received.append(frame))
_server.control_received.connect(func(_id, msg): _controls.append(msg))
_server.client_connected.connect(func(_id, _device): _connected += 1)
_server.listen("127.0.0.1", TEST_PORT)
_client = WebSocketPeer.new()
_client.supported_protocols = PackedStringArray([SUBPROTOCOL])
_client.connect_to_url("ws://127.0.0.1:%d/pose" % TEST_PORT)
# 等待握手完成
var guard := 0
while _client.get_ready_state() == WebSocketPeer.STATE_CONNECTING and guard < 240:
_client.poll()
await _pump()
guard += 1
func _teardown() -> void:
if _client != null:
_client.close()
_client = null
if _server != null:
_server.stop()
_server.queue_free()
_server = null
await _pump(2)
func _sample_points(offset: float = 0.0) -> PackedVector3Array:
var points := PackedVector3Array()
points.resize(33)
for i in 33:
points[i] = Vector3(
clampf(0.5 + offset + float(i % 9) * 0.01, 0.0, 1.0),
clampf(0.5 + float(i % 7) * 0.02, 0.0, 1.0),
float((i % 5) - 2) * 0.1
)
return points
func _sample_confidence() -> PackedFloat32Array:
var conf := PackedFloat32Array()
conf.resize(33)
conf.fill(1.0)
return conf
## 把客户端收到的包取出来
func _drain_client() -> Array:
var out: Array = []
while _client.get_available_packet_count() > 0:
var packet := _client.get_packet()
out.append({
"text": packet.get_string_from_utf8() if _client.was_string_packet() else "",
"binary": packet if not _client.was_string_packet() else PackedByteArray(),
"is_text": _client.was_string_packet(),
})
return out
func test_listen_and_handshake() -> void:
await _setup()
is_true(_server.is_running(), "服务端进入监听状态")
eq(_server.port(), TEST_PORT, "监听端口正确")
eq(_client.get_ready_state(), WebSocketPeer.STATE_OPEN, "客户端握手成功(STATE_OPEN)")
await _pump(3)
eq(_connected, 1, "服务端发出 client_connected 信号")
eq(_server.connected_count(), 1, "已握手连接数为 1")
await _teardown()
func test_subprotocol_is_negotiated() -> void:
await _setup()
eq(_client.get_selected_protocol(), SUBPROTOCOL, "子协议协商为 opendance.pose.v1")
await _teardown()
func test_hello_gets_welcome() -> void:
await _setup()
_client.send_text(JSON.stringify({
"type": "hello",
"protocol": SUBPROTOCOL,
"device": "Pixel 8 (测试)",
"appVersion": "0.1.0",
"cameraFacing": "front",
"targetFps": 30,
"resolution": "640x480",
}))
var got_welcome := false
var welcome: Dictionary = {}
for i in 60:
_client.poll()
await _pump()
for packet in _drain_client():
if packet["is_text"]:
var parsed: Variant = JSON.parse_string(packet["text"])
if typeof(parsed) == TYPE_DICTIONARY and str((parsed as Dictionary).get("type", "")) == "welcome":
welcome = parsed
got_welcome = true
if got_welcome:
break
is_true(got_welcome, "收到 welcome 控制帧")
eq(str(welcome.get("protocol", "")), SUBPROTOCOL, "welcome 回显协议版本")
ok(welcome.has("sessionId"), "welcome 含 sessionId")
ok(_controls.size() > 0, "服务端发出 control_received 信号")
eq(str(_controls[0].get("device", "")), "Pixel 8 (测试)", "hello 中的设备名被记录")
await _teardown()
func test_binary_frame_is_received_and_decoded() -> void:
await _setup()
var points := _sample_points()
var conf := _sample_confidence()
_client.send(KeypointFrame.encode(1, points, conf, 0))
var got := false
for i in 60:
_client.poll()
await _pump()
if _received.size() > 0:
got = true
break
is_true(got, "服务端收到并解析出关键点帧")
if got:
var frame: KeypointFrame.PoseFrame = _received[0]
eq(frame.frame_id, 1, "frame_id 解析正确")
eq(frame.points.size(), 33, "33 个关键点")
var max_delta := 0.0
for i in 33:
max_delta = maxf(max_delta, absf(frame.points[i].x - points[i].x))
ok(max_delta < 1e-4, "坐标经网络往返后仍在定点精度内", "最大误差 %.8f" % max_delta)
eq(_server.frames_total, 1, "累计帧计数为 1")
eq(_server.malformed_frames, 0, "无畸形帧")
await _teardown()
func test_malformed_frame_is_rejected_without_crashing() -> void:
await _setup()
# 长度错误
_client.send(PackedByteArray([1, 2, 3]))
# magic 错误但长度正确
var bad := PackedByteArray()
bad.resize(239)
_client.send(bad)
# 正常帧,用于确认连接未被破坏
_client.send(KeypointFrame.encode(1, _sample_points(), _sample_confidence(), 0))
var good_arrived := false
for i in 90:
_client.poll()
await _pump()
if _received.size() > 0:
good_arrived = true
break
eq(_server.malformed_frames, 2, "两个畸形帧被拒绝并计数")
is_true(good_arrived, "畸形帧之后连接仍可正常收帧")
eq(_server.frames_total, 1, "仅有效帧计入累计")
await _teardown()
func test_frame_id_gap_is_counted_as_dropped() -> void:
await _setup()
var conf := _sample_confidence()
_client.send(KeypointFrame.encode(1, _sample_points(), conf))
_client.send(KeypointFrame.encode(2, _sample_points(0.01), conf))
# 跳过 3、4、5
_client.send(KeypointFrame.encode(6, _sample_points(0.02), conf))
var guard := 0
while _received.size() < 3 and guard < 120:
_client.poll()
await _pump()
guard += 1
eq(_received.size(), 3, "收到 3 帧")
eq(_server.frames_dropped, 3, "frame_id 跳号被计为丢包(3 帧)")
eq(_server.frames_total, 3, "累计收到 3 帧")
await _teardown()
func test_upper_body_and_mirror_flags_survive_transport() -> void:
await _setup()
var flags := KeypointFrame.FLAG_MIRRORED | KeypointFrame.FLAG_UPPER_BODY_ONLY
_client.send(KeypointFrame.encode(7, _sample_points(), _sample_confidence(), flags))
var guard := 0
while _received.is_empty() and guard < 90:
_client.poll()
await _pump()
guard += 1
if _received.is_empty():
ok(false, "未收到帧")
else:
var frame: KeypointFrame.PoseFrame = _received[0]
is_true(frame.is_mirrored(), "镜像标志经网络传输保留")
is_true(frame.is_upper_body_only(), "上半身模式标志经网络传输保留")
await _teardown()
func test_config_broadcast_reaches_client() -> void:
await _setup()
await _pump(3)
_server.mirror = true
_server.expected_fps = 24
_server.broadcast_config()
var config: Dictionary = {}
for i in 60:
_client.poll()
await _pump()
for packet in _drain_client():
if packet["is_text"]:
var parsed: Variant = JSON.parse_string(packet["text"])
if typeof(parsed) == TYPE_DICTIONARY and str((parsed as Dictionary).get("type", "")) == "config":
config = parsed
if not config.is_empty():
break
is_false(config.is_empty(), "客户端收到 config 控制帧")
eq(bool(config.get("mirror", false)), true, "config.mirror 正确")
eq(int(config.get("targetFps", 0)), 24, "config.targetFps 正确")
ok(config.has("upperBodyOnly"), "config 含 upperBodyOnly")
ok(config.has("resolution"), "config 含 resolution")
await _teardown()
func test_snapshot_reports_state() -> void:
await _setup()
await _pump(3)
var snap := _server.snapshot()
is_true(bool(snap["running"]), "snapshot 报告运行中")
eq(int(snap["port"]), TEST_PORT, "snapshot 报告端口")
eq(int(snap["connected"]), 1, "snapshot 报告已连接 1 个客户端")
await _teardown()
func test_stop_closes_listener() -> void:
await _setup()
_server.stop()
is_false(_server.is_running(), "stop() 后不再监听")
eq(_server.connected_count(), 0, "stop() 后连接清空")
await _teardown()