返回教程正文

配套源码

client.py

app/bricks/zdt_x57s_can/client.py
zdt-x57s-can-test:封装单电机 Brick 并组合验证四台电机app/bricks/zdt_x57s_can/client.py
Python292 行
  1. """
  2. @description : 封装单台ZDT X57S电机到Linux原生CAN网关的可复用Brick API
  3. @param : 无
  4. @return : 无
  5. """
  6. import json
  7. import os
  8. import socket
  9. import threading
  10. import uuid
  11. from arduino.app_utils import brick
  12. PROTOCOL_VERSION = 1
  13. MAX_RESPONSE_BYTES = 65536
  14. MOTION_CONFIRMATION = "RUN_ZDT_X57S_V1_0"
  15. class ZdtX57SCanError(RuntimeError):
  16. """
  17. @description : 表示Brick参数、连接或Linux CAN网关返回的错误
  18. @param message : 错误说明
  19. @return : ZdtX57SCanError实例
  20. """
  21. def __init__(self, message):
  22. """
  23. @description : 初始化Brick异常
  24. @param message : 错误说明
  25. @return : 无
  26. """
  27. super().__init__(message)
  28. def validate_motor_id(motor_id):
  29. """
  30. @description : 校验单台ZDT电机对象绑定的协议地址
  31. @param motor_id : 整数电机地址,范围1至255
  32. @return : 合法电机地址
  33. """
  34. if isinstance(motor_id, bool) or not isinstance(motor_id, int):
  35. raise ZdtX57SCanError("motor_id must be an integer")
  36. if motor_id < 1 or motor_id > 255:
  37. raise ZdtX57SCanError("motor_id must be in range 1-255")
  38. return motor_id
  39. @brick
  40. class ZdtX57SCan:
  41. """
  42. @description : 表示一台使用第二代FW_Emm固定CAN协议的ZDT X57S电机
  43. @param motor_id : 当前对象绑定的电机地址,范围1至255
  44. @param host : Linux原生CAN网关地址; 默认读取环境变量
  45. @param port : Linux原生CAN网关端口; 默认读取环境变量
  46. @param token : 网关鉴权令牌; 默认读取环境变量
  47. @param timeout_s : 单次请求超时时间; 默认读取环境变量
  48. @return : 单台ZdtX57SCan电机对象
  49. """
  50. def __init__(
  51. self,
  52. motor_id,
  53. host=None,
  54. port=None,
  55. token=None,
  56. timeout_s=None,
  57. ):
  58. """
  59. @description : 绑定一台电机地址并初始化网关连接参数
  60. @param motor_id : 当前对象绑定的电机地址,范围1至255
  61. @param host : Linux原生CAN网关地址
  62. @param port : Linux原生CAN网关端口
  63. @param token : 网关鉴权令牌
  64. @param timeout_s : 单次请求超时时间
  65. @return : 无
  66. """
  67. self._motor_id = validate_motor_id(motor_id)
  68. self._host = host or os.getenv(
  69. "ZDT_CAN_GATEWAY_HOST", "msgpack-rpc-router"
  70. )
  71. self._port = int(port or os.getenv("ZDT_CAN_GATEWAY_PORT", "8766"))
  72. self._token = token or os.getenv("ZDT_CAN_GATEWAY_TOKEN", "")
  73. self._timeout_s = float(
  74. timeout_s or os.getenv("ZDT_CAN_REQUEST_TIMEOUT_S", "1.5")
  75. )
  76. self._lock = threading.Lock()
  77. if not self._token or self._token == "replace-me":
  78. raise ZdtX57SCanError("ZDT CAN gateway token is not configured")
  79. if self._port < 1 or self._port > 65535:
  80. raise ZdtX57SCanError("gateway port must be in range 1-65535")
  81. if self._timeout_s <= 0:
  82. raise ZdtX57SCanError("request timeout must be greater than zero")
  83. @property
  84. def motor_id(self):
  85. """
  86. @description : 获取当前对象绑定的电机地址
  87. @param : 无参数
  88. @return : 范围1至255的电机地址
  89. """
  90. return self._motor_id
  91. def status(self):
  92. """
  93. @description : 查询Linux原生CAN网关状态且不发送CAN帧
  94. @param : 无参数
  95. @return : 包含本对象地址和网关运行状态的字典
  96. """
  97. result = dict(self._call("status", {}))
  98. result["motor_id"] = self._motor_id
  99. return result
  100. def read_speed(self):
  101. """
  102. @description : 读取当前对象绑定电机的实时转速
  103. @param : 无参数
  104. @return : 带符号实时转速,单位整数RPM
  105. """
  106. result = self._call("read_speed", {"motor_id": self._motor_id})
  107. if result.get("motor_id") != self._motor_id:
  108. raise ZdtX57SCanError("CAN gateway returned a different motor_id")
  109. speed_rpm = result.get("speed_rpm")
  110. if isinstance(speed_rpm, bool) or not isinstance(speed_rpm, int):
  111. raise ZdtX57SCanError("CAN gateway returned an invalid speed_rpm")
  112. return speed_rpm
  113. def probe(self):
  114. """
  115. @description : 读取当前单台电机速度并返回便于诊断的结构化结果
  116. @param : 无参数
  117. @return : 包含motor_id和speed_rpm的字典
  118. """
  119. return {"motor_id": self._motor_id, "speed_rpm": self.read_speed()}
  120. def enable(self, confirmation):
  121. """
  122. @description : 在显式确认后使能当前单台电机
  123. @param confirmation : 固定运动确认口令
  124. @return : 网关执行结果
  125. """
  126. return self._motion_call("enable", {}, confirmation)
  127. def disable(self):
  128. """
  129. @description : 失能当前单台电机
  130. @param : 无参数
  131. @return : 网关执行结果
  132. """
  133. return self._call("disable", {"motor_id": self._motor_id})
  134. def set_speed(self, rpm, acceleration_level, confirmation):
  135. """
  136. @description : 在显式确认后设置当前单台电机转速
  137. @param rpm : 带符号目标转速
  138. @param acceleration_level: 加减速档位0至255
  139. @param confirmation : 固定运动确认口令
  140. @return : 网关执行结果
  141. """
  142. return self._motion_call(
  143. "set_speed",
  144. {
  145. "rpm": int(rpm),
  146. "acceleration_level": int(acceleration_level),
  147. },
  148. confirmation,
  149. )
  150. def stop(self):
  151. """
  152. @description : 向当前单台电机依次发送零速、停止和失能安全命令
  153. @param : 无参数
  154. @return : 包含各安全命令结果的字典
  155. """
  156. return self._call("stop", {"motor_id": self._motor_id})
  157. def timed_speed_test(
  158. self,
  159. rpm,
  160. acceleration_level,
  161. duration_s,
  162. confirmation,
  163. ):
  164. """
  165. @description : 对当前单台电机执行限时测试并在finally阶段安全停车
  166. @param rpm : 带符号目标转速
  167. @param acceleration_level: 加减速档位0至255
  168. @param duration_s : 测试持续时间0.2至5秒
  169. @param confirmation : 固定运动确认口令
  170. @return : 采样速度和停车结果
  171. """
  172. return self._motion_call(
  173. "timed_speed_test",
  174. {
  175. "rpm": int(rpm),
  176. "acceleration_level": int(acceleration_level),
  177. "duration_s": float(duration_s),
  178. },
  179. confirmation,
  180. timeout_s=max(self._timeout_s, float(duration_s) + 4.0),
  181. )
  182. def _motion_call(self, method, params, confirmation, timeout_s=None):
  183. """
  184. @description : 绑定对象地址并校验运动确认后调用网关控制方法
  185. @param method : 网关方法名称
  186. @param params : 不含motor_id的网关方法参数
  187. @param confirmation : 固定运动确认口令
  188. @param timeout_s : 可选的本次请求超时时间
  189. @return : 网关结果字典
  190. """
  191. if confirmation != MOTION_CONFIRMATION:
  192. raise ZdtX57SCanError(
  193. "motion confirmation must be " + MOTION_CONFIRMATION
  194. )
  195. request_params = dict(params)
  196. request_params["motor_id"] = self._motor_id
  197. request_params["confirmation"] = confirmation
  198. return self._call(method, request_params, timeout_s=timeout_s)
  199. def _call(self, method, params, timeout_s=None):
  200. """
  201. @description : 通过换行分隔JSON请求调用Linux原生CAN网关
  202. @param method : 网关方法名称
  203. @param params : 网关方法参数字典
  204. @param timeout_s : 可选的本次请求超时时间
  205. @return : 网关成功结果
  206. """
  207. request_id = uuid.uuid4().hex
  208. request = {
  209. "version": PROTOCOL_VERSION,
  210. "request_id": request_id,
  211. "token": self._token,
  212. "method": str(method),
  213. "params": dict(params),
  214. }
  215. payload = (
  216. json.dumps(request, separators=(",", ":"), ensure_ascii=False)
  217. + "\n"
  218. ).encode("utf-8")
  219. effective_timeout = float(timeout_s or self._timeout_s)
  220. try:
  221. with self._lock:
  222. with socket.create_connection(
  223. (self._host, self._port), timeout=effective_timeout
  224. ) as connection:
  225. connection.settimeout(effective_timeout)
  226. connection.sendall(payload)
  227. response_payload = self._receive_line(connection)
  228. except (OSError, TimeoutError) as error:
  229. raise ZdtX57SCanError(
  230. f"CAN gateway connection failed: {error}"
  231. ) from error
  232. try:
  233. response = json.loads(response_payload.decode("utf-8"))
  234. except (UnicodeDecodeError, json.JSONDecodeError) as error:
  235. raise ZdtX57SCanError("CAN gateway returned invalid JSON") from error
  236. if response.get("version") != PROTOCOL_VERSION:
  237. raise ZdtX57SCanError("CAN gateway protocol version mismatch")
  238. if response.get("request_id") != request_id:
  239. raise ZdtX57SCanError("CAN gateway request_id mismatch")
  240. if response.get("ok") is not True:
  241. error_info = response.get("error") or {}
  242. error_code = error_info.get("code", "gateway_error")
  243. error_message = error_info.get("message", "unknown gateway error")
  244. raise ZdtX57SCanError(f"{error_code}: {error_message}")
  245. return response.get("result", {})
  246. def _receive_line(self, connection):
  247. """
  248. @description : 从TCP连接读取一条有长度上限的换行分隔JSON响应
  249. @param connection : 已连接的socket对象
  250. @return : 不含换行符的响应字节
  251. """
  252. chunks = bytearray()
  253. while len(chunks) <= MAX_RESPONSE_BYTES:
  254. chunk = connection.recv(4096)
  255. if not chunk:
  256. break
  257. chunks.extend(chunk)
  258. newline_index = chunks.find(b"\n")
  259. if newline_index >= 0:
  260. return bytes(chunks[:newline_index])
  261. if len(chunks) > MAX_RESPONSE_BYTES:
  262. raise ZdtX57SCanError("CAN gateway response is too large")
  263. raise ZdtX57SCanError("CAN gateway closed without a complete response")