You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

271 line
9.7 KiB

  1. #!/usr/bin/env python3
  2. """PLSR P13 Modbus COMMIT/START/command/status integration test."""
  3. from __future__ import annotations
  4. import argparse
  5. import time
  6. import serial
  7. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  8. CONTROL_BASE = 1200
  9. S0_BASE = 1600
  10. S1_BASE = 1700
  11. CALL_REQUEST = CONTROL_BASE + 8
  12. CALL_RESPONSE = CONTROL_BASE + 24
  13. COMMAND_REQUEST = CONTROL_BASE + 40
  14. COMMAND_RESPONSE = CONTROL_BASE + 48
  15. AXIS0_STATUS = CONTROL_BASE + 64
  16. RESULT_OK = 0
  17. RESULT_QUEUED = 1
  18. RESULT_BUSY = 8
  19. STATE_ACCEL = 2
  20. STATE_RUN = 3
  21. STATE_DECEL = 4
  22. STATE_PAUSED = 6
  23. STATE_STOPPED = 8
  24. CALL_COMMIT = 1
  25. CALL_START = 2
  26. CMD_STOP_DECEL = 1
  27. CMD_PAUSE = 3
  28. CMD_RESUME = 4
  29. def put_u32(words: list[int], offset: int, value: int) -> None:
  30. words[offset : offset + 2] = signed_dword_words(value)
  31. def put_u64(words: list[int], offset: int, value: int) -> None:
  32. raw = value & 0xFFFFFFFFFFFFFFFF
  33. words[offset : offset + 4] = [(raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48)]
  34. def get_u32(words: list[int], offset: int) -> int:
  35. return words[offset] | (words[offset + 1] << 16)
  36. def get_u64(words: list[int], offset: int, signed: bool = False) -> int:
  37. raw = sum(words[offset + index] << (index * 16) for index in range(4))
  38. if signed and raw & (1 << 63):
  39. return raw - (1 << 64)
  40. return raw
  41. def wait_call_response(client: RtuClient, sequence: int, timeout: float = 2.0) -> list[int]:
  42. deadline = time.monotonic() + timeout
  43. while time.monotonic() < deadline:
  44. response = client.read_holding(CALL_RESPONSE, 12)
  45. if get_u32(response, 0) == sequence:
  46. return response
  47. raise RuntimeError(f"等待调用应答序号 {sequence} 超时")
  48. def send_call(client: RtuClient, sequence: int, operation: int) -> list[int]:
  49. request = [0] * 16
  50. put_u32(request, 0, sequence)
  51. request[2] = 0 # S0 device D
  52. put_u32(request, 3, S0_BASE)
  53. request[5] = 0 # S1 device D
  54. put_u32(request, 6, S1_BASE)
  55. request[8] = 0 # S2 constant
  56. request[9] = 0
  57. put_u32(request, 10, 1) # K1
  58. request[12] = 0 # axis Y0/Q0
  59. request[13] = 0 # PULSE/DIR
  60. request[14] = operation
  61. client.write_multiple(CALL_REQUEST, request)
  62. return wait_call_response(client, sequence)
  63. def wait_command_response(client: RtuClient, sequence: int, timeout: float = 2.0) -> list[int]:
  64. deadline = time.monotonic() + timeout
  65. while time.monotonic() < deadline:
  66. response = client.read_holding(COMMAND_RESPONSE, 8)
  67. if get_u32(response, 0) == sequence:
  68. return response
  69. raise RuntimeError(f"等待命令应答序号 {sequence} 超时")
  70. def send_command(
  71. client: RtuClient, sequence: int, opcode: int, argument: int = 0
  72. ) -> list[int]:
  73. request = [0] * 8
  74. put_u32(request, 0, sequence)
  75. request[2] = opcode
  76. request[3] = 0
  77. put_u64(request, 4, argument)
  78. client.write_multiple(COMMAND_REQUEST, request)
  79. return wait_command_response(client, sequence)
  80. def read_axis_status(client: RtuClient) -> dict[str, int]:
  81. words = client.read_holding(AXIS0_STATUS, 48)
  82. generation_begin = get_u32(words, 0)
  83. generation_end = get_u32(words, 46)
  84. if generation_begin != generation_end or generation_begin & 1:
  85. raise RuntimeError(
  86. f"状态快照版本不一致:begin={generation_begin}, end={generation_end}"
  87. )
  88. return {
  89. "generation": generation_begin,
  90. "state": words[2],
  91. "flags": get_u32(words, 3),
  92. "error": words[6],
  93. "stop_reason": words[7],
  94. "last_result": words[8],
  95. "last_sequence": get_u32(words, 10),
  96. "logical_position": get_u64(words, 16, signed=True),
  97. "task_pulses": get_u64(words, 20, signed=True),
  98. "physical_pulses": get_u64(words, 28),
  99. "current_frequency": get_u32(words, 38),
  100. "target_frequency": get_u32(words, 40),
  101. }
  102. def wait_status(
  103. client: RtuClient,
  104. states: set[int],
  105. sequence: int | None = None,
  106. timeout: float = 5.0,
  107. ) -> dict[str, int]:
  108. deadline = time.monotonic() + timeout
  109. latest: dict[str, int] | None = None
  110. while time.monotonic() < deadline:
  111. latest = read_axis_status(client)
  112. sequence_ok = sequence is None or latest["last_sequence"] == sequence
  113. if latest["state"] in states and sequence_ok:
  114. return latest
  115. raise RuntimeError(f"等待状态 {sorted(states)} 超时,最后状态:{latest}")
  116. def wait_running_output(
  117. client: RtuClient, sequence: int, timeout: float = 5.0
  118. ) -> dict[str, int]:
  119. """等待真实脉冲恢复,不能只依据 ACCEL/RUN 状态标签。"""
  120. deadline = time.monotonic() + timeout
  121. latest: dict[str, int] | None = None
  122. while time.monotonic() < deadline:
  123. latest = read_axis_status(client)
  124. pulse_active = (latest["flags"] & (1 << 1)) != 0
  125. if (
  126. latest["last_sequence"] == sequence
  127. and latest["state"] in {STATE_ACCEL, STATE_RUN}
  128. and pulse_active
  129. and latest["current_frequency"] > 0
  130. ):
  131. return latest
  132. raise RuntimeError(f"等待实际脉冲恢复超时,最后状态:{latest}")
  133. def check_result(response: list[int], expected: int, label: str) -> None:
  134. result = response[3] if len(response) == 12 else response[4]
  135. if result != expected:
  136. raise RuntimeError(f"{label} 返回 {result},期望 {expected};应答={response}")
  137. def main() -> int:
  138. parser = argparse.ArgumentParser(description="PLSR P13 Modbus 控制接口自动测试")
  139. parser.add_argument("--port", help="串口,例如 COM5;只有一个串口时可省略")
  140. parser.add_argument("--baud", type=int, default=9600)
  141. parser.add_argument("--slave", type=int, default=1)
  142. args = parser.parse_args()
  143. with serial.Serial(
  144. port=choose_port(args.port),
  145. baudrate=args.baud,
  146. bytesize=serial.EIGHTBITS,
  147. parity=serial.PARITY_EVEN,
  148. stopbits=serial.STOPBITS_ONE,
  149. timeout=1.0,
  150. write_timeout=1.0,
  151. ) as uart:
  152. client = RtuClient(uart, args.slave)
  153. header = client.read_holding(CONTROL_BASE, 8)
  154. if header[:5] != [0x504C, 0x5352, 0x0100, 256, 0x0007]:
  155. raise RuntimeError(f"P13 控制窗口未就绪:{header}")
  156. print("P13 控制窗口就绪:D1200~D1455,协议 V1.0")
  157. s0 = [0] * 20
  158. put_u32(s0, 0, 1)
  159. put_u32(s0, 10, 2000)
  160. put_u32(s0, 12, 50000)
  161. s1 = [0] * 4
  162. client.write_multiple(S0_BASE, s0)
  163. client.write_multiple(S1_BASE, s1)
  164. print("S0=D1600、S1=D1700 已用 0x10 原子写入")
  165. response = send_call(client, 1, CALL_COMMIT)
  166. check_result(response, RESULT_OK, "COMMIT#1")
  167. if response[11] != 1:
  168. raise RuntimeError("COMMIT#1 未建立有效提交")
  169. print("COMMIT#1:完整校验通过")
  170. client.write_multiple(S0_BASE + 12, signed_dword_words(50001))
  171. response = send_call(client, 2, CALL_START)
  172. check_result(response, RESULT_BUSY, "篡改后的 START#2")
  173. print("START#2:正确拒绝 COMMIT 后被修改的 S0")
  174. client.write_multiple(S0_BASE + 12, signed_dword_words(50000))
  175. response = send_call(client, 3, CALL_COMMIT)
  176. check_result(response, RESULT_OK, "COMMIT#3")
  177. response = send_call(client, 4, CALL_START)
  178. check_result(response, RESULT_QUEUED, "START#4")
  179. status = wait_status(client, {STATE_ACCEL, STATE_RUN}, sequence=4)
  180. if status["last_result"] != RESULT_OK:
  181. raise RuntimeError(f"START#4 内核执行失败:{status}")
  182. print(
  183. f"START#4:Q0 已启动,当前 {status['current_frequency']}Hz,"
  184. f"目标 {status['target_frequency']}Hz"
  185. )
  186. time.sleep(0.5)
  187. response = send_command(client, 100, CMD_PAUSE)
  188. check_result(response, RESULT_QUEUED, "PAUSE#100")
  189. status = wait_status(client, {STATE_PAUSED}, sequence=100)
  190. print(f"PAUSE#100:已暂停,任务累计 {status['task_pulses']} 脉冲")
  191. # Resending the exact sequence must only replay the existing response.
  192. response = send_command(client, 100, CMD_PAUSE)
  193. check_result(response, RESULT_QUEUED, "重复 PAUSE#100")
  194. status = read_axis_status(client)
  195. if status["state"] != STATE_PAUSED or status["last_sequence"] != 100:
  196. raise RuntimeError(f"重复序号导致状态变化:{status}")
  197. print("重复 PAUSE#100:未重复执行")
  198. time.sleep(0.25)
  199. response = send_command(client, 101, CMD_RESUME)
  200. check_result(response, RESULT_QUEUED, "RESUME#101")
  201. status = wait_running_output(client, sequence=101)
  202. print(f"RESUME#101:恢复输出,当前 {status['current_frequency']}Hz")
  203. resumed_pulses = status["task_pulses"]
  204. time.sleep(0.5)
  205. status = read_axis_status(client)
  206. if status["task_pulses"] <= resumed_pulses:
  207. raise RuntimeError(f"RESUME#101 后脉冲计数未增长:{status}")
  208. response = send_command(client, 102, CMD_STOP_DECEL)
  209. check_result(response, RESULT_QUEUED, "STOP_DECEL#102")
  210. status = wait_status(client, {STATE_STOPPED}, sequence=102)
  211. print(
  212. f"STOP_DECEL#102:减速停止完成,逻辑位置={status['logical_position']},"
  213. f"任务脉冲={status['task_pulses']},物理脉冲={status['physical_pulses']}"
  214. )
  215. if status["error"] != 0:
  216. raise RuntimeError(f"最终状态存在错误:{status}")
  217. print("P13 全部自动测试 PASS,请核对 Q0 的启动/暂停/恢复/减速停止波形。")
  218. return 0
  219. if __name__ == "__main__":
  220. try:
  221. raise SystemExit(main())
  222. except (RuntimeError, serial.SerialException) as error:
  223. print(f"测试失败:{error}")
  224. raise SystemExit(1)