Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

317 řádky
10 KiB

  1. #!/usr/bin/env python3
  2. """P19 XDM-60T4-E Y1..Y4 -> X0..X3 physical loopback tests."""
  3. from __future__ import annotations
  4. import argparse
  5. import struct
  6. import time
  7. import serial
  8. from plsr_modbus_control_test import (
  9. CALL_COMMIT,
  10. CALL_START,
  11. CMD_STOP_DECEL,
  12. CONTROL_BASE,
  13. CONTROL_WINDOW_WORDS,
  14. RESULT_OK,
  15. RESULT_QUEUED,
  16. S0_BASE,
  17. S1_BASE,
  18. check_result,
  19. read_axis_status,
  20. send_call,
  21. send_command,
  22. )
  23. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  24. LOOPBACK_BASE = 1540
  25. LOOPBACK_SIGNATURE = 0x5019
  26. STATE_IDLE = 1
  27. STATE_ACCEL = 2
  28. STATE_RUN = 3
  29. STATE_WAIT = 5
  30. STATE_COMPLETED = 7
  31. STATE_STOPPED = 8
  32. CMD_RESET_ERROR = 10
  33. ERROR_LIMIT_POSITIVE = 6
  34. ERROR_LIMIT_NEGATIVE = 7
  35. STOP_LIMIT_POSITIVE = 5
  36. STOP_LIMIT_NEGATIVE = 6
  37. def read_bits(client: RtuClient, address: int, count: int) -> list[int]:
  38. byte_count = (count + 7) // 8
  39. pdu = bytes((0x02,)) + struct.pack(">HH", address, count)
  40. response = client.exchange(pdu, 5 + byte_count)
  41. if response[1] != 0x02 or response[2] != byte_count:
  42. raise RuntimeError(f"FC02 响应格式错误:{response.hex(' ')}")
  43. return [
  44. (response[3 + index // 8] >> (index % 8)) & 1
  45. for index in range(count)
  46. ]
  47. class LoopbackLease:
  48. def __init__(self, client: RtuClient) -> None:
  49. self.client = client
  50. self.sequence = int(time.time() * 1000) & 0xFFFF
  51. self.mask = 0
  52. self.last_write = 0.0
  53. def set(self, mask: int) -> None:
  54. self.mask = mask & 0x0F
  55. self.sequence = (self.sequence + 1) & 0xFFFF
  56. self.client.write_multiple(LOOPBACK_BASE, [self.mask, self.sequence])
  57. self.last_write = time.monotonic()
  58. def heartbeat(self) -> None:
  59. if time.monotonic() - self.last_write >= 0.20:
  60. self.set(self.mask)
  61. def diagnostics(self) -> dict[str, int]:
  62. words = self.client.read_holding(LOOPBACK_BASE + 2, 5)
  63. return {
  64. "applied": words[0],
  65. "raw": words[1],
  66. "logical": words[2],
  67. "signature": words[3],
  68. "flags": words[4],
  69. }
  70. def wait_until(
  71. lease: LoopbackLease,
  72. predicate,
  73. timeout: float,
  74. label: str,
  75. ) -> dict[str, int]:
  76. deadline = time.monotonic() + timeout
  77. latest: dict[str, int] | None = None
  78. while time.monotonic() < deadline:
  79. lease.heartbeat()
  80. latest = read_axis_status(lease.client)
  81. if predicate(latest):
  82. return latest
  83. raise RuntimeError(f"{label}超时,最后状态={latest}")
  84. def prepare_job(
  85. client: RtuClient,
  86. pulses: int,
  87. wait_condition: int = 0,
  88. wait_source: int = 0,
  89. wait_point: int = 0,
  90. ) -> None:
  91. words = [0] * 20
  92. words[0:2] = signed_dword_words(1)
  93. words[10:12] = signed_dword_words(2000)
  94. words[12:14] = signed_dword_words(pulses)
  95. words[14] = ((wait_condition & 0xFF) << 8) | (wait_source & 0xFF)
  96. words[15:17] = signed_dword_words(wait_point)
  97. words[17] = 0
  98. words[18:20] = signed_dword_words(0)
  99. client.write_multiple(S0_BASE, words)
  100. client.write_multiple(S1_BASE, [0, 0, 0, 0])
  101. def start_job(client: RtuClient, sequence: int) -> int:
  102. response = send_call(client, sequence, CALL_COMMIT)
  103. check_result(response, RESULT_OK, "COMMIT")
  104. response = send_call(client, sequence + 1, CALL_START)
  105. check_result(response, RESULT_QUEUED, "START")
  106. return sequence + 1
  107. def reset_limit_error(lease: LoopbackLease, sequence: int) -> None:
  108. lease.set(0)
  109. time.sleep(0.050)
  110. response = send_command(lease.client, sequence, CMD_RESET_ERROR)
  111. check_result(response, RESULT_QUEUED, "RESET_ERROR")
  112. status = wait_until(
  113. lease,
  114. lambda value: value["state"] == STATE_IDLE
  115. and value["last_sequence"] == sequence,
  116. 2.0,
  117. "RESET_ERROR",
  118. )
  119. if status["error"] != 0:
  120. raise RuntimeError(f"RESET_ERROR 后错误未清除:{status}")
  121. def test_electrical(lease: LoopbackLease) -> None:
  122. patterns = (0x0, 0x1, 0x2, 0x4, 0x8, 0x3, 0xC, 0xF, 0x0)
  123. for pattern in patterns:
  124. lease.set(pattern)
  125. time.sleep(0.050)
  126. diagnostics = lease.diagnostics()
  127. x_bits = read_bits(lease.client, 0, 4)
  128. observed = sum(bit << index for index, bit in enumerate(x_bits))
  129. if diagnostics["signature"] != LOOPBACK_SIGNATURE:
  130. raise RuntimeError(f"不是 P19 固件:{diagnostics}")
  131. if (diagnostics["flags"] & 0x0013) != 0x0013:
  132. raise RuntimeError(f"P19 输入/租约未就绪:{diagnostics}")
  133. if diagnostics["applied"] != pattern or observed != pattern:
  134. raise RuntimeError(
  135. f"回环不一致:写Y掩码=0x{pattern:X}, FC02 X=0x{observed:X}, "
  136. f"诊断={diagnostics}。请检查对应 Y→X 和 COM 接线。"
  137. )
  138. print(
  139. f"Y1~Y4=0x{pattern:X} -> X0~X3=0x{observed:X} PASS "
  140. f"(MCU raw=0x{diagnostics['raw'] & 0xF:X})"
  141. )
  142. print("电气回环 PASS:Y1→X0、Y2→X1、Y3→X2、Y4→X3")
  143. def test_wait(lease: LoopbackLease, sequence: int) -> None:
  144. lease.set(0)
  145. prepare_job(lease.client, 500, wait_condition=2, wait_source=4, wait_point=3)
  146. start_sequence = start_job(lease.client, sequence)
  147. waiting = wait_until(
  148. lease,
  149. lambda value: value["state"] == STATE_WAIT,
  150. 3.0,
  151. "进入 X3 WAIT",
  152. )
  153. if waiting["task_pulses"] != 500:
  154. raise RuntimeError(f"WAIT 前脉冲数不是500:{waiting}")
  155. lease.set(0x8)
  156. completed = wait_until(
  157. lease,
  158. lambda value: value["state"] == STATE_COMPLETED,
  159. 2.0,
  160. "X3 释放 WAIT",
  161. )
  162. if completed["task_pulses"] != 500 or completed["error"] != 0:
  163. raise RuntimeError(f"WAIT 完成状态异常:{completed}")
  164. lease.set(0)
  165. print(f"X3/WAIT PASS:500脉冲后等待,Y4→X3释放,序号={start_sequence}")
  166. def test_ext(lease: LoopbackLease, sequence: int) -> None:
  167. lease.set(0)
  168. prepare_job(lease.client, 50000, wait_condition=4, wait_source=4, wait_point=2)
  169. start_job(lease.client, sequence)
  170. running = wait_until(
  171. lease,
  172. lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
  173. and abs(value["task_pulses"]) >= 200,
  174. 3.0,
  175. "EXT 触发前运行",
  176. )
  177. lease.set(0x4)
  178. completed = wait_until(
  179. lease,
  180. lambda value: value["state"] == STATE_COMPLETED,
  181. 2.0,
  182. "X2 EXT 上升沿切段",
  183. )
  184. lease.set(0)
  185. if not (running["task_pulses"] <= completed["task_pulses"] < 50000):
  186. raise RuntimeError(f"EXT 未提前结束任务:{completed}")
  187. if completed["error"] != 0:
  188. raise RuntimeError(f"EXT 完成存在错误:{completed}")
  189. print(f"X2/EXT PASS:Y3→X2 上升沿在 {completed['task_pulses']} 脉冲提前结束")
  190. def test_limit(
  191. lease: LoopbackLease,
  192. sequence: int,
  193. positive: bool,
  194. ) -> None:
  195. lease.set(0)
  196. pulses = 50000 if positive else -50000
  197. bit_mask = 0x1 if positive else 0x2
  198. expected_error = ERROR_LIMIT_POSITIVE if positive else ERROR_LIMIT_NEGATIVE
  199. expected_reason = STOP_LIMIT_POSITIVE if positive else STOP_LIMIT_NEGATIVE
  200. flag_mask = 1 << (9 if positive else 10)
  201. label = "正硬限位 X0" if positive else "负硬限位 X1"
  202. prepare_job(lease.client, pulses)
  203. start_job(lease.client, sequence)
  204. wait_until(
  205. lease,
  206. lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
  207. and abs(value["task_pulses"]) >= 200,
  208. 3.0,
  209. f"{label}触发前运行",
  210. )
  211. lease.set(bit_mask)
  212. stopped = wait_until(
  213. lease,
  214. lambda value: value["state"] == STATE_STOPPED,
  215. 3.0,
  216. f"{label}停止",
  217. )
  218. if (
  219. stopped["error"] != expected_error
  220. or stopped["stop_reason"] != expected_reason
  221. or (stopped["flags"] & flag_mask) == 0
  222. or abs(stopped["task_pulses"]) >= 50000
  223. ):
  224. raise RuntimeError(f"{label}状态不符合预期:{stopped}")
  225. reset_limit_error(lease, sequence + 100)
  226. print(
  227. f"{label} PASS:提前停止于 {stopped['task_pulses']} 脉冲,"
  228. f"error={expected_error}, stop_reason={expected_reason}"
  229. )
  230. def main() -> int:
  231. parser = argparse.ArgumentParser(
  232. description="P19 XDM-60T4-E Y1~Y4→X0~X3真实输入、WAIT/EXT/硬限位测试"
  233. )
  234. parser.add_argument("--port", default="COM5")
  235. parser.add_argument("--baud", type=int, default=9600)
  236. parser.add_argument("--slave", type=int, default=1)
  237. parser.add_argument(
  238. "--case",
  239. choices=("electrical", "wait", "ext", "positive-limit", "negative-limit", "all"),
  240. default="electrical",
  241. )
  242. args = parser.parse_args()
  243. with serial.Serial(
  244. port=choose_port(args.port),
  245. baudrate=args.baud,
  246. bytesize=serial.EIGHTBITS,
  247. parity=serial.PARITY_EVEN,
  248. stopbits=serial.STOPBITS_ONE,
  249. timeout=1.0,
  250. write_timeout=1.0,
  251. ) as uart:
  252. client = RtuClient(uart, args.slave)
  253. header = client.read_holding(CONTROL_BASE, 8)
  254. if header[3] != CONTROL_WINDOW_WORDS:
  255. raise RuntimeError(f"控制窗口不匹配:{header}")
  256. lease = LoopbackLease(client)
  257. sequence = int(time.time()) & 0x7FFFFFFF
  258. try:
  259. cases = (
  260. ("electrical", lambda: test_electrical(lease)),
  261. ("wait", lambda: test_wait(lease, sequence + 10)),
  262. ("ext", lambda: test_ext(lease, sequence + 20)),
  263. ("positive-limit", lambda: test_limit(lease, sequence + 30, True)),
  264. ("negative-limit", lambda: test_limit(lease, sequence + 40, False)),
  265. )
  266. for case_name, operation in cases:
  267. if args.case in {case_name, "all"}:
  268. print(f"\n开始 P19 用例:{case_name}")
  269. operation()
  270. print("\n全部所选 P19 自动检查 PASS")
  271. finally:
  272. try:
  273. lease.set(0)
  274. except (RuntimeError, serial.SerialException):
  275. pass
  276. return 0
  277. if __name__ == "__main__":
  278. try:
  279. raise SystemExit(main())
  280. except (RuntimeError, serial.SerialException) as error:
  281. print(f"测试失败:{error}")
  282. raise SystemExit(1)