25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

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