返回教程正文

配套源码

loopback_client.py

app/tests/loopback_client.py
ros-gateway-loopback:用 Custom Brick 打通 App Lab 与 ROS 2app/tests/loopback_client.py
Python205 行
# SPDX-License-Identifier: MIT

import argparse
import json
import time

from websockets.sync.client import connect


def now_ms():
    """
    @description         : 获取当前 Unix 毫秒时间戳
    @param               : 无参数
    @return              : 当前 Unix 毫秒时间戳
    """
    return time.time_ns() // 1_000_000


def 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() + timeout
    while time.monotonic() < deadline:
        remaining = max(0.01, deadline - time.monotonic())
        message = receive_json(websocket, timeout=remaining)
        if predicate(message):
            return message
    raise 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 True

        send_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.2
        while 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"]:
                break

        send_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 results


def 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()