ros_gateway Brick:在 App Lab 中建立可靠的 WebSocket 通道app/tests/loopback_client.py
# SPDX-License-Identifier: MITimport argparseimport jsonimport timefrom websockets.sync.client import connectdef now_ms():"""@description : 获取当前 Unix 毫秒时间戳@param : 无参数@return : 当前 Unix 毫秒时间戳"""return time.time_ns() // 1_000_000def send_message(websocket, message):"""@description : 发送紧凑 JSON 文本消息@param websocket : 已建立的 WebSocket 客户端连接@param message : 待发送消息字典@return : 无返回值"""websocket.send(json.dumps(message, separators=(",", ":")))def receive_json(websocket, timeout=3.0):"""@description : 接收并解析一条 JSON 文本消息@param websocket : 已建立的 WebSocket 客户端连接@param timeout : 接收超时秒数@return : 解析后的消息字典"""return json.loads(websocket.recv(timeout=timeout))def receive_until(websocket, predicate, timeout=4.0):"""@description : 在限定时间内接收消息直到满足断言函数@param websocket : 已建立的 WebSocket 客户端连接@param predicate : 判断目标消息的函数@param timeout : 总超时秒数@return : 首条满足条件的消息"""deadline = time.monotonic() + timeoutwhile time.monotonic() < deadline:remaining = max(0.01, deadline - time.monotonic())message = receive_json(websocket, timeout=remaining)if predicate(message):return messageraise TimeoutError("expected WebSocket message was not received")def run_test(url):"""@description : 执行端口、握手、心跳、状态和错误路径的回环测试@param url : WebSocket 服务地址@return : 测试结果字典"""results = {"url": url,"hello": False,"mode_change": False,"base_state": False,"heartbeat": False,"overspeed_rejected": False,"stale_rejected": False,"invalid_field_rejected": False,}with connect(url, open_timeout=3.0, close_timeout=2.0) as websocket:send_message(websocket,{"version": 1,"type": "hello","role": "ros2","node": "loopback_client",},)hello = receive_until(websocket, lambda item: item.get("type") == "hello")results["hello"] = hello.get("role") == "app"send_message(websocket,{"version": 1,"type": "mode_change","seq": 1,"timestamp_ms": now_ms(),"mode": "ROS_TELEOP",},)acknowledgement = receive_until(websocket,lambda item: item.get("type") == "ack" and item.get("seq") == 1,)results["mode_change"] = acknowledgement.get("accepted") is Truesend_message(websocket,{"version": 1,"type": "heartbeat","seq": 2,"timestamp_ms": now_ms(),},)send_message(websocket,{"version": 1,"type": "cmd_vel","seq": 3,"timestamp_ms": now_ms(),"vx": 0.2,"vy": -0.1,"wz": 0.3,},)deadline = time.monotonic() + 2.2while time.monotonic() < deadline:message = receive_json(websocket, timeout=max(0.01, deadline - time.monotonic()))results["base_state"] |= message.get("type") == "base_state"results["heartbeat"] |= message.get("type") == "heartbeat"if results["base_state"] and results["heartbeat"]:breaksend_message(websocket,{"version": 1,"type": "cmd_vel","seq": 4,"timestamp_ms": now_ms(),"vx": 99.0,"vy": 0.0,"wz": 0.0,},)overspeed = receive_until(websocket,lambda item: item.get("type") == "error" and item.get("seq") == 4,)results["overspeed_rejected"] = overspeed.get("code") == "out_of_range"send_message(websocket,{"version": 1,"type": "cmd_vel","seq": 5,"timestamp_ms": now_ms() - 1000,"vx": 0.1,"vy": 0.0,"wz": 0.0,},)stale = receive_until(websocket,lambda item: item.get("type") == "error" and item.get("seq") == 5,)results["stale_rejected"] = stale.get("code") == "stale_command"send_message(websocket,{"version": 1,"type": "cmd_vel","seq": 6,"timestamp_ms": now_ms(),"vx": "fast","vy": 0.0,"wz": 0.0,},)invalid_field = receive_until(websocket,lambda item: item.get("type") == "error" and item.get("seq") == 6,)results["invalid_field_rejected"] = invalid_field.get("code") == "invalid_field"return resultsdef main():"""@description : 解析命令行参数、执行测试并设置进程退出码@param : 无参数@return : 无返回值"""parser = argparse.ArgumentParser(description="Test the ROS Gateway WebSocket endpoint")parser.add_argument("--url", default="ws://127.0.0.1:8765/ros")arguments = parser.parse_args()results = run_test(arguments.url)print(json.dumps(results, ensure_ascii=False, indent=2))if not all(value for key, value in results.items() if key != "url"):raise SystemExit(1)if __name__ == "__main__":main()