|
- #!/usr/bin/env python3
- """PLSR P13 Modbus COMMIT/START/command/status integration test."""
-
- from __future__ import annotations
-
- import argparse
- import time
-
- import serial
-
- from plsr_modbus_frequency_test import RtuClient, choose_port, signed_dword_words
-
-
- CONTROL_BASE = 1200
- CONTROL_WINDOW_WORDS = 338
- S0_BASE = 1600
- S1_BASE = 1700
- CALL_REQUEST = CONTROL_BASE + 8
- CALL_RESPONSE = CONTROL_BASE + 24
- COMMAND_REQUEST = CONTROL_BASE + 40
- COMMAND_RESPONSE = CONTROL_BASE + 48
- AXIS0_STATUS = CONTROL_BASE + 64
-
- RESULT_OK = 0
- RESULT_QUEUED = 1
- RESULT_BUSY = 8
- STATE_ACCEL = 2
- STATE_RUN = 3
- STATE_DECEL = 4
- STATE_PAUSED = 6
- STATE_STOPPED = 8
-
- CALL_COMMIT = 1
- CALL_START = 2
- CMD_STOP_DECEL = 1
- CMD_PAUSE = 3
- CMD_RESUME = 4
-
-
- def put_u32(words: list[int], offset: int, value: int) -> None:
- words[offset : offset + 2] = signed_dword_words(value)
-
-
- def put_u64(words: list[int], offset: int, value: int) -> None:
- raw = value & 0xFFFFFFFFFFFFFFFF
- words[offset : offset + 4] = [(raw >> shift) & 0xFFFF for shift in (0, 16, 32, 48)]
-
-
- def get_u32(words: list[int], offset: int) -> int:
- return words[offset] | (words[offset + 1] << 16)
-
-
- def get_u64(words: list[int], offset: int, signed: bool = False) -> int:
- raw = sum(words[offset + index] << (index * 16) for index in range(4))
- if signed and raw & (1 << 63):
- return raw - (1 << 64)
- return raw
-
-
- def wait_call_response(client: RtuClient, sequence: int, timeout: float = 2.0) -> list[int]:
- deadline = time.monotonic() + timeout
- while time.monotonic() < deadline:
- response = client.read_holding(CALL_RESPONSE, 12)
- if get_u32(response, 0) == sequence:
- return response
- raise RuntimeError(f"等待调用应答序号 {sequence} 超时")
-
-
- def send_call(client: RtuClient, sequence: int, operation: int) -> list[int]:
- request = [0] * 16
- put_u32(request, 0, sequence)
- request[2] = 0 # S0 device D
- put_u32(request, 3, S0_BASE)
- request[5] = 0 # S1 device D
- put_u32(request, 6, S1_BASE)
- request[8] = 0 # S2 constant
- request[9] = 0
- put_u32(request, 10, 1) # K1
- request[12] = 0 # axis Y0/Q0
- request[13] = 0 # PULSE/DIR
- request[14] = operation
- client.write_multiple(CALL_REQUEST, request)
- return wait_call_response(client, sequence)
-
-
- def wait_command_response(client: RtuClient, sequence: int, timeout: float = 2.0) -> list[int]:
- deadline = time.monotonic() + timeout
- while time.monotonic() < deadline:
- response = client.read_holding(COMMAND_RESPONSE, 8)
- if get_u32(response, 0) == sequence:
- return response
- raise RuntimeError(f"等待命令应答序号 {sequence} 超时")
-
-
- def send_command(
- client: RtuClient, sequence: int, opcode: int, argument: int = 0
- ) -> list[int]:
- request = [0] * 8
- put_u32(request, 0, sequence)
- request[2] = opcode
- request[3] = 0
- put_u64(request, 4, argument)
- client.write_multiple(COMMAND_REQUEST, request)
- return wait_command_response(client, sequence)
-
-
- def read_axis_status(client: RtuClient) -> dict[str, int]:
- words = client.read_holding(AXIS0_STATUS, 48)
- generation_begin = get_u32(words, 0)
- generation_end = get_u32(words, 46)
- if generation_begin != generation_end or generation_begin & 1:
- raise RuntimeError(
- f"状态快照版本不一致:begin={generation_begin}, end={generation_end}"
- )
- return {
- "generation": generation_begin,
- "state": words[2],
- "flags": get_u32(words, 3),
- "error": words[6],
- "stop_reason": words[7],
- "last_result": words[8],
- "last_sequence": get_u32(words, 10),
- "logical_position": get_u64(words, 16, signed=True),
- "task_pulses": get_u64(words, 20, signed=True),
- "physical_pulses": get_u64(words, 28),
- "counter_mode": words[37],
- "current_frequency": get_u32(words, 38),
- "target_frequency": get_u32(words, 40),
- }
-
-
- def wait_status(
- client: RtuClient,
- states: set[int],
- sequence: int | None = None,
- timeout: float = 5.0,
- ) -> dict[str, int]:
- deadline = time.monotonic() + timeout
- latest: dict[str, int] | None = None
- while time.monotonic() < deadline:
- latest = read_axis_status(client)
- sequence_ok = sequence is None or latest["last_sequence"] == sequence
- if latest["state"] in states and sequence_ok:
- return latest
- raise RuntimeError(f"等待状态 {sorted(states)} 超时,最后状态:{latest}")
-
-
- def wait_running_output(
- client: RtuClient, sequence: int, timeout: float = 5.0
- ) -> dict[str, int]:
- """等待真实脉冲恢复,不能只依据 ACCEL/RUN 状态标签。"""
- deadline = time.monotonic() + timeout
- latest: dict[str, int] | None = None
- while time.monotonic() < deadline:
- latest = read_axis_status(client)
- pulse_active = (latest["flags"] & (1 << 1)) != 0
- if (
- latest["last_sequence"] == sequence
- and latest["state"] in {STATE_ACCEL, STATE_RUN}
- and pulse_active
- and latest["current_frequency"] > 0
- ):
- return latest
- raise RuntimeError(f"等待实际脉冲恢复超时,最后状态:{latest}")
-
-
- def check_result(response: list[int], expected: int, label: str) -> None:
- result = response[3] if len(response) == 12 else response[4]
- if result != expected:
- raise RuntimeError(f"{label} 返回 {result},期望 {expected};应答={response}")
-
-
- def main() -> int:
- parser = argparse.ArgumentParser(description="PLSR P13 Modbus 控制接口自动测试")
- parser.add_argument("--port", help="串口,例如 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[:5] != [
- 0x504C,
- 0x5352,
- 0x0100,
- CONTROL_WINDOW_WORDS,
- 0x0007,
- ]:
- raise RuntimeError(f"P13 控制窗口未就绪:{header}")
- print("P13 控制窗口就绪:D1200~D1537,协议 V1.0")
-
- s0 = [0] * 20
- put_u32(s0, 0, 1)
- put_u32(s0, 10, 2000)
- put_u32(s0, 12, 50000)
- s1 = [0] * 4
- client.write_multiple(S0_BASE, s0)
- client.write_multiple(S1_BASE, s1)
- print("S0=D1600、S1=D1700 已用 0x10 原子写入")
-
- response = send_call(client, 1, CALL_COMMIT)
- check_result(response, RESULT_OK, "COMMIT#1")
- if response[11] != 1:
- raise RuntimeError("COMMIT#1 未建立有效提交")
- print("COMMIT#1:完整校验通过")
-
- client.write_multiple(S0_BASE + 12, signed_dword_words(50001))
- response = send_call(client, 2, CALL_START)
- check_result(response, RESULT_BUSY, "篡改后的 START#2")
- print("START#2:正确拒绝 COMMIT 后被修改的 S0")
-
- client.write_multiple(S0_BASE + 12, signed_dword_words(50000))
- response = send_call(client, 3, CALL_COMMIT)
- check_result(response, RESULT_OK, "COMMIT#3")
- response = send_call(client, 4, CALL_START)
- check_result(response, RESULT_QUEUED, "START#4")
- status = wait_status(client, {STATE_ACCEL, STATE_RUN}, sequence=4)
- if status["last_result"] != RESULT_OK:
- raise RuntimeError(f"START#4 内核执行失败:{status}")
- print(
- f"START#4:Q0 已启动,当前 {status['current_frequency']}Hz,"
- f"目标 {status['target_frequency']}Hz"
- )
-
- time.sleep(0.5)
- response = send_command(client, 100, CMD_PAUSE)
- check_result(response, RESULT_QUEUED, "PAUSE#100")
- status = wait_status(client, {STATE_PAUSED}, sequence=100)
- print(f"PAUSE#100:已暂停,任务累计 {status['task_pulses']} 脉冲")
-
- # Resending the exact sequence must only replay the existing response.
- response = send_command(client, 100, CMD_PAUSE)
- check_result(response, RESULT_QUEUED, "重复 PAUSE#100")
- status = read_axis_status(client)
- if status["state"] != STATE_PAUSED or status["last_sequence"] != 100:
- raise RuntimeError(f"重复序号导致状态变化:{status}")
- print("重复 PAUSE#100:未重复执行")
-
- time.sleep(0.25)
- response = send_command(client, 101, CMD_RESUME)
- check_result(response, RESULT_QUEUED, "RESUME#101")
- status = wait_running_output(client, sequence=101)
- print(f"RESUME#101:恢复输出,当前 {status['current_frequency']}Hz")
-
- resumed_pulses = status["task_pulses"]
- time.sleep(0.5)
- status = read_axis_status(client)
- if status["task_pulses"] <= resumed_pulses:
- raise RuntimeError(f"RESUME#101 后脉冲计数未增长:{status}")
- response = send_command(client, 102, CMD_STOP_DECEL)
- check_result(response, RESULT_QUEUED, "STOP_DECEL#102")
- status = wait_status(client, {STATE_STOPPED}, sequence=102)
- print(
- f"STOP_DECEL#102:减速停止完成,逻辑位置={status['logical_position']},"
- f"任务脉冲={status['task_pulses']},物理脉冲={status['physical_pulses']}"
- )
- if status["error"] != 0:
- raise RuntimeError(f"最终状态存在错误:{status}")
-
- print("P13 全部自动测试 PASS,请核对 Q0 的启动/暂停/恢复/减速停止波形。")
- return 0
-
-
- if __name__ == "__main__":
- try:
- raise SystemExit(main())
- except (RuntimeError, serial.SerialException) as error:
- print(f"测试失败:{error}")
- raise SystemExit(1)
|