#!/usr/bin/env python3 """Validate the production Modbus M -> PLSR WAIT bit-data path. X is intentionally read-only through function 0x02. A real X/EXT/hard-limit test is only possible after the board-specific GPIO-to-X mapping is supplied. """ from __future__ import annotations import argparse import struct import time import serial from plsr_modbus_control_test import ( CALL_COMMIT, CALL_START, CONTROL_BASE, CONTROL_WINDOW_WORDS, RESULT_OK, RESULT_QUEUED, S0_BASE, S1_BASE, check_result, read_axis_status, send_call, ) from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words STATE_WAIT = 5 STATE_COMPLETED = 7 M_TEST_POINT = 123 def write_coil(client: RtuClient, address: int, value: bool) -> None: encoded = 0xFF00 if value else 0x0000 pdu = bytes((0x05,)) + struct.pack(">HH", address, encoded) response = client.exchange(pdu, 8) expected = bytes((client.slave, 0x05)) + struct.pack(">HH", address, encoded) if response[:6] != expected: raise RuntimeError(f"FC05 回显错误: {response.hex(' ')}") def read_bits(client: RtuClient, function: int, address: int, count: int) -> list[int]: byte_count = (count + 7) // 8 pdu = bytes((function,)) + struct.pack(">HH", address, count) response = client.exchange(pdu, 5 + byte_count) if response[1] != function or response[2] != byte_count: raise RuntimeError(f"FC{function:02X} 响应格式错误: {response.hex(' ')}") return [ (response[3 + (index // 8)] >> (index % 8)) & 1 for index in range(count) ] def wait_for_state(client: RtuClient, expected: int, timeout: float) -> dict[str, int]: deadline = time.monotonic() + timeout latest: dict[str, int] | None = None while time.monotonic() < deadline: latest = read_axis_status(client) if latest["state"] == expected: return latest raise RuntimeError(f"等待状态 {expected} 超时,最后状态: {latest}") def prepare_wait_job(client: RtuClient) -> None: words = [0] * 20 words[0:2] = signed_dword_words(1) # one segment words[10:12] = signed_dword_words(1000) # 1000 Hz words[12:14] = signed_dword_words(500) # +500 pulses words[14] = (2 << 8) | 5 # WAIT_SIGNAL, source M words[15:17] = signed_dword_words(M_TEST_POINT) words[17] = 0 # constant fall-through jump words[18:20] = signed_dword_words(0) client.write_multiple(S0_BASE, words) client.write_multiple(S1_BASE, [0, 0, 0, 0]) def main() -> int: parser = argparse.ArgumentParser( description="PLSR 真实 Modbus M 位源、WAIT 与 FC02 X 只读视图测试" ) parser.add_argument("--port", default="COM5") parser.add_argument("--baud", type=int, default=9600) parser.add_argument("--slave", type=int, default=1) args = parser.parse_args() with serial.Serial( port=choose_port(args.port), baudrate=args.baud, bytesize=serial.EIGHTBITS, parity=serial.PARITY_EVEN, stopbits=serial.STOPBITS_ONE, timeout=1.0, write_timeout=1.0, ) as uart: client = RtuClient(uart, args.slave) header = client.read_holding(CONTROL_BASE, 8) if header[3] != CONTROL_WINDOW_WORDS: raise RuntimeError( f"控制窗口版本不匹配: firmware={header[3]}, script={CONTROL_WINDOW_WORDS}" ) # FC02 must exist and must not alias writable M coils. x_before = read_bits(client, 0x02, M_TEST_POINT, 1)[0] write_coil(client, M_TEST_POINT, False) if read_bits(client, 0x01, M_TEST_POINT, 1) != [0]: raise RuntimeError("FC05 写 M=0 后 FC01 回读不一致") if read_bits(client, 0x02, M_TEST_POINT, 1)[0] != x_before: raise RuntimeError("X 与 M 发生别名:写 M 意外改变了 FC02 X") prepare_wait_job(client) sequence = int(time.time()) & 0x7FFFFFFF response = send_call(client, sequence, CALL_COMMIT) check_result(response, RESULT_OK, "COMMIT") response = send_call(client, sequence + 1, CALL_START) # START queues the immutable snapshot; PlsrTask applies it # asynchronously, so QUEUED is the successful protocol reply. check_result(response, RESULT_QUEUED, "START") waiting = wait_for_state(client, STATE_WAIT, 5.0) if waiting["task_pulses"] != 500: raise RuntimeError(f"进入 WAIT 时任务脉冲不是 500: {waiting}") print("M123=0:500 脉冲完成后稳定进入 WAIT") write_coil(client, M_TEST_POINT, True) if read_bits(client, 0x01, M_TEST_POINT, 1) != [1]: raise RuntimeError("FC05 写 M=1 后 FC01 回读不一致") completed = wait_for_state(client, STATE_COMPLETED, 3.0) if completed["task_pulses"] != 500: raise RuntimeError(f"WAIT 释放后任务计数异常: {completed}") print("M123 0->1:PLSR WAIT 已释放并正常 COMPLETED") print(f"FC02 X123 只读值={x_before};写 M 不会改变 X") print("PASS:Modbus FC05/FC01 -> M image -> PLSR readBit/WAIT 生产链通过") print("待硬件映射后再测:实际 X 输入、EXT 上升沿及正/负硬限位。") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except (RuntimeError, serial.SerialException) as error: print(f"测试失败:{error}") raise SystemExit(1)