選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

147 行
5.4 KiB

  1. #!/usr/bin/env python3
  2. """Validate the production Modbus M -> PLSR WAIT bit-data path.
  3. X is intentionally read-only through function 0x02. A real X/EXT/hard-limit
  4. test is only possible after the board-specific GPIO-to-X mapping is supplied.
  5. """
  6. from __future__ import annotations
  7. import argparse
  8. import struct
  9. import time
  10. import serial
  11. from plsr_modbus_control_test import (
  12. CALL_COMMIT,
  13. CALL_START,
  14. CONTROL_BASE,
  15. CONTROL_WINDOW_WORDS,
  16. RESULT_OK,
  17. RESULT_QUEUED,
  18. S0_BASE,
  19. S1_BASE,
  20. check_result,
  21. read_axis_status,
  22. send_call,
  23. )
  24. from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
  25. STATE_WAIT = 5
  26. STATE_COMPLETED = 7
  27. M_TEST_POINT = 123
  28. def write_coil(client: RtuClient, address: int, value: bool) -> None:
  29. encoded = 0xFF00 if value else 0x0000
  30. pdu = bytes((0x05,)) + struct.pack(">HH", address, encoded)
  31. response = client.exchange(pdu, 8)
  32. expected = bytes((client.slave, 0x05)) + struct.pack(">HH", address, encoded)
  33. if response[:6] != expected:
  34. raise RuntimeError(f"FC05 回显错误: {response.hex(' ')}")
  35. def read_bits(client: RtuClient, function: int, address: int, count: int) -> list[int]:
  36. byte_count = (count + 7) // 8
  37. pdu = bytes((function,)) + struct.pack(">HH", address, count)
  38. response = client.exchange(pdu, 5 + byte_count)
  39. if response[1] != function or response[2] != byte_count:
  40. raise RuntimeError(f"FC{function:02X} 响应格式错误: {response.hex(' ')}")
  41. return [
  42. (response[3 + (index // 8)] >> (index % 8)) & 1
  43. for index in range(count)
  44. ]
  45. def wait_for_state(client: RtuClient, expected: int, timeout: float) -> dict[str, int]:
  46. deadline = time.monotonic() + timeout
  47. latest: dict[str, int] | None = None
  48. while time.monotonic() < deadline:
  49. latest = read_axis_status(client)
  50. if latest["state"] == expected:
  51. return latest
  52. raise RuntimeError(f"等待状态 {expected} 超时,最后状态: {latest}")
  53. def prepare_wait_job(client: RtuClient) -> None:
  54. words = [0] * 20
  55. words[0:2] = signed_dword_words(1) # one segment
  56. words[10:12] = signed_dword_words(1000) # 1000 Hz
  57. words[12:14] = signed_dword_words(500) # +500 pulses
  58. words[14] = (2 << 8) | 5 # WAIT_SIGNAL, source M
  59. words[15:17] = signed_dword_words(M_TEST_POINT)
  60. words[17] = 0 # constant fall-through jump
  61. words[18:20] = signed_dword_words(0)
  62. client.write_multiple(S0_BASE, words)
  63. client.write_multiple(S1_BASE, [0, 0, 0, 0])
  64. def main() -> int:
  65. parser = argparse.ArgumentParser(
  66. description="PLSR 真实 Modbus M 位源、WAIT 与 FC02 X 只读视图测试"
  67. )
  68. parser.add_argument("--port", default="COM5")
  69. parser.add_argument("--baud", type=int, default=9600)
  70. parser.add_argument("--slave", type=int, default=1)
  71. args = parser.parse_args()
  72. with serial.Serial(
  73. port=choose_port(args.port),
  74. baudrate=args.baud,
  75. bytesize=serial.EIGHTBITS,
  76. parity=serial.PARITY_EVEN,
  77. stopbits=serial.STOPBITS_ONE,
  78. timeout=1.0,
  79. write_timeout=1.0,
  80. ) as uart:
  81. client = RtuClient(uart, args.slave)
  82. header = client.read_holding(CONTROL_BASE, 8)
  83. if header[3] != CONTROL_WINDOW_WORDS:
  84. raise RuntimeError(
  85. f"控制窗口版本不匹配: firmware={header[3]}, script={CONTROL_WINDOW_WORDS}"
  86. )
  87. # FC02 must exist and must not alias writable M coils.
  88. x_before = read_bits(client, 0x02, M_TEST_POINT, 1)[0]
  89. write_coil(client, M_TEST_POINT, False)
  90. if read_bits(client, 0x01, M_TEST_POINT, 1) != [0]:
  91. raise RuntimeError("FC05 写 M=0 后 FC01 回读不一致")
  92. if read_bits(client, 0x02, M_TEST_POINT, 1)[0] != x_before:
  93. raise RuntimeError("X 与 M 发生别名:写 M 意外改变了 FC02 X")
  94. prepare_wait_job(client)
  95. sequence = int(time.time()) & 0x7FFFFFFF
  96. response = send_call(client, sequence, CALL_COMMIT)
  97. check_result(response, RESULT_OK, "COMMIT")
  98. response = send_call(client, sequence + 1, CALL_START)
  99. # START queues the immutable snapshot; PlsrTask applies it
  100. # asynchronously, so QUEUED is the successful protocol reply.
  101. check_result(response, RESULT_QUEUED, "START")
  102. waiting = wait_for_state(client, STATE_WAIT, 5.0)
  103. if waiting["task_pulses"] != 500:
  104. raise RuntimeError(f"进入 WAIT 时任务脉冲不是 500: {waiting}")
  105. print("M123=0:500 脉冲完成后稳定进入 WAIT")
  106. write_coil(client, M_TEST_POINT, True)
  107. if read_bits(client, 0x01, M_TEST_POINT, 1) != [1]:
  108. raise RuntimeError("FC05 写 M=1 后 FC01 回读不一致")
  109. completed = wait_for_state(client, STATE_COMPLETED, 3.0)
  110. if completed["task_pulses"] != 500:
  111. raise RuntimeError(f"WAIT 释放后任务计数异常: {completed}")
  112. print("M123 0->1:PLSR WAIT 已释放并正常 COMPLETED")
  113. print(f"FC02 X123 只读值={x_before};写 M 不会改变 X")
  114. print("PASS:Modbus FC05/FC01 -> M image -> PLSR readBit/WAIT 生产链通过")
  115. print("待硬件映射后再测:实际 X 输入、EXT 上升沿及正/负硬限位。")
  116. return 0
  117. if __name__ == "__main__":
  118. try:
  119. raise SystemExit(main())
  120. except (RuntimeError, serial.SerialException) as error:
  121. print(f"测试失败:{error}")
  122. raise SystemExit(1)