"""PLSR 参数表-2026 的 Modbus RTU 地址、枚举和编解码工具。""" from enum import IntEnum from typing import Dict, List, Sequence, Tuple DEFAULT_SLAVE_ADDRESS = 1 DEFAULT_BAUD_RATE = 115200 MIN_FREQUENCY_HZ = 1 MAX_FREQUENCY_HZ = 100000 MAX_RAMP_TIME_MS = 0xFFFF class Register(IntEnum): PULSE_POINT = 0x1000 DIRECTION_POINT = 0x1001 WAIT_SIGNAL = 0x1002 EXT_SIGNAL = 0x1003 SEND_MODE = 0x1004 DIRECTION_DELAY_MS = 0x1005 DIRECTION_LOGIC = 0x1006 PROFILE_TYPE = 0x1007 RUN_MODE = 0x1008 TOTAL_SEGMENTS = 0x1009 START_SEGMENT = 0x100A TARGET_FREQ_LO = 0x100B TARGET_FREQ_HI = 0x100C START_FREQ_LO = 0x100D START_FREQ_HI = 0x100E OUTPUT_MODE = 0x100F END_FREQ_LO = 0x1010 END_FREQ_HI = 0x1011 ACCEL_TIME = 0x1012 DECEL_TIME = 0x1013 MAX_FREQ_LO = 0x1014 MAX_FREQ_HI = 0x1015 # 瞬时运行参数,不属于公共参数,也不保存到Flash。 RUNTIME_FREQ_LO = 0x1016 RUNTIME_FREQ_HI = 0x1017 SEGMENT_TABLE_BASE = 0x1100 CUMULATIVE_POSITION_LO = 0x2000 CUMULATIVE_POSITION_HI = 0x2001 CURRENT_FREQ_LO = 0x2002 CURRENT_FREQ_HI = 0x2003 STATE = 0x2004 CURRENT_SEGMENT = 0x2005 ERROR = 0x2006 CURRENT_PULSES_LO = 0x2007 CURRENT_PULSES_HI = 0x2008 TOTAL_PULSES_LO = 0x2009 TOTAL_PULSES_HI = 0x200A DIAG_STATUS = 0x200B DIAG_PROFILE_TYPE = 0x200C DIAG_PHASE = 0x200D DIAG_EXPECTED_PULSES_LO = 0x200E DIAG_EXPECTED_PULSES_HI = 0x200F DIAG_OBSERVED_PULSES_LO = 0x2010 DIAG_OBSERVED_PULSES_HI = 0x2011 DIAG_EXPECTED_FREQ_LO = 0x2012 DIAG_EXPECTED_FREQ_HI = 0x2013 DIAG_ACTUAL_FREQ_LO = 0x2014 DIAG_ACTUAL_FREQ_HI = 0x2015 DIAG_MAX_FREQ_ERROR_LO = 0x2016 DIAG_MAX_FREQ_ERROR_HI = 0x2017 DIAG_COUNT_ERRORS = 0x2018 DIAG_FREQ_ERRORS = 0x2019 DIAG_PROFILE_ERRORS = 0x201A CONTROL = 0x3000 # 旧界面源码的兼容别名;实际通讯流程只使用上面的参数表地址。 COMMAND = CONTROL DIRECTION = DIRECTION_LOGIC SEGMENT_META_BASE = SEGMENT_TABLE_BASE SEGMENT_PULSES_DONE_HI = CURRENT_FREQ_LO SEGMENT_PULSES_DONE_LO = CURRENT_FREQ_HI ACTIVE_WAIT = STATE WAIT_ELAPSED_HI = CURRENT_SEGMENT WAIT_ELAPSED_LO = ERROR SEGMENT_STRIDE = 0x10 SEGMENT_USED_REGISTERS = 8 def segment_base(index: int) -> int: if not 0 <= index < 10: raise ValueError("段索引必须为0~9") return int(Register.SEGMENT_TABLE_BASE) + index * SEGMENT_STRIDE def register_read_count(_first: Register = Register.PULSE_POINT) -> int: """全部公共参数连续块0x1000~0x1015的长度。""" return int(Register.MAX_FREQ_HI) - int(Register.PULSE_POINT) + 1 class Command(IntEnum): START = 0x0001 STOP = 0x0002 CLEAR_POSITION = 0x0004 RESUME = 0x0001 # 暂停状态下再次置START即继续 # 兼容旧界面分支;新参数表没有这些独立命令码。 CLEAR_TOTAL = 0x0008 SAVE_PUBLIC = 0x0010 SAVE_OUTPUTS = SAVE_PUBLIC # 兼容旧名称;设备端现在保存全部公共参数 CLEAR_DIAGNOSTIC = 0x0020 UPDATE_FREQUENCY = 0x0040 START_SEGMENTS = 0x0001 STATUS_REGISTER_COUNT = ( int(Register.DIAG_PROFILE_ERRORS) - int(Register.CUMULATIVE_POSITION_LO) + 1 ) class DiagnosticStatus(IntEnum): ACTIVE = 0x0001 COMPLETE = 0x0002 COUNT_PASS = 0x0004 FREQUENCY_PASS = 0x0008 PROFILE_PASS = 0x0010 COUNT_FAIL = 0x0100 FREQUENCY_FAIL = 0x0200 PROFILE_FAIL = 0x0400 DIAGNOSTIC_PHASE_TEXT = { 0: "空闲", 1: "加速段", 2: "匀速段", 3: "减速段", } class State(IntEnum): IDLE = 0 RUNNING = 1 COMPLETE = 2 ERROR = 3 PAUSED = 4 WAITING = 5 class WaitCondition(IntEnum): WAIT_TIME = 0 WAIT_SIGNAL = 1 ACT_TIME = 2 EXT_SIGNAL = 3 EXT_OR_COMPLETE = 4 class RunMode(IntEnum): RELATIVE = 0 ABSOLUTE = 1 class SendMode(IntEnum): COMPLETE = 0 FOLLOW = 1 class DirectionLogic(IntEnum): POSITIVE = 0 NEGATIVE = 1 class ProfileType(IntEnum): LINEAR = 0 S_CURVE = 1 SINE = 2 class OutputMode(IntEnum): PULSE_DIRECTION = 0 AB_QUADRATURE = 1 class Error(IntEnum): NONE = 0 BUSY = 1 INVALID_PULSE_COUNT = 2 INVALID_FREQUENCY = 3 INVALID_DIRECTION = 4 TIMER_RANGE = 5 INVALID_COMMAND = 6 NOT_PAUSED = 7 NOT_ACTIVE = 8 INVALID_PULSE_POINT = 9 INVALID_DIRECTION_POINT = 10 SAME_OUTPUT_POINT = 11 FLASH_SAVE = 12 INVALID_MODE = 13 INVALID_PROFILE = 14 INVALID_RAMP_TIME = 15 INVALID_SPEED = 16 INVALID_SEGMENT_COUNT = 17 INVALID_START_SEGMENT = 18 INVALID_SEGMENT_PARAM = 19 SEGMENT_TOTAL_OVERFLOW = 20 INVALID_WAIT_CONDITION = 21 INVALID_INPUT_POINT = 22 STATE_TEXT = { State.IDLE: "空闲", State.RUNNING: "运行中", State.COMPLETE: "完成", State.ERROR: "错误", State.PAUSED: "已暂停", State.WAITING: "条件等待中", } ERROR_TEXT = {item: item.name for item in Error} ERROR_TEXT[Error.NONE] = "无错误" WAIT_CONDITION_TEXT = { WaitCondition.WAIT_TIME: "Wait时间", WaitCondition.WAIT_SIGNAL: "Wait信号(全局X4/X5)", WaitCondition.ACT_TIME: "ACT时间", WaitCondition.EXT_SIGNAL: "EXT信号(全局X4/X5)", WaitCondition.EXT_OR_COMPLETE: "EXT信号或脉冲完成", } MODE_TEXT = {RunMode.RELATIVE: "相对模式", RunMode.ABSOLUTE: "绝对模式"} SEND_MODE_TEXT = { SendMode.COMPLETE: "完成模式(段间停止)", SendMode.FOLLOW: "后续模式(满足条件时连续衔接)", } DIRECTION_LOGIC_TEXT = { DirectionLogic.POSITIVE: "正逻辑(正向ON,反向OFF)", DirectionLogic.NEGATIVE: "负逻辑(正向OFF,反向ON)", } PROFILE_TEXT = { ProfileType.LINEAR: "直线加减速", ProfileType.S_CURVE: "S曲线加减速", ProfileType.SINE: "正弦曲线加减速", } OUTPUT_MODE_TEXT = { OutputMode.PULSE_DIRECTION: "PUL/DIR(脉冲+方向)", OutputMode.AB_QUADRATURE: "AB正交脉冲", } INPUT_POINTS = (4, 5) PULSE_POINTS = { 0: "Y0 / PF6 / TIM10_CH1", 1: "Y1 / PF8 / TIM13_CH1", 2: "Y2 / PF7 / TIM11_CH1", 3: "Y3 / PF9 / TIM14_CH1", } SECONDARY_POINTS = { 0: "Y12 / PH9", 1: "Y13 / PH8", 2: "Y14 / PH7", 3: "Y15 / PH6", } def point_id_from_label(options: Dict[int, str], label: str) -> int: for point, text in options.items(): if text == label: return point raise ValueError(f"未知输出点:{label}") def calculate_execution_total( segments: Sequence[Sequence[int]], start_segment: int ) -> Tuple[int, bool]: """按跳转段实际路径累计脉冲;检测到循环时返回(cycle前累计值, True)。""" if not segments or not 1 <= start_segment <= len(segments): raise ValueError("起始执行段超出有效段范围") current = start_segment visited = set() total = 0 while current not in visited: visited.add(current) segment = segments[current - 1] total += abs(int(segment[0])) jump = int(segment[2]) if jump == 0: if current >= len(segments): return total, False current += 1 else: if not 1 <= jump <= len(segments): raise ValueError("跳转段超出有效段范围") current = jump return total, True PRESETS = [ {"name": "LED可视测试:10脉冲 / 1Hz", "pulses": 10, "frequency": 1, "acceleration_ms": 0, "deceleration_ms": 0, "start_frequency": 1, "end_frequency": 1}, {"name": "基础测试:1000脉冲 / 1000Hz", "pulses": 1000, "frequency": 1000, "acceleration_ms": 1000, "deceleration_ms": 1000, "start_frequency": 100, "end_frequency": 100}, {"name": "长行程测试:100000脉冲 / 5000Hz", "pulses": 100000, "frequency": 5000, "acceleration_ms": 1000, "deceleration_ms": 1000, "start_frequency": 100, "end_frequency": 100}, {"name": "最高频率测试:100000脉冲 / 100000Hz", "pulses": 100000, "frequency": MAX_FREQUENCY_HZ, "acceleration_ms": 500, "deceleration_ms": 500, "start_frequency": 100, "end_frequency": 100}, ] def split_u32(value: int) -> List[int]: """参数表规定:低地址存低16位。""" if not 0 <= value <= 0xFFFFFFFF: raise ValueError("32位参数超出范围") return [value & 0xFFFF, (value >> 16) & 0xFFFF] def signed_to_u32(value: int) -> int: if not -0x80000000 <= value <= 0x7FFFFFFF: raise ValueError("有符号32位参数超出范围") return value & 0xFFFFFFFF def join_u32(low: int, high: int) -> int: return (low & 0xFFFF) | ((high & 0xFFFF) << 16) def join_i32(low: int, high: int) -> int: value = join_u32(low, high) return value - 0x100000000 if value >= 0x80000000 else value def state_text(value: int) -> str: try: return STATE_TEXT[State(value)] except (ValueError, KeyError): return f"未知状态 {value}" def error_text(value: int) -> str: try: return ERROR_TEXT[Error(value)] except (ValueError, KeyError): return f"未知错误 {value}" def parse_status(registers: List[int]) -> Dict[str, int]: """解析0x2000开始的状态区;兼容旧固件的11寄存器响应。""" if len(registers) < 11: raise ValueError("状态寄存器数量不足11个") result = { "cumulative_position": join_i32(registers[0], registers[1]), "current_frequency": join_u32(registers[2], registers[3]), "state": registers[4], "current_segment": registers[5], "error": registers[6], "current_pulses": join_u32(registers[7], registers[8]), "total_pulses": join_u32(registers[9], registers[10]), } if len(registers) >= STATUS_REGISTER_COUNT: result.update({ "diagnostic_status": registers[11], "diagnostic_profile_type": registers[12], "diagnostic_phase": registers[13], "diagnostic_expected_pulses": join_u32(registers[14], registers[15]), "diagnostic_observed_pulses": join_u32(registers[16], registers[17]), "diagnostic_expected_frequency": join_u32(registers[18], registers[19]), "diagnostic_actual_frequency": join_u32(registers[20], registers[21]), "diagnostic_max_frequency_error": join_u32(registers[22], registers[23]), "diagnostic_count_errors": registers[24], "diagnostic_frequency_errors": registers[25], "diagnostic_profile_errors": registers[26], }) else: result.update({ "diagnostic_status": 0, "diagnostic_profile_type": 0, "diagnostic_phase": 0, "diagnostic_expected_pulses": 0, "diagnostic_observed_pulses": 0, "diagnostic_expected_frequency": 0, "diagnostic_actual_frequency": 0, "diagnostic_max_frequency_error": 0, "diagnostic_count_errors": 0, "diagnostic_frequency_errors": 0, "diagnostic_profile_errors": 0, }) return result