|
- #!/usr/bin/env python3
- """P19 XDM-60T4-E Y1..Y4 -> X0..X3 physical loopback tests."""
-
- from __future__ import annotations
-
- import argparse
- import struct
- import time
-
- import serial
-
- from plsr_modbus_control_test import (
- CALL_COMMIT,
- CALL_START,
- CMD_STOP_DECEL,
- CONTROL_BASE,
- CONTROL_WINDOW_WORDS,
- RESULT_OK,
- RESULT_QUEUED,
- S0_BASE,
- S1_BASE,
- check_result,
- read_axis_status,
- send_call,
- send_command,
- )
- from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
-
-
- LOOPBACK_BASE = 1540
- LOOPBACK_SIGNATURE = 0x5019
- STATE_IDLE = 1
- STATE_ACCEL = 2
- STATE_RUN = 3
- STATE_WAIT = 5
- STATE_COMPLETED = 7
- STATE_STOPPED = 8
- CMD_RESET_ERROR = 10
- ERROR_LIMIT_POSITIVE = 6
- ERROR_LIMIT_NEGATIVE = 7
- STOP_LIMIT_POSITIVE = 5
- STOP_LIMIT_NEGATIVE = 6
-
-
- def read_bits(client: RtuClient, address: int, count: int) -> list[int]:
- byte_count = (count + 7) // 8
- pdu = bytes((0x02,)) + struct.pack(">HH", address, count)
- response = client.exchange(pdu, 5 + byte_count)
- if response[1] != 0x02 or response[2] != byte_count:
- raise RuntimeError(f"FC02 响应格式错误:{response.hex(' ')}")
- return [
- (response[3 + index // 8] >> (index % 8)) & 1
- for index in range(count)
- ]
-
-
- class LoopbackLease:
- def __init__(self, client: RtuClient) -> None:
- self.client = client
- self.sequence = int(time.time() * 1000) & 0xFFFF
- self.mask = 0
- self.last_write = 0.0
-
- def set(self, mask: int) -> None:
- self.mask = mask & 0x0F
- self.sequence = (self.sequence + 1) & 0xFFFF
- self.client.write_multiple(LOOPBACK_BASE, [self.mask, self.sequence])
- self.last_write = time.monotonic()
-
- def heartbeat(self) -> None:
- if time.monotonic() - self.last_write >= 0.20:
- self.set(self.mask)
-
- def diagnostics(self) -> dict[str, int]:
- words = self.client.read_holding(LOOPBACK_BASE + 2, 5)
- return {
- "applied": words[0],
- "raw": words[1],
- "logical": words[2],
- "signature": words[3],
- "flags": words[4],
- }
-
-
- def wait_until(
- lease: LoopbackLease,
- predicate,
- timeout: float,
- label: str,
- ) -> dict[str, int]:
- deadline = time.monotonic() + timeout
- latest: dict[str, int] | None = None
- while time.monotonic() < deadline:
- lease.heartbeat()
- latest = read_axis_status(lease.client)
- if predicate(latest):
- return latest
- raise RuntimeError(f"{label}超时,最后状态={latest}")
-
-
- def prepare_job(
- client: RtuClient,
- pulses: int,
- wait_condition: int = 0,
- wait_source: int = 0,
- wait_point: int = 0,
- ) -> None:
- words = [0] * 20
- words[0:2] = signed_dword_words(1)
- words[10:12] = signed_dword_words(2000)
- words[12:14] = signed_dword_words(pulses)
- words[14] = ((wait_condition & 0xFF) << 8) | (wait_source & 0xFF)
- words[15:17] = signed_dword_words(wait_point)
- words[17] = 0
- words[18:20] = signed_dword_words(0)
- client.write_multiple(S0_BASE, words)
- client.write_multiple(S1_BASE, [0, 0, 0, 0])
-
-
- def start_job(client: RtuClient, sequence: int) -> int:
- response = send_call(client, sequence, CALL_COMMIT)
- check_result(response, RESULT_OK, "COMMIT")
- response = send_call(client, sequence + 1, CALL_START)
- check_result(response, RESULT_QUEUED, "START")
- return sequence + 1
-
-
- def reset_limit_error(lease: LoopbackLease, sequence: int) -> None:
- lease.set(0)
- time.sleep(0.050)
- response = send_command(lease.client, sequence, CMD_RESET_ERROR)
- check_result(response, RESULT_QUEUED, "RESET_ERROR")
- status = wait_until(
- lease,
- lambda value: value["state"] == STATE_IDLE
- and value["last_sequence"] == sequence,
- 2.0,
- "RESET_ERROR",
- )
- if status["error"] != 0:
- raise RuntimeError(f"RESET_ERROR 后错误未清除:{status}")
-
-
- def test_electrical(lease: LoopbackLease) -> None:
- patterns = (0x0, 0x1, 0x2, 0x4, 0x8, 0x3, 0xC, 0xF, 0x0)
- for pattern in patterns:
- lease.set(pattern)
- time.sleep(0.050)
- diagnostics = lease.diagnostics()
- x_bits = read_bits(lease.client, 0, 4)
- observed = sum(bit << index for index, bit in enumerate(x_bits))
- if diagnostics["signature"] != LOOPBACK_SIGNATURE:
- raise RuntimeError(f"不是 P19 固件:{diagnostics}")
- if (diagnostics["flags"] & 0x0013) != 0x0013:
- raise RuntimeError(f"P19 输入/租约未就绪:{diagnostics}")
- if diagnostics["applied"] != pattern or observed != pattern:
- raise RuntimeError(
- f"回环不一致:写Y掩码=0x{pattern:X}, FC02 X=0x{observed:X}, "
- f"诊断={diagnostics}。请检查对应 Y→X 和 COM 接线。"
- )
- print(
- f"Y1~Y4=0x{pattern:X} -> X0~X3=0x{observed:X} PASS "
- f"(MCU raw=0x{diagnostics['raw'] & 0xF:X})"
- )
- print("电气回环 PASS:Y1→X0、Y2→X1、Y3→X2、Y4→X3")
-
-
- def test_wait(lease: LoopbackLease, sequence: int) -> None:
- lease.set(0)
- prepare_job(lease.client, 500, wait_condition=2, wait_source=4, wait_point=3)
- start_sequence = start_job(lease.client, sequence)
- waiting = wait_until(
- lease,
- lambda value: value["state"] == STATE_WAIT,
- 3.0,
- "进入 X3 WAIT",
- )
- if waiting["task_pulses"] != 500:
- raise RuntimeError(f"WAIT 前脉冲数不是500:{waiting}")
- lease.set(0x8)
- completed = wait_until(
- lease,
- lambda value: value["state"] == STATE_COMPLETED,
- 2.0,
- "X3 释放 WAIT",
- )
- if completed["task_pulses"] != 500 or completed["error"] != 0:
- raise RuntimeError(f"WAIT 完成状态异常:{completed}")
- lease.set(0)
- print(f"X3/WAIT PASS:500脉冲后等待,Y4→X3释放,序号={start_sequence}")
-
-
- def test_ext(lease: LoopbackLease, sequence: int) -> None:
- lease.set(0)
- prepare_job(lease.client, 50000, wait_condition=4, wait_source=4, wait_point=2)
- start_job(lease.client, sequence)
- running = wait_until(
- lease,
- lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
- and abs(value["task_pulses"]) >= 200,
- 3.0,
- "EXT 触发前运行",
- )
- lease.set(0x4)
- completed = wait_until(
- lease,
- lambda value: value["state"] == STATE_COMPLETED,
- 2.0,
- "X2 EXT 上升沿切段",
- )
- lease.set(0)
- if not (running["task_pulses"] <= completed["task_pulses"] < 50000):
- raise RuntimeError(f"EXT 未提前结束任务:{completed}")
- if completed["error"] != 0:
- raise RuntimeError(f"EXT 完成存在错误:{completed}")
- print(f"X2/EXT PASS:Y3→X2 上升沿在 {completed['task_pulses']} 脉冲提前结束")
-
-
- def test_limit(
- lease: LoopbackLease,
- sequence: int,
- positive: bool,
- ) -> None:
- lease.set(0)
- pulses = 50000 if positive else -50000
- bit_mask = 0x1 if positive else 0x2
- expected_error = ERROR_LIMIT_POSITIVE if positive else ERROR_LIMIT_NEGATIVE
- expected_reason = STOP_LIMIT_POSITIVE if positive else STOP_LIMIT_NEGATIVE
- flag_mask = 1 << (9 if positive else 10)
- label = "正硬限位 X0" if positive else "负硬限位 X1"
- prepare_job(lease.client, pulses)
- start_job(lease.client, sequence)
- wait_until(
- lease,
- lambda value: value["state"] in {STATE_ACCEL, STATE_RUN}
- and abs(value["task_pulses"]) >= 200,
- 3.0,
- f"{label}触发前运行",
- )
- lease.set(bit_mask)
- stopped = wait_until(
- lease,
- lambda value: value["state"] == STATE_STOPPED,
- 3.0,
- f"{label}停止",
- )
- if (
- stopped["error"] != expected_error
- or stopped["stop_reason"] != expected_reason
- or (stopped["flags"] & flag_mask) == 0
- or abs(stopped["task_pulses"]) >= 50000
- ):
- raise RuntimeError(f"{label}状态不符合预期:{stopped}")
- reset_limit_error(lease, sequence + 100)
- print(
- f"{label} PASS:提前停止于 {stopped['task_pulses']} 脉冲,"
- f"error={expected_error}, stop_reason={expected_reason}"
- )
-
-
- def main() -> int:
- parser = argparse.ArgumentParser(
- description="P19 XDM-60T4-E Y1~Y4→X0~X3真实输入、WAIT/EXT/硬限位测试"
- )
- parser.add_argument("--port", default="COM5")
- parser.add_argument("--baud", type=int, default=9600)
- parser.add_argument("--slave", type=int, default=1)
- parser.add_argument(
- "--case",
- choices=("electrical", "wait", "ext", "positive-limit", "negative-limit", "all"),
- default="electrical",
- )
- 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"控制窗口不匹配:{header}")
- lease = LoopbackLease(client)
- sequence = int(time.time()) & 0x7FFFFFFF
- try:
- cases = (
- ("electrical", lambda: test_electrical(lease)),
- ("wait", lambda: test_wait(lease, sequence + 10)),
- ("ext", lambda: test_ext(lease, sequence + 20)),
- ("positive-limit", lambda: test_limit(lease, sequence + 30, True)),
- ("negative-limit", lambda: test_limit(lease, sequence + 40, False)),
- )
- for case_name, operation in cases:
- if args.case in {case_name, "all"}:
- print(f"\n开始 P19 用例:{case_name}")
- operation()
- print("\n全部所选 P19 自动检查 PASS")
- finally:
- try:
- lease.set(0)
- except (RuntimeError, serial.SerialException):
- pass
- return 0
-
-
- if __name__ == "__main__":
- try:
- raise SystemExit(main())
- except (RuntimeError, serial.SerialException) as error:
- print(f"测试失败:{error}")
- raise SystemExit(1)
|