返回教程正文

配套源码

loopback_client.py

app/tests/loopback_client.py
ros_gateway Brick:在 App Lab 中建立可靠的 WebSocket 通道app/tests/loopback_client.py
Python206 行
  1. # SPDX-License-Identifier: MIT
  2. import argparse
  3. import json
  4. import time
  5. from websockets.sync.client import connect
  6. def now_ms():
  7. """
  8. @description : 获取当前 Unix 毫秒时间戳
  9. @param : 无参数
  10. @return : 当前 Unix 毫秒时间戳
  11. """
  12. return time.time_ns() // 1_000_000
  13. def send_message(websocket, message):
  14. """
  15. @description : 发送紧凑 JSON 文本消息
  16. @param websocket : 已建立的 WebSocket 客户端连接
  17. @param message : 待发送消息字典
  18. @return : 无返回值
  19. """
  20. websocket.send(json.dumps(message, separators=(",", ":")))
  21. def receive_json(websocket, timeout=3.0):
  22. """
  23. @description : 接收并解析一条 JSON 文本消息
  24. @param websocket : 已建立的 WebSocket 客户端连接
  25. @param timeout : 接收超时秒数
  26. @return : 解析后的消息字典
  27. """
  28. return json.loads(websocket.recv(timeout=timeout))
  29. def receive_until(websocket, predicate, timeout=4.0):
  30. """
  31. @description : 在限定时间内接收消息直到满足断言函数
  32. @param websocket : 已建立的 WebSocket 客户端连接
  33. @param predicate : 判断目标消息的函数
  34. @param timeout : 总超时秒数
  35. @return : 首条满足条件的消息
  36. """
  37. deadline = time.monotonic() + timeout
  38. while time.monotonic() < deadline:
  39. remaining = max(0.01, deadline - time.monotonic())
  40. message = receive_json(websocket, timeout=remaining)
  41. if predicate(message):
  42. return message
  43. raise TimeoutError("expected WebSocket message was not received")
  44. def run_test(url):
  45. """
  46. @description : 执行端口、握手、心跳、状态和错误路径的回环测试
  47. @param url : WebSocket 服务地址
  48. @return : 测试结果字典
  49. """
  50. results = {
  51. "url": url,
  52. "hello": False,
  53. "mode_change": False,
  54. "base_state": False,
  55. "heartbeat": False,
  56. "overspeed_rejected": False,
  57. "stale_rejected": False,
  58. "invalid_field_rejected": False,
  59. }
  60. with connect(url, open_timeout=3.0, close_timeout=2.0) as websocket:
  61. send_message(
  62. websocket,
  63. {
  64. "version": 1,
  65. "type": "hello",
  66. "role": "ros2",
  67. "node": "loopback_client",
  68. },
  69. )
  70. hello = receive_until(websocket, lambda item: item.get("type") == "hello")
  71. results["hello"] = hello.get("role") == "app"
  72. send_message(
  73. websocket,
  74. {
  75. "version": 1,
  76. "type": "mode_change",
  77. "seq": 1,
  78. "timestamp_ms": now_ms(),
  79. "mode": "ROS_TELEOP",
  80. },
  81. )
  82. acknowledgement = receive_until(
  83. websocket,
  84. lambda item: item.get("type") == "ack" and item.get("seq") == 1,
  85. )
  86. results["mode_change"] = acknowledgement.get("accepted") is True
  87. send_message(
  88. websocket,
  89. {
  90. "version": 1,
  91. "type": "heartbeat",
  92. "seq": 2,
  93. "timestamp_ms": now_ms(),
  94. },
  95. )
  96. send_message(
  97. websocket,
  98. {
  99. "version": 1,
  100. "type": "cmd_vel",
  101. "seq": 3,
  102. "timestamp_ms": now_ms(),
  103. "vx": 0.2,
  104. "vy": -0.1,
  105. "wz": 0.3,
  106. },
  107. )
  108. deadline = time.monotonic() + 2.2
  109. while time.monotonic() < deadline:
  110. message = receive_json(websocket, timeout=max(0.01, deadline - time.monotonic()))
  111. results["base_state"] |= message.get("type") == "base_state"
  112. results["heartbeat"] |= message.get("type") == "heartbeat"
  113. if results["base_state"] and results["heartbeat"]:
  114. break
  115. send_message(
  116. websocket,
  117. {
  118. "version": 1,
  119. "type": "cmd_vel",
  120. "seq": 4,
  121. "timestamp_ms": now_ms(),
  122. "vx": 99.0,
  123. "vy": 0.0,
  124. "wz": 0.0,
  125. },
  126. )
  127. overspeed = receive_until(
  128. websocket,
  129. lambda item: item.get("type") == "error" and item.get("seq") == 4,
  130. )
  131. results["overspeed_rejected"] = overspeed.get("code") == "out_of_range"
  132. send_message(
  133. websocket,
  134. {
  135. "version": 1,
  136. "type": "cmd_vel",
  137. "seq": 5,
  138. "timestamp_ms": now_ms() - 1000,
  139. "vx": 0.1,
  140. "vy": 0.0,
  141. "wz": 0.0,
  142. },
  143. )
  144. stale = receive_until(
  145. websocket,
  146. lambda item: item.get("type") == "error" and item.get("seq") == 5,
  147. )
  148. results["stale_rejected"] = stale.get("code") == "stale_command"
  149. send_message(
  150. websocket,
  151. {
  152. "version": 1,
  153. "type": "cmd_vel",
  154. "seq": 6,
  155. "timestamp_ms": now_ms(),
  156. "vx": "fast",
  157. "vy": 0.0,
  158. "wz": 0.0,
  159. },
  160. )
  161. invalid_field = receive_until(
  162. websocket,
  163. lambda item: item.get("type") == "error" and item.get("seq") == 6,
  164. )
  165. results["invalid_field_rejected"] = invalid_field.get("code") == "invalid_field"
  166. return results
  167. def main():
  168. """
  169. @description : 解析命令行参数、执行测试并设置进程退出码
  170. @param : 无参数
  171. @return : 无返回值
  172. """
  173. parser = argparse.ArgumentParser(description="Test the ROS Gateway WebSocket endpoint")
  174. parser.add_argument("--url", default="ws://127.0.0.1:8765/ros")
  175. arguments = parser.parse_args()
  176. results = run_test(arguments.url)
  177. print(json.dumps(results, ensure_ascii=False, indent=2))
  178. if not all(value for key, value in results.items() if key != "url"):
  179. raise SystemExit(1)
  180. if __name__ == "__main__":
  181. main()