|
- """PLSR单轴脉冲测试上位机 - tkinter版本。"""
-
- from __future__ import annotations
-
- import json
- import queue
- import sys
- import time
- from datetime import datetime
- from pathlib import Path
- from typing import Dict, List
-
- import tkinter as tk
- from tkinter import messagebox, ttk
- from tkinter.scrolledtext import ScrolledText
-
- from modbus_rtu import ModbusClient, list_serial_ports
- from plsr_protocol import (
- Command,
- DiagnosticStatus,
- DIAGNOSTIC_PHASE_TEXT,
- DirectionLogic,
- DIRECTION_LOGIC_TEXT,
- DEFAULT_BAUD_RATE,
- DEFAULT_SLAVE_ADDRESS,
- SECONDARY_POINTS,
- Error,
- MAX_FREQUENCY_HZ,
- MAX_RAMP_TIME_MS,
- MIN_FREQUENCY_HZ,
- MODE_TEXT,
- PRESETS,
- PROFILE_TEXT,
- OUTPUT_MODE_TEXT,
- PULSE_POINTS,
- Register,
- WaitCondition,
- WAIT_CONDITION_TEXT,
- INPUT_POINTS,
- ProfileType,
- OutputMode,
- RunMode,
- SendMode,
- SEND_MODE_TEXT,
- State,
- STATUS_REGISTER_COUNT,
- error_text,
- calculate_execution_total,
- join_i32,
- join_u32,
- parse_status,
- point_id_from_label,
- register_read_count,
- segment_base,
- signed_to_u32,
- split_u32,
- state_text,
- )
-
-
- APP_DIR = Path(__file__).resolve().parent
- SETTINGS_FILE = APP_DIR / "settings.json"
- DIRECTION_TEXT = {0: "正向", 1: "反向"}
-
-
- class PlsrTesterApp:
- POLL_INTERVAL_MS = 250
- EVENT_INTERVAL_MS = 40
-
- def __init__(self, root: tk.Tk) -> None:
- self.root = root
- self.client = ModbusClient()
- self.closing = False
- self.expected_pulses = 0
- self.expected_constant_frequency = 0
- self.display_target = 0
- self.test_active = False
- self.monitoring_active = False
- self.seen_running = False
- self.test_started_at = 0.0
- self.last_state = -1
- self.last_error = -1
- self.pending_frequency = 0
- self.pending_motion = None
- self.pending_segments = None
- self.written_segments = None
- self.segments_written_ready = False
- self.start_after_segment_write = False
- self.multi_test_active = False
- self.wait_signal_select = 0
- self.ext_signal_select = 0
-
- self._build_style()
- self._build_ui()
- self.refresh_ports()
- self.apply_preset(log_message=False)
- self._load_settings()
- self._set_connected(False)
-
- self.root.protocol("WM_DELETE_WINDOW", self.close)
- self.root.after(self.EVENT_INTERVAL_MS, self._process_client_events)
- self.root.after(self.POLL_INTERVAL_MS, self._poll_status)
- self.log("上位机已启动。请选择USART1对应的COM端口。")
-
- def _build_style(self) -> None:
- self.root.title("PLSR 单轴脉冲测试台 - Python")
- self.root.geometry("1120x900")
- self.root.minsize(980, 820)
- self.root.configure(bg="#F3F6FA")
-
- style = ttk.Style(self.root)
- if "clam" in style.theme_names():
- style.theme_use("clam")
- style.configure("TFrame", background="#F3F6FA")
- style.configure("Card.TFrame", background="#FFFFFF")
- style.configure(
- "TLabel", background="#F3F6FA", foreground="#172033", font=("Microsoft YaHei UI", 10)
- )
- style.configure(
- "Card.TLabel", background="#FFFFFF", foreground="#172033", font=("Microsoft YaHei UI", 10)
- )
- style.configure(
- "TLabelframe", background="#FFFFFF", bordercolor="#D8E1EB", relief="solid"
- )
- style.configure(
- "TLabelframe.Label", background="#FFFFFF", foreground="#29445F", font=("Microsoft YaHei UI", 10, "bold")
- )
- style.configure("TButton", font=("Microsoft YaHei UI", 10), padding=(12, 7))
- style.configure("Primary.TButton", background="#1769AA", foreground="#FFFFFF")
- style.map("Primary.TButton", background=[("active", "#0F5B98"), ("disabled", "#CBD4DE")])
- style.configure("Danger.TButton", background="#D64545", foreground="#FFFFFF")
- style.map("Danger.TButton", background=[("active", "#BE3434"), ("disabled", "#CBD4DE")])
- style.configure("Secondary.TButton", background="#E5ECF4", foreground="#29445F")
- style.map("Secondary.TButton", background=[("active", "#D6E1EC")])
- style.configure("TNotebook", background="#F3F6FA", borderwidth=0)
- style.configure("TNotebook.Tab", padding=(16, 9), font=("Microsoft YaHei UI", 10))
- style.map(
- "TNotebook.Tab",
- background=[("selected", "#FFFFFF"), ("!selected", "#E4EAF1")],
- foreground=[("selected", "#15558A"), ("!selected", "#53677C")],
- )
- style.configure("Treeview", rowheight=54, font=("Microsoft YaHei UI", 9))
- style.configure("Treeview.Heading", font=("Microsoft YaHei UI", 9, "bold"), background="#E8EEF5")
- style.configure("Horizontal.TProgressbar", troughcolor="#E7EDF3", background="#2B83D5")
-
- def _build_ui(self) -> None:
- container = ttk.Frame(self.root, padding=(22, 17, 22, 20))
- container.pack(fill=tk.BOTH, expand=True)
-
- connection = ttk.LabelFrame(container, text="串口连接", padding=(14, 11))
- connection.pack(fill=tk.X, pady=(0, 12))
- connection.columnconfigure(8, weight=1)
- ttk.Label(connection, text="端口", style="Card.TLabel").grid(row=0, column=0, padx=(0, 6))
- self.port_var = tk.StringVar()
- self.port_combo = ttk.Combobox(connection, textvariable=self.port_var, state="readonly", width=8)
- self.port_combo.grid(row=0, column=1, padx=(0, 6))
- self.refresh_button = ttk.Button(
- connection, text="刷新", width=5,
- style="Secondary.TButton", command=self.refresh_ports
- )
- self.refresh_button.grid(row=0, column=2, padx=(0, 10))
- ttk.Label(connection, text="波特率", style="Card.TLabel").grid(row=0, column=3, padx=(0, 6))
- self.baud_var = tk.StringVar(value=str(DEFAULT_BAUD_RATE))
- self.baud_combo = ttk.Combobox(
- connection,
- textvariable=self.baud_var,
- values=("115200",),
- state="readonly",
- width=7,
- )
- self.baud_combo.grid(row=0, column=4, padx=(0, 10))
- ttk.Label(connection, text="格式 8-E-1", style="Card.TLabel").grid(row=0, column=5, padx=(0, 10))
- ttk.Label(connection, text="从站", style="Card.TLabel").grid(row=0, column=6, padx=(0, 6))
- self.slave_var = tk.IntVar(value=DEFAULT_SLAVE_ADDRESS)
- self.slave_spin = ttk.Spinbox(connection, from_=1, to=247, textvariable=self.slave_var, width=4)
- self.slave_spin.grid(row=0, column=7, padx=(0, 10))
- connection_actions = ttk.Frame(connection, style="Card.TFrame")
- connection_actions.grid(row=0, column=8, sticky="e")
- self.connect_button = ttk.Button(
- connection_actions,
- text="连接设备",
- style="Primary.TButton",
- command=self.toggle_connection,
- )
- self.connect_button.pack(side=tk.LEFT)
- self.connection_badge = tk.Label(
- connection_actions,
- text="未连接",
- bg="#FDECEC",
- fg="#A61B1B",
- font=("Microsoft YaHei UI", 10, "bold"),
- padx=10,
- pady=7,
- )
- self.connection_badge.pack(side=tk.LEFT, padx=(10, 0))
-
- notebook = ttk.Notebook(container)
- notebook.pack(fill=tk.BOTH, expand=True)
- self.control_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
- self.diagnostic_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
- self.log_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
- notebook.add(self.control_page, text="控制、状态与多段脉冲")
- notebook.add(self.diagnostic_page, text="输出诊断")
- notebook.add(self.log_page, text="通信日志")
-
- # 单段控制、实时状态和多段参数统一放进同一个可滚动页面。
- # 内容按实际需要的高度排布,避免10段表格挤压上方按钮或撑大窗口。
- self.control_page.columnconfigure(0, weight=1)
- self.control_page.rowconfigure(0, weight=1)
- self.control_canvas = tk.Canvas(
- self.control_page,
- bg="#F3F6FA",
- highlightthickness=0,
- borderwidth=0,
- )
- control_scrollbar = ttk.Scrollbar(
- self.control_page,
- orient=tk.VERTICAL,
- command=self.control_canvas.yview,
- )
- self.control_canvas.configure(yscrollcommand=control_scrollbar.set)
- self.control_canvas.grid(row=0, column=0, sticky="nsew")
- control_scrollbar.grid(row=0, column=1, sticky="ns")
-
- self.control_content = ttk.Frame(self.control_canvas)
- self.control_window = self.control_canvas.create_window(
- (0, 0), window=self.control_content, anchor="nw"
- )
- self.control_canvas.bind("<Configure>", self._resize_control_content)
- self.control_content.bind("<Configure>", self._update_control_scrollregion)
- # 鼠标进入整个合并页后启用滚轮;进入Canvas中的子控件时不会失效。
- self.control_page.bind("<Enter>", self._enable_control_mousewheel)
- self.control_page.bind("<Leave>", self._disable_control_mousewheel)
-
- self._build_control_page()
- self._build_multi_page()
- self._build_diagnostic_page()
- self._build_log_page()
-
- def _resize_control_content(self, event) -> None:
- """让滚动页内容始终占满可见宽度。"""
- self.control_canvas.itemconfigure(self.control_window, width=event.width)
-
- def _update_control_scrollregion(self, _event=None) -> None:
- """单段或多段区域尺寸变化后刷新垂直滚动范围。"""
- self.control_canvas.configure(scrollregion=self.control_canvas.bbox("all"))
-
- def _enable_control_mousewheel(self, _event=None) -> None:
- self.root.bind_all("<MouseWheel>", self._scroll_control_page)
-
- def _disable_control_mousewheel(self, _event=None) -> None:
- self.root.unbind_all("<MouseWheel>")
-
- def _scroll_control_page(self, event) -> None:
- if event.delta != 0:
- self.control_canvas.yview_scroll(int(-event.delta / 120), "units")
-
- def _build_control_page(self) -> None:
- """建立公共参数、10段参数和实时状态组成的主操作页。"""
- self.control_content.columnconfigure(0, weight=3, uniform="motion")
- self.control_content.columnconfigure(1, weight=2, uniform="motion")
- # 左右区域位于同一可伸缩网格行,边框高度始终保持一致。
- self.control_content.rowconfigure(1, weight=1)
-
- common = ttk.LabelFrame(
- self.control_content, text="公共参数", padding=(14, 10)
- )
- common.grid(
- row=0, column=0, columnspan=2, sticky="nsew", pady=(0, 10)
- )
- for column in (1, 3, 5):
- common.columnconfigure(column, weight=1)
-
- # 这些变量仍由既有通信、Flash同步和参数校验流程统一使用。
- self.preset_var = tk.StringVar(value=PRESETS[0]["name"])
- self.pulses_var = tk.StringVar(value="10")
- self.run_mode_var = tk.StringVar(value=MODE_TEXT[RunMode.RELATIVE])
- self.target_caption_var = tk.StringVar(value="目标脉冲数")
- self.direction_var = tk.StringVar(value=DIRECTION_TEXT[0])
- self.profile_var = tk.StringVar(value=PROFILE_TEXT[ProfileType.LINEAR])
-
- self.direction_logic_var = tk.StringVar(
- value=DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
- )
- self.output_mode_var = tk.StringVar(
- value=OUTPUT_MODE_TEXT[OutputMode.PULSE_DIRECTION]
- )
- self.send_mode_var = tk.StringVar(value=SEND_MODE_TEXT[SendMode.COMPLETE])
- self.pulse_point_var = tk.StringVar(value=PULSE_POINTS[0])
- self.secondary_point_var = tk.StringVar(value=SECONDARY_POINTS[0])
- self.frequency_var = tk.IntVar(value=1)
- self.runtime_frequency_var = tk.IntVar(value=1)
- self.acceleration_var = tk.IntVar(value=1000)
- self.deceleration_var = tk.IntVar(value=1000)
- self.maximum_frequency_var = tk.IntVar(value=MAX_FREQUENCY_HZ)
- self.start_frequency_var = tk.IntVar(value=100)
- self.end_frequency_var = tk.IntVar(value=100)
-
- def add_label(row: int, column: int, text: str) -> None:
- ttk.Label(common, text=text, style="Card.TLabel").grid(
- row=row, column=column, sticky="w", padx=(0, 8), pady=5
- )
-
- add_label(0, 0, "脉冲方向逻辑")
- self.direction_logic_combo = ttk.Combobox(
- common,
- textvariable=self.direction_logic_var,
- values=tuple(DIRECTION_LOGIC_TEXT[item] for item in DirectionLogic),
- state="readonly",
- width=24,
- )
- self.direction_logic_combo.grid(row=0, column=1, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(0, 2, "脉冲类型")
- self.output_mode_combo = ttk.Combobox(
- common,
- textvariable=self.output_mode_var,
- values=tuple(OUTPUT_MODE_TEXT.values()),
- state="readonly",
- width=23,
- )
- self.output_mode_combo.grid(row=0, column=3, sticky="ew", padx=(0, 18), pady=5)
- self.output_mode_combo.bind("<<ComboboxSelected>>", self._on_output_mode_changed)
-
- add_label(0, 4, "脉冲发送模式")
- self.send_mode_combo = ttk.Combobox(
- common,
- textvariable=self.send_mode_var,
- values=tuple(SEND_MODE_TEXT[item] for item in SendMode),
- state="readonly",
- width=24,
- )
- self.send_mode_combo.grid(row=0, column=5, sticky="ew", pady=5)
-
- add_label(1, 0, "脉冲输出端子")
- self.pulse_point_combo = ttk.Combobox(
- common,
- textvariable=self.pulse_point_var,
- values=list(PULSE_POINTS.values()),
- state="readonly",
- width=24,
- )
- self.pulse_point_combo.grid(row=1, column=1, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(1, 2, "脉冲方向端子")
- self.direction_point_combo = ttk.Combobox(
- common,
- textvariable=self.secondary_point_var,
- values=list(SECONDARY_POINTS.values()),
- state="readonly",
- width=23,
- )
- self.direction_point_combo.grid(row=1, column=3, sticky="ew", padx=(0, 18), pady=5)
- self.save_outputs_button = ttk.Button(
- common,
- text="保存公共参数",
- style="Primary.TButton",
- command=self.save_output_mapping,
- )
- self.save_outputs_button.grid(row=1, column=4, columnspan=2, sticky="ew", pady=5)
-
- add_label(2, 0, "脉冲默认速度(Hz)")
- self.frequency_spin = ttk.Spinbox(
- common,
- from_=MIN_FREQUENCY_HZ,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.frequency_var,
- )
- self.frequency_spin.grid(row=2, column=1, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(2, 2, "脉冲默认加速时间(ms)")
- self.acceleration_spin = ttk.Spinbox(
- common, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.acceleration_var
- )
- self.acceleration_spin.grid(row=2, column=3, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(2, 4, "脉冲默认减速时间(ms)")
- self.deceleration_spin = ttk.Spinbox(
- common, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.deceleration_var
- )
- self.deceleration_spin.grid(row=2, column=5, sticky="ew", pady=5)
-
- add_label(3, 0, "最高速度(Hz)")
- self.maximum_frequency_spin = ttk.Spinbox(
- common,
- from_=MIN_FREQUENCY_HZ,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.maximum_frequency_var,
- )
- self.maximum_frequency_spin.grid(row=3, column=1, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(3, 2, "起始速度(Hz)")
- self.start_frequency_spin = ttk.Spinbox(
- common,
- from_=0,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.start_frequency_var,
- )
- self.start_frequency_spin.grid(row=3, column=3, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(3, 4, "终止速度(Hz)")
- self.end_frequency_spin = ttk.Spinbox(
- common,
- from_=0,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.end_frequency_var,
- )
- self.end_frequency_spin.grid(row=3, column=5, sticky="ew", pady=5)
-
- add_label(4, 0, "脉冲加减速模式")
- self.profile_combo = ttk.Combobox(
- common,
- textvariable=self.profile_var,
- values=tuple(PROFILE_TEXT[item] for item in ProfileType),
- state="readonly",
- width=24,
- )
- self.profile_combo.grid(row=4, column=1, sticky="ew", padx=(0, 18), pady=5)
-
- add_label(5, 0, "运行中目标频率(Hz)")
- runtime_frequency_box = ttk.Frame(common, style="Card.TFrame")
- runtime_frequency_box.grid(row=5, column=1, sticky="ew", padx=(0, 18), pady=5)
- runtime_frequency_box.columnconfigure(0, weight=1)
- self.runtime_frequency_spin = ttk.Spinbox(
- runtime_frequency_box,
- from_=MIN_FREQUENCY_HZ,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.runtime_frequency_var,
- width=11,
- )
- self.runtime_frequency_spin.grid(row=0, column=0, sticky="ew")
- self.update_frequency_button = ttk.Button(
- runtime_frequency_box,
- text="立即修改当前段",
- style="Secondary.TButton",
- command=self.update_frequency,
- )
- self.update_frequency_button.grid(row=0, column=1, padx=(6, 0))
- ttk.Label(
- common,
- text="仅影响本次当前段,不修改默认速度、段表或Flash",
- style="Card.TLabel",
- foreground="#5B7083",
- ).grid(row=5, column=2, columnspan=4, sticky="w", pady=5)
-
- status = ttk.LabelFrame(self.control_content, text="实时状态", padding=16)
- status.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
- status.columnconfigure(1, weight=1)
- self.state_badge = tk.Label(
- status,
- text="空闲",
- bg="#E8EEF5",
- fg="#425466",
- font=("Microsoft YaHei UI", 10, "bold"),
- padx=13,
- pady=6,
- )
- ttk.Label(status, text="运行状态", style="Card.TLabel").grid(
- row=0, column=0, sticky="w", pady=6
- )
- self.state_badge.grid(row=0, column=1, sticky="e", pady=6)
-
- self.error_var = tk.StringVar(value="0 · 无错误")
- self.direction_status_var = tk.StringVar(value=DIRECTION_TEXT[0])
- self.current_pulses_var = tk.StringVar(value="0")
- self.current_frequency_var = tk.StringVar(value="0 Hz")
- self.actual_frequency_var = tk.StringVar(value="0 Hz")
- self.running_segment_var = tk.StringVar(value="未运行")
- self.cumulative_position_var = tk.StringVar(value="0")
- self.total_pulses_var = tk.StringVar(value="0")
- status_rows = [
- ("错误码", self.error_var),
- ("方向", self.direction_status_var),
- ("当前设定频率", self.current_frequency_var),
- ("运行中修改后的实际频率", self.actual_frequency_var),
- ("当前运行段", self.running_segment_var),
- ("当前Y点累计位置", self.cumulative_position_var),
- ("总脉冲数(不计方向)", self.total_pulses_var),
- ]
- for index, (caption, variable) in enumerate(status_rows, start=1):
- ttk.Label(status, text=caption, style="Card.TLabel").grid(
- row=index, column=0, sticky="w", pady=7
- )
- ttk.Label(
- status,
- textvariable=variable,
- style="Card.TLabel",
- foreground="#183B56",
- font=("Microsoft YaHei UI", 10, "bold"),
- ).grid(row=index, column=1, sticky="e", pady=7)
-
- ttk.Label(status, text="本次运动进度", style="Card.TLabel").grid(
- row=8, column=0, columnspan=2, sticky="w", pady=(14, 6)
- )
- self.progress_var = tk.DoubleVar(value=0.0)
- self.progress = ttk.Progressbar(
- status,
- maximum=100.0,
- variable=self.progress_var,
- style="Horizontal.TProgressbar",
- )
- self.progress.grid(row=9, column=0, columnspan=2, sticky="ew")
- self.pulse_fraction_var = tk.StringVar(value="0 / 0")
- ttk.Label(
- status,
- textvariable=self.pulse_fraction_var,
- style="Card.TLabel",
- foreground="#6E7F91",
- ).grid(row=10, column=0, columnspan=2, sticky="e", pady=(4, 10))
-
- status_actions = ttk.Frame(status, style="Card.TFrame")
- status_actions.grid(row=11, column=0, columnspan=2, sticky="ew")
- for column in range(3):
- status_actions.columnconfigure(column, weight=1)
- self.read_button = ttk.Button(
- status_actions,
- text="立即读取状态",
- style="Secondary.TButton",
- command=self.request_status,
- )
- self.read_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
- self.clear_position_button = ttk.Button(
- status_actions,
- text="清零当前Y点位置",
- style="Danger.TButton",
- command=self.clear_current_position,
- )
- self.clear_position_button.grid(row=0, column=1, sticky="ew", padx=4)
- self.clear_total_button = ttk.Button(
- status_actions,
- text="清零总脉冲数",
- style="Danger.TButton",
- command=self.clear_total_count,
- )
- self.clear_total_button.grid(row=0, column=2, sticky="ew", padx=(4, 0))
-
- def _build_control_page_legacy(self) -> None:
- self.control_content.columnconfigure(0, weight=1, uniform="control")
- self.control_content.columnconfigure(1, weight=1, uniform="control")
-
- params = ttk.LabelFrame(self.control_content, text="测试参数", padding=(14, 10))
- params.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
- params.columnconfigure(1, weight=1)
-
- mapping = tk.Frame(params, bg="#EAF4FF", padx=10, pady=7)
- mapping.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 8))
- mapping.columnconfigure(1, weight=1)
- mapping.columnconfigure(3, weight=1)
- tk.Label(
- mapping,
- text="输出方式与端子映射(保存到设备Flash)",
- bg="#EAF4FF",
- fg="#15558A",
- anchor="w",
- font=("Microsoft YaHei UI", 10, "bold"),
- ).grid(row=0, column=0, columnspan=5, sticky="w", pady=(0, 7))
- tk.Label(mapping, text="脉冲/A相", bg="#EAF4FF", fg="#29445F").grid(
- row=1, column=0, sticky="w", padx=(0, 5)
- )
- self.pulse_point_var = tk.StringVar(value=PULSE_POINTS[0])
- self.pulse_point_combo = ttk.Combobox(
- mapping,
- textvariable=self.pulse_point_var,
- values=list(PULSE_POINTS.values()),
- state="readonly",
- width=21,
- )
- self.pulse_point_combo.grid(row=1, column=1, sticky="ew", padx=(0, 9))
- tk.Label(mapping, text="方向/B相", bg="#EAF4FF", fg="#29445F").grid(
- row=1, column=2, sticky="w", padx=(0, 5)
- )
- # 第二输出点在协议中仍使用0~3编码,分别对应Y12~Y15。
- self.secondary_point_var = tk.StringVar(value=SECONDARY_POINTS[0])
- self.direction_point_combo = ttk.Combobox(
- mapping,
- textvariable=self.secondary_point_var,
- values=list(SECONDARY_POINTS.values()),
- state="readonly",
- width=13,
- )
- self.direction_point_combo.grid(row=1, column=3, sticky="ew", padx=(0, 9))
- self.save_outputs_button = ttk.Button(
- mapping,
- text="应用并保存",
- style="Primary.TButton",
- command=self.save_output_mapping,
- )
- self.save_outputs_button.grid(row=1, column=4, sticky="ew")
- tk.Label(mapping, text="输出方式", bg="#EAF4FF", fg="#29445F").grid(
- row=2, column=0, sticky="w", padx=(0, 5), pady=(7, 0)
- )
- self.output_mode_var = tk.StringVar(
- value=OUTPUT_MODE_TEXT[OutputMode.PULSE_DIRECTION]
- )
- self.output_mode_combo = ttk.Combobox(
- mapping,
- textvariable=self.output_mode_var,
- values=tuple(OUTPUT_MODE_TEXT.values()),
- state="readonly",
- )
- self.output_mode_combo.grid(
- row=2, column=1, columnspan=2, sticky="ew", padx=(0, 9), pady=(7, 0)
- )
- self.output_mode_combo.bind("<<ComboboxSelected>>", self._on_output_mode_changed)
- tk.Label(mapping, text="发送模式", bg="#EAF4FF", fg="#29445F").grid(
- row=2, column=3, sticky="e", padx=(0, 5), pady=(7, 0)
- )
- self.send_mode_var = tk.StringVar(value=SEND_MODE_TEXT[SendMode.COMPLETE])
- self.send_mode_combo = ttk.Combobox(
- mapping,
- textvariable=self.send_mode_var,
- values=tuple(SEND_MODE_TEXT[item] for item in SendMode),
- state="readonly",
- width=24,
- )
- self.send_mode_combo.grid(row=2, column=4, sticky="ew", pady=(7, 0))
- tk.Label(mapping, text="方向逻辑", bg="#EAF4FF", fg="#29445F").grid(
- row=3, column=0, sticky="w", padx=(0, 5), pady=(7, 0)
- )
- self.direction_logic_var = tk.StringVar(
- value=DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
- )
- self.direction_logic_combo = ttk.Combobox(
- mapping,
- textvariable=self.direction_logic_var,
- values=tuple(DIRECTION_LOGIC_TEXT[item] for item in DirectionLogic),
- state="readonly",
- )
- self.direction_logic_combo.grid(
- row=3, column=1, columnspan=4, sticky="ew", pady=(7, 0)
- )
- self.output_mode_hint = tk.Label(
- mapping,
- text="",
- bg="#EAF4FF",
- fg="#476A89",
- anchor="w",
- )
- # 详细说明通过选择项本身表达,不再单独占用一行,避免小屏幕裁掉运行按钮。
- self._on_output_mode_changed()
-
- ttk.Label(params, text="测试预设", style="Card.TLabel").grid(row=1, column=0, sticky="w", pady=4)
- self.preset_var = tk.StringVar(value=PRESETS[0]["name"])
- self.preset_combo = ttk.Combobox(
- params,
- textvariable=self.preset_var,
- values=[item["name"] for item in PRESETS],
- state="readonly",
- )
- self.preset_combo.grid(row=1, column=1, sticky="ew", padx=10, pady=4)
- ttk.Button(params, text="应用", style="Secondary.TButton", command=self.apply_preset).grid(
- row=1, column=2, pady=4
- )
-
- ttk.Label(params, text="运行模式", style="Card.TLabel").grid(row=2, column=0, sticky="w", pady=4)
- self.run_mode_var = tk.StringVar(value=MODE_TEXT[RunMode.RELATIVE])
- self.run_mode_combo = ttk.Combobox(
- params,
- textvariable=self.run_mode_var,
- values=(MODE_TEXT[RunMode.RELATIVE], MODE_TEXT[RunMode.ABSOLUTE]),
- state="readonly",
- )
- self.run_mode_combo.grid(row=2, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
- self.run_mode_combo.bind("<<ComboboxSelected>>", self._on_run_mode_changed)
-
- self.target_caption_var = tk.StringVar(value="目标脉冲数")
- ttk.Label(params, textvariable=self.target_caption_var, style="Card.TLabel").grid(row=3, column=0, sticky="w", pady=4)
- self.pulses_var = tk.StringVar(value="10")
- self.pulses_entry = ttk.Entry(params, textvariable=self.pulses_var)
- self.pulses_entry.grid(row=3, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="默认速度/匀速(Hz)", style="Card.TLabel").grid(row=4, column=0, sticky="w", pady=4)
- self.frequency_var = tk.IntVar(value=1)
- self.frequency_spin = ttk.Spinbox(
- params,
- from_=MIN_FREQUENCY_HZ,
- to=MAX_FREQUENCY_HZ,
- textvariable=self.frequency_var,
- )
- self.frequency_spin.grid(row=4, column=1, sticky="ew", padx=10, pady=4)
- self.update_frequency_button = ttk.Button(
- params,
- text="立即修改默认速度",
- style="Secondary.TButton",
- command=self.update_frequency,
- )
- self.update_frequency_button.grid(row=4, column=2, sticky="ew", pady=4)
-
- ttk.Label(params, text="起始速度(Hz)", style="Card.TLabel").grid(row=5, column=0, sticky="w", pady=4)
- self.start_frequency_var = tk.IntVar(value=100)
- self.start_frequency_spin = ttk.Spinbox(
- params, from_=0, to=MAX_FREQUENCY_HZ,
- textvariable=self.start_frequency_var
- )
- self.start_frequency_spin.grid(row=5, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="终止速度(Hz)", style="Card.TLabel").grid(row=6, column=0, sticky="w", pady=4)
- self.end_frequency_var = tk.IntVar(value=100)
- self.end_frequency_spin = ttk.Spinbox(
- params, from_=0, to=MAX_FREQUENCY_HZ,
- textvariable=self.end_frequency_var
- )
- self.end_frequency_spin.grid(row=6, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="加减速模式", style="Card.TLabel").grid(row=7, column=0, sticky="w", pady=4)
- self.profile_var = tk.StringVar(value=PROFILE_TEXT[ProfileType.LINEAR])
- self.profile_combo = ttk.Combobox(
- params,
- textvariable=self.profile_var,
- values=tuple(PROFILE_TEXT[item] for item in ProfileType),
- state="readonly",
- )
- self.profile_combo.grid(row=7, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="加速时间(ms)", style="Card.TLabel").grid(row=8, column=0, sticky="w", pady=4)
- self.acceleration_var = tk.IntVar(value=1000)
- self.acceleration_spin = ttk.Spinbox(
- params, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.acceleration_var
- )
- self.acceleration_spin.grid(row=8, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="减速时间(ms)", style="Card.TLabel").grid(row=9, column=0, sticky="w", pady=4)
- self.deceleration_var = tk.IntVar(value=1000)
- self.deceleration_spin = ttk.Spinbox(
- params, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.deceleration_var
- )
- self.deceleration_spin.grid(row=9, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- ttk.Label(params, text="运动方向", style="Card.TLabel").grid(row=10, column=0, sticky="w", pady=4)
- self.direction_var = tk.StringVar(value=DIRECTION_TEXT[0])
- self.direction_combo = ttk.Combobox(
- params,
- textvariable=self.direction_var,
- values=(DIRECTION_TEXT[0], DIRECTION_TEXT[1]),
- state="readonly",
- )
- self.direction_combo.grid(row=10, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
-
- button_box = ttk.Frame(params, style="Card.TFrame")
- button_box.grid(row=11, column=0, columnspan=3, sticky="ew", pady=(8, 0))
- button_box.columnconfigure(0, weight=2)
- button_box.columnconfigure(1, weight=1)
- button_box.columnconfigure(2, weight=1)
- self.start_button = ttk.Button(
- button_box,
- text="写参数并启动",
- style="Primary.TButton",
- command=self.start_motion,
- )
- self.start_button.grid(row=0, column=0, sticky="ew", padx=(0, 6))
- self.stop_button = ttk.Button(
- button_box,
- text="暂停发送",
- style="Danger.TButton",
- command=self.stop_motion,
- )
- self.stop_button.grid(row=0, column=1, sticky="ew", padx=6)
- self.resume_button = ttk.Button(
- button_box,
- text="继续当前脉冲",
- style="Secondary.TButton",
- command=self.resume_motion,
- )
- self.resume_button.grid(row=0, column=2, sticky="ew", padx=(0, 0))
-
- status = ttk.LabelFrame(self.control_content, text="实时状态", padding=16)
- status.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
- status.columnconfigure(1, weight=1)
- self.state_badge = tk.Label(
- status,
- text="空闲",
- bg="#E8EEF5",
- fg="#425466",
- font=("Microsoft YaHei UI", 10, "bold"),
- padx=13,
- pady=6,
- )
- ttk.Label(status, text="运行状态", style="Card.TLabel").grid(row=0, column=0, sticky="w", pady=6)
- self.state_badge.grid(row=0, column=1, sticky="e", pady=6)
-
- self.error_var = tk.StringVar(value="0 · 无错误")
- self.direction_status_var = tk.StringVar(value=DIRECTION_TEXT[0])
- self.current_pulses_var = tk.StringVar(value="0")
- self.current_frequency_var = tk.StringVar(value="0 Hz")
- self.actual_frequency_var = tk.StringVar(value="0 Hz")
- self.running_segment_var = tk.StringVar(value="未运行")
- self.cumulative_position_var = tk.StringVar(value="0")
- self.total_pulses_var = tk.StringVar(value="0")
- status_rows = [
- ("错误码", self.error_var),
- ("方向", self.direction_status_var),
- ("已发送脉冲", self.current_pulses_var),
- ("当前设定频率", self.current_frequency_var),
- ("运行中修改后的实际频率", self.actual_frequency_var),
- ("当前运行段", self.running_segment_var),
- ("当前Y点累计位置", self.cumulative_position_var),
- ("总脉冲数(不计方向)", self.total_pulses_var),
- ]
- for index, (caption, variable) in enumerate(status_rows, start=1):
- ttk.Label(status, text=caption, style="Card.TLabel").grid(row=index, column=0, sticky="w", pady=7)
- ttk.Label(
- status,
- textvariable=variable,
- style="Card.TLabel",
- foreground="#183B56",
- font=("Microsoft YaHei UI", 10, "bold"),
- ).grid(row=index, column=1, sticky="e", pady=7)
-
- ttk.Label(status, text="本次运动进度", style="Card.TLabel").grid(
- row=9, column=0, columnspan=2, sticky="w", pady=(14, 6)
- )
- self.progress_var = tk.DoubleVar(value=0.0)
- self.progress = ttk.Progressbar(
- status,
- maximum=100.0,
- variable=self.progress_var,
- style="Horizontal.TProgressbar",
- )
- self.progress.grid(row=10, column=0, columnspan=2, sticky="ew")
- self.pulse_fraction_var = tk.StringVar(value="0 / 0")
- ttk.Label(
- status,
- textvariable=self.pulse_fraction_var,
- style="Card.TLabel",
- foreground="#6E7F91",
- ).grid(row=11, column=0, columnspan=2, sticky="e", pady=(4, 10))
-
- self.test_result_label = tk.Label(
- status,
- text="等待测试",
- bg="#EDF1F5",
- fg="#526170",
- anchor="w",
- justify="left",
- wraplength=430,
- padx=11,
- pady=10,
- font=("Microsoft YaHei UI", 9),
- )
- self.test_result_label.grid(row=12, column=0, columnspan=2, sticky="ew", pady=(3, 11))
-
- status_actions = ttk.Frame(status, style="Card.TFrame")
- status_actions.grid(row=13, column=0, columnspan=2, sticky="ew")
- status_actions.columnconfigure(0, weight=1)
- status_actions.columnconfigure(1, weight=1)
- status_actions.columnconfigure(2, weight=1)
- self.read_button = ttk.Button(
- status_actions,
- text="立即读取状态",
- style="Secondary.TButton",
- command=self.request_status,
- )
- self.read_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
- self.clear_position_button = ttk.Button(
- status_actions,
- text="清零当前Y点位置",
- style="Danger.TButton",
- command=self.clear_current_position,
- )
- self.clear_position_button.grid(row=0, column=1, sticky="ew", padx=4)
- self.clear_total_button = ttk.Button(
- status_actions,
- text="清零总脉冲数",
- style="Danger.TButton",
- command=self.clear_total_count,
- )
- self.clear_total_button.grid(row=0, column=2, sticky="ew", padx=(4, 0))
-
- def _build_multi_page(self) -> None:
- """建立1~10段脉冲表;脉冲数符号决定各段运动方向。"""
- self.multi_page = ttk.Frame(self.control_content)
- self.multi_page.grid(row=1, column=0, sticky="nsew", padx=(0, 8))
- card = ttk.LabelFrame(self.multi_page, text="10段参数配置", padding=12)
- card.pack(fill=tk.BOTH, expand=True)
- card.columnconfigure(0, weight=1)
-
- header = ttk.Frame(card, style="Card.TFrame")
- header.grid(row=0, column=0, sticky="ew", pady=(0, 12))
- ttk.Label(header, text="脉冲总段数", style="Card.TLabel").pack(side=tk.LEFT)
- self.total_segments_var = tk.IntVar(value=3)
- ttk.Spinbox(header, from_=1, to=10, width=7,
- textvariable=self.total_segments_var).pack(side=tk.LEFT, padx=(8, 24))
- ttk.Label(header, text="起始执行段数", style="Card.TLabel").pack(side=tk.LEFT)
- self.start_segment_var = tk.IntVar(value=1)
- ttk.Spinbox(header, from_=1, to=10, width=7,
- textvariable=self.start_segment_var).pack(side=tk.LEFT, padx=(8, 24))
- self.current_segment_var = tk.StringVar(value="0(未执行)")
- ttk.Label(header, text="当前段", style="Card.TLabel").pack(side=tk.LEFT)
- ttk.Label(header, textvariable=self.current_segment_var,
- style="Card.TLabel", foreground="#15558A").pack(side=tk.LEFT, padx=8)
-
- table = ttk.Frame(card, style="Card.TFrame")
- table.grid(row=1, column=0, sticky="nsew")
- headings = ("段", "有符号脉冲数", "频率(Hz)", "跳转段", "等待条件", "条件值(ms/X点)")
- for column, text in enumerate(headings):
- table.columnconfigure(column, weight=1 if column in (1, 2, 4, 5) else 0)
- ttk.Label(table, text=text, style="Card.TLabel",
- font=("Microsoft YaHei UI", 9, "bold")).grid(
- row=0, column=column, padx=4, pady=4, sticky="ew")
-
- self.segment_pulse_vars = []
- self.segment_frequency_vars = []
- self.segment_jump_vars = []
- self.segment_wait_vars = []
- self.segment_condition_vars = []
- for index in range(10):
- pulses = tk.StringVar(value=str((index + 1) * 100))
- frequency = tk.StringVar(value=str((index + 1) * 100))
- self.segment_pulse_vars.append(pulses)
- self.segment_frequency_vars.append(frequency)
- jump = tk.IntVar(value=0)
- wait = tk.StringVar(
- value=WAIT_CONDITION_TEXT[WaitCondition.WAIT_TIME]
- )
- condition = tk.StringVar(value="0")
- self.segment_jump_vars.append(jump)
- self.segment_wait_vars.append(wait)
- self.segment_condition_vars.append(condition)
- row = index + 1
- ttk.Label(table, text=str(index + 1), style="Card.TLabel").grid(
- row=row, column=0, padx=4, pady=3)
- ttk.Entry(table, textvariable=pulses, width=11).grid(
- row=row, column=1, padx=4, pady=3, sticky="ew")
- ttk.Entry(table, textvariable=frequency, width=9).grid(
- row=row, column=2, padx=4, pady=3, sticky="ew")
- ttk.Spinbox(table, from_=0, to=10, textvariable=jump, width=5).grid(
- row=row, column=3, padx=4, pady=3, sticky="ew")
- ttk.Combobox(table, textvariable=wait,
- values=tuple(WAIT_CONDITION_TEXT.values()), state="readonly",
- width=13).grid(row=row, column=4, padx=4, pady=3, sticky="ew")
- ttk.Entry(table, textvariable=condition, width=10).grid(
- row=row, column=5, padx=4, pady=3, sticky="ew")
-
- segment_actions = ttk.Frame(card, style="Card.TFrame")
- segment_actions.grid(row=2, column=0, sticky="ew", pady=(14, 0))
- segment_actions.columnconfigure(0, weight=1)
- segment_actions.columnconfigure(1, weight=1)
- segment_actions.columnconfigure(2, weight=1)
- segment_actions.columnconfigure(3, weight=1)
- self.write_segments_button = ttk.Button(
- segment_actions, text="写入段表", style="Secondary.TButton",
- command=self.write_segment_table,
- )
- self.write_segments_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
- self.start_segments_button = ttk.Button(
- segment_actions, text="启动", style="Primary.TButton",
- command=self.start_segment_motion,
- )
- self.start_segments_button.grid(row=0, column=1, sticky="ew", padx=4)
- self.stop_button = ttk.Button(
- segment_actions, text="暂停", style="Danger.TButton",
- command=self.stop_motion,
- )
- self.stop_button.grid(row=0, column=2, sticky="ew", padx=4)
- self.resume_button = ttk.Button(
- segment_actions, text="继续", style="Secondary.TButton",
- command=self.resume_motion,
- )
- self.resume_button.grid(row=0, column=3, sticky="ew", padx=(4, 0))
-
- def _build_diagnostic_page(self) -> None:
- """构建固件在线诊断结果页。"""
- card = ttk.LabelFrame(
- self.diagnostic_page, text="PLSR输出信号在线诊断", padding=(18, 14)
- )
- card.pack(fill=tk.BOTH, expand=True)
- card.columnconfigure(1, weight=1)
-
- self.diag_overall_var = tk.StringVar(value="未运行诊断")
- self.diag_count_var = tk.StringVar(value="未诊断")
- self.diag_frequency_var = tk.StringVar(value="未诊断")
- self.diag_profile_var = tk.StringVar(value="未诊断")
- self.diag_phase_var = tk.StringVar(value="空闲")
- self.diag_pulses_var = tk.StringVar(value="0 / 0")
- self.diag_frequency_values_var = tk.StringVar(value="0 Hz / 0 Hz")
- self.diag_max_error_var = tk.StringVar(value="0 Hz")
- self.diag_error_counts_var = tk.StringVar(value="个数0,频率0,曲线0")
-
- rows = [
- ("综合结果", self.diag_overall_var),
- ("脉冲个数", self.diag_count_var),
- ("脉冲频率", self.diag_frequency_var),
- ("加减速曲线", self.diag_profile_var),
- ("当前曲线阶段", self.diag_phase_var),
- ("实际/设定脉冲", self.diag_pulses_var),
- ("实际/设定频率", self.diag_frequency_values_var),
- ("最大频率偏差", self.diag_max_error_var),
- ("异常记录", self.diag_error_counts_var),
- ]
- for row, (caption, variable) in enumerate(rows):
- ttk.Label(card, text=caption).grid(
- row=row, column=0, sticky="w", padx=(0, 24), pady=7
- )
- ttk.Label(card, textvariable=variable).grid(
- row=row, column=1, sticky="w", pady=7
- )
-
- actions = ttk.Frame(card)
- actions.grid(row=len(rows), column=0, columnspan=2, sticky="ew", pady=(14, 8))
- actions.columnconfigure(0, weight=1)
- actions.columnconfigure(1, weight=1)
- ttk.Button(
- actions, text="立即读取诊断", style="Secondary.TButton",
- command=self.request_diagnostic,
- ).grid(row=0, column=0, sticky="ew", padx=(0, 6))
- ttk.Button(
- actions, text="清除诊断记录", style="Danger.TButton",
- command=self.clear_diagnostic,
- ).grid(row=0, column=1, sticky="ew", padx=(6, 0))
-
- tk.Label(
- card,
- text=("说明:诊断依据硬件定时器实际分频参数和脉冲完成中断计数,"
- "可发现内部配置、频率及曲线执行异常;若要诊断端子断线或驱动器漏收,"
- "还需将输出接回计数输入或使用示波器。"),
- bg="#FFF3D8", fg="#865600", anchor="w", justify=tk.LEFT,
- padx=10, pady=9, wraplength=820,
- ).grid(row=len(rows) + 1, column=0, columnspan=2, sticky="ew", pady=(8, 0))
-
- def _build_log_page(self) -> None:
- toolbar = ttk.Frame(self.log_page)
- toolbar.pack(fill=tk.X, pady=(0, 8))
- ttk.Label(
- toolbar,
- text="显示应用事件以及实际发送、接收的Modbus RTU十六进制帧。",
- foreground="#6E7F91",
- ).pack(side=tk.LEFT)
- ttk.Button(
- toolbar,
- text="清空日志",
- style="Secondary.TButton",
- command=self.clear_log,
- ).pack(side=tk.RIGHT)
- self.log_text = ScrolledText(
- self.log_page,
- wrap=tk.WORD,
- bg="#101A26",
- fg="#D9E5F2",
- insertbackground="#FFFFFF",
- font=("Consolas", 10),
- relief=tk.FLAT,
- padx=10,
- pady=10,
- )
- self.log_text.pack(fill=tk.BOTH, expand=True)
- self.log_text.configure(state=tk.DISABLED)
-
- def refresh_ports(self) -> None:
- previous = self.port_var.get()
- ports = list_serial_ports()
- self.port_combo["values"] = ports
- if previous in ports:
- self.port_var.set(previous)
- elif ports:
- self.port_var.set(ports[0])
- else:
- self.port_var.set("")
- self.log(f"检测到串口:{', '.join(ports) if ports else '无'}")
-
- def toggle_connection(self) -> None:
- if self.client.is_open:
- self._disconnect("串口已手动断开。")
- return
- port = self.port_var.get().strip()
- if not port:
- messagebox.showwarning("未选择串口", "没有检测到可用串口,请连接设备后刷新。")
- return
- try:
- slave = int(self.slave_var.get())
- baud = int(self.baud_var.get())
- self.client.open(port, baud, slave)
- except Exception as exc:
- messagebox.showerror("连接失败", str(exc))
- self.log(f"连接失败:{exc}")
- return
- self._set_connected(True)
- self.log(f"已连接 {port},{baud},8-E-1,从站{slave}。")
- self.client.read_holding(
- int(Register.PULSE_POINT), register_read_count(), "INITIAL_SYSTEM"
- )
-
- def _disconnect(self, reason: str) -> None:
- """关闭串口并统一恢复界面状态。"""
- if self.client.is_open:
- self.client.close()
- self.pending_motion = None
- self.pending_segments = None
- self.written_segments = None
- self.segments_written_ready = False
- self.start_after_segment_write = False
- # 断开时丢弃已排队的旧事件,避免日志又出现一组过期TX/RX。
- while True:
- try:
- self.client.events.get_nowait()
- except queue.Empty:
- break
- self.test_active = False
- self.monitoring_active = False
- self.seen_running = False
- self._set_connected(False)
- self.log(reason)
-
- def _set_connected(self, connected: bool) -> None:
- self.connection_badge.configure(
- text="已连接" if connected else "未连接",
- bg="#DDF7EC" if connected else "#FDECEC",
- fg="#087A55" if connected else "#A61B1B",
- )
- self.connect_button.configure(text="断开连接" if connected else "连接设备")
- port_state = tk.DISABLED if connected else "readonly"
- self.port_combo.configure(state=port_state)
- self.baud_combo.configure(state=port_state)
- self.slave_spin.configure(state=tk.DISABLED if connected else tk.NORMAL)
- self.refresh_button.configure(state=tk.DISABLED if connected else tk.NORMAL)
- control_state = tk.NORMAL if connected else tk.DISABLED
- self.stop_button.configure(state=control_state)
- self.resume_button.configure(state=control_state)
- self.update_frequency_button.configure(state=control_state)
- self.save_outputs_button.configure(state=control_state)
- self.read_button.configure(state=control_state)
- self.clear_position_button.configure(state=control_state)
- self.clear_total_button.configure(state=control_state)
- self.write_segments_button.configure(state=control_state)
- # 未手动写入段表时也允许点击启动;启动流程会先
- # 自动写入当前界面的默认参数,设备确认后再启动。
- self.start_segments_button.configure(
- state=(tk.NORMAL if connected else tk.DISABLED)
- )
-
- def apply_preset(self, log_message: bool = True) -> None:
- selected = self.preset_var.get()
- preset = next((item for item in PRESETS if item["name"] == selected), PRESETS[0])
- self.pulses_var.set(str(preset["pulses"]))
- self.frequency_var.set(preset["frequency"])
- self.runtime_frequency_var.set(preset["frequency"])
- self.acceleration_var.set(preset["acceleration_ms"])
- self.deceleration_var.set(preset["deceleration_ms"])
- self.start_frequency_var.set(preset["start_frequency"])
- self.end_frequency_var.set(preset["end_frequency"])
- self.display_target = preset["pulses"]
- if log_message:
- self.log(f"已应用预设:{preset['name']}")
-
- def _selected_output_mode(self) -> OutputMode:
- return next(
- (mode for mode, text in OUTPUT_MODE_TEXT.items()
- if text == self.output_mode_var.get()),
- OutputMode.PULSE_DIRECTION,
- )
-
- def _selected_send_mode(self) -> SendMode:
- return next(
- (mode for mode, text in SEND_MODE_TEXT.items()
- if text == self.send_mode_var.get()),
- SendMode.COMPLETE,
- )
-
- def _selected_direction_logic(self) -> DirectionLogic:
- return next(
- (logic for logic, text in DIRECTION_LOGIC_TEXT.items()
- if text == self.direction_logic_var.get()),
- DirectionLogic.POSITIVE,
- )
-
- def _direction_display_text(self, direction: int) -> str:
- if self._selected_output_mode() == OutputMode.AB_QUADRATURE:
- return (
- "正向(A相超前B相90°)"
- if direction == 0 else "反向(B相超前A相90°)"
- )
- return DIRECTION_TEXT[1 if direction else 0]
-
- def _on_output_mode_changed(self, _event=None) -> None:
- if hasattr(self, "direction_status_var"):
- direction = 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0
- self.direction_status_var.set(self._direction_display_text(direction))
-
- def _on_run_mode_changed(self, _event=None) -> None:
- """根据模式切换目标含义;绝对模式的实际方向由固件自动计算。"""
- absolute = self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
- if hasattr(self, "target_caption_var"):
- self.target_caption_var.set("目标绝对位置" if absolute else "目标脉冲数")
-
- def _read_motion_inputs(self) -> Dict[str, int]:
- try:
- pulses = int(self.pulses_var.get().strip())
- frequency = int(self.frequency_var.get())
- acceleration_ms = int(self.acceleration_var.get())
- deceleration_ms = int(self.deceleration_var.get())
- maximum_frequency = int(self.maximum_frequency_var.get())
- start_frequency = int(self.start_frequency_var.get())
- end_frequency = int(self.end_frequency_var.get())
- except ValueError as exc:
- raise ValueError("脉冲数、三项速度和加减速时间必须是整数") from exc
- run_mode = (
- RunMode.ABSOLUTE
- if self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
- else RunMode.RELATIVE
- )
- direction = 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0
- if run_mode == RunMode.RELATIVE:
- if not -0x80000000 <= pulses <= 0x7FFFFFFF:
- raise ValueError("相对脉冲数必须在-2147483648到2147483647之间")
- # 负数按需求直接表示反向;正数仍允许通过“运动方向”选择反向,
- # 兼容原有上位机操作。INT32_MIN只能表示为反向2147483648脉冲。
- if pulses < 0:
- direction = 1
- magnitude = abs(pulses)
- command_pulses = -magnitude if direction else magnitude
- target_u32 = signed_to_u32(command_pulses)
- else:
- target_u32 = signed_to_u32(pulses)
- if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
- raise ValueError("最高速度必须在1到100000Hz之间")
- if not MIN_FREQUENCY_HZ <= frequency <= maximum_frequency:
- raise ValueError(f"默认速度必须在1到最高速度{maximum_frequency}Hz之间")
- if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not 0 <= start_frequency <= frequency:
- raise ValueError("起始速度必须在0Hz到默认速度之间")
- if not 0 <= end_frequency <= frequency:
- raise ValueError("终止速度必须在0Hz到默认速度之间")
- profile_type = next(
- (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
- ProfileType.LINEAR,
- )
- return {
- "pulses": pulses,
- "target_u32": target_u32,
- "frequency": frequency,
- "direction": direction,
- "run_mode": int(run_mode),
- "profile_type": int(profile_type),
- "acceleration_ms": acceleration_ms,
- "deceleration_ms": deceleration_ms,
- "start_frequency": start_frequency,
- "end_frequency": end_frequency,
- "maximum_frequency": maximum_frequency,
- }
-
- def start_motion(self) -> None:
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- try:
- params = self._read_motion_inputs()
- except ValueError as exc:
- messagebox.showwarning("参数错误", str(exc))
- return
- if params["run_mode"] == int(RunMode.ABSOLUTE):
- current_position = int(self.cumulative_position_var.get())
- self.expected_pulses = abs(params["pulses"] - current_position)
- else:
- self.expected_pulses = abs(params["pulses"])
- self.display_target = self.expected_pulses
- self.pending_motion = params
- self.expected_constant_frequency = (
- params["frequency"]
- if params["start_frequency"] == params["frequency"]
- and params["end_frequency"] == params["frequency"]
- and params["acceleration_ms"] == 0
- and params["deceleration_ms"] == 0
- else 0
- )
- pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
- direction_point = point_id_from_label(SECONDARY_POINTS, self.secondary_point_var.get())
- profile_values = [pulse_point, direction_point, 0, 0,
- int(self._selected_send_mode()), 20,
- int(self._selected_direction_logic()),
- params["profile_type"], params["run_mode"], 1, 1]
- profile_values += split_u32(params["frequency"])
- profile_values += split_u32(params["start_frequency"])
- profile_values += [int(self._selected_output_mode())]
- self.client.write_multiple(
- int(Register.PULSE_POINT), profile_values, "SET_SYSTEM_A"
- )
- self.test_active = True
- self.multi_test_active = False
- # 参数写入链完成并收到START响应后再开始周期查询,避免查询帧插队。
- self.monitoring_active = False
- self.seen_running = False
- self.test_started_at = time.monotonic()
- if params["run_mode"] == int(RunMode.ABSOLUTE):
- actual_direction = 1 if params["pulses"] < current_position else 0
- else:
- actual_direction = params["direction"]
- self.direction_status_var.set(self._direction_display_text(actual_direction))
- self._set_test_result("启动命令发送中,等待设备确认。", "running")
- self.log(f"提交单段启动:目标{params['pulses']},频率{params['frequency']}Hz。")
-
- def write_segment_table(self, start_after_write: bool = False) -> None:
- """校验并写入公共曲线参数、段数、起始段和段表。
-
- start_after_write为True时,用于“未写段表直接启动”的自动
- 流程:设备完成段表保存后再发送启动命令。
- """
- self.start_after_segment_write = False
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- try:
- total_segments = int(self.total_segments_var.get())
- start_segment = int(self.start_segment_var.get())
- start_frequency = int(self.start_frequency_var.get())
- end_frequency = int(self.end_frequency_var.get())
- acceleration_ms = int(self.acceleration_var.get())
- deceleration_ms = int(self.deceleration_var.get())
- default_frequency = int(self.frequency_var.get())
- maximum_frequency = int(self.maximum_frequency_var.get())
- if not 1 <= total_segments <= 10:
- raise ValueError("脉冲总段数必须在1到10之间")
- if not 1 <= start_segment <= total_segments:
- raise ValueError("起始执行段数必须在1到脉冲总段数之间")
- if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
- raise ValueError("最高速度必须在1到100000Hz之间")
- if not MIN_FREQUENCY_HZ <= default_frequency <= maximum_frequency:
- raise ValueError(f"默认速度必须在1到最高速度{maximum_frequency}Hz之间")
- if not 0 <= start_frequency <= maximum_frequency:
- raise ValueError(f"起始速度必须在0到最高速度{maximum_frequency}Hz之间")
- if not 0 <= end_frequency <= maximum_frequency:
- raise ValueError(f"终止速度必须在0到最高速度{maximum_frequency}Hz之间")
- segments = []
- for index in range(total_segments):
- pulses = int(self.segment_pulse_vars[index].get().strip())
- frequency = int(self.segment_frequency_vars[index].get().strip())
- jump = int(self.segment_jump_vars[index].get())
- condition_value = int(self.segment_condition_vars[index].get().strip())
- wait_condition = next(
- (item for item, text in WAIT_CONDITION_TEXT.items()
- if text == self.segment_wait_vars[index].get()), WaitCondition.WAIT_TIME)
- if not -0x80000000 <= pulses <= 0x7FFFFFFF:
- raise ValueError(f"第{index + 1}段脉冲数必须在-2147483648到2147483647之间")
- if not 0 <= frequency <= maximum_frequency:
- raise ValueError(
- f"第{index + 1}段频率必须为0或1到最高速度{maximum_frequency}Hz"
- )
- effective_frequency = default_frequency if frequency == 0 else frequency
- if start_frequency > effective_frequency or end_frequency > effective_frequency:
- raise ValueError(f"第{index + 1}段实际目标速度不能低于起始或终止速度")
- if not 0 <= jump <= total_segments:
- raise ValueError(f"第{index + 1}段跳转段必须是0到{total_segments}")
- if not 0 <= condition_value <= 0xFFFFFFFF:
- raise ValueError(f"第{index + 1}段条件值超出32位无符号范围")
- if wait_condition in (WaitCondition.WAIT_SIGNAL,
- WaitCondition.EXT_SIGNAL,
- WaitCondition.EXT_OR_COMPLETE) and condition_value not in INPUT_POINTS:
- raise ValueError(f"第{index + 1}段信号X点无效")
- segments.append((pulses, frequency, jump,
- int(wait_condition), condition_value))
- except (ValueError, tk.TclError) as exc:
- messagebox.showwarning("多段参数错误", str(exc))
- return
- execution_total, execution_has_cycle = calculate_execution_total(
- segments, start_segment
- )
-
- profile_type = next(
- (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
- ProfileType.LINEAR,
- )
- direction = 1 if segments[start_segment - 1][0] < 0 else 0
- self.pending_segments = {
- "total": total_segments, "start": start_segment,
- "segments": segments, "direction": direction,
- "profile_type": int(profile_type), "acceleration_ms": acceleration_ms,
- "deceleration_ms": deceleration_ms, "start_frequency": start_frequency,
- "end_frequency": end_frequency, "default_frequency": default_frequency,
- "maximum_frequency": maximum_frequency,
- "execution_total": execution_total,
- "execution_has_cycle": execution_has_cycle,
- }
- wait_points = {
- item[4] for item in segments
- if item[3] == int(WaitCondition.WAIT_SIGNAL)
- }
- ext_points = {
- item[4] for item in segments
- if item[3] in (int(WaitCondition.EXT_SIGNAL),
- int(WaitCondition.EXT_OR_COMPLETE))
- }
- if any(point not in INPUT_POINTS for point in wait_points | ext_points):
- messagebox.showwarning("信号点无效", "参数表仅允许WAIT/EXT选择X4或X5。")
- self.pending_segments = None
- return
- if len(wait_points) > 1 or len(ext_points) > 1:
- messagebox.showwarning(
- "全局信号配置冲突",
- "参数表中WAIT信号和EXT信号分别是全局配置,同类条件必须统一使用X4或X5。",
- )
- self.pending_segments = None
- return
- wait_select = next(iter(wait_points), 4) - 4
- ext_select = next(iter(ext_points), 4) - 4
- self.wait_signal_select = wait_select
- self.ext_signal_select = ext_select
- pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
- direction_point = point_id_from_label(SECONDARY_POINTS, self.secondary_point_var.get())
- profile_values = [pulse_point, direction_point, wait_select, ext_select,
- int(self._selected_send_mode()), 20,
- int(self._selected_direction_logic()),
- int(profile_type), int(RunMode.RELATIVE),
- total_segments, start_segment]
- # 0x100B保存公共默认速度;各段自己的运行速度仍来自段表。
- profile_values += split_u32(default_frequency)
- profile_values += split_u32(start_frequency)
- profile_values += [int(self._selected_output_mode())]
- self.start_after_segment_write = bool(start_after_write)
- self.client.write_multiple(int(Register.PULSE_POINT), profile_values,
- "SET_SEGMENT_SYSTEM_A")
- self.written_segments = None
- self.segments_written_ready = False
- self.start_segments_button.configure(state=tk.DISABLED)
- self.test_active = False
- self.multi_test_active = False
- self.monitoring_active = False
- self._set_test_result("正在写入多段参数和段表,不会自动启动。", "running")
- if execution_has_cycle:
- self.log(f"提交段表写入:共{total_segments}段,从第{start_segment}段开始,"
- "检测到循环跳转。")
- else:
- self.log(f"提交段表写入:共{total_segments}段,从第{start_segment}段开始,"
- f"预计输出{execution_total}个脉冲。")
-
- def start_segment_motion(self) -> None:
- """启动多段运动;未写段表时先自动写入当前默认参数。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if self.last_state in (
- int(State.RUNNING), int(State.WAITING), int(State.PAUSED)
- ):
- messagebox.showwarning("当前不可启动", "设备正在运行、等待或暂停,请先结束当前运动。")
- return
- if not self.segments_written_ready or self.written_segments is None:
- self.log("段表尚未写入,正在按当前默认参数自动写入后启动。")
- self.write_segment_table(start_after_write=True)
- return
-
- params = self.written_segments
- execution_has_cycle = bool(params["execution_has_cycle"])
- execution_total = int(params["execution_total"])
- self.expected_pulses = 0 if execution_has_cycle else execution_total
- self.display_target = 0 if execution_has_cycle else execution_total
- self.test_active = True
- self.multi_test_active = True
- self.expected_constant_frequency = 0
- self.monitoring_active = False
- self.seen_running = False
- self.test_started_at = time.monotonic()
- self.direction_status_var.set(
- self._direction_display_text(int(params["direction"]))
- )
- self._set_test_result("多段启动命令发送中,等待设备确认。", "running")
- self.client.write_single(
- int(Register.CONTROL), int(Command.START_SEGMENTS), "START_SEGMENTS"
- )
- self.log(
- f"提交多段启动:共{params['total']}段,从第{params['start']}段开始。"
- )
-
- def stop_motion(self) -> None:
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- self.client.write_single(int(Register.CONTROL), int(Command.STOP), "STOP")
- self.test_active = False
- self.monitoring_active = False
- self._set_test_result("暂停命令发送中。", "running")
- self.log("提交暂停命令,将保留当前脉冲位置。")
-
- def resume_motion(self) -> None:
- """继续STM32中已暂停的运动,不重写参数也不清零本次计数。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- self.client.write_single(
- int(Register.CONTROL), int(Command.RESUME), "RESUME"
- )
- self.expected_pulses = self.display_target
- self.test_active = True
- self.monitoring_active = True
- self.seen_running = False
- self.test_started_at = time.monotonic()
- self._set_test_result("继续命令发送中,等待设备确认。", "running")
- self.log("提交继续命令,将从暂停的脉冲位置继续。")
-
- def update_frequency(self) -> None:
- """写入独立的瞬时目标频率,不改变默认速度、段表或当前脉冲计数。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if self.last_state not in (int(State.RUNNING), int(State.PAUSED)):
- messagebox.showwarning("当前不可改频", "只有运行中或暂停状态可以修改频率。")
- return
- try:
- frequency = int(self.runtime_frequency_var.get())
- maximum_frequency = int(self.maximum_frequency_var.get())
- start_frequency = int(self.start_frequency_var.get())
- end_frequency = int(self.end_frequency_var.get())
- except (ValueError, tk.TclError):
- messagebox.showwarning("参数错误", "频率必须是整数。")
- return
- if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
- messagebox.showwarning("参数错误", "最高速度必须在1到100000Hz之间。")
- return
- if not MIN_FREQUENCY_HZ <= frequency <= maximum_frequency:
- messagebox.showwarning(
- "参数错误", f"频率必须在1到最高速度{maximum_frequency}Hz之间。"
- )
- return
- if frequency < start_frequency or frequency < end_frequency:
- messagebox.showwarning(
- "参数错误", "运行中目标频率不能低于起始速度或终止速度。"
- )
- return
-
- self.pending_frequency = frequency
- self.client.write_multiple(
- int(Register.RUNTIME_FREQ_LO),
- split_u32(frequency),
- "SET_RUNTIME_FREQ_VALUE",
- )
- self._set_test_result(f"正在将频率修改为{frequency}Hz。", "running")
- self.log(f"提交动态改频:{frequency}Hz,不清零当前脉冲计数。")
-
- def save_output_mapping(self) -> None:
- """一次写入12项公共参数,并请求设备保存到Flash。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if self.last_state in (
- int(State.RUNNING), int(State.PAUSED), int(State.WAITING)
- ):
- messagebox.showwarning(
- "当前不可配置",
- "请等待脉冲完成;暂停中的任务需要先完成或重新启动后再保存点位。",
- )
- return
- try:
- pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
- direction_point = point_id_from_label(
- SECONDARY_POINTS, self.secondary_point_var.get()
- )
- default_frequency = int(self.frequency_var.get())
- maximum_frequency = int(self.maximum_frequency_var.get())
- start_frequency = int(self.start_frequency_var.get())
- end_frequency = int(self.end_frequency_var.get())
- acceleration_ms = int(self.acceleration_var.get())
- deceleration_ms = int(self.deceleration_var.get())
- total_segments = int(self.total_segments_var.get())
- start_segment = int(self.start_segment_var.get())
- if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
- raise ValueError("最高速度必须在1到100000Hz之间")
- if not MIN_FREQUENCY_HZ <= default_frequency <= maximum_frequency:
- raise ValueError("默认速度必须在1Hz到最高速度之间")
- if not 0 <= start_frequency <= maximum_frequency:
- raise ValueError("起始速度必须在0Hz到最高速度之间")
- if not 0 <= end_frequency <= maximum_frequency:
- raise ValueError("终止速度必须在0Hz到最高速度之间")
- if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"默认加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
- raise ValueError(f"默认减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
- if not 1 <= total_segments <= 10:
- raise ValueError("脉冲总段数必须在1到10之间")
- if not 1 <= start_segment <= total_segments:
- raise ValueError("起始执行段数必须在有效段范围内")
- for index in range(total_segments):
- segment_frequency = int(self.segment_frequency_vars[index].get())
- if not 0 <= segment_frequency <= maximum_frequency:
- raise ValueError(f"第{index + 1}段频率必须为0或不超过公共最高速度")
- effective_frequency = (
- default_frequency if segment_frequency == 0 else segment_frequency
- )
- if start_frequency > effective_frequency or end_frequency > effective_frequency:
- raise ValueError(f"第{index + 1}段实际目标速度不能低于公共起始或终止速度")
- except (ValueError, tk.TclError) as exc:
- messagebox.showwarning("公共参数错误", str(exc))
- return
-
- profile_type = next(
- (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
- ProfileType.LINEAR,
- )
- values = [
- pulse_point,
- direction_point,
- int(getattr(self, "wait_signal_select", 0)),
- int(getattr(self, "ext_signal_select", 0)),
- int(self._selected_send_mode()),
- 20,
- int(self._selected_direction_logic()),
- int(profile_type),
- int(RunMode.RELATIVE),
- total_segments,
- start_segment,
- ]
- values += split_u32(default_frequency)
- values += split_u32(start_frequency)
- values += [int(self._selected_output_mode())]
- values += split_u32(end_frequency)
- values += [acceleration_ms, deceleration_ms]
- values += split_u32(maximum_frequency)
- self.client.write_multiple(
- int(Register.PULSE_POINT),
- values,
- "SET_PUBLIC_PARAMETERS",
- )
- self._set_test_result(
- "正在应用并保存全部公共参数。",
- "running",
- )
- self.log(
- f"提交公共参数:{PROFILE_TEXT[profile_type]},"
- f"默认{default_frequency}Hz,最高{maximum_frequency}Hz,"
- f"起始{start_frequency}Hz,终止{end_frequency}Hz,"
- f"加速{acceleration_ms}ms,减速{deceleration_ms}ms,请求掉电保存。"
- )
-
- def clear_current_position(self) -> None:
- """清零当前所选脉冲Y点的累计位置。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if not messagebox.askyesno(
- "确认清零", "确定将当前脉冲Y点的累计位置清零吗?其他脉冲Y点不受影响。"
- ):
- return
- self.client.write_single(
- int(Register.CONTROL), int(Command.CLEAR_POSITION), "CLEAR_POSITION"
- )
- self.log("提交当前脉冲Y点累计位置清零命令。")
-
- def clear_total_count(self) -> None:
- """清零不计方向和输出点的全局总脉冲数。"""
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if not messagebox.askyesno(
- "确认清零", "确定将掉电保持的总脉冲数清零吗?Y0~Y3累计位置不受影响。"
- ):
- return
- self.client.write_single(
- int(Register.CONTROL), int(Command.CLEAR_TOTAL), "CLEAR_TOTAL"
- )
- self.log("提交总脉冲数清零命令,等待设备确认。")
-
- def request_diagnostic(self) -> None:
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- if not self.client.is_busy:
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "DIAGNOSTIC_STATUS",
- )
-
- def clear_diagnostic(self) -> None:
- if not self.client.is_open:
- messagebox.showwarning("尚未连接", "请先连接串口设备。")
- return
- self.client.write_single(
- int(Register.CONTROL),
- int(Command.CLEAR_DIAGNOSTIC),
- "CLEAR_DIAGNOSTIC",
- )
- self.log("已提交诊断记录清零命令。")
-
- def request_status(self) -> None:
- if self.client.is_open and not self.client.is_busy:
- # 状态区保持轮询;AB实际方向在启动时或多段切换时同步到界面。
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "POLL",
- )
-
- def _request_public_save_status(self) -> None:
- """稍作延时后读取错误码,确认Flash任务已完成。"""
- if self.closing:
- return
- if not self.client.is_open:
- message = "公共参数已写入,但连接已断开,无法确认Flash保存结果。"
- self._set_test_result(message, "fail")
- self.log(message)
- messagebox.showerror("无法确认保存结果", message)
- return
- if self.client.is_busy:
- self.root.after(50, self._request_public_save_status)
- return
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "PUBLIC_SAVE_STATUS",
- )
-
- def _request_segment_save_status(self) -> None:
- """等待设备完成段表Flash保存,再判定写入成功。"""
- if self.closing:
- return
- if not self.client.is_open:
- self._abort_segment_table_write(
- "段表已写入寄存器,但连接已断开,无法确认Flash保存结果。"
- )
- return
- if self.client.is_busy:
- self.root.after(50, self._request_segment_save_status)
- return
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "SEGMENT_SAVE_STATUS",
- )
-
- def _poll_status(self) -> None:
- if self.closing:
- return
- if self.monitoring_active:
- self.request_status()
- self.root.after(self.POLL_INTERVAL_MS, self._poll_status)
-
- def _process_client_events(self) -> None:
- if self.closing:
- return
- while True:
- try:
- event = self.client.events.get_nowait()
- except queue.Empty:
- break
- kind = event[0]
- if kind == "tx":
- self.log(f"TX {event[1].hex(' ').upper()}")
- elif kind == "rx":
- self.log(f"RX {event[1].hex(' ').upper()}")
- elif kind == "read":
- try:
- self._handle_read(event[1], event[2], event[3])
- except (IndexError, ValueError) as exc:
- # 状态回读异常不能终止Tkinter事件循环,否则后续启动写入
- # 的确认事件也不会再被处理,界面会一直停在“等待确认”。
- self.log(f"寄存器数据解析失败:{exc}")
- self._set_test_result(f"设备回读数据不完整:{exc}", "fail")
- self.test_active = False
- self.monitoring_active = False
- elif kind == "write":
- self._handle_write(event[1], event[2], event[3], event[4])
- elif kind == "retry":
- _, context, attempt = event
- self.log(f"{context}未收到响应,正在进行第{attempt}次发送。")
- elif kind == "exception":
- _, function, code, context = event
- message = f"Modbus异常:功能码0x{function:02X},异常码0x{code:02X},操作{context}。"
- self.log(message)
- self._set_test_result(message, "fail")
- if ((isinstance(context, str) and
- context.startswith("SET_SEGMENT_")) or
- context in ("SAVE_SEGMENT_CONFIGURATION",
- "SEGMENT_SAVE_STATUS")):
- self._abort_segment_table_write(message)
- if context in (
- "SET_PUBLIC_PARAMETERS",
- "SAVE_PUBLIC",
- "PUBLIC_CONFIG_STATUS",
- "PUBLIC_SAVE_STATUS",
- ):
- messagebox.showerror("公共参数保存失败", message)
- self.test_active = False
- self.monitoring_active = False
- elif kind == "error":
- _, context, message = event
- text = f"{context}:{message}" if context else message
- self.log(f"通信错误:{text}")
- if ((isinstance(context, str) and
- context.startswith("SET_SEGMENT_")) or
- context in ("SAVE_SEGMENT_CONFIGURATION",
- "SEGMENT_SAVE_STATUS")):
- self._abort_segment_table_write(text)
- if context in (
- "SET_PUBLIC_PARAMETERS",
- "SAVE_PUBLIC",
- "PUBLIC_CONFIG_STATUS",
- "PUBLIC_SAVE_STATUS",
- ):
- messagebox.showerror("公共参数保存失败", text)
- if self.test_active:
- self._set_test_result(f"测试中断:{text}", "fail")
- self.test_active = False
- self.monitoring_active = False
- self.root.after(self.EVENT_INTERVAL_MS, self._process_client_events)
-
- def _handle_read(self, address: int, values: List[int], context: str) -> None:
- if address == int(Register.CUMULATIVE_POSITION_LO) and len(values) >= 11:
- status = parse_status(values)
- self._update_status(status)
- if context == "INITIAL_STATUS":
- self._set_test_result("设备参数和状态读取完成。", "neutral")
- self.log("设备初始化同步完成,可以写入参数并启动。")
- elif context == "PUBLIC_SAVE_STATUS":
- error_code = status["error"]
- if error_code == int(Error.NONE):
- message = "12项公共参数已成功保存到设备Flash。"
- self._set_test_result(message, "pass")
- self.log(message)
- messagebox.showinfo("保存成功", message)
- else:
- message = (
- f"公共参数保存失败:错误码{error_code},"
- f"{error_text(error_code)}。"
- )
- self._set_test_result(message, "fail")
- self.log(message)
- messagebox.showerror("保存失败", message)
- elif context == "SEGMENT_SAVE_STATUS":
- error_code = status["error"]
- if error_code == int(Error.NONE):
- self._finish_segment_table_write()
- else:
- self._abort_segment_table_write(
- f"段表保存失败:错误码{error_code},"
- f"{error_text(error_code)}。"
- )
- return
- if address == int(Register.PULSE_POINT) and len(values) >= 22:
- pulse_point = values[0]
- direction_point = values[1]
- self.wait_signal_select = values[2] if values[2] in (0, 1) else 0
- self.ext_signal_select = values[3] if values[3] in (0, 1) else 0
- if pulse_point in PULSE_POINTS:
- self.pulse_point_var.set(PULSE_POINTS[pulse_point])
- if direction_point in SECONDARY_POINTS:
- self.secondary_point_var.set(SECONDARY_POINTS[direction_point])
- if values[4] in [int(item) for item in SendMode]:
- self.send_mode_var.set(SEND_MODE_TEXT[SendMode(values[4])])
- if values[6] in [int(item) for item in DirectionLogic]:
- self.direction_logic_var.set(
- DIRECTION_LOGIC_TEXT[DirectionLogic(values[6])]
- )
- self.profile_var.set(PROFILE_TEXT.get(ProfileType(values[7]), PROFILE_TEXT[ProfileType.LINEAR]))
- self.run_mode_var.set(MODE_TEXT[RunMode.ABSOLUTE if values[8] else RunMode.RELATIVE])
- self.total_segments_var.set(values[9])
- self.start_segment_var.set(values[10])
- self.frequency_var.set(join_u32(values[11], values[12]))
- self.start_frequency_var.set(join_u32(values[13], values[14]))
- output_mode = values[15]
- if output_mode in [int(item) for item in OutputMode]:
- self.output_mode_var.set(OUTPUT_MODE_TEXT[OutputMode(output_mode)])
- self._on_output_mode_changed()
- self.end_frequency_var.set(join_u32(values[16], values[17]))
- self.acceleration_var.set(values[18])
- self.deceleration_var.set(values[19])
- self.maximum_frequency_var.set(join_u32(values[20], values[21]))
- if context == "INITIAL_SYSTEM":
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "INITIAL_STATUS",
- )
- elif context == "PUBLIC_CONFIG_STATUS":
- self.log("公共参数回读完成,正在确认Flash保存结果。")
- self.root.after(200, self._request_public_save_status)
- return
- if address == int(Register.END_FREQ_LO) and len(values) >= 4:
- self.end_frequency_var.set(join_u32(values[0], values[1]))
- self.acceleration_var.set(values[2])
- self.deceleration_var.set(values[3])
- if context == "INITIAL_RAMP":
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO), STATUS_REGISTER_COUNT, "INITIAL_STATUS"
- )
- return
- if False and address == int(Register.COMMAND) and len(values) >= 28:
- run_mode = RunMode.ABSOLUTE if values[18] == 1 else RunMode.RELATIVE
- target_raw = join_u32(values[2], values[3])
- target = join_i32(values[2], values[3]) if run_mode == RunMode.ABSOLUTE else target_raw
- frequency = join_u32(values[4], values[5])
- self.pulses_var.set(str(target))
- self.run_mode_var.set(MODE_TEXT[run_mode])
- self._on_run_mode_changed()
- self.frequency_var.set(max(MIN_FREQUENCY_HZ, min(frequency, MAX_FREQUENCY_HZ)))
- profile_value = values[19]
- if profile_value in [int(item) for item in ProfileType]:
- self.profile_var.set(PROFILE_TEXT[ProfileType(profile_value)])
- self.acceleration_var.set(join_u32(values[20], values[21]))
- self.deceleration_var.set(join_u32(values[22], values[23]))
- self.start_frequency_var.set(join_u32(values[24], values[25]))
- self.end_frequency_var.set(join_u32(values[26], values[27]))
- self.direction_var.set(DIRECTION_TEXT[1 if values[1] else 0])
- self.direction_status_var.set(self._direction_display_text(1 if values[1] else 0))
- current_position = join_i32(values[12], values[13])
- self.display_target = (
- abs(target - current_position)
- if run_mode == RunMode.ABSOLUTE
- else target
- )
- self._update_status(parse_status(values[6:18]))
- pulse_point = values[14]
- direction_point = values[15]
- if pulse_point in PULSE_POINTS:
- self.pulse_point_var.set(PULSE_POINTS[pulse_point])
- if direction_point in SECONDARY_POINTS:
- self.secondary_point_var.set(SECONDARY_POINTS[direction_point])
- if len(values) >= register_read_count(Register.COMMAND):
- self._load_segment_registers(values, int(Register.COMMAND))
- if context == "OUTPUT_CONFIG_STATUS":
- if values[7] == int(Error.NONE):
- self._set_test_result(
- f"输出映射已保存:第一端子Y{pulse_point},第二端子Y{direction_point + 12}。",
- "pass",
- )
- self.log("设备已确认映射生效并写入Flash;断电重启后会自动恢复。")
- else:
- self._set_test_result(
- f"模块映射保存失败:{error_text(values[7])}。", "fail"
- )
- else:
- self.log(
- f"已同步设备参数:第一端子Y{pulse_point},第二端子Y{direction_point + 12},"
- f"{self.profile_var.get()},加速{self.acceleration_var.get()}ms,"
- f"减速{self.deceleration_var.get()}ms,起始{self.start_frequency_var.get()}Hz,"
- f"默认{self.frequency_var.get()}Hz,终止{self.end_frequency_var.get()}Hz。"
- )
- elif address == int(Register.DIRECTION) and len(values) >= 17:
- actual_direction = 1 if values[0] else 0
- self.direction_status_var.set(self._direction_display_text(actual_direction))
- self._update_status(parse_status(values[5:17]))
- if len(values) >= register_read_count(Register.DIRECTION):
- self._load_segment_registers(values, int(Register.DIRECTION))
- if context == "STOP_STATUS":
- self.log("已回读一次暂停位置和实际方向,后续不再周期查询。")
- elif context == "FREQ_STATUS":
- self.log("已回读动态改频后的状态和实际方向。")
- elif address == int(Register.STATE) and len(values) >= 12:
- self._update_status(parse_status(values))
- if context == "STOP_STATUS":
- self.log("已回读一次暂停位置,后续不再周期查询。")
- elif context == "FREQ_STATUS":
- self.log("已回读动态改频后的状态。")
- elif address == int(Register.CUMULATIVE_POSITION_HI) and len(values) >= 2:
- position = join_i32(values[0], values[1])
- self.cumulative_position_var.set(str(position))
- self.log(f"当前脉冲Y点累计位置已更新为 {position}。")
- elif address == int(Register.TOTAL_PULSES_LO) and len(values) >= 2:
- total = join_u32(values[0], values[1])
- self.total_pulses_var.set(str(total))
- self.log(f"总脉冲数已更新为 {total}。")
- _ = context
-
- def _load_segment_registers(self, values: List[int], base_address: int) -> None:
- """把一次连续回读中的多段配置和当前段进度同步到界面。"""
- def at(address: int) -> int:
- index = address - base_address
- if index < 0 or index >= len(values):
- raise ValueError(
- f"寄存器回读不完整:需要0x{address:04X},"
- f"实际范围0x{base_address:04X}~"
- f"0x{base_address + len(values) - 1:04X}"
- )
- return values[index]
-
- total = at(int(Register.TOTAL_SEGMENTS))
- start = at(int(Register.START_SEGMENT))
- current = at(int(Register.CURRENT_SEGMENT))
- if 1 <= total <= 10:
- self.total_segments_var.set(total)
- if 1 <= start <= 10:
- self.start_segment_var.set(start)
- self.current_segment_var.set(str(current) if current else "0(未执行)")
- table_base = int(Register.SEGMENT_TABLE_BASE)
- for index in range(10):
- register = table_base + index * 4
- pulses = join_i32(at(register), at(register + 1))
- frequency = join_u32(at(register + 2), at(register + 3))
- self.segment_pulse_vars[index].set(str(pulses))
- self.segment_frequency_vars[index].set(str(frequency))
- meta = int(Register.SEGMENT_META_BASE) + index * 4
- jump = at(meta)
- wait_value = at(meta + 1)
- condition = join_u32(at(meta + 2), at(meta + 3))
- self.segment_jump_vars[index].set(jump)
- if wait_value in [int(item) for item in WaitCondition]:
- self.segment_wait_vars[index].set(
- WAIT_CONDITION_TEXT[WaitCondition(wait_value)])
- self.segment_condition_vars[index].set(str(condition))
-
- def _handle_write(
- self, function: int, address: int, quantity_or_value: int, context: str
- ) -> None:
- if context in ("SET_SYSTEM_A", "SET_SEGMENT_SYSTEM_A"):
- params = self.pending_motion if context == "SET_SYSTEM_A" else self.pending_segments
- if params is None:
- self.monitoring_active = False
- return
- self.client.write_multiple(
- int(Register.END_FREQ_LO),
- split_u32(params["end_frequency"]) +
- [params["acceleration_ms"], params["deceleration_ms"]] +
- split_u32(params["maximum_frequency"]),
- "SET_SYSTEM_B" if context == "SET_SYSTEM_A" else "SET_SEGMENT_SYSTEM_B",
- )
- return
- if context == "SET_SYSTEM_B":
- params = self.pending_motion
- if params is None:
- return
- segment_target = params["target_u32"]
- values = split_u32(params["frequency"])
- values += split_u32(segment_target)
- values += [int(WaitCondition.WAIT_TIME), 0, 0, 0]
- self.client.write_multiple(segment_base(0), values, "SET_SINGLE_SEGMENT")
- return
- if context == "SET_SINGLE_SEGMENT":
- self.client.write_single(int(Register.CONTROL), int(Command.START), "START")
- self.pending_motion = None
- return
- if context == "SET_SEGMENT_SYSTEM_B":
- self._write_segment_at(0)
- return
- if context.startswith("SET_SEGMENT_") and context[12:].isdigit():
- self._write_segment_at(int(context[12:]) + 1)
- return
- if context == "SET_PROFILE":
- params = self.pending_motion
- if params is None:
- self._set_test_result("启动参数已丢失,请重新启动。", "fail")
- self.monitoring_active = False
- return
- self.client.write_single(
- int(Register.RUN_MODE), params["run_mode"], "SET_RUN_MODE"
- )
- self.log("加减速参数写入成功,正在写入运行模式。")
- elif context == "SET_SEGMENT_PROFILE":
- params = self.pending_segments
- if params is None:
- self._set_test_result("多段参数已丢失,请重新启动。", "fail")
- self.monitoring_active = False
- return
- self.client.write_multiple(
- int(Register.TOTAL_SEGMENTS),
- [params["total"], params["start"]],
- "SET_SEGMENT_RANGE",
- )
- self.log("公共曲线参数写入成功,正在写入总段数和起始段。")
- elif context == "SET_SEGMENT_RANGE":
- params = self.pending_segments
- if params is None:
- return
- values = []
- for index in range(10):
- segment = params["segments"][index] if index < params["total"] else (0, 0, 0, 0, 0)
- pulses, frequency = segment[0], segment[1]
- values += split_u32(pulses & 0xFFFFFFFF) + split_u32(frequency)
- self.client.write_multiple(int(Register.SEGMENT_TABLE_BASE), values,
- "SET_SEGMENT_TABLE")
- self.log("段范围写入成功,正在写入10段参数表。")
- elif context == "SET_SEGMENT_TABLE":
- params = self.pending_segments
- if params is None:
- return
- values = []
- for index in range(10):
- segment = params["segments"][index] if index < params["total"] else (0, 0, 0, 0, 0)
- values += [segment[2], segment[3]] + split_u32(segment[4])
- self.client.write_multiple(int(Register.SEGMENT_META_BASE), values,
- "SET_SEGMENT_META")
- self.log("10段脉冲/频率写入成功,正在写入跳转和等待条件。")
- elif context == "SET_SEGMENT_META":
- params = self.pending_segments
- if params is None:
- return
- self.client.write_single(
- int(Register.CONTROL),
- int(Command.SAVE_PUBLIC),
- "SAVE_SEGMENT_CONFIGURATION",
- )
- elif context == "START_SEGMENTS":
- self.monitoring_active = True
- self._set_test_result("设备已确认多段启动命令,正在监控运行。", "running")
- self.log("多段启动命令写入成功。")
- elif context == "SET_RUN_MODE":
- params = self.pending_motion
- if params is None:
- self._set_test_result("启动参数已丢失,请重新启动。", "fail")
- self.monitoring_active = False
- return
- values = [int(Command.START), params["direction"]]
- values += split_u32(params["target_u32"])
- values += split_u32(params["frequency"])
- self.client.write_multiple(int(Register.COMMAND), values, "START")
- self.pending_motion = None
- self.log("运行模式写入成功,正在发送启动参数。")
- elif context == "START":
- self.monitoring_active = True
- self._set_test_result("设备已确认启动命令,正在监控运行状态。", "running")
- self.log("启动参数写入成功。")
- elif context == "STOP":
- self.monitoring_active = False
- self._set_test_result("设备已确认暂停,可点击“继续当前脉冲”。", "neutral")
- self.log("暂停命令写入成功,串口保持连接,已停止周期查询。")
- if self.client.is_open:
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "STOP_STATUS",
- )
- elif context == "RESUME":
- self.monitoring_active = True
- self._set_test_result("设备已确认继续命令,正在监控剩余脉冲。", "running")
- self.log("继续命令写入成功,已恢复周期查询。")
- elif context == "SET_RUNTIME_FREQ_VALUE":
- self.client.write_single(
- int(Register.CONTROL),
- int(Command.UPDATE_FREQUENCY),
- "APPLY_RUNTIME_FREQ",
- )
- self.log("运行中目标频率写入成功,正在发送立即应用命令。")
- elif context == "APPLY_RUNTIME_FREQ":
- self.log("设备已接收运行中改频命令,将按公共加减速斜率调整当前段。")
- if self.client.is_open and not self.monitoring_active:
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- STATUS_REGISTER_COUNT,
- "FREQ_STATUS",
- )
- elif context == "SET_PUBLIC_PARAMETERS":
- self.client.write_single(
- int(Register.CONTROL), int(Command.SAVE_PUBLIC), "SAVE_PUBLIC"
- )
- self.log("公共参数寄存器写入成功,正在请求设备保存Flash。")
- elif context == "SAVE_PUBLIC":
- self.log("设备已接收公共参数保存命令,正在回读确认。")
- if self.client.is_open:
- self.client.read_holding(
- int(Register.PULSE_POINT),
- register_read_count(),
- "PUBLIC_CONFIG_STATUS",
- )
- elif context == "SAVE_SEGMENT_CONFIGURATION":
- self.log("设备已接收段表保存命令,正在确认Flash保存结果。")
- self.root.after(200, self._request_segment_save_status)
- elif context == "SET_OUTPUT_POINTS":
- self.client.write_single(
- int(Register.OUTPUT_MODE),
- int(self._selected_output_mode()),
- "SET_OUTPUT_MODE",
- )
- self.log("输出点寄存器写入成功,正在写入输出方式。")
- elif context == "SET_OUTPUT_MODE":
- self.client.write_single(
- int(Register.SEND_MODE),
- int(self._selected_send_mode()),
- "SET_SEND_MODE",
- )
- self.log("输出方式写入成功,正在写入完成/后续发送模式。")
- elif context == "SET_SEND_MODE":
- self.client.write_single(
- int(Register.DIRECTION_LOGIC),
- int(self._selected_direction_logic()),
- "SET_DIRECTION_LOGIC",
- )
- self.log("发送模式写入成功,正在写入正/负方向逻辑。")
- elif context == "SET_DIRECTION_LOGIC":
- self.client.write_single(
- int(Register.CONTROL), int(Command.SAVE_OUTPUTS), "SAVE_OUTPUTS"
- )
- self.log("方向逻辑写入成功,正在发送应用并保存命令。")
- elif context == "SAVE_OUTPUTS":
- self.log("设备已接收保存命令,正在回读Flash保存结果。")
- if self.client.is_open:
- self.client.read_holding(
- int(Register.PULSE_POINT),
- register_read_count(),
- "OUTPUT_CONFIG_STATUS",
- )
- elif context == "CLEAR_TOTAL":
- self.total_pulses_var.set("0")
- self.log("总脉冲数清零命令写入成功,正在回读确认。")
- if self.client.is_open:
- self.client.read_holding(
- int(Register.TOTAL_PULSES_LO), 2, "TOTAL_AFTER_CLEAR"
- )
- elif context == "CLEAR_POSITION":
- self.cumulative_position_var.set("0")
- self.log("当前Y点位置清零命令写入成功,正在回读确认。")
- if self.client.is_open:
- self.client.read_holding(
- int(Register.CUMULATIVE_POSITION_LO),
- 2,
- "POSITION_AFTER_CLEAR",
- )
- elif context == "CLEAR_DIAGNOSTIC":
- self.log("诊断记录已清零,正在回读确认。")
- self.request_diagnostic()
- _ = function, address, quantity_or_value
-
- def _finish_segment_table_write(self) -> None:
- """设备确认段表已写入Flash后,开放启动或自动启动。"""
- params = self.pending_segments
- if params is None:
- return
- start_after_write = self.start_after_segment_write
- self.start_after_segment_write = False
- self.written_segments = params
- self.pending_segments = None
- self.segments_written_ready = True
- if self.client.is_open:
- self.start_segments_button.configure(state=tk.NORMAL)
- self._set_test_result("段表写入完成,可以点击“启动”。", "pass")
- self.log("多段系统参数和段表写入成功,设备尚未启动。")
- messagebox.showinfo(
- "写入成功",
- f"段表写入成功:有效段数{params['total']},"
- f"起始执行段{params['start']}。\n"
- "参数已保存到设备Flash。",
- )
- if start_after_write:
- self.root.after(0, self.start_segment_motion)
-
- def _abort_segment_table_write(self, message: str) -> None:
- """统一结束失败的段表写入或自动启动流程。"""
- self.pending_segments = None
- self.written_segments = None
- self.segments_written_ready = False
- self.start_after_segment_write = False
- if self.client.is_open:
- # 失败后仍允许再次点击启动,重试自动写入。
- self.start_segments_button.configure(state=tk.NORMAL)
- self._set_test_result(message, "fail")
- self.log(message)
- messagebox.showerror("段表写入失败", message)
-
- def _write_segment_at(self, index: int) -> None:
- """按参数表每段0x10步长依次写入8个有效寄存器。"""
- params = self.pending_segments
- if params is None:
- return
- if index >= params["total"]:
- # 段表写完后由设备统一校验公共参数和10段表并保存Flash。
- self.client.write_single(
- int(Register.CONTROL),
- int(Command.SAVE_PUBLIC),
- "SAVE_SEGMENT_CONFIGURATION",
- )
- return
- segment = params["segments"][index]
- pulses, frequency, jump, wait_condition, condition_value = segment
- wait_time = condition_value if wait_condition == int(WaitCondition.WAIT_TIME) else 0
- act_time = condition_value if wait_condition == int(WaitCondition.ACT_TIME) else 0
- values = split_u32(frequency) + split_u32(pulses & 0xFFFFFFFF)
- values += [wait_condition, wait_time, act_time, jump]
- self.client.write_multiple(segment_base(index), values, f"SET_SEGMENT_{index}")
-
- def _update_status(self, status: Dict[str, int]) -> None:
- state = status["state"]
- error = status["error"]
- current = status["current_pulses"]
- current_segment = status["current_segment"]
- display_segment = current_segment
- # 固件在总段数为1时复用单段执行器并上报段号0;只要本次由段表
- # 启动,上位机仍按段表的起始段显示目标频率和段号。
- if current_segment == 0 and self.multi_test_active:
- if self.written_segments is not None:
- display_segment = int(self.written_segments.get("start", 1))
- else:
- display_segment = max(1, int(self.start_segment_var.get()))
- self._set_state_badge(state)
- self.error_var.set(f"{error} · {error_text(error)}")
- self.current_pulses_var.set(str(current))
- self.actual_frequency_var.set(f"{status['current_frequency']} Hz")
-
- # “设定频率”显示当前段的目标值;“实际频率”直接使用设备状态区
- # 上报的定时器生效值。加减速或运行中改频时,两者可能暂时不同。
- configured_frequency = self.frequency_var.get()
- if 1 <= display_segment <= len(self.segment_frequency_vars):
- try:
- configured_frequency = int(
- self.segment_frequency_vars[display_segment - 1].get()
- )
- if configured_frequency == 0:
- configured_frequency = int(self.frequency_var.get())
- except ValueError:
- pass
- self.current_frequency_var.set(f"{configured_frequency} Hz")
-
- # 单段运行时固件按协议上报段号0;多段运行上报1~10。
- self.current_segment_var.set(
- str(display_segment) if display_segment else "0(未执行)"
- )
- if display_segment > 0:
- suffix = "(等待)" if state == int(State.WAITING) else ""
- self.running_segment_var.set(f"第 {display_segment} 段{suffix}")
- elif state in (int(State.RUNNING), int(State.PAUSED)):
- suffix = "(暂停)" if state == int(State.PAUSED) else ""
- self.running_segment_var.set(f"单段{suffix}")
- else:
- self.running_segment_var.set("未运行")
-
- self.cumulative_position_var.set(str(status["cumulative_position"]))
- self.total_pulses_var.set(str(status["total_pulses"]))
- self._update_diagnostic(status)
-
- # 多段运动可逐段改变正反向,当前段变化后同步显示实际方向。
- if self.multi_test_active and 1 <= display_segment <= len(self.segment_pulse_vars):
- try:
- segment_direction = 1 if int(self.segment_pulse_vars[display_segment - 1].get()) < 0 else 0
- self.direction_status_var.set(self._direction_display_text(segment_direction))
- except ValueError:
- pass
-
- target = self.expected_pulses if self.test_active and self.expected_pulses else self.display_target
- self.pulse_fraction_var.set(f"{current} / {target}")
- progress = min(100.0, (current * 100.0 / target)) if target else 0.0
- self.progress_var.set(progress)
-
- running = state == int(State.RUNNING)
- if (
- self.test_active
- and not self.multi_test_active
- and running
- and self.expected_constant_frequency
- and status["current_frequency"] != self.expected_constant_frequency
- ):
- actual_frequency = status["current_frequency"]
- expected_frequency = self.expected_constant_frequency
- self.test_active = False
- self.monitoring_active = False
- self.client.write_single(
- int(Register.CONTROL), int(Command.STOP), "FREQUENCY_MISMATCH_STOP"
- )
- self._set_test_result(
- f"测试失败:设定{expected_frequency}Hz,设备实际运行"
- f"{actual_frequency}Hz,已自动暂停输出。",
- "fail",
- )
- self.log(
- f"频率一致性检查失败:设定{expected_frequency}Hz,"
- f"状态寄存器报告{actual_frequency}Hz,已发送暂停命令。"
- )
- return
- frequency_applied = False
- if self.pending_frequency:
- if error != int(Error.NONE):
- self.log(
- f"动态改频失败:设备错误{error},{error_text(error)}。"
- )
- self.pending_frequency = 0
- elif running:
- self.log(
- f"动态改频已生效:新目标{self.pending_frequency}Hz,"
- f"当前曲线频率{status['current_frequency']}Hz。"
- )
- self._set_test_result(
- f"目标频率已修改为{self.pending_frequency}Hz,曲线立即重新计算。",
- "running",
- )
- self.pending_frequency = 0
- frequency_applied = True
- elif state == int(State.PAUSED):
- self.log(
- f"新频率{self.pending_frequency}Hz已保存,将在继续发送时生效。"
- )
- self._set_test_result(
- f"频率{self.pending_frequency}Hz已保存,继续发送时生效。",
- "neutral",
- )
- self.pending_frequency = 0
-
- if running:
- self.seen_running = True
- if self.test_active and not frequency_applied:
- self._set_test_result(
- f"运行中:{state_text(state)},已发送{current}个脉冲。",
- "running",
- )
- elif self.test_active and state == int(State.WAITING):
- self._set_test_result("当前段已停止输出,正在等待设定条件。", "running")
- elif self.test_active and state == int(State.COMPLETE):
- elapsed_ok = (time.monotonic() - self.test_started_at) >= 0.05
- diagnostic_flags = status.get("diagnostic_status", 0)
- diagnostic_ok = (
- (diagnostic_flags & int(DiagnosticStatus.COMPLETE)) != 0
- and (diagnostic_flags & (
- int(DiagnosticStatus.COUNT_FAIL)
- | int(DiagnosticStatus.FREQUENCY_FAIL)
- | int(DiagnosticStatus.PROFILE_FAIL)
- )) == 0
- )
- if (
- error == int(Error.NONE)
- and (self.multi_test_active or current == self.expected_pulses)
- and (self.seen_running or elapsed_ok)
- and diagnostic_ok
- ):
- if self._selected_output_mode() == OutputMode.AB_QUADRATURE:
- verification = "请用双通道示波器核对A/B相周期数及90°相位关系。"
- else:
- verification = "请核对脉冲端边沿数以及方向端的稳定电平。"
- self._set_test_result(
- f"测试通过:设备完成并报告{current}个脉冲,"
- f"个数、频率和加减速曲线诊断均通过。{verification}",
- "pass",
- )
- self.log("测试通过:完成状态、脉冲计数、频率和曲线诊断均符合目标。")
- else:
- self._set_test_result(
- f"测试失败:目标{self.expected_pulses},设备报告{current},"
- f"错误码{error};诊断异常数为个数"
- f"{status.get('diagnostic_count_errors', 0)}、频率"
- f"{status.get('diagnostic_frequency_errors', 0)}、曲线"
- f"{status.get('diagnostic_profile_errors', 0)}。",
- "fail",
- )
- self.test_active = False
- self.multi_test_active = False
- self.monitoring_active = False
- self.log("脉冲输出完成,串口保持连接,已停止周期查询。")
- elif self.test_active and (state == int(State.ERROR) or error != int(Error.NONE)):
- self._set_test_result(f"测试失败:{error_text(error)}。", "fail")
- self.test_active = False
- self.monitoring_active = False
-
- if state != self.last_state or error != self.last_error:
- self.log(f"状态变化:{state_text(state)},错误:{error_text(error)}。")
- self.last_state = state
- self.last_error = error
-
- def _update_diagnostic(self, status: Dict[str, int]) -> None:
- """把固件诊断位和采样值转换为易读结果。"""
- flags = status.get("diagnostic_status", 0)
-
- def result_text(pass_flag: DiagnosticStatus,
- fail_flag: DiagnosticStatus) -> str:
- if flags & int(fail_flag):
- return "异常"
- if flags & int(pass_flag):
- return "一致"
- if flags & int(DiagnosticStatus.ACTIVE):
- return "诊断中"
- return "未诊断"
-
- if flags & int(DiagnosticStatus.ACTIVE):
- overall = "诊断中"
- elif flags & (int(DiagnosticStatus.COUNT_FAIL) |
- int(DiagnosticStatus.FREQUENCY_FAIL) |
- int(DiagnosticStatus.PROFILE_FAIL)):
- overall = "诊断失败"
- elif flags & int(DiagnosticStatus.COMPLETE):
- overall = "诊断通过"
- else:
- overall = "未运行诊断"
-
- profile_value = status.get("diagnostic_profile_type", 0)
- try:
- profile_name = PROFILE_TEXT[ProfileType(profile_value)]
- except (ValueError, KeyError):
- profile_name = f"未知曲线{profile_value}"
- phase = DIAGNOSTIC_PHASE_TEXT.get(
- status.get("diagnostic_phase", 0), "未知阶段"
- )
-
- self.diag_overall_var.set(overall)
- self.diag_count_var.set(result_text(
- DiagnosticStatus.COUNT_PASS, DiagnosticStatus.COUNT_FAIL
- ))
- self.diag_frequency_var.set(result_text(
- DiagnosticStatus.FREQUENCY_PASS, DiagnosticStatus.FREQUENCY_FAIL
- ))
- self.diag_profile_var.set(
- f"{result_text(DiagnosticStatus.PROFILE_PASS, DiagnosticStatus.PROFILE_FAIL)}"
- f"({profile_name})"
- )
- self.diag_phase_var.set(phase)
- self.diag_pulses_var.set(
- f"{status.get('diagnostic_observed_pulses', 0)} / "
- f"{status.get('diagnostic_expected_pulses', 0)}"
- )
- self.diag_frequency_values_var.set(
- f"{status.get('diagnostic_actual_frequency', 0)} Hz / "
- f"{status.get('diagnostic_expected_frequency', 0)} Hz"
- )
- self.diag_max_error_var.set(
- f"{status.get('diagnostic_max_frequency_error', 0)} Hz"
- )
- self.diag_error_counts_var.set(
- f"个数{status.get('diagnostic_count_errors', 0)},"
- f"频率{status.get('diagnostic_frequency_errors', 0)},"
- f"曲线{status.get('diagnostic_profile_errors', 0)}"
- )
-
- def _set_state_badge(self, state: int) -> None:
- if state == int(State.RUNNING):
- colors = ("#DDEEFF", "#15558A")
- elif state == int(State.COMPLETE):
- colors = ("#DDF7EC", "#087A55")
- elif state == int(State.ERROR):
- colors = ("#FDECEC", "#B42318")
- elif state == int(State.PAUSED):
- colors = ("#FFF3D8", "#865600")
- elif state == int(State.WAITING):
- colors = ("#E8E2FF", "#5B35A5")
- else:
- colors = ("#E8EEF5", "#425466")
- self.state_badge.configure(text=state_text(state), bg=colors[0], fg=colors[1])
-
- def _set_test_result(self, text: str, kind: str) -> None:
- # 主页面不再显示独立的运行提示条;保留最近结果供流程和调试使用。
- self.last_result_text = text
- self.last_result_kind = kind
-
- def log(self, message: str) -> None:
- if not hasattr(self, "log_text"):
- return
- timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
- self.log_text.configure(state=tk.NORMAL)
- self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
- self.log_text.see(tk.END)
- self.log_text.configure(state=tk.DISABLED)
-
- def clear_log(self) -> None:
- self.log_text.configure(state=tk.NORMAL)
- self.log_text.delete("1.0", tk.END)
- self.log_text.configure(state=tk.DISABLED)
- self.log("日志已清空。")
-
- def _load_settings(self) -> None:
- try:
- settings = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
- except (OSError, ValueError):
- return
- port = str(settings.get("port", ""))
- if port in self.port_combo["values"]:
- self.port_var.set(port)
- self.slave_var.set(int(settings.get("slave", DEFAULT_SLAVE_ADDRESS)))
- preset_index = max(0, min(int(settings.get("preset", 0)), len(PRESETS) - 1))
- self.preset_var.set(PRESETS[preset_index]["name"])
- self.pulses_var.set(str(settings.get("pulses", self.pulses_var.get())))
- self.frequency_var.set(int(settings.get("frequency", self.frequency_var.get())))
- self.runtime_frequency_var.set(
- int(settings.get("runtime_frequency", self.frequency_var.get()))
- )
- self.maximum_frequency_var.set(
- int(settings.get("maximum_frequency", MAX_FREQUENCY_HZ))
- )
- saved_profile = int(settings.get("profile_type", int(ProfileType.LINEAR)))
- self.profile_var.set(
- PROFILE_TEXT[ProfileType(saved_profile)]
- if saved_profile in [int(item) for item in ProfileType]
- else PROFILE_TEXT[ProfileType.LINEAR]
- )
- self.acceleration_var.set(int(settings.get("acceleration_ms", 1000)))
- self.deceleration_var.set(int(settings.get("deceleration_ms", 1000)))
- legacy_speed_default = min(100, max(1, int(self.frequency_var.get())))
- self.start_frequency_var.set(
- int(settings.get("start_frequency", legacy_speed_default))
- )
- self.end_frequency_var.set(
- int(settings.get("end_frequency", legacy_speed_default))
- )
- self.direction_var.set(
- DIRECTION_TEXT[1 if int(settings.get("direction", 0)) else 0]
- )
- saved_run_mode = int(settings.get("run_mode", int(RunMode.RELATIVE)))
- self.run_mode_var.set(
- MODE_TEXT[RunMode.ABSOLUTE]
- if saved_run_mode == int(RunMode.ABSOLUTE)
- else MODE_TEXT[RunMode.RELATIVE]
- )
- self._on_run_mode_changed()
- saved_send_mode = int(settings.get("send_mode", int(SendMode.COMPLETE)))
- self.send_mode_var.set(
- SEND_MODE_TEXT[SendMode(saved_send_mode)]
- if saved_send_mode in [int(item) for item in SendMode]
- else SEND_MODE_TEXT[SendMode.COMPLETE]
- )
- saved_direction_logic = int(settings.get(
- "direction_logic", int(DirectionLogic.POSITIVE)
- ))
- self.direction_logic_var.set(
- DIRECTION_LOGIC_TEXT[DirectionLogic(saved_direction_logic)]
- if saved_direction_logic in [int(item) for item in DirectionLogic]
- else DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
- )
- saved_pulse_point = int(settings.get("pulse_point", 0))
- saved_direction_point = int(settings.get(
- "secondary_point",
- settings.get("b_phase_point", settings.get("direction_point", 0)),
- ))
- if saved_pulse_point in PULSE_POINTS:
- self.pulse_point_var.set(PULSE_POINTS[saved_pulse_point])
- if saved_direction_point in SECONDARY_POINTS:
- self.secondary_point_var.set(SECONDARY_POINTS[saved_direction_point])
- saved_output_mode = int(settings.get("output_mode", int(OutputMode.PULSE_DIRECTION)))
- if saved_output_mode in [int(item) for item in OutputMode]:
- self.output_mode_var.set(OUTPUT_MODE_TEXT[OutputMode(saved_output_mode)])
- self._on_output_mode_changed()
- try:
- self.display_target = int(self.pulses_var.get())
- except ValueError:
- self.display_target = 0
-
- def _save_settings(self) -> None:
- preset_index = next(
- (index for index, item in enumerate(PRESETS) if item["name"] == self.preset_var.get()),
- 0,
- )
- settings = {
- "port": self.port_var.get(),
- "slave": self.slave_var.get(),
- "preset": preset_index,
- "pulses": self.pulses_var.get(),
- "frequency": self.frequency_var.get(),
- "runtime_frequency": self.runtime_frequency_var.get(),
- "maximum_frequency": self.maximum_frequency_var.get(),
- "profile_type": next(
- (int(item) for item, text in PROFILE_TEXT.items()
- if text == self.profile_var.get()),
- int(ProfileType.LINEAR),
- ),
- "acceleration_ms": self.acceleration_var.get(),
- "deceleration_ms": self.deceleration_var.get(),
- "start_frequency": self.start_frequency_var.get(),
- "end_frequency": self.end_frequency_var.get(),
- "direction": 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0,
- "run_mode": (
- int(RunMode.ABSOLUTE)
- if self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
- else int(RunMode.RELATIVE)
- ),
- "send_mode": int(self._selected_send_mode()),
- "direction_logic": int(self._selected_direction_logic()),
- "pulse_point": point_id_from_label(PULSE_POINTS, self.pulse_point_var.get()),
- "secondary_point": point_id_from_label(
- SECONDARY_POINTS, self.secondary_point_var.get()
- ),
- "output_mode": int(self._selected_output_mode()),
- }
- try:
- SETTINGS_FILE.write_text(
- json.dumps(settings, ensure_ascii=False, indent=2),
- encoding="utf-8",
- )
- except OSError as exc:
- self.log(f"保存界面设置失败:{exc}")
-
- def close(self) -> None:
- if self.closing:
- return
- self.closing = True
- self._save_settings()
- self.client.close()
- self.root.destroy()
-
-
- def main() -> int:
- if sys.platform != "win32":
- print("该工具使用Windows原生串口API,只支持Windows。")
- return 1
- root = tk.Tk()
- PlsrTesterApp(root)
- root.mainloop()
- return 0
-
-
- if __name__ == "__main__":
- raise SystemExit(main())
|