Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

2672 rader
123 KiB

  1. """PLSR单轴脉冲测试上位机 - tkinter版本。"""
  2. from __future__ import annotations
  3. import json
  4. import queue
  5. import sys
  6. import time
  7. from datetime import datetime
  8. from pathlib import Path
  9. from typing import Dict, List
  10. import tkinter as tk
  11. from tkinter import messagebox, ttk
  12. from tkinter.scrolledtext import ScrolledText
  13. from modbus_rtu import ModbusClient, list_serial_ports
  14. from plsr_protocol import (
  15. Command,
  16. DiagnosticStatus,
  17. DIAGNOSTIC_PHASE_TEXT,
  18. DirectionLogic,
  19. DIRECTION_LOGIC_TEXT,
  20. DEFAULT_BAUD_RATE,
  21. DEFAULT_SLAVE_ADDRESS,
  22. SECONDARY_POINTS,
  23. Error,
  24. MAX_FREQUENCY_HZ,
  25. MAX_RAMP_TIME_MS,
  26. MIN_FREQUENCY_HZ,
  27. MODE_TEXT,
  28. PRESETS,
  29. PROFILE_TEXT,
  30. OUTPUT_MODE_TEXT,
  31. PULSE_POINTS,
  32. Register,
  33. WaitCondition,
  34. WAIT_CONDITION_TEXT,
  35. INPUT_POINTS,
  36. ProfileType,
  37. OutputMode,
  38. RunMode,
  39. SendMode,
  40. SEND_MODE_TEXT,
  41. State,
  42. STATUS_REGISTER_COUNT,
  43. error_text,
  44. calculate_execution_total,
  45. join_i32,
  46. join_u32,
  47. parse_status,
  48. point_id_from_label,
  49. register_read_count,
  50. segment_base,
  51. signed_to_u32,
  52. split_u32,
  53. state_text,
  54. )
  55. APP_DIR = Path(__file__).resolve().parent
  56. SETTINGS_FILE = APP_DIR / "settings.json"
  57. DIRECTION_TEXT = {0: "正向", 1: "反向"}
  58. class PlsrTesterApp:
  59. POLL_INTERVAL_MS = 250
  60. EVENT_INTERVAL_MS = 40
  61. def __init__(self, root: tk.Tk) -> None:
  62. self.root = root
  63. self.client = ModbusClient()
  64. self.closing = False
  65. self.expected_pulses = 0
  66. self.expected_constant_frequency = 0
  67. self.display_target = 0
  68. self.test_active = False
  69. self.monitoring_active = False
  70. self.seen_running = False
  71. self.test_started_at = 0.0
  72. self.last_state = -1
  73. self.last_error = -1
  74. self.pending_frequency = 0
  75. self.pending_motion = None
  76. self.pending_segments = None
  77. self.written_segments = None
  78. self.segments_written_ready = False
  79. self.start_after_segment_write = False
  80. self.multi_test_active = False
  81. self.wait_signal_select = 0
  82. self.ext_signal_select = 0
  83. self._build_style()
  84. self._build_ui()
  85. self.refresh_ports()
  86. self.apply_preset(log_message=False)
  87. self._load_settings()
  88. self._set_connected(False)
  89. self.root.protocol("WM_DELETE_WINDOW", self.close)
  90. self.root.after(self.EVENT_INTERVAL_MS, self._process_client_events)
  91. self.root.after(self.POLL_INTERVAL_MS, self._poll_status)
  92. self.log("上位机已启动。请选择USART1对应的COM端口。")
  93. def _build_style(self) -> None:
  94. self.root.title("PLSR 单轴脉冲测试台 - Python")
  95. self.root.geometry("1120x900")
  96. self.root.minsize(980, 820)
  97. self.root.configure(bg="#F3F6FA")
  98. style = ttk.Style(self.root)
  99. if "clam" in style.theme_names():
  100. style.theme_use("clam")
  101. style.configure("TFrame", background="#F3F6FA")
  102. style.configure("Card.TFrame", background="#FFFFFF")
  103. style.configure(
  104. "TLabel", background="#F3F6FA", foreground="#172033", font=("Microsoft YaHei UI", 10)
  105. )
  106. style.configure(
  107. "Card.TLabel", background="#FFFFFF", foreground="#172033", font=("Microsoft YaHei UI", 10)
  108. )
  109. style.configure(
  110. "TLabelframe", background="#FFFFFF", bordercolor="#D8E1EB", relief="solid"
  111. )
  112. style.configure(
  113. "TLabelframe.Label", background="#FFFFFF", foreground="#29445F", font=("Microsoft YaHei UI", 10, "bold")
  114. )
  115. style.configure("TButton", font=("Microsoft YaHei UI", 10), padding=(12, 7))
  116. style.configure("Primary.TButton", background="#1769AA", foreground="#FFFFFF")
  117. style.map("Primary.TButton", background=[("active", "#0F5B98"), ("disabled", "#CBD4DE")])
  118. style.configure("Danger.TButton", background="#D64545", foreground="#FFFFFF")
  119. style.map("Danger.TButton", background=[("active", "#BE3434"), ("disabled", "#CBD4DE")])
  120. style.configure("Secondary.TButton", background="#E5ECF4", foreground="#29445F")
  121. style.map("Secondary.TButton", background=[("active", "#D6E1EC")])
  122. style.configure("TNotebook", background="#F3F6FA", borderwidth=0)
  123. style.configure("TNotebook.Tab", padding=(16, 9), font=("Microsoft YaHei UI", 10))
  124. style.map(
  125. "TNotebook.Tab",
  126. background=[("selected", "#FFFFFF"), ("!selected", "#E4EAF1")],
  127. foreground=[("selected", "#15558A"), ("!selected", "#53677C")],
  128. )
  129. style.configure("Treeview", rowheight=54, font=("Microsoft YaHei UI", 9))
  130. style.configure("Treeview.Heading", font=("Microsoft YaHei UI", 9, "bold"), background="#E8EEF5")
  131. style.configure("Horizontal.TProgressbar", troughcolor="#E7EDF3", background="#2B83D5")
  132. def _build_ui(self) -> None:
  133. container = ttk.Frame(self.root, padding=(22, 17, 22, 20))
  134. container.pack(fill=tk.BOTH, expand=True)
  135. connection = ttk.LabelFrame(container, text="串口连接", padding=(14, 11))
  136. connection.pack(fill=tk.X, pady=(0, 12))
  137. connection.columnconfigure(8, weight=1)
  138. ttk.Label(connection, text="端口", style="Card.TLabel").grid(row=0, column=0, padx=(0, 6))
  139. self.port_var = tk.StringVar()
  140. self.port_combo = ttk.Combobox(connection, textvariable=self.port_var, state="readonly", width=8)
  141. self.port_combo.grid(row=0, column=1, padx=(0, 6))
  142. self.refresh_button = ttk.Button(
  143. connection, text="刷新", width=5,
  144. style="Secondary.TButton", command=self.refresh_ports
  145. )
  146. self.refresh_button.grid(row=0, column=2, padx=(0, 10))
  147. ttk.Label(connection, text="波特率", style="Card.TLabel").grid(row=0, column=3, padx=(0, 6))
  148. self.baud_var = tk.StringVar(value=str(DEFAULT_BAUD_RATE))
  149. self.baud_combo = ttk.Combobox(
  150. connection,
  151. textvariable=self.baud_var,
  152. values=("115200",),
  153. state="readonly",
  154. width=7,
  155. )
  156. self.baud_combo.grid(row=0, column=4, padx=(0, 10))
  157. ttk.Label(connection, text="格式 8-E-1", style="Card.TLabel").grid(row=0, column=5, padx=(0, 10))
  158. ttk.Label(connection, text="从站", style="Card.TLabel").grid(row=0, column=6, padx=(0, 6))
  159. self.slave_var = tk.IntVar(value=DEFAULT_SLAVE_ADDRESS)
  160. self.slave_spin = ttk.Spinbox(connection, from_=1, to=247, textvariable=self.slave_var, width=4)
  161. self.slave_spin.grid(row=0, column=7, padx=(0, 10))
  162. connection_actions = ttk.Frame(connection, style="Card.TFrame")
  163. connection_actions.grid(row=0, column=8, sticky="e")
  164. self.connect_button = ttk.Button(
  165. connection_actions,
  166. text="连接设备",
  167. style="Primary.TButton",
  168. command=self.toggle_connection,
  169. )
  170. self.connect_button.pack(side=tk.LEFT)
  171. self.connection_badge = tk.Label(
  172. connection_actions,
  173. text="未连接",
  174. bg="#FDECEC",
  175. fg="#A61B1B",
  176. font=("Microsoft YaHei UI", 10, "bold"),
  177. padx=10,
  178. pady=7,
  179. )
  180. self.connection_badge.pack(side=tk.LEFT, padx=(10, 0))
  181. notebook = ttk.Notebook(container)
  182. notebook.pack(fill=tk.BOTH, expand=True)
  183. self.control_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
  184. self.diagnostic_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
  185. self.log_page = ttk.Frame(notebook, padding=(0, 14, 0, 0))
  186. notebook.add(self.control_page, text="控制、状态与多段脉冲")
  187. notebook.add(self.diagnostic_page, text="输出诊断")
  188. notebook.add(self.log_page, text="通信日志")
  189. # 单段控制、实时状态和多段参数统一放进同一个可滚动页面。
  190. # 内容按实际需要的高度排布,避免10段表格挤压上方按钮或撑大窗口。
  191. self.control_page.columnconfigure(0, weight=1)
  192. self.control_page.rowconfigure(0, weight=1)
  193. self.control_canvas = tk.Canvas(
  194. self.control_page,
  195. bg="#F3F6FA",
  196. highlightthickness=0,
  197. borderwidth=0,
  198. )
  199. control_scrollbar = ttk.Scrollbar(
  200. self.control_page,
  201. orient=tk.VERTICAL,
  202. command=self.control_canvas.yview,
  203. )
  204. self.control_canvas.configure(yscrollcommand=control_scrollbar.set)
  205. self.control_canvas.grid(row=0, column=0, sticky="nsew")
  206. control_scrollbar.grid(row=0, column=1, sticky="ns")
  207. self.control_content = ttk.Frame(self.control_canvas)
  208. self.control_window = self.control_canvas.create_window(
  209. (0, 0), window=self.control_content, anchor="nw"
  210. )
  211. self.control_canvas.bind("<Configure>", self._resize_control_content)
  212. self.control_content.bind("<Configure>", self._update_control_scrollregion)
  213. # 鼠标进入整个合并页后启用滚轮;进入Canvas中的子控件时不会失效。
  214. self.control_page.bind("<Enter>", self._enable_control_mousewheel)
  215. self.control_page.bind("<Leave>", self._disable_control_mousewheel)
  216. self._build_control_page()
  217. self._build_multi_page()
  218. self._build_diagnostic_page()
  219. self._build_log_page()
  220. def _resize_control_content(self, event) -> None:
  221. """让滚动页内容始终占满可见宽度。"""
  222. self.control_canvas.itemconfigure(self.control_window, width=event.width)
  223. def _update_control_scrollregion(self, _event=None) -> None:
  224. """单段或多段区域尺寸变化后刷新垂直滚动范围。"""
  225. self.control_canvas.configure(scrollregion=self.control_canvas.bbox("all"))
  226. def _enable_control_mousewheel(self, _event=None) -> None:
  227. self.root.bind_all("<MouseWheel>", self._scroll_control_page)
  228. def _disable_control_mousewheel(self, _event=None) -> None:
  229. self.root.unbind_all("<MouseWheel>")
  230. def _scroll_control_page(self, event) -> None:
  231. if event.delta != 0:
  232. self.control_canvas.yview_scroll(int(-event.delta / 120), "units")
  233. def _build_control_page(self) -> None:
  234. """建立公共参数、10段参数和实时状态组成的主操作页。"""
  235. self.control_content.columnconfigure(0, weight=3, uniform="motion")
  236. self.control_content.columnconfigure(1, weight=2, uniform="motion")
  237. # 左右区域位于同一可伸缩网格行,边框高度始终保持一致。
  238. self.control_content.rowconfigure(1, weight=1)
  239. common = ttk.LabelFrame(
  240. self.control_content, text="公共参数", padding=(14, 10)
  241. )
  242. common.grid(
  243. row=0, column=0, columnspan=2, sticky="nsew", pady=(0, 10)
  244. )
  245. for column in (1, 3, 5):
  246. common.columnconfigure(column, weight=1)
  247. # 这些变量仍由既有通信、Flash同步和参数校验流程统一使用。
  248. self.preset_var = tk.StringVar(value=PRESETS[0]["name"])
  249. self.pulses_var = tk.StringVar(value="10")
  250. self.run_mode_var = tk.StringVar(value=MODE_TEXT[RunMode.RELATIVE])
  251. self.target_caption_var = tk.StringVar(value="目标脉冲数")
  252. self.direction_var = tk.StringVar(value=DIRECTION_TEXT[0])
  253. self.profile_var = tk.StringVar(value=PROFILE_TEXT[ProfileType.LINEAR])
  254. self.direction_logic_var = tk.StringVar(
  255. value=DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
  256. )
  257. self.output_mode_var = tk.StringVar(
  258. value=OUTPUT_MODE_TEXT[OutputMode.PULSE_DIRECTION]
  259. )
  260. self.send_mode_var = tk.StringVar(value=SEND_MODE_TEXT[SendMode.COMPLETE])
  261. self.pulse_point_var = tk.StringVar(value=PULSE_POINTS[0])
  262. self.secondary_point_var = tk.StringVar(value=SECONDARY_POINTS[0])
  263. self.frequency_var = tk.IntVar(value=1)
  264. self.runtime_frequency_var = tk.IntVar(value=1)
  265. self.acceleration_var = tk.IntVar(value=1000)
  266. self.deceleration_var = tk.IntVar(value=1000)
  267. self.maximum_frequency_var = tk.IntVar(value=MAX_FREQUENCY_HZ)
  268. self.start_frequency_var = tk.IntVar(value=100)
  269. self.end_frequency_var = tk.IntVar(value=100)
  270. def add_label(row: int, column: int, text: str) -> None:
  271. ttk.Label(common, text=text, style="Card.TLabel").grid(
  272. row=row, column=column, sticky="w", padx=(0, 8), pady=5
  273. )
  274. add_label(0, 0, "脉冲方向逻辑")
  275. self.direction_logic_combo = ttk.Combobox(
  276. common,
  277. textvariable=self.direction_logic_var,
  278. values=tuple(DIRECTION_LOGIC_TEXT[item] for item in DirectionLogic),
  279. state="readonly",
  280. width=24,
  281. )
  282. self.direction_logic_combo.grid(row=0, column=1, sticky="ew", padx=(0, 18), pady=5)
  283. add_label(0, 2, "脉冲类型")
  284. self.output_mode_combo = ttk.Combobox(
  285. common,
  286. textvariable=self.output_mode_var,
  287. values=tuple(OUTPUT_MODE_TEXT.values()),
  288. state="readonly",
  289. width=23,
  290. )
  291. self.output_mode_combo.grid(row=0, column=3, sticky="ew", padx=(0, 18), pady=5)
  292. self.output_mode_combo.bind("<<ComboboxSelected>>", self._on_output_mode_changed)
  293. add_label(0, 4, "脉冲发送模式")
  294. self.send_mode_combo = ttk.Combobox(
  295. common,
  296. textvariable=self.send_mode_var,
  297. values=tuple(SEND_MODE_TEXT[item] for item in SendMode),
  298. state="readonly",
  299. width=24,
  300. )
  301. self.send_mode_combo.grid(row=0, column=5, sticky="ew", pady=5)
  302. add_label(1, 0, "脉冲输出端子")
  303. self.pulse_point_combo = ttk.Combobox(
  304. common,
  305. textvariable=self.pulse_point_var,
  306. values=list(PULSE_POINTS.values()),
  307. state="readonly",
  308. width=24,
  309. )
  310. self.pulse_point_combo.grid(row=1, column=1, sticky="ew", padx=(0, 18), pady=5)
  311. add_label(1, 2, "脉冲方向端子")
  312. self.direction_point_combo = ttk.Combobox(
  313. common,
  314. textvariable=self.secondary_point_var,
  315. values=list(SECONDARY_POINTS.values()),
  316. state="readonly",
  317. width=23,
  318. )
  319. self.direction_point_combo.grid(row=1, column=3, sticky="ew", padx=(0, 18), pady=5)
  320. self.save_outputs_button = ttk.Button(
  321. common,
  322. text="保存公共参数",
  323. style="Primary.TButton",
  324. command=self.save_output_mapping,
  325. )
  326. self.save_outputs_button.grid(row=1, column=4, columnspan=2, sticky="ew", pady=5)
  327. add_label(2, 0, "脉冲默认速度(Hz)")
  328. self.frequency_spin = ttk.Spinbox(
  329. common,
  330. from_=MIN_FREQUENCY_HZ,
  331. to=MAX_FREQUENCY_HZ,
  332. textvariable=self.frequency_var,
  333. )
  334. self.frequency_spin.grid(row=2, column=1, sticky="ew", padx=(0, 18), pady=5)
  335. add_label(2, 2, "脉冲默认加速时间(ms)")
  336. self.acceleration_spin = ttk.Spinbox(
  337. common, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.acceleration_var
  338. )
  339. self.acceleration_spin.grid(row=2, column=3, sticky="ew", padx=(0, 18), pady=5)
  340. add_label(2, 4, "脉冲默认减速时间(ms)")
  341. self.deceleration_spin = ttk.Spinbox(
  342. common, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.deceleration_var
  343. )
  344. self.deceleration_spin.grid(row=2, column=5, sticky="ew", pady=5)
  345. add_label(3, 0, "最高速度(Hz)")
  346. self.maximum_frequency_spin = ttk.Spinbox(
  347. common,
  348. from_=MIN_FREQUENCY_HZ,
  349. to=MAX_FREQUENCY_HZ,
  350. textvariable=self.maximum_frequency_var,
  351. )
  352. self.maximum_frequency_spin.grid(row=3, column=1, sticky="ew", padx=(0, 18), pady=5)
  353. add_label(3, 2, "起始速度(Hz)")
  354. self.start_frequency_spin = ttk.Spinbox(
  355. common,
  356. from_=0,
  357. to=MAX_FREQUENCY_HZ,
  358. textvariable=self.start_frequency_var,
  359. )
  360. self.start_frequency_spin.grid(row=3, column=3, sticky="ew", padx=(0, 18), pady=5)
  361. add_label(3, 4, "终止速度(Hz)")
  362. self.end_frequency_spin = ttk.Spinbox(
  363. common,
  364. from_=0,
  365. to=MAX_FREQUENCY_HZ,
  366. textvariable=self.end_frequency_var,
  367. )
  368. self.end_frequency_spin.grid(row=3, column=5, sticky="ew", pady=5)
  369. add_label(4, 0, "脉冲加减速模式")
  370. self.profile_combo = ttk.Combobox(
  371. common,
  372. textvariable=self.profile_var,
  373. values=tuple(PROFILE_TEXT[item] for item in ProfileType),
  374. state="readonly",
  375. width=24,
  376. )
  377. self.profile_combo.grid(row=4, column=1, sticky="ew", padx=(0, 18), pady=5)
  378. add_label(5, 0, "运行中目标频率(Hz)")
  379. runtime_frequency_box = ttk.Frame(common, style="Card.TFrame")
  380. runtime_frequency_box.grid(row=5, column=1, sticky="ew", padx=(0, 18), pady=5)
  381. runtime_frequency_box.columnconfigure(0, weight=1)
  382. self.runtime_frequency_spin = ttk.Spinbox(
  383. runtime_frequency_box,
  384. from_=MIN_FREQUENCY_HZ,
  385. to=MAX_FREQUENCY_HZ,
  386. textvariable=self.runtime_frequency_var,
  387. width=11,
  388. )
  389. self.runtime_frequency_spin.grid(row=0, column=0, sticky="ew")
  390. self.update_frequency_button = ttk.Button(
  391. runtime_frequency_box,
  392. text="立即修改当前段",
  393. style="Secondary.TButton",
  394. command=self.update_frequency,
  395. )
  396. self.update_frequency_button.grid(row=0, column=1, padx=(6, 0))
  397. ttk.Label(
  398. common,
  399. text="仅影响本次当前段,不修改默认速度、段表或Flash",
  400. style="Card.TLabel",
  401. foreground="#5B7083",
  402. ).grid(row=5, column=2, columnspan=4, sticky="w", pady=5)
  403. status = ttk.LabelFrame(self.control_content, text="实时状态", padding=16)
  404. status.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
  405. status.columnconfigure(1, weight=1)
  406. self.state_badge = tk.Label(
  407. status,
  408. text="空闲",
  409. bg="#E8EEF5",
  410. fg="#425466",
  411. font=("Microsoft YaHei UI", 10, "bold"),
  412. padx=13,
  413. pady=6,
  414. )
  415. ttk.Label(status, text="运行状态", style="Card.TLabel").grid(
  416. row=0, column=0, sticky="w", pady=6
  417. )
  418. self.state_badge.grid(row=0, column=1, sticky="e", pady=6)
  419. self.error_var = tk.StringVar(value="0 · 无错误")
  420. self.direction_status_var = tk.StringVar(value=DIRECTION_TEXT[0])
  421. self.current_pulses_var = tk.StringVar(value="0")
  422. self.current_frequency_var = tk.StringVar(value="0 Hz")
  423. self.actual_frequency_var = tk.StringVar(value="0 Hz")
  424. self.running_segment_var = tk.StringVar(value="未运行")
  425. self.cumulative_position_var = tk.StringVar(value="0")
  426. self.total_pulses_var = tk.StringVar(value="0")
  427. status_rows = [
  428. ("错误码", self.error_var),
  429. ("方向", self.direction_status_var),
  430. ("当前设定频率", self.current_frequency_var),
  431. ("运行中修改后的实际频率", self.actual_frequency_var),
  432. ("当前运行段", self.running_segment_var),
  433. ("当前Y点累计位置", self.cumulative_position_var),
  434. ("总脉冲数(不计方向)", self.total_pulses_var),
  435. ]
  436. for index, (caption, variable) in enumerate(status_rows, start=1):
  437. ttk.Label(status, text=caption, style="Card.TLabel").grid(
  438. row=index, column=0, sticky="w", pady=7
  439. )
  440. ttk.Label(
  441. status,
  442. textvariable=variable,
  443. style="Card.TLabel",
  444. foreground="#183B56",
  445. font=("Microsoft YaHei UI", 10, "bold"),
  446. ).grid(row=index, column=1, sticky="e", pady=7)
  447. ttk.Label(status, text="本次运动进度", style="Card.TLabel").grid(
  448. row=8, column=0, columnspan=2, sticky="w", pady=(14, 6)
  449. )
  450. self.progress_var = tk.DoubleVar(value=0.0)
  451. self.progress = ttk.Progressbar(
  452. status,
  453. maximum=100.0,
  454. variable=self.progress_var,
  455. style="Horizontal.TProgressbar",
  456. )
  457. self.progress.grid(row=9, column=0, columnspan=2, sticky="ew")
  458. self.pulse_fraction_var = tk.StringVar(value="0 / 0")
  459. ttk.Label(
  460. status,
  461. textvariable=self.pulse_fraction_var,
  462. style="Card.TLabel",
  463. foreground="#6E7F91",
  464. ).grid(row=10, column=0, columnspan=2, sticky="e", pady=(4, 10))
  465. status_actions = ttk.Frame(status, style="Card.TFrame")
  466. status_actions.grid(row=11, column=0, columnspan=2, sticky="ew")
  467. for column in range(3):
  468. status_actions.columnconfigure(column, weight=1)
  469. self.read_button = ttk.Button(
  470. status_actions,
  471. text="立即读取状态",
  472. style="Secondary.TButton",
  473. command=self.request_status,
  474. )
  475. self.read_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
  476. self.clear_position_button = ttk.Button(
  477. status_actions,
  478. text="清零当前Y点位置",
  479. style="Danger.TButton",
  480. command=self.clear_current_position,
  481. )
  482. self.clear_position_button.grid(row=0, column=1, sticky="ew", padx=4)
  483. self.clear_total_button = ttk.Button(
  484. status_actions,
  485. text="清零总脉冲数",
  486. style="Danger.TButton",
  487. command=self.clear_total_count,
  488. )
  489. self.clear_total_button.grid(row=0, column=2, sticky="ew", padx=(4, 0))
  490. def _build_control_page_legacy(self) -> None:
  491. self.control_content.columnconfigure(0, weight=1, uniform="control")
  492. self.control_content.columnconfigure(1, weight=1, uniform="control")
  493. params = ttk.LabelFrame(self.control_content, text="测试参数", padding=(14, 10))
  494. params.grid(row=0, column=0, sticky="nsew", padx=(0, 8))
  495. params.columnconfigure(1, weight=1)
  496. mapping = tk.Frame(params, bg="#EAF4FF", padx=10, pady=7)
  497. mapping.grid(row=0, column=0, columnspan=3, sticky="ew", pady=(0, 8))
  498. mapping.columnconfigure(1, weight=1)
  499. mapping.columnconfigure(3, weight=1)
  500. tk.Label(
  501. mapping,
  502. text="输出方式与端子映射(保存到设备Flash)",
  503. bg="#EAF4FF",
  504. fg="#15558A",
  505. anchor="w",
  506. font=("Microsoft YaHei UI", 10, "bold"),
  507. ).grid(row=0, column=0, columnspan=5, sticky="w", pady=(0, 7))
  508. tk.Label(mapping, text="脉冲/A相", bg="#EAF4FF", fg="#29445F").grid(
  509. row=1, column=0, sticky="w", padx=(0, 5)
  510. )
  511. self.pulse_point_var = tk.StringVar(value=PULSE_POINTS[0])
  512. self.pulse_point_combo = ttk.Combobox(
  513. mapping,
  514. textvariable=self.pulse_point_var,
  515. values=list(PULSE_POINTS.values()),
  516. state="readonly",
  517. width=21,
  518. )
  519. self.pulse_point_combo.grid(row=1, column=1, sticky="ew", padx=(0, 9))
  520. tk.Label(mapping, text="方向/B相", bg="#EAF4FF", fg="#29445F").grid(
  521. row=1, column=2, sticky="w", padx=(0, 5)
  522. )
  523. # 第二输出点在协议中仍使用0~3编码,分别对应Y12~Y15。
  524. self.secondary_point_var = tk.StringVar(value=SECONDARY_POINTS[0])
  525. self.direction_point_combo = ttk.Combobox(
  526. mapping,
  527. textvariable=self.secondary_point_var,
  528. values=list(SECONDARY_POINTS.values()),
  529. state="readonly",
  530. width=13,
  531. )
  532. self.direction_point_combo.grid(row=1, column=3, sticky="ew", padx=(0, 9))
  533. self.save_outputs_button = ttk.Button(
  534. mapping,
  535. text="应用并保存",
  536. style="Primary.TButton",
  537. command=self.save_output_mapping,
  538. )
  539. self.save_outputs_button.grid(row=1, column=4, sticky="ew")
  540. tk.Label(mapping, text="输出方式", bg="#EAF4FF", fg="#29445F").grid(
  541. row=2, column=0, sticky="w", padx=(0, 5), pady=(7, 0)
  542. )
  543. self.output_mode_var = tk.StringVar(
  544. value=OUTPUT_MODE_TEXT[OutputMode.PULSE_DIRECTION]
  545. )
  546. self.output_mode_combo = ttk.Combobox(
  547. mapping,
  548. textvariable=self.output_mode_var,
  549. values=tuple(OUTPUT_MODE_TEXT.values()),
  550. state="readonly",
  551. )
  552. self.output_mode_combo.grid(
  553. row=2, column=1, columnspan=2, sticky="ew", padx=(0, 9), pady=(7, 0)
  554. )
  555. self.output_mode_combo.bind("<<ComboboxSelected>>", self._on_output_mode_changed)
  556. tk.Label(mapping, text="发送模式", bg="#EAF4FF", fg="#29445F").grid(
  557. row=2, column=3, sticky="e", padx=(0, 5), pady=(7, 0)
  558. )
  559. self.send_mode_var = tk.StringVar(value=SEND_MODE_TEXT[SendMode.COMPLETE])
  560. self.send_mode_combo = ttk.Combobox(
  561. mapping,
  562. textvariable=self.send_mode_var,
  563. values=tuple(SEND_MODE_TEXT[item] for item in SendMode),
  564. state="readonly",
  565. width=24,
  566. )
  567. self.send_mode_combo.grid(row=2, column=4, sticky="ew", pady=(7, 0))
  568. tk.Label(mapping, text="方向逻辑", bg="#EAF4FF", fg="#29445F").grid(
  569. row=3, column=0, sticky="w", padx=(0, 5), pady=(7, 0)
  570. )
  571. self.direction_logic_var = tk.StringVar(
  572. value=DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
  573. )
  574. self.direction_logic_combo = ttk.Combobox(
  575. mapping,
  576. textvariable=self.direction_logic_var,
  577. values=tuple(DIRECTION_LOGIC_TEXT[item] for item in DirectionLogic),
  578. state="readonly",
  579. )
  580. self.direction_logic_combo.grid(
  581. row=3, column=1, columnspan=4, sticky="ew", pady=(7, 0)
  582. )
  583. self.output_mode_hint = tk.Label(
  584. mapping,
  585. text="",
  586. bg="#EAF4FF",
  587. fg="#476A89",
  588. anchor="w",
  589. )
  590. # 详细说明通过选择项本身表达,不再单独占用一行,避免小屏幕裁掉运行按钮。
  591. self._on_output_mode_changed()
  592. ttk.Label(params, text="测试预设", style="Card.TLabel").grid(row=1, column=0, sticky="w", pady=4)
  593. self.preset_var = tk.StringVar(value=PRESETS[0]["name"])
  594. self.preset_combo = ttk.Combobox(
  595. params,
  596. textvariable=self.preset_var,
  597. values=[item["name"] for item in PRESETS],
  598. state="readonly",
  599. )
  600. self.preset_combo.grid(row=1, column=1, sticky="ew", padx=10, pady=4)
  601. ttk.Button(params, text="应用", style="Secondary.TButton", command=self.apply_preset).grid(
  602. row=1, column=2, pady=4
  603. )
  604. ttk.Label(params, text="运行模式", style="Card.TLabel").grid(row=2, column=0, sticky="w", pady=4)
  605. self.run_mode_var = tk.StringVar(value=MODE_TEXT[RunMode.RELATIVE])
  606. self.run_mode_combo = ttk.Combobox(
  607. params,
  608. textvariable=self.run_mode_var,
  609. values=(MODE_TEXT[RunMode.RELATIVE], MODE_TEXT[RunMode.ABSOLUTE]),
  610. state="readonly",
  611. )
  612. self.run_mode_combo.grid(row=2, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  613. self.run_mode_combo.bind("<<ComboboxSelected>>", self._on_run_mode_changed)
  614. self.target_caption_var = tk.StringVar(value="目标脉冲数")
  615. ttk.Label(params, textvariable=self.target_caption_var, style="Card.TLabel").grid(row=3, column=0, sticky="w", pady=4)
  616. self.pulses_var = tk.StringVar(value="10")
  617. self.pulses_entry = ttk.Entry(params, textvariable=self.pulses_var)
  618. self.pulses_entry.grid(row=3, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  619. ttk.Label(params, text="默认速度/匀速(Hz)", style="Card.TLabel").grid(row=4, column=0, sticky="w", pady=4)
  620. self.frequency_var = tk.IntVar(value=1)
  621. self.frequency_spin = ttk.Spinbox(
  622. params,
  623. from_=MIN_FREQUENCY_HZ,
  624. to=MAX_FREQUENCY_HZ,
  625. textvariable=self.frequency_var,
  626. )
  627. self.frequency_spin.grid(row=4, column=1, sticky="ew", padx=10, pady=4)
  628. self.update_frequency_button = ttk.Button(
  629. params,
  630. text="立即修改默认速度",
  631. style="Secondary.TButton",
  632. command=self.update_frequency,
  633. )
  634. self.update_frequency_button.grid(row=4, column=2, sticky="ew", pady=4)
  635. ttk.Label(params, text="起始速度(Hz)", style="Card.TLabel").grid(row=5, column=0, sticky="w", pady=4)
  636. self.start_frequency_var = tk.IntVar(value=100)
  637. self.start_frequency_spin = ttk.Spinbox(
  638. params, from_=0, to=MAX_FREQUENCY_HZ,
  639. textvariable=self.start_frequency_var
  640. )
  641. self.start_frequency_spin.grid(row=5, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  642. ttk.Label(params, text="终止速度(Hz)", style="Card.TLabel").grid(row=6, column=0, sticky="w", pady=4)
  643. self.end_frequency_var = tk.IntVar(value=100)
  644. self.end_frequency_spin = ttk.Spinbox(
  645. params, from_=0, to=MAX_FREQUENCY_HZ,
  646. textvariable=self.end_frequency_var
  647. )
  648. self.end_frequency_spin.grid(row=6, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  649. ttk.Label(params, text="加减速模式", style="Card.TLabel").grid(row=7, column=0, sticky="w", pady=4)
  650. self.profile_var = tk.StringVar(value=PROFILE_TEXT[ProfileType.LINEAR])
  651. self.profile_combo = ttk.Combobox(
  652. params,
  653. textvariable=self.profile_var,
  654. values=tuple(PROFILE_TEXT[item] for item in ProfileType),
  655. state="readonly",
  656. )
  657. self.profile_combo.grid(row=7, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  658. ttk.Label(params, text="加速时间(ms)", style="Card.TLabel").grid(row=8, column=0, sticky="w", pady=4)
  659. self.acceleration_var = tk.IntVar(value=1000)
  660. self.acceleration_spin = ttk.Spinbox(
  661. params, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.acceleration_var
  662. )
  663. self.acceleration_spin.grid(row=8, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  664. ttk.Label(params, text="减速时间(ms)", style="Card.TLabel").grid(row=9, column=0, sticky="w", pady=4)
  665. self.deceleration_var = tk.IntVar(value=1000)
  666. self.deceleration_spin = ttk.Spinbox(
  667. params, from_=0, to=MAX_RAMP_TIME_MS, textvariable=self.deceleration_var
  668. )
  669. self.deceleration_spin.grid(row=9, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  670. ttk.Label(params, text="运动方向", style="Card.TLabel").grid(row=10, column=0, sticky="w", pady=4)
  671. self.direction_var = tk.StringVar(value=DIRECTION_TEXT[0])
  672. self.direction_combo = ttk.Combobox(
  673. params,
  674. textvariable=self.direction_var,
  675. values=(DIRECTION_TEXT[0], DIRECTION_TEXT[1]),
  676. state="readonly",
  677. )
  678. self.direction_combo.grid(row=10, column=1, columnspan=2, sticky="ew", padx=(10, 0), pady=4)
  679. button_box = ttk.Frame(params, style="Card.TFrame")
  680. button_box.grid(row=11, column=0, columnspan=3, sticky="ew", pady=(8, 0))
  681. button_box.columnconfigure(0, weight=2)
  682. button_box.columnconfigure(1, weight=1)
  683. button_box.columnconfigure(2, weight=1)
  684. self.start_button = ttk.Button(
  685. button_box,
  686. text="写参数并启动",
  687. style="Primary.TButton",
  688. command=self.start_motion,
  689. )
  690. self.start_button.grid(row=0, column=0, sticky="ew", padx=(0, 6))
  691. self.stop_button = ttk.Button(
  692. button_box,
  693. text="暂停发送",
  694. style="Danger.TButton",
  695. command=self.stop_motion,
  696. )
  697. self.stop_button.grid(row=0, column=1, sticky="ew", padx=6)
  698. self.resume_button = ttk.Button(
  699. button_box,
  700. text="继续当前脉冲",
  701. style="Secondary.TButton",
  702. command=self.resume_motion,
  703. )
  704. self.resume_button.grid(row=0, column=2, sticky="ew", padx=(0, 0))
  705. status = ttk.LabelFrame(self.control_content, text="实时状态", padding=16)
  706. status.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
  707. status.columnconfigure(1, weight=1)
  708. self.state_badge = tk.Label(
  709. status,
  710. text="空闲",
  711. bg="#E8EEF5",
  712. fg="#425466",
  713. font=("Microsoft YaHei UI", 10, "bold"),
  714. padx=13,
  715. pady=6,
  716. )
  717. ttk.Label(status, text="运行状态", style="Card.TLabel").grid(row=0, column=0, sticky="w", pady=6)
  718. self.state_badge.grid(row=0, column=1, sticky="e", pady=6)
  719. self.error_var = tk.StringVar(value="0 · 无错误")
  720. self.direction_status_var = tk.StringVar(value=DIRECTION_TEXT[0])
  721. self.current_pulses_var = tk.StringVar(value="0")
  722. self.current_frequency_var = tk.StringVar(value="0 Hz")
  723. self.actual_frequency_var = tk.StringVar(value="0 Hz")
  724. self.running_segment_var = tk.StringVar(value="未运行")
  725. self.cumulative_position_var = tk.StringVar(value="0")
  726. self.total_pulses_var = tk.StringVar(value="0")
  727. status_rows = [
  728. ("错误码", self.error_var),
  729. ("方向", self.direction_status_var),
  730. ("已发送脉冲", self.current_pulses_var),
  731. ("当前设定频率", self.current_frequency_var),
  732. ("运行中修改后的实际频率", self.actual_frequency_var),
  733. ("当前运行段", self.running_segment_var),
  734. ("当前Y点累计位置", self.cumulative_position_var),
  735. ("总脉冲数(不计方向)", self.total_pulses_var),
  736. ]
  737. for index, (caption, variable) in enumerate(status_rows, start=1):
  738. ttk.Label(status, text=caption, style="Card.TLabel").grid(row=index, column=0, sticky="w", pady=7)
  739. ttk.Label(
  740. status,
  741. textvariable=variable,
  742. style="Card.TLabel",
  743. foreground="#183B56",
  744. font=("Microsoft YaHei UI", 10, "bold"),
  745. ).grid(row=index, column=1, sticky="e", pady=7)
  746. ttk.Label(status, text="本次运动进度", style="Card.TLabel").grid(
  747. row=9, column=0, columnspan=2, sticky="w", pady=(14, 6)
  748. )
  749. self.progress_var = tk.DoubleVar(value=0.0)
  750. self.progress = ttk.Progressbar(
  751. status,
  752. maximum=100.0,
  753. variable=self.progress_var,
  754. style="Horizontal.TProgressbar",
  755. )
  756. self.progress.grid(row=10, column=0, columnspan=2, sticky="ew")
  757. self.pulse_fraction_var = tk.StringVar(value="0 / 0")
  758. ttk.Label(
  759. status,
  760. textvariable=self.pulse_fraction_var,
  761. style="Card.TLabel",
  762. foreground="#6E7F91",
  763. ).grid(row=11, column=0, columnspan=2, sticky="e", pady=(4, 10))
  764. self.test_result_label = tk.Label(
  765. status,
  766. text="等待测试",
  767. bg="#EDF1F5",
  768. fg="#526170",
  769. anchor="w",
  770. justify="left",
  771. wraplength=430,
  772. padx=11,
  773. pady=10,
  774. font=("Microsoft YaHei UI", 9),
  775. )
  776. self.test_result_label.grid(row=12, column=0, columnspan=2, sticky="ew", pady=(3, 11))
  777. status_actions = ttk.Frame(status, style="Card.TFrame")
  778. status_actions.grid(row=13, column=0, columnspan=2, sticky="ew")
  779. status_actions.columnconfigure(0, weight=1)
  780. status_actions.columnconfigure(1, weight=1)
  781. status_actions.columnconfigure(2, weight=1)
  782. self.read_button = ttk.Button(
  783. status_actions,
  784. text="立即读取状态",
  785. style="Secondary.TButton",
  786. command=self.request_status,
  787. )
  788. self.read_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
  789. self.clear_position_button = ttk.Button(
  790. status_actions,
  791. text="清零当前Y点位置",
  792. style="Danger.TButton",
  793. command=self.clear_current_position,
  794. )
  795. self.clear_position_button.grid(row=0, column=1, sticky="ew", padx=4)
  796. self.clear_total_button = ttk.Button(
  797. status_actions,
  798. text="清零总脉冲数",
  799. style="Danger.TButton",
  800. command=self.clear_total_count,
  801. )
  802. self.clear_total_button.grid(row=0, column=2, sticky="ew", padx=(4, 0))
  803. def _build_multi_page(self) -> None:
  804. """建立1~10段脉冲表;脉冲数符号决定各段运动方向。"""
  805. self.multi_page = ttk.Frame(self.control_content)
  806. self.multi_page.grid(row=1, column=0, sticky="nsew", padx=(0, 8))
  807. card = ttk.LabelFrame(self.multi_page, text="10段参数配置", padding=12)
  808. card.pack(fill=tk.BOTH, expand=True)
  809. card.columnconfigure(0, weight=1)
  810. header = ttk.Frame(card, style="Card.TFrame")
  811. header.grid(row=0, column=0, sticky="ew", pady=(0, 12))
  812. ttk.Label(header, text="脉冲总段数", style="Card.TLabel").pack(side=tk.LEFT)
  813. self.total_segments_var = tk.IntVar(value=3)
  814. ttk.Spinbox(header, from_=1, to=10, width=7,
  815. textvariable=self.total_segments_var).pack(side=tk.LEFT, padx=(8, 24))
  816. ttk.Label(header, text="起始执行段数", style="Card.TLabel").pack(side=tk.LEFT)
  817. self.start_segment_var = tk.IntVar(value=1)
  818. ttk.Spinbox(header, from_=1, to=10, width=7,
  819. textvariable=self.start_segment_var).pack(side=tk.LEFT, padx=(8, 24))
  820. self.current_segment_var = tk.StringVar(value="0(未执行)")
  821. ttk.Label(header, text="当前段", style="Card.TLabel").pack(side=tk.LEFT)
  822. ttk.Label(header, textvariable=self.current_segment_var,
  823. style="Card.TLabel", foreground="#15558A").pack(side=tk.LEFT, padx=8)
  824. table = ttk.Frame(card, style="Card.TFrame")
  825. table.grid(row=1, column=0, sticky="nsew")
  826. headings = ("段", "有符号脉冲数", "频率(Hz)", "跳转段", "等待条件", "条件值(ms/X点)")
  827. for column, text in enumerate(headings):
  828. table.columnconfigure(column, weight=1 if column in (1, 2, 4, 5) else 0)
  829. ttk.Label(table, text=text, style="Card.TLabel",
  830. font=("Microsoft YaHei UI", 9, "bold")).grid(
  831. row=0, column=column, padx=4, pady=4, sticky="ew")
  832. self.segment_pulse_vars = []
  833. self.segment_frequency_vars = []
  834. self.segment_jump_vars = []
  835. self.segment_wait_vars = []
  836. self.segment_condition_vars = []
  837. for index in range(10):
  838. pulses = tk.StringVar(value=str((index + 1) * 100))
  839. frequency = tk.StringVar(value=str((index + 1) * 100))
  840. self.segment_pulse_vars.append(pulses)
  841. self.segment_frequency_vars.append(frequency)
  842. jump = tk.IntVar(value=0)
  843. wait = tk.StringVar(
  844. value=WAIT_CONDITION_TEXT[WaitCondition.WAIT_TIME]
  845. )
  846. condition = tk.StringVar(value="0")
  847. self.segment_jump_vars.append(jump)
  848. self.segment_wait_vars.append(wait)
  849. self.segment_condition_vars.append(condition)
  850. row = index + 1
  851. ttk.Label(table, text=str(index + 1), style="Card.TLabel").grid(
  852. row=row, column=0, padx=4, pady=3)
  853. ttk.Entry(table, textvariable=pulses, width=11).grid(
  854. row=row, column=1, padx=4, pady=3, sticky="ew")
  855. ttk.Entry(table, textvariable=frequency, width=9).grid(
  856. row=row, column=2, padx=4, pady=3, sticky="ew")
  857. ttk.Spinbox(table, from_=0, to=10, textvariable=jump, width=5).grid(
  858. row=row, column=3, padx=4, pady=3, sticky="ew")
  859. ttk.Combobox(table, textvariable=wait,
  860. values=tuple(WAIT_CONDITION_TEXT.values()), state="readonly",
  861. width=13).grid(row=row, column=4, padx=4, pady=3, sticky="ew")
  862. ttk.Entry(table, textvariable=condition, width=10).grid(
  863. row=row, column=5, padx=4, pady=3, sticky="ew")
  864. segment_actions = ttk.Frame(card, style="Card.TFrame")
  865. segment_actions.grid(row=2, column=0, sticky="ew", pady=(14, 0))
  866. segment_actions.columnconfigure(0, weight=1)
  867. segment_actions.columnconfigure(1, weight=1)
  868. segment_actions.columnconfigure(2, weight=1)
  869. segment_actions.columnconfigure(3, weight=1)
  870. self.write_segments_button = ttk.Button(
  871. segment_actions, text="写入段表", style="Secondary.TButton",
  872. command=self.write_segment_table,
  873. )
  874. self.write_segments_button.grid(row=0, column=0, sticky="ew", padx=(0, 4))
  875. self.start_segments_button = ttk.Button(
  876. segment_actions, text="启动", style="Primary.TButton",
  877. command=self.start_segment_motion,
  878. )
  879. self.start_segments_button.grid(row=0, column=1, sticky="ew", padx=4)
  880. self.stop_button = ttk.Button(
  881. segment_actions, text="暂停", style="Danger.TButton",
  882. command=self.stop_motion,
  883. )
  884. self.stop_button.grid(row=0, column=2, sticky="ew", padx=4)
  885. self.resume_button = ttk.Button(
  886. segment_actions, text="继续", style="Secondary.TButton",
  887. command=self.resume_motion,
  888. )
  889. self.resume_button.grid(row=0, column=3, sticky="ew", padx=(4, 0))
  890. def _build_diagnostic_page(self) -> None:
  891. """构建固件在线诊断结果页。"""
  892. card = ttk.LabelFrame(
  893. self.diagnostic_page, text="PLSR输出信号在线诊断", padding=(18, 14)
  894. )
  895. card.pack(fill=tk.BOTH, expand=True)
  896. card.columnconfigure(1, weight=1)
  897. self.diag_overall_var = tk.StringVar(value="未运行诊断")
  898. self.diag_count_var = tk.StringVar(value="未诊断")
  899. self.diag_frequency_var = tk.StringVar(value="未诊断")
  900. self.diag_profile_var = tk.StringVar(value="未诊断")
  901. self.diag_phase_var = tk.StringVar(value="空闲")
  902. self.diag_pulses_var = tk.StringVar(value="0 / 0")
  903. self.diag_frequency_values_var = tk.StringVar(value="0 Hz / 0 Hz")
  904. self.diag_max_error_var = tk.StringVar(value="0 Hz")
  905. self.diag_error_counts_var = tk.StringVar(value="个数0,频率0,曲线0")
  906. rows = [
  907. ("综合结果", self.diag_overall_var),
  908. ("脉冲个数", self.diag_count_var),
  909. ("脉冲频率", self.diag_frequency_var),
  910. ("加减速曲线", self.diag_profile_var),
  911. ("当前曲线阶段", self.diag_phase_var),
  912. ("实际/设定脉冲", self.diag_pulses_var),
  913. ("实际/设定频率", self.diag_frequency_values_var),
  914. ("最大频率偏差", self.diag_max_error_var),
  915. ("异常记录", self.diag_error_counts_var),
  916. ]
  917. for row, (caption, variable) in enumerate(rows):
  918. ttk.Label(card, text=caption).grid(
  919. row=row, column=0, sticky="w", padx=(0, 24), pady=7
  920. )
  921. ttk.Label(card, textvariable=variable).grid(
  922. row=row, column=1, sticky="w", pady=7
  923. )
  924. actions = ttk.Frame(card)
  925. actions.grid(row=len(rows), column=0, columnspan=2, sticky="ew", pady=(14, 8))
  926. actions.columnconfigure(0, weight=1)
  927. actions.columnconfigure(1, weight=1)
  928. ttk.Button(
  929. actions, text="立即读取诊断", style="Secondary.TButton",
  930. command=self.request_diagnostic,
  931. ).grid(row=0, column=0, sticky="ew", padx=(0, 6))
  932. ttk.Button(
  933. actions, text="清除诊断记录", style="Danger.TButton",
  934. command=self.clear_diagnostic,
  935. ).grid(row=0, column=1, sticky="ew", padx=(6, 0))
  936. tk.Label(
  937. card,
  938. text=("说明:诊断依据硬件定时器实际分频参数和脉冲完成中断计数,"
  939. "可发现内部配置、频率及曲线执行异常;若要诊断端子断线或驱动器漏收,"
  940. "还需将输出接回计数输入或使用示波器。"),
  941. bg="#FFF3D8", fg="#865600", anchor="w", justify=tk.LEFT,
  942. padx=10, pady=9, wraplength=820,
  943. ).grid(row=len(rows) + 1, column=0, columnspan=2, sticky="ew", pady=(8, 0))
  944. def _build_log_page(self) -> None:
  945. toolbar = ttk.Frame(self.log_page)
  946. toolbar.pack(fill=tk.X, pady=(0, 8))
  947. ttk.Label(
  948. toolbar,
  949. text="显示应用事件以及实际发送、接收的Modbus RTU十六进制帧。",
  950. foreground="#6E7F91",
  951. ).pack(side=tk.LEFT)
  952. ttk.Button(
  953. toolbar,
  954. text="清空日志",
  955. style="Secondary.TButton",
  956. command=self.clear_log,
  957. ).pack(side=tk.RIGHT)
  958. self.log_text = ScrolledText(
  959. self.log_page,
  960. wrap=tk.WORD,
  961. bg="#101A26",
  962. fg="#D9E5F2",
  963. insertbackground="#FFFFFF",
  964. font=("Consolas", 10),
  965. relief=tk.FLAT,
  966. padx=10,
  967. pady=10,
  968. )
  969. self.log_text.pack(fill=tk.BOTH, expand=True)
  970. self.log_text.configure(state=tk.DISABLED)
  971. def refresh_ports(self) -> None:
  972. previous = self.port_var.get()
  973. ports = list_serial_ports()
  974. self.port_combo["values"] = ports
  975. if previous in ports:
  976. self.port_var.set(previous)
  977. elif ports:
  978. self.port_var.set(ports[0])
  979. else:
  980. self.port_var.set("")
  981. self.log(f"检测到串口:{', '.join(ports) if ports else '无'}")
  982. def toggle_connection(self) -> None:
  983. if self.client.is_open:
  984. self._disconnect("串口已手动断开。")
  985. return
  986. port = self.port_var.get().strip()
  987. if not port:
  988. messagebox.showwarning("未选择串口", "没有检测到可用串口,请连接设备后刷新。")
  989. return
  990. try:
  991. slave = int(self.slave_var.get())
  992. baud = int(self.baud_var.get())
  993. self.client.open(port, baud, slave)
  994. except Exception as exc:
  995. messagebox.showerror("连接失败", str(exc))
  996. self.log(f"连接失败:{exc}")
  997. return
  998. self._set_connected(True)
  999. self.log(f"已连接 {port},{baud},8-E-1,从站{slave}。")
  1000. self.client.read_holding(
  1001. int(Register.PULSE_POINT), register_read_count(), "INITIAL_SYSTEM"
  1002. )
  1003. def _disconnect(self, reason: str) -> None:
  1004. """关闭串口并统一恢复界面状态。"""
  1005. if self.client.is_open:
  1006. self.client.close()
  1007. self.pending_motion = None
  1008. self.pending_segments = None
  1009. self.written_segments = None
  1010. self.segments_written_ready = False
  1011. self.start_after_segment_write = False
  1012. # 断开时丢弃已排队的旧事件,避免日志又出现一组过期TX/RX。
  1013. while True:
  1014. try:
  1015. self.client.events.get_nowait()
  1016. except queue.Empty:
  1017. break
  1018. self.test_active = False
  1019. self.monitoring_active = False
  1020. self.seen_running = False
  1021. self._set_connected(False)
  1022. self.log(reason)
  1023. def _set_connected(self, connected: bool) -> None:
  1024. self.connection_badge.configure(
  1025. text="已连接" if connected else "未连接",
  1026. bg="#DDF7EC" if connected else "#FDECEC",
  1027. fg="#087A55" if connected else "#A61B1B",
  1028. )
  1029. self.connect_button.configure(text="断开连接" if connected else "连接设备")
  1030. port_state = tk.DISABLED if connected else "readonly"
  1031. self.port_combo.configure(state=port_state)
  1032. self.baud_combo.configure(state=port_state)
  1033. self.slave_spin.configure(state=tk.DISABLED if connected else tk.NORMAL)
  1034. self.refresh_button.configure(state=tk.DISABLED if connected else tk.NORMAL)
  1035. control_state = tk.NORMAL if connected else tk.DISABLED
  1036. self.stop_button.configure(state=control_state)
  1037. self.resume_button.configure(state=control_state)
  1038. self.update_frequency_button.configure(state=control_state)
  1039. self.save_outputs_button.configure(state=control_state)
  1040. self.read_button.configure(state=control_state)
  1041. self.clear_position_button.configure(state=control_state)
  1042. self.clear_total_button.configure(state=control_state)
  1043. self.write_segments_button.configure(state=control_state)
  1044. # 未手动写入段表时也允许点击启动;启动流程会先
  1045. # 自动写入当前界面的默认参数,设备确认后再启动。
  1046. self.start_segments_button.configure(
  1047. state=(tk.NORMAL if connected else tk.DISABLED)
  1048. )
  1049. def apply_preset(self, log_message: bool = True) -> None:
  1050. selected = self.preset_var.get()
  1051. preset = next((item for item in PRESETS if item["name"] == selected), PRESETS[0])
  1052. self.pulses_var.set(str(preset["pulses"]))
  1053. self.frequency_var.set(preset["frequency"])
  1054. self.runtime_frequency_var.set(preset["frequency"])
  1055. self.acceleration_var.set(preset["acceleration_ms"])
  1056. self.deceleration_var.set(preset["deceleration_ms"])
  1057. self.start_frequency_var.set(preset["start_frequency"])
  1058. self.end_frequency_var.set(preset["end_frequency"])
  1059. self.display_target = preset["pulses"]
  1060. if log_message:
  1061. self.log(f"已应用预设:{preset['name']}")
  1062. def _selected_output_mode(self) -> OutputMode:
  1063. return next(
  1064. (mode for mode, text in OUTPUT_MODE_TEXT.items()
  1065. if text == self.output_mode_var.get()),
  1066. OutputMode.PULSE_DIRECTION,
  1067. )
  1068. def _selected_send_mode(self) -> SendMode:
  1069. return next(
  1070. (mode for mode, text in SEND_MODE_TEXT.items()
  1071. if text == self.send_mode_var.get()),
  1072. SendMode.COMPLETE,
  1073. )
  1074. def _selected_direction_logic(self) -> DirectionLogic:
  1075. return next(
  1076. (logic for logic, text in DIRECTION_LOGIC_TEXT.items()
  1077. if text == self.direction_logic_var.get()),
  1078. DirectionLogic.POSITIVE,
  1079. )
  1080. def _direction_display_text(self, direction: int) -> str:
  1081. if self._selected_output_mode() == OutputMode.AB_QUADRATURE:
  1082. return (
  1083. "正向(A相超前B相90°)"
  1084. if direction == 0 else "反向(B相超前A相90°)"
  1085. )
  1086. return DIRECTION_TEXT[1 if direction else 0]
  1087. def _on_output_mode_changed(self, _event=None) -> None:
  1088. if hasattr(self, "direction_status_var"):
  1089. direction = 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0
  1090. self.direction_status_var.set(self._direction_display_text(direction))
  1091. def _on_run_mode_changed(self, _event=None) -> None:
  1092. """根据模式切换目标含义;绝对模式的实际方向由固件自动计算。"""
  1093. absolute = self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
  1094. if hasattr(self, "target_caption_var"):
  1095. self.target_caption_var.set("目标绝对位置" if absolute else "目标脉冲数")
  1096. def _read_motion_inputs(self) -> Dict[str, int]:
  1097. try:
  1098. pulses = int(self.pulses_var.get().strip())
  1099. frequency = int(self.frequency_var.get())
  1100. acceleration_ms = int(self.acceleration_var.get())
  1101. deceleration_ms = int(self.deceleration_var.get())
  1102. maximum_frequency = int(self.maximum_frequency_var.get())
  1103. start_frequency = int(self.start_frequency_var.get())
  1104. end_frequency = int(self.end_frequency_var.get())
  1105. except ValueError as exc:
  1106. raise ValueError("脉冲数、三项速度和加减速时间必须是整数") from exc
  1107. run_mode = (
  1108. RunMode.ABSOLUTE
  1109. if self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
  1110. else RunMode.RELATIVE
  1111. )
  1112. direction = 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0
  1113. if run_mode == RunMode.RELATIVE:
  1114. if not -0x80000000 <= pulses <= 0x7FFFFFFF:
  1115. raise ValueError("相对脉冲数必须在-2147483648到2147483647之间")
  1116. # 负数按需求直接表示反向;正数仍允许通过“运动方向”选择反向,
  1117. # 兼容原有上位机操作。INT32_MIN只能表示为反向2147483648脉冲。
  1118. if pulses < 0:
  1119. direction = 1
  1120. magnitude = abs(pulses)
  1121. command_pulses = -magnitude if direction else magnitude
  1122. target_u32 = signed_to_u32(command_pulses)
  1123. else:
  1124. target_u32 = signed_to_u32(pulses)
  1125. if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
  1126. raise ValueError("最高速度必须在1到100000Hz之间")
  1127. if not MIN_FREQUENCY_HZ <= frequency <= maximum_frequency:
  1128. raise ValueError(f"默认速度必须在1到最高速度{maximum_frequency}Hz之间")
  1129. if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
  1130. raise ValueError(f"加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1131. if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
  1132. raise ValueError(f"减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1133. if not 0 <= start_frequency <= frequency:
  1134. raise ValueError("起始速度必须在0Hz到默认速度之间")
  1135. if not 0 <= end_frequency <= frequency:
  1136. raise ValueError("终止速度必须在0Hz到默认速度之间")
  1137. profile_type = next(
  1138. (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
  1139. ProfileType.LINEAR,
  1140. )
  1141. return {
  1142. "pulses": pulses,
  1143. "target_u32": target_u32,
  1144. "frequency": frequency,
  1145. "direction": direction,
  1146. "run_mode": int(run_mode),
  1147. "profile_type": int(profile_type),
  1148. "acceleration_ms": acceleration_ms,
  1149. "deceleration_ms": deceleration_ms,
  1150. "start_frequency": start_frequency,
  1151. "end_frequency": end_frequency,
  1152. "maximum_frequency": maximum_frequency,
  1153. }
  1154. def start_motion(self) -> None:
  1155. if not self.client.is_open:
  1156. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1157. return
  1158. try:
  1159. params = self._read_motion_inputs()
  1160. except ValueError as exc:
  1161. messagebox.showwarning("参数错误", str(exc))
  1162. return
  1163. if params["run_mode"] == int(RunMode.ABSOLUTE):
  1164. current_position = int(self.cumulative_position_var.get())
  1165. self.expected_pulses = abs(params["pulses"] - current_position)
  1166. else:
  1167. self.expected_pulses = abs(params["pulses"])
  1168. self.display_target = self.expected_pulses
  1169. self.pending_motion = params
  1170. self.expected_constant_frequency = (
  1171. params["frequency"]
  1172. if params["start_frequency"] == params["frequency"]
  1173. and params["end_frequency"] == params["frequency"]
  1174. and params["acceleration_ms"] == 0
  1175. and params["deceleration_ms"] == 0
  1176. else 0
  1177. )
  1178. pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
  1179. direction_point = point_id_from_label(SECONDARY_POINTS, self.secondary_point_var.get())
  1180. profile_values = [pulse_point, direction_point, 0, 0,
  1181. int(self._selected_send_mode()), 20,
  1182. int(self._selected_direction_logic()),
  1183. params["profile_type"], params["run_mode"], 1, 1]
  1184. profile_values += split_u32(params["frequency"])
  1185. profile_values += split_u32(params["start_frequency"])
  1186. profile_values += [int(self._selected_output_mode())]
  1187. self.client.write_multiple(
  1188. int(Register.PULSE_POINT), profile_values, "SET_SYSTEM_A"
  1189. )
  1190. self.test_active = True
  1191. self.multi_test_active = False
  1192. # 参数写入链完成并收到START响应后再开始周期查询,避免查询帧插队。
  1193. self.monitoring_active = False
  1194. self.seen_running = False
  1195. self.test_started_at = time.monotonic()
  1196. if params["run_mode"] == int(RunMode.ABSOLUTE):
  1197. actual_direction = 1 if params["pulses"] < current_position else 0
  1198. else:
  1199. actual_direction = params["direction"]
  1200. self.direction_status_var.set(self._direction_display_text(actual_direction))
  1201. self._set_test_result("启动命令发送中,等待设备确认。", "running")
  1202. self.log(f"提交单段启动:目标{params['pulses']},频率{params['frequency']}Hz。")
  1203. def write_segment_table(self, start_after_write: bool = False) -> None:
  1204. """校验并写入公共曲线参数、段数、起始段和段表。
  1205. start_after_write为True时,用于“未写段表直接启动”的自动
  1206. 流程:设备完成段表保存后再发送启动命令。
  1207. """
  1208. self.start_after_segment_write = False
  1209. if not self.client.is_open:
  1210. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1211. return
  1212. try:
  1213. total_segments = int(self.total_segments_var.get())
  1214. start_segment = int(self.start_segment_var.get())
  1215. start_frequency = int(self.start_frequency_var.get())
  1216. end_frequency = int(self.end_frequency_var.get())
  1217. acceleration_ms = int(self.acceleration_var.get())
  1218. deceleration_ms = int(self.deceleration_var.get())
  1219. default_frequency = int(self.frequency_var.get())
  1220. maximum_frequency = int(self.maximum_frequency_var.get())
  1221. if not 1 <= total_segments <= 10:
  1222. raise ValueError("脉冲总段数必须在1到10之间")
  1223. if not 1 <= start_segment <= total_segments:
  1224. raise ValueError("起始执行段数必须在1到脉冲总段数之间")
  1225. if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
  1226. raise ValueError(f"加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1227. if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
  1228. raise ValueError(f"减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1229. if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
  1230. raise ValueError("最高速度必须在1到100000Hz之间")
  1231. if not MIN_FREQUENCY_HZ <= default_frequency <= maximum_frequency:
  1232. raise ValueError(f"默认速度必须在1到最高速度{maximum_frequency}Hz之间")
  1233. if not 0 <= start_frequency <= maximum_frequency:
  1234. raise ValueError(f"起始速度必须在0到最高速度{maximum_frequency}Hz之间")
  1235. if not 0 <= end_frequency <= maximum_frequency:
  1236. raise ValueError(f"终止速度必须在0到最高速度{maximum_frequency}Hz之间")
  1237. segments = []
  1238. for index in range(total_segments):
  1239. pulses = int(self.segment_pulse_vars[index].get().strip())
  1240. frequency = int(self.segment_frequency_vars[index].get().strip())
  1241. jump = int(self.segment_jump_vars[index].get())
  1242. condition_value = int(self.segment_condition_vars[index].get().strip())
  1243. wait_condition = next(
  1244. (item for item, text in WAIT_CONDITION_TEXT.items()
  1245. if text == self.segment_wait_vars[index].get()), WaitCondition.WAIT_TIME)
  1246. if not -0x80000000 <= pulses <= 0x7FFFFFFF:
  1247. raise ValueError(f"第{index + 1}段脉冲数必须在-2147483648到2147483647之间")
  1248. if not 0 <= frequency <= maximum_frequency:
  1249. raise ValueError(
  1250. f"第{index + 1}段频率必须为0或1到最高速度{maximum_frequency}Hz"
  1251. )
  1252. effective_frequency = default_frequency if frequency == 0 else frequency
  1253. if start_frequency > effective_frequency or end_frequency > effective_frequency:
  1254. raise ValueError(f"第{index + 1}段实际目标速度不能低于起始或终止速度")
  1255. if not 0 <= jump <= total_segments:
  1256. raise ValueError(f"第{index + 1}段跳转段必须是0到{total_segments}")
  1257. if not 0 <= condition_value <= 0xFFFFFFFF:
  1258. raise ValueError(f"第{index + 1}段条件值超出32位无符号范围")
  1259. if wait_condition in (WaitCondition.WAIT_SIGNAL,
  1260. WaitCondition.EXT_SIGNAL,
  1261. WaitCondition.EXT_OR_COMPLETE) and condition_value not in INPUT_POINTS:
  1262. raise ValueError(f"第{index + 1}段信号X点无效")
  1263. segments.append((pulses, frequency, jump,
  1264. int(wait_condition), condition_value))
  1265. except (ValueError, tk.TclError) as exc:
  1266. messagebox.showwarning("多段参数错误", str(exc))
  1267. return
  1268. execution_total, execution_has_cycle = calculate_execution_total(
  1269. segments, start_segment
  1270. )
  1271. profile_type = next(
  1272. (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
  1273. ProfileType.LINEAR,
  1274. )
  1275. direction = 1 if segments[start_segment - 1][0] < 0 else 0
  1276. self.pending_segments = {
  1277. "total": total_segments, "start": start_segment,
  1278. "segments": segments, "direction": direction,
  1279. "profile_type": int(profile_type), "acceleration_ms": acceleration_ms,
  1280. "deceleration_ms": deceleration_ms, "start_frequency": start_frequency,
  1281. "end_frequency": end_frequency, "default_frequency": default_frequency,
  1282. "maximum_frequency": maximum_frequency,
  1283. "execution_total": execution_total,
  1284. "execution_has_cycle": execution_has_cycle,
  1285. }
  1286. wait_points = {
  1287. item[4] for item in segments
  1288. if item[3] == int(WaitCondition.WAIT_SIGNAL)
  1289. }
  1290. ext_points = {
  1291. item[4] for item in segments
  1292. if item[3] in (int(WaitCondition.EXT_SIGNAL),
  1293. int(WaitCondition.EXT_OR_COMPLETE))
  1294. }
  1295. if any(point not in INPUT_POINTS for point in wait_points | ext_points):
  1296. messagebox.showwarning("信号点无效", "参数表仅允许WAIT/EXT选择X4或X5。")
  1297. self.pending_segments = None
  1298. return
  1299. if len(wait_points) > 1 or len(ext_points) > 1:
  1300. messagebox.showwarning(
  1301. "全局信号配置冲突",
  1302. "参数表中WAIT信号和EXT信号分别是全局配置,同类条件必须统一使用X4或X5。",
  1303. )
  1304. self.pending_segments = None
  1305. return
  1306. wait_select = next(iter(wait_points), 4) - 4
  1307. ext_select = next(iter(ext_points), 4) - 4
  1308. self.wait_signal_select = wait_select
  1309. self.ext_signal_select = ext_select
  1310. pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
  1311. direction_point = point_id_from_label(SECONDARY_POINTS, self.secondary_point_var.get())
  1312. profile_values = [pulse_point, direction_point, wait_select, ext_select,
  1313. int(self._selected_send_mode()), 20,
  1314. int(self._selected_direction_logic()),
  1315. int(profile_type), int(RunMode.RELATIVE),
  1316. total_segments, start_segment]
  1317. # 0x100B保存公共默认速度;各段自己的运行速度仍来自段表。
  1318. profile_values += split_u32(default_frequency)
  1319. profile_values += split_u32(start_frequency)
  1320. profile_values += [int(self._selected_output_mode())]
  1321. self.start_after_segment_write = bool(start_after_write)
  1322. self.client.write_multiple(int(Register.PULSE_POINT), profile_values,
  1323. "SET_SEGMENT_SYSTEM_A")
  1324. self.written_segments = None
  1325. self.segments_written_ready = False
  1326. self.start_segments_button.configure(state=tk.DISABLED)
  1327. self.test_active = False
  1328. self.multi_test_active = False
  1329. self.monitoring_active = False
  1330. self._set_test_result("正在写入多段参数和段表,不会自动启动。", "running")
  1331. if execution_has_cycle:
  1332. self.log(f"提交段表写入:共{total_segments}段,从第{start_segment}段开始,"
  1333. "检测到循环跳转。")
  1334. else:
  1335. self.log(f"提交段表写入:共{total_segments}段,从第{start_segment}段开始,"
  1336. f"预计输出{execution_total}个脉冲。")
  1337. def start_segment_motion(self) -> None:
  1338. """启动多段运动;未写段表时先自动写入当前默认参数。"""
  1339. if not self.client.is_open:
  1340. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1341. return
  1342. if self.last_state in (
  1343. int(State.RUNNING), int(State.WAITING), int(State.PAUSED)
  1344. ):
  1345. messagebox.showwarning("当前不可启动", "设备正在运行、等待或暂停,请先结束当前运动。")
  1346. return
  1347. if not self.segments_written_ready or self.written_segments is None:
  1348. self.log("段表尚未写入,正在按当前默认参数自动写入后启动。")
  1349. self.write_segment_table(start_after_write=True)
  1350. return
  1351. params = self.written_segments
  1352. execution_has_cycle = bool(params["execution_has_cycle"])
  1353. execution_total = int(params["execution_total"])
  1354. self.expected_pulses = 0 if execution_has_cycle else execution_total
  1355. self.display_target = 0 if execution_has_cycle else execution_total
  1356. self.test_active = True
  1357. self.multi_test_active = True
  1358. self.expected_constant_frequency = 0
  1359. self.monitoring_active = False
  1360. self.seen_running = False
  1361. self.test_started_at = time.monotonic()
  1362. self.direction_status_var.set(
  1363. self._direction_display_text(int(params["direction"]))
  1364. )
  1365. self._set_test_result("多段启动命令发送中,等待设备确认。", "running")
  1366. self.client.write_single(
  1367. int(Register.CONTROL), int(Command.START_SEGMENTS), "START_SEGMENTS"
  1368. )
  1369. self.log(
  1370. f"提交多段启动:共{params['total']}段,从第{params['start']}段开始。"
  1371. )
  1372. def stop_motion(self) -> None:
  1373. if not self.client.is_open:
  1374. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1375. return
  1376. self.client.write_single(int(Register.CONTROL), int(Command.STOP), "STOP")
  1377. self.test_active = False
  1378. self.monitoring_active = False
  1379. self._set_test_result("暂停命令发送中。", "running")
  1380. self.log("提交暂停命令,将保留当前脉冲位置。")
  1381. def resume_motion(self) -> None:
  1382. """继续STM32中已暂停的运动,不重写参数也不清零本次计数。"""
  1383. if not self.client.is_open:
  1384. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1385. return
  1386. self.client.write_single(
  1387. int(Register.CONTROL), int(Command.RESUME), "RESUME"
  1388. )
  1389. self.expected_pulses = self.display_target
  1390. self.test_active = True
  1391. self.monitoring_active = True
  1392. self.seen_running = False
  1393. self.test_started_at = time.monotonic()
  1394. self._set_test_result("继续命令发送中,等待设备确认。", "running")
  1395. self.log("提交继续命令,将从暂停的脉冲位置继续。")
  1396. def update_frequency(self) -> None:
  1397. """写入独立的瞬时目标频率,不改变默认速度、段表或当前脉冲计数。"""
  1398. if not self.client.is_open:
  1399. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1400. return
  1401. if self.last_state not in (int(State.RUNNING), int(State.PAUSED)):
  1402. messagebox.showwarning("当前不可改频", "只有运行中或暂停状态可以修改频率。")
  1403. return
  1404. try:
  1405. frequency = int(self.runtime_frequency_var.get())
  1406. maximum_frequency = int(self.maximum_frequency_var.get())
  1407. start_frequency = int(self.start_frequency_var.get())
  1408. end_frequency = int(self.end_frequency_var.get())
  1409. except (ValueError, tk.TclError):
  1410. messagebox.showwarning("参数错误", "频率必须是整数。")
  1411. return
  1412. if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
  1413. messagebox.showwarning("参数错误", "最高速度必须在1到100000Hz之间。")
  1414. return
  1415. if not MIN_FREQUENCY_HZ <= frequency <= maximum_frequency:
  1416. messagebox.showwarning(
  1417. "参数错误", f"频率必须在1到最高速度{maximum_frequency}Hz之间。"
  1418. )
  1419. return
  1420. if frequency < start_frequency or frequency < end_frequency:
  1421. messagebox.showwarning(
  1422. "参数错误", "运行中目标频率不能低于起始速度或终止速度。"
  1423. )
  1424. return
  1425. self.pending_frequency = frequency
  1426. self.client.write_multiple(
  1427. int(Register.RUNTIME_FREQ_LO),
  1428. split_u32(frequency),
  1429. "SET_RUNTIME_FREQ_VALUE",
  1430. )
  1431. self._set_test_result(f"正在将频率修改为{frequency}Hz。", "running")
  1432. self.log(f"提交动态改频:{frequency}Hz,不清零当前脉冲计数。")
  1433. def save_output_mapping(self) -> None:
  1434. """一次写入12项公共参数,并请求设备保存到Flash。"""
  1435. if not self.client.is_open:
  1436. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1437. return
  1438. if self.last_state in (
  1439. int(State.RUNNING), int(State.PAUSED), int(State.WAITING)
  1440. ):
  1441. messagebox.showwarning(
  1442. "当前不可配置",
  1443. "请等待脉冲完成;暂停中的任务需要先完成或重新启动后再保存点位。",
  1444. )
  1445. return
  1446. try:
  1447. pulse_point = point_id_from_label(PULSE_POINTS, self.pulse_point_var.get())
  1448. direction_point = point_id_from_label(
  1449. SECONDARY_POINTS, self.secondary_point_var.get()
  1450. )
  1451. default_frequency = int(self.frequency_var.get())
  1452. maximum_frequency = int(self.maximum_frequency_var.get())
  1453. start_frequency = int(self.start_frequency_var.get())
  1454. end_frequency = int(self.end_frequency_var.get())
  1455. acceleration_ms = int(self.acceleration_var.get())
  1456. deceleration_ms = int(self.deceleration_var.get())
  1457. total_segments = int(self.total_segments_var.get())
  1458. start_segment = int(self.start_segment_var.get())
  1459. if not MIN_FREQUENCY_HZ <= maximum_frequency <= MAX_FREQUENCY_HZ:
  1460. raise ValueError("最高速度必须在1到100000Hz之间")
  1461. if not MIN_FREQUENCY_HZ <= default_frequency <= maximum_frequency:
  1462. raise ValueError("默认速度必须在1Hz到最高速度之间")
  1463. if not 0 <= start_frequency <= maximum_frequency:
  1464. raise ValueError("起始速度必须在0Hz到最高速度之间")
  1465. if not 0 <= end_frequency <= maximum_frequency:
  1466. raise ValueError("终止速度必须在0Hz到最高速度之间")
  1467. if not 0 <= acceleration_ms <= MAX_RAMP_TIME_MS:
  1468. raise ValueError(f"默认加速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1469. if not 0 <= deceleration_ms <= MAX_RAMP_TIME_MS:
  1470. raise ValueError(f"默认减速时间必须在0到{MAX_RAMP_TIME_MS}ms之间")
  1471. if not 1 <= total_segments <= 10:
  1472. raise ValueError("脉冲总段数必须在1到10之间")
  1473. if not 1 <= start_segment <= total_segments:
  1474. raise ValueError("起始执行段数必须在有效段范围内")
  1475. for index in range(total_segments):
  1476. segment_frequency = int(self.segment_frequency_vars[index].get())
  1477. if not 0 <= segment_frequency <= maximum_frequency:
  1478. raise ValueError(f"第{index + 1}段频率必须为0或不超过公共最高速度")
  1479. effective_frequency = (
  1480. default_frequency if segment_frequency == 0 else segment_frequency
  1481. )
  1482. if start_frequency > effective_frequency or end_frequency > effective_frequency:
  1483. raise ValueError(f"第{index + 1}段实际目标速度不能低于公共起始或终止速度")
  1484. except (ValueError, tk.TclError) as exc:
  1485. messagebox.showwarning("公共参数错误", str(exc))
  1486. return
  1487. profile_type = next(
  1488. (item for item, text in PROFILE_TEXT.items() if text == self.profile_var.get()),
  1489. ProfileType.LINEAR,
  1490. )
  1491. values = [
  1492. pulse_point,
  1493. direction_point,
  1494. int(getattr(self, "wait_signal_select", 0)),
  1495. int(getattr(self, "ext_signal_select", 0)),
  1496. int(self._selected_send_mode()),
  1497. 20,
  1498. int(self._selected_direction_logic()),
  1499. int(profile_type),
  1500. int(RunMode.RELATIVE),
  1501. total_segments,
  1502. start_segment,
  1503. ]
  1504. values += split_u32(default_frequency)
  1505. values += split_u32(start_frequency)
  1506. values += [int(self._selected_output_mode())]
  1507. values += split_u32(end_frequency)
  1508. values += [acceleration_ms, deceleration_ms]
  1509. values += split_u32(maximum_frequency)
  1510. self.client.write_multiple(
  1511. int(Register.PULSE_POINT),
  1512. values,
  1513. "SET_PUBLIC_PARAMETERS",
  1514. )
  1515. self._set_test_result(
  1516. "正在应用并保存全部公共参数。",
  1517. "running",
  1518. )
  1519. self.log(
  1520. f"提交公共参数:{PROFILE_TEXT[profile_type]},"
  1521. f"默认{default_frequency}Hz,最高{maximum_frequency}Hz,"
  1522. f"起始{start_frequency}Hz,终止{end_frequency}Hz,"
  1523. f"加速{acceleration_ms}ms,减速{deceleration_ms}ms,请求掉电保存。"
  1524. )
  1525. def clear_current_position(self) -> None:
  1526. """清零当前所选脉冲Y点的累计位置。"""
  1527. if not self.client.is_open:
  1528. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1529. return
  1530. if not messagebox.askyesno(
  1531. "确认清零", "确定将当前脉冲Y点的累计位置清零吗?其他脉冲Y点不受影响。"
  1532. ):
  1533. return
  1534. self.client.write_single(
  1535. int(Register.CONTROL), int(Command.CLEAR_POSITION), "CLEAR_POSITION"
  1536. )
  1537. self.log("提交当前脉冲Y点累计位置清零命令。")
  1538. def clear_total_count(self) -> None:
  1539. """清零不计方向和输出点的全局总脉冲数。"""
  1540. if not self.client.is_open:
  1541. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1542. return
  1543. if not messagebox.askyesno(
  1544. "确认清零", "确定将掉电保持的总脉冲数清零吗?Y0~Y3累计位置不受影响。"
  1545. ):
  1546. return
  1547. self.client.write_single(
  1548. int(Register.CONTROL), int(Command.CLEAR_TOTAL), "CLEAR_TOTAL"
  1549. )
  1550. self.log("提交总脉冲数清零命令,等待设备确认。")
  1551. def request_diagnostic(self) -> None:
  1552. if not self.client.is_open:
  1553. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1554. return
  1555. if not self.client.is_busy:
  1556. self.client.read_holding(
  1557. int(Register.CUMULATIVE_POSITION_LO),
  1558. STATUS_REGISTER_COUNT,
  1559. "DIAGNOSTIC_STATUS",
  1560. )
  1561. def clear_diagnostic(self) -> None:
  1562. if not self.client.is_open:
  1563. messagebox.showwarning("尚未连接", "请先连接串口设备。")
  1564. return
  1565. self.client.write_single(
  1566. int(Register.CONTROL),
  1567. int(Command.CLEAR_DIAGNOSTIC),
  1568. "CLEAR_DIAGNOSTIC",
  1569. )
  1570. self.log("已提交诊断记录清零命令。")
  1571. def request_status(self) -> None:
  1572. if self.client.is_open and not self.client.is_busy:
  1573. # 状态区保持轮询;AB实际方向在启动时或多段切换时同步到界面。
  1574. self.client.read_holding(
  1575. int(Register.CUMULATIVE_POSITION_LO),
  1576. STATUS_REGISTER_COUNT,
  1577. "POLL",
  1578. )
  1579. def _request_public_save_status(self) -> None:
  1580. """稍作延时后读取错误码,确认Flash任务已完成。"""
  1581. if self.closing:
  1582. return
  1583. if not self.client.is_open:
  1584. message = "公共参数已写入,但连接已断开,无法确认Flash保存结果。"
  1585. self._set_test_result(message, "fail")
  1586. self.log(message)
  1587. messagebox.showerror("无法确认保存结果", message)
  1588. return
  1589. if self.client.is_busy:
  1590. self.root.after(50, self._request_public_save_status)
  1591. return
  1592. self.client.read_holding(
  1593. int(Register.CUMULATIVE_POSITION_LO),
  1594. STATUS_REGISTER_COUNT,
  1595. "PUBLIC_SAVE_STATUS",
  1596. )
  1597. def _request_segment_save_status(self) -> None:
  1598. """等待设备完成段表Flash保存,再判定写入成功。"""
  1599. if self.closing:
  1600. return
  1601. if not self.client.is_open:
  1602. self._abort_segment_table_write(
  1603. "段表已写入寄存器,但连接已断开,无法确认Flash保存结果。"
  1604. )
  1605. return
  1606. if self.client.is_busy:
  1607. self.root.after(50, self._request_segment_save_status)
  1608. return
  1609. self.client.read_holding(
  1610. int(Register.CUMULATIVE_POSITION_LO),
  1611. STATUS_REGISTER_COUNT,
  1612. "SEGMENT_SAVE_STATUS",
  1613. )
  1614. def _poll_status(self) -> None:
  1615. if self.closing:
  1616. return
  1617. if self.monitoring_active:
  1618. self.request_status()
  1619. self.root.after(self.POLL_INTERVAL_MS, self._poll_status)
  1620. def _process_client_events(self) -> None:
  1621. if self.closing:
  1622. return
  1623. while True:
  1624. try:
  1625. event = self.client.events.get_nowait()
  1626. except queue.Empty:
  1627. break
  1628. kind = event[0]
  1629. if kind == "tx":
  1630. self.log(f"TX {event[1].hex(' ').upper()}")
  1631. elif kind == "rx":
  1632. self.log(f"RX {event[1].hex(' ').upper()}")
  1633. elif kind == "read":
  1634. try:
  1635. self._handle_read(event[1], event[2], event[3])
  1636. except (IndexError, ValueError) as exc:
  1637. # 状态回读异常不能终止Tkinter事件循环,否则后续启动写入
  1638. # 的确认事件也不会再被处理,界面会一直停在“等待确认”。
  1639. self.log(f"寄存器数据解析失败:{exc}")
  1640. self._set_test_result(f"设备回读数据不完整:{exc}", "fail")
  1641. self.test_active = False
  1642. self.monitoring_active = False
  1643. elif kind == "write":
  1644. self._handle_write(event[1], event[2], event[3], event[4])
  1645. elif kind == "retry":
  1646. _, context, attempt = event
  1647. self.log(f"{context}未收到响应,正在进行第{attempt}次发送。")
  1648. elif kind == "exception":
  1649. _, function, code, context = event
  1650. message = f"Modbus异常:功能码0x{function:02X},异常码0x{code:02X},操作{context}。"
  1651. self.log(message)
  1652. self._set_test_result(message, "fail")
  1653. if ((isinstance(context, str) and
  1654. context.startswith("SET_SEGMENT_")) or
  1655. context in ("SAVE_SEGMENT_CONFIGURATION",
  1656. "SEGMENT_SAVE_STATUS")):
  1657. self._abort_segment_table_write(message)
  1658. if context in (
  1659. "SET_PUBLIC_PARAMETERS",
  1660. "SAVE_PUBLIC",
  1661. "PUBLIC_CONFIG_STATUS",
  1662. "PUBLIC_SAVE_STATUS",
  1663. ):
  1664. messagebox.showerror("公共参数保存失败", message)
  1665. self.test_active = False
  1666. self.monitoring_active = False
  1667. elif kind == "error":
  1668. _, context, message = event
  1669. text = f"{context}:{message}" if context else message
  1670. self.log(f"通信错误:{text}")
  1671. if ((isinstance(context, str) and
  1672. context.startswith("SET_SEGMENT_")) or
  1673. context in ("SAVE_SEGMENT_CONFIGURATION",
  1674. "SEGMENT_SAVE_STATUS")):
  1675. self._abort_segment_table_write(text)
  1676. if context in (
  1677. "SET_PUBLIC_PARAMETERS",
  1678. "SAVE_PUBLIC",
  1679. "PUBLIC_CONFIG_STATUS",
  1680. "PUBLIC_SAVE_STATUS",
  1681. ):
  1682. messagebox.showerror("公共参数保存失败", text)
  1683. if self.test_active:
  1684. self._set_test_result(f"测试中断:{text}", "fail")
  1685. self.test_active = False
  1686. self.monitoring_active = False
  1687. self.root.after(self.EVENT_INTERVAL_MS, self._process_client_events)
  1688. def _handle_read(self, address: int, values: List[int], context: str) -> None:
  1689. if address == int(Register.CUMULATIVE_POSITION_LO) and len(values) >= 11:
  1690. status = parse_status(values)
  1691. self._update_status(status)
  1692. if context == "INITIAL_STATUS":
  1693. self._set_test_result("设备参数和状态读取完成。", "neutral")
  1694. self.log("设备初始化同步完成,可以写入参数并启动。")
  1695. elif context == "PUBLIC_SAVE_STATUS":
  1696. error_code = status["error"]
  1697. if error_code == int(Error.NONE):
  1698. message = "12项公共参数已成功保存到设备Flash。"
  1699. self._set_test_result(message, "pass")
  1700. self.log(message)
  1701. messagebox.showinfo("保存成功", message)
  1702. else:
  1703. message = (
  1704. f"公共参数保存失败:错误码{error_code},"
  1705. f"{error_text(error_code)}。"
  1706. )
  1707. self._set_test_result(message, "fail")
  1708. self.log(message)
  1709. messagebox.showerror("保存失败", message)
  1710. elif context == "SEGMENT_SAVE_STATUS":
  1711. error_code = status["error"]
  1712. if error_code == int(Error.NONE):
  1713. self._finish_segment_table_write()
  1714. else:
  1715. self._abort_segment_table_write(
  1716. f"段表保存失败:错误码{error_code},"
  1717. f"{error_text(error_code)}。"
  1718. )
  1719. return
  1720. if address == int(Register.PULSE_POINT) and len(values) >= 22:
  1721. pulse_point = values[0]
  1722. direction_point = values[1]
  1723. self.wait_signal_select = values[2] if values[2] in (0, 1) else 0
  1724. self.ext_signal_select = values[3] if values[3] in (0, 1) else 0
  1725. if pulse_point in PULSE_POINTS:
  1726. self.pulse_point_var.set(PULSE_POINTS[pulse_point])
  1727. if direction_point in SECONDARY_POINTS:
  1728. self.secondary_point_var.set(SECONDARY_POINTS[direction_point])
  1729. if values[4] in [int(item) for item in SendMode]:
  1730. self.send_mode_var.set(SEND_MODE_TEXT[SendMode(values[4])])
  1731. if values[6] in [int(item) for item in DirectionLogic]:
  1732. self.direction_logic_var.set(
  1733. DIRECTION_LOGIC_TEXT[DirectionLogic(values[6])]
  1734. )
  1735. self.profile_var.set(PROFILE_TEXT.get(ProfileType(values[7]), PROFILE_TEXT[ProfileType.LINEAR]))
  1736. self.run_mode_var.set(MODE_TEXT[RunMode.ABSOLUTE if values[8] else RunMode.RELATIVE])
  1737. self.total_segments_var.set(values[9])
  1738. self.start_segment_var.set(values[10])
  1739. self.frequency_var.set(join_u32(values[11], values[12]))
  1740. self.start_frequency_var.set(join_u32(values[13], values[14]))
  1741. output_mode = values[15]
  1742. if output_mode in [int(item) for item in OutputMode]:
  1743. self.output_mode_var.set(OUTPUT_MODE_TEXT[OutputMode(output_mode)])
  1744. self._on_output_mode_changed()
  1745. self.end_frequency_var.set(join_u32(values[16], values[17]))
  1746. self.acceleration_var.set(values[18])
  1747. self.deceleration_var.set(values[19])
  1748. self.maximum_frequency_var.set(join_u32(values[20], values[21]))
  1749. if context == "INITIAL_SYSTEM":
  1750. self.client.read_holding(
  1751. int(Register.CUMULATIVE_POSITION_LO),
  1752. STATUS_REGISTER_COUNT,
  1753. "INITIAL_STATUS",
  1754. )
  1755. elif context == "PUBLIC_CONFIG_STATUS":
  1756. self.log("公共参数回读完成,正在确认Flash保存结果。")
  1757. self.root.after(200, self._request_public_save_status)
  1758. return
  1759. if address == int(Register.END_FREQ_LO) and len(values) >= 4:
  1760. self.end_frequency_var.set(join_u32(values[0], values[1]))
  1761. self.acceleration_var.set(values[2])
  1762. self.deceleration_var.set(values[3])
  1763. if context == "INITIAL_RAMP":
  1764. self.client.read_holding(
  1765. int(Register.CUMULATIVE_POSITION_LO), STATUS_REGISTER_COUNT, "INITIAL_STATUS"
  1766. )
  1767. return
  1768. if False and address == int(Register.COMMAND) and len(values) >= 28:
  1769. run_mode = RunMode.ABSOLUTE if values[18] == 1 else RunMode.RELATIVE
  1770. target_raw = join_u32(values[2], values[3])
  1771. target = join_i32(values[2], values[3]) if run_mode == RunMode.ABSOLUTE else target_raw
  1772. frequency = join_u32(values[4], values[5])
  1773. self.pulses_var.set(str(target))
  1774. self.run_mode_var.set(MODE_TEXT[run_mode])
  1775. self._on_run_mode_changed()
  1776. self.frequency_var.set(max(MIN_FREQUENCY_HZ, min(frequency, MAX_FREQUENCY_HZ)))
  1777. profile_value = values[19]
  1778. if profile_value in [int(item) for item in ProfileType]:
  1779. self.profile_var.set(PROFILE_TEXT[ProfileType(profile_value)])
  1780. self.acceleration_var.set(join_u32(values[20], values[21]))
  1781. self.deceleration_var.set(join_u32(values[22], values[23]))
  1782. self.start_frequency_var.set(join_u32(values[24], values[25]))
  1783. self.end_frequency_var.set(join_u32(values[26], values[27]))
  1784. self.direction_var.set(DIRECTION_TEXT[1 if values[1] else 0])
  1785. self.direction_status_var.set(self._direction_display_text(1 if values[1] else 0))
  1786. current_position = join_i32(values[12], values[13])
  1787. self.display_target = (
  1788. abs(target - current_position)
  1789. if run_mode == RunMode.ABSOLUTE
  1790. else target
  1791. )
  1792. self._update_status(parse_status(values[6:18]))
  1793. pulse_point = values[14]
  1794. direction_point = values[15]
  1795. if pulse_point in PULSE_POINTS:
  1796. self.pulse_point_var.set(PULSE_POINTS[pulse_point])
  1797. if direction_point in SECONDARY_POINTS:
  1798. self.secondary_point_var.set(SECONDARY_POINTS[direction_point])
  1799. if len(values) >= register_read_count(Register.COMMAND):
  1800. self._load_segment_registers(values, int(Register.COMMAND))
  1801. if context == "OUTPUT_CONFIG_STATUS":
  1802. if values[7] == int(Error.NONE):
  1803. self._set_test_result(
  1804. f"输出映射已保存:第一端子Y{pulse_point},第二端子Y{direction_point + 12}。",
  1805. "pass",
  1806. )
  1807. self.log("设备已确认映射生效并写入Flash;断电重启后会自动恢复。")
  1808. else:
  1809. self._set_test_result(
  1810. f"模块映射保存失败:{error_text(values[7])}。", "fail"
  1811. )
  1812. else:
  1813. self.log(
  1814. f"已同步设备参数:第一端子Y{pulse_point},第二端子Y{direction_point + 12},"
  1815. f"{self.profile_var.get()},加速{self.acceleration_var.get()}ms,"
  1816. f"减速{self.deceleration_var.get()}ms,起始{self.start_frequency_var.get()}Hz,"
  1817. f"默认{self.frequency_var.get()}Hz,终止{self.end_frequency_var.get()}Hz。"
  1818. )
  1819. elif address == int(Register.DIRECTION) and len(values) >= 17:
  1820. actual_direction = 1 if values[0] else 0
  1821. self.direction_status_var.set(self._direction_display_text(actual_direction))
  1822. self._update_status(parse_status(values[5:17]))
  1823. if len(values) >= register_read_count(Register.DIRECTION):
  1824. self._load_segment_registers(values, int(Register.DIRECTION))
  1825. if context == "STOP_STATUS":
  1826. self.log("已回读一次暂停位置和实际方向,后续不再周期查询。")
  1827. elif context == "FREQ_STATUS":
  1828. self.log("已回读动态改频后的状态和实际方向。")
  1829. elif address == int(Register.STATE) and len(values) >= 12:
  1830. self._update_status(parse_status(values))
  1831. if context == "STOP_STATUS":
  1832. self.log("已回读一次暂停位置,后续不再周期查询。")
  1833. elif context == "FREQ_STATUS":
  1834. self.log("已回读动态改频后的状态。")
  1835. elif address == int(Register.CUMULATIVE_POSITION_HI) and len(values) >= 2:
  1836. position = join_i32(values[0], values[1])
  1837. self.cumulative_position_var.set(str(position))
  1838. self.log(f"当前脉冲Y点累计位置已更新为 {position}。")
  1839. elif address == int(Register.TOTAL_PULSES_LO) and len(values) >= 2:
  1840. total = join_u32(values[0], values[1])
  1841. self.total_pulses_var.set(str(total))
  1842. self.log(f"总脉冲数已更新为 {total}。")
  1843. _ = context
  1844. def _load_segment_registers(self, values: List[int], base_address: int) -> None:
  1845. """把一次连续回读中的多段配置和当前段进度同步到界面。"""
  1846. def at(address: int) -> int:
  1847. index = address - base_address
  1848. if index < 0 or index >= len(values):
  1849. raise ValueError(
  1850. f"寄存器回读不完整:需要0x{address:04X},"
  1851. f"实际范围0x{base_address:04X}~"
  1852. f"0x{base_address + len(values) - 1:04X}"
  1853. )
  1854. return values[index]
  1855. total = at(int(Register.TOTAL_SEGMENTS))
  1856. start = at(int(Register.START_SEGMENT))
  1857. current = at(int(Register.CURRENT_SEGMENT))
  1858. if 1 <= total <= 10:
  1859. self.total_segments_var.set(total)
  1860. if 1 <= start <= 10:
  1861. self.start_segment_var.set(start)
  1862. self.current_segment_var.set(str(current) if current else "0(未执行)")
  1863. table_base = int(Register.SEGMENT_TABLE_BASE)
  1864. for index in range(10):
  1865. register = table_base + index * 4
  1866. pulses = join_i32(at(register), at(register + 1))
  1867. frequency = join_u32(at(register + 2), at(register + 3))
  1868. self.segment_pulse_vars[index].set(str(pulses))
  1869. self.segment_frequency_vars[index].set(str(frequency))
  1870. meta = int(Register.SEGMENT_META_BASE) + index * 4
  1871. jump = at(meta)
  1872. wait_value = at(meta + 1)
  1873. condition = join_u32(at(meta + 2), at(meta + 3))
  1874. self.segment_jump_vars[index].set(jump)
  1875. if wait_value in [int(item) for item in WaitCondition]:
  1876. self.segment_wait_vars[index].set(
  1877. WAIT_CONDITION_TEXT[WaitCondition(wait_value)])
  1878. self.segment_condition_vars[index].set(str(condition))
  1879. def _handle_write(
  1880. self, function: int, address: int, quantity_or_value: int, context: str
  1881. ) -> None:
  1882. if context in ("SET_SYSTEM_A", "SET_SEGMENT_SYSTEM_A"):
  1883. params = self.pending_motion if context == "SET_SYSTEM_A" else self.pending_segments
  1884. if params is None:
  1885. self.monitoring_active = False
  1886. return
  1887. self.client.write_multiple(
  1888. int(Register.END_FREQ_LO),
  1889. split_u32(params["end_frequency"]) +
  1890. [params["acceleration_ms"], params["deceleration_ms"]] +
  1891. split_u32(params["maximum_frequency"]),
  1892. "SET_SYSTEM_B" if context == "SET_SYSTEM_A" else "SET_SEGMENT_SYSTEM_B",
  1893. )
  1894. return
  1895. if context == "SET_SYSTEM_B":
  1896. params = self.pending_motion
  1897. if params is None:
  1898. return
  1899. segment_target = params["target_u32"]
  1900. values = split_u32(params["frequency"])
  1901. values += split_u32(segment_target)
  1902. values += [int(WaitCondition.WAIT_TIME), 0, 0, 0]
  1903. self.client.write_multiple(segment_base(0), values, "SET_SINGLE_SEGMENT")
  1904. return
  1905. if context == "SET_SINGLE_SEGMENT":
  1906. self.client.write_single(int(Register.CONTROL), int(Command.START), "START")
  1907. self.pending_motion = None
  1908. return
  1909. if context == "SET_SEGMENT_SYSTEM_B":
  1910. self._write_segment_at(0)
  1911. return
  1912. if context.startswith("SET_SEGMENT_") and context[12:].isdigit():
  1913. self._write_segment_at(int(context[12:]) + 1)
  1914. return
  1915. if context == "SET_PROFILE":
  1916. params = self.pending_motion
  1917. if params is None:
  1918. self._set_test_result("启动参数已丢失,请重新启动。", "fail")
  1919. self.monitoring_active = False
  1920. return
  1921. self.client.write_single(
  1922. int(Register.RUN_MODE), params["run_mode"], "SET_RUN_MODE"
  1923. )
  1924. self.log("加减速参数写入成功,正在写入运行模式。")
  1925. elif context == "SET_SEGMENT_PROFILE":
  1926. params = self.pending_segments
  1927. if params is None:
  1928. self._set_test_result("多段参数已丢失,请重新启动。", "fail")
  1929. self.monitoring_active = False
  1930. return
  1931. self.client.write_multiple(
  1932. int(Register.TOTAL_SEGMENTS),
  1933. [params["total"], params["start"]],
  1934. "SET_SEGMENT_RANGE",
  1935. )
  1936. self.log("公共曲线参数写入成功,正在写入总段数和起始段。")
  1937. elif context == "SET_SEGMENT_RANGE":
  1938. params = self.pending_segments
  1939. if params is None:
  1940. return
  1941. values = []
  1942. for index in range(10):
  1943. segment = params["segments"][index] if index < params["total"] else (0, 0, 0, 0, 0)
  1944. pulses, frequency = segment[0], segment[1]
  1945. values += split_u32(pulses & 0xFFFFFFFF) + split_u32(frequency)
  1946. self.client.write_multiple(int(Register.SEGMENT_TABLE_BASE), values,
  1947. "SET_SEGMENT_TABLE")
  1948. self.log("段范围写入成功,正在写入10段参数表。")
  1949. elif context == "SET_SEGMENT_TABLE":
  1950. params = self.pending_segments
  1951. if params is None:
  1952. return
  1953. values = []
  1954. for index in range(10):
  1955. segment = params["segments"][index] if index < params["total"] else (0, 0, 0, 0, 0)
  1956. values += [segment[2], segment[3]] + split_u32(segment[4])
  1957. self.client.write_multiple(int(Register.SEGMENT_META_BASE), values,
  1958. "SET_SEGMENT_META")
  1959. self.log("10段脉冲/频率写入成功,正在写入跳转和等待条件。")
  1960. elif context == "SET_SEGMENT_META":
  1961. params = self.pending_segments
  1962. if params is None:
  1963. return
  1964. self.client.write_single(
  1965. int(Register.CONTROL),
  1966. int(Command.SAVE_PUBLIC),
  1967. "SAVE_SEGMENT_CONFIGURATION",
  1968. )
  1969. elif context == "START_SEGMENTS":
  1970. self.monitoring_active = True
  1971. self._set_test_result("设备已确认多段启动命令,正在监控运行。", "running")
  1972. self.log("多段启动命令写入成功。")
  1973. elif context == "SET_RUN_MODE":
  1974. params = self.pending_motion
  1975. if params is None:
  1976. self._set_test_result("启动参数已丢失,请重新启动。", "fail")
  1977. self.monitoring_active = False
  1978. return
  1979. values = [int(Command.START), params["direction"]]
  1980. values += split_u32(params["target_u32"])
  1981. values += split_u32(params["frequency"])
  1982. self.client.write_multiple(int(Register.COMMAND), values, "START")
  1983. self.pending_motion = None
  1984. self.log("运行模式写入成功,正在发送启动参数。")
  1985. elif context == "START":
  1986. self.monitoring_active = True
  1987. self._set_test_result("设备已确认启动命令,正在监控运行状态。", "running")
  1988. self.log("启动参数写入成功。")
  1989. elif context == "STOP":
  1990. self.monitoring_active = False
  1991. self._set_test_result("设备已确认暂停,可点击“继续当前脉冲”。", "neutral")
  1992. self.log("暂停命令写入成功,串口保持连接,已停止周期查询。")
  1993. if self.client.is_open:
  1994. self.client.read_holding(
  1995. int(Register.CUMULATIVE_POSITION_LO),
  1996. STATUS_REGISTER_COUNT,
  1997. "STOP_STATUS",
  1998. )
  1999. elif context == "RESUME":
  2000. self.monitoring_active = True
  2001. self._set_test_result("设备已确认继续命令,正在监控剩余脉冲。", "running")
  2002. self.log("继续命令写入成功,已恢复周期查询。")
  2003. elif context == "SET_RUNTIME_FREQ_VALUE":
  2004. self.client.write_single(
  2005. int(Register.CONTROL),
  2006. int(Command.UPDATE_FREQUENCY),
  2007. "APPLY_RUNTIME_FREQ",
  2008. )
  2009. self.log("运行中目标频率写入成功,正在发送立即应用命令。")
  2010. elif context == "APPLY_RUNTIME_FREQ":
  2011. self.log("设备已接收运行中改频命令,将按公共加减速斜率调整当前段。")
  2012. if self.client.is_open and not self.monitoring_active:
  2013. self.client.read_holding(
  2014. int(Register.CUMULATIVE_POSITION_LO),
  2015. STATUS_REGISTER_COUNT,
  2016. "FREQ_STATUS",
  2017. )
  2018. elif context == "SET_PUBLIC_PARAMETERS":
  2019. self.client.write_single(
  2020. int(Register.CONTROL), int(Command.SAVE_PUBLIC), "SAVE_PUBLIC"
  2021. )
  2022. self.log("公共参数寄存器写入成功,正在请求设备保存Flash。")
  2023. elif context == "SAVE_PUBLIC":
  2024. self.log("设备已接收公共参数保存命令,正在回读确认。")
  2025. if self.client.is_open:
  2026. self.client.read_holding(
  2027. int(Register.PULSE_POINT),
  2028. register_read_count(),
  2029. "PUBLIC_CONFIG_STATUS",
  2030. )
  2031. elif context == "SAVE_SEGMENT_CONFIGURATION":
  2032. self.log("设备已接收段表保存命令,正在确认Flash保存结果。")
  2033. self.root.after(200, self._request_segment_save_status)
  2034. elif context == "SET_OUTPUT_POINTS":
  2035. self.client.write_single(
  2036. int(Register.OUTPUT_MODE),
  2037. int(self._selected_output_mode()),
  2038. "SET_OUTPUT_MODE",
  2039. )
  2040. self.log("输出点寄存器写入成功,正在写入输出方式。")
  2041. elif context == "SET_OUTPUT_MODE":
  2042. self.client.write_single(
  2043. int(Register.SEND_MODE),
  2044. int(self._selected_send_mode()),
  2045. "SET_SEND_MODE",
  2046. )
  2047. self.log("输出方式写入成功,正在写入完成/后续发送模式。")
  2048. elif context == "SET_SEND_MODE":
  2049. self.client.write_single(
  2050. int(Register.DIRECTION_LOGIC),
  2051. int(self._selected_direction_logic()),
  2052. "SET_DIRECTION_LOGIC",
  2053. )
  2054. self.log("发送模式写入成功,正在写入正/负方向逻辑。")
  2055. elif context == "SET_DIRECTION_LOGIC":
  2056. self.client.write_single(
  2057. int(Register.CONTROL), int(Command.SAVE_OUTPUTS), "SAVE_OUTPUTS"
  2058. )
  2059. self.log("方向逻辑写入成功,正在发送应用并保存命令。")
  2060. elif context == "SAVE_OUTPUTS":
  2061. self.log("设备已接收保存命令,正在回读Flash保存结果。")
  2062. if self.client.is_open:
  2063. self.client.read_holding(
  2064. int(Register.PULSE_POINT),
  2065. register_read_count(),
  2066. "OUTPUT_CONFIG_STATUS",
  2067. )
  2068. elif context == "CLEAR_TOTAL":
  2069. self.total_pulses_var.set("0")
  2070. self.log("总脉冲数清零命令写入成功,正在回读确认。")
  2071. if self.client.is_open:
  2072. self.client.read_holding(
  2073. int(Register.TOTAL_PULSES_LO), 2, "TOTAL_AFTER_CLEAR"
  2074. )
  2075. elif context == "CLEAR_POSITION":
  2076. self.cumulative_position_var.set("0")
  2077. self.log("当前Y点位置清零命令写入成功,正在回读确认。")
  2078. if self.client.is_open:
  2079. self.client.read_holding(
  2080. int(Register.CUMULATIVE_POSITION_LO),
  2081. 2,
  2082. "POSITION_AFTER_CLEAR",
  2083. )
  2084. elif context == "CLEAR_DIAGNOSTIC":
  2085. self.log("诊断记录已清零,正在回读确认。")
  2086. self.request_diagnostic()
  2087. _ = function, address, quantity_or_value
  2088. def _finish_segment_table_write(self) -> None:
  2089. """设备确认段表已写入Flash后,开放启动或自动启动。"""
  2090. params = self.pending_segments
  2091. if params is None:
  2092. return
  2093. start_after_write = self.start_after_segment_write
  2094. self.start_after_segment_write = False
  2095. self.written_segments = params
  2096. self.pending_segments = None
  2097. self.segments_written_ready = True
  2098. if self.client.is_open:
  2099. self.start_segments_button.configure(state=tk.NORMAL)
  2100. self._set_test_result("段表写入完成,可以点击“启动”。", "pass")
  2101. self.log("多段系统参数和段表写入成功,设备尚未启动。")
  2102. messagebox.showinfo(
  2103. "写入成功",
  2104. f"段表写入成功:有效段数{params['total']},"
  2105. f"起始执行段{params['start']}。\n"
  2106. "参数已保存到设备Flash。",
  2107. )
  2108. if start_after_write:
  2109. self.root.after(0, self.start_segment_motion)
  2110. def _abort_segment_table_write(self, message: str) -> None:
  2111. """统一结束失败的段表写入或自动启动流程。"""
  2112. self.pending_segments = None
  2113. self.written_segments = None
  2114. self.segments_written_ready = False
  2115. self.start_after_segment_write = False
  2116. if self.client.is_open:
  2117. # 失败后仍允许再次点击启动,重试自动写入。
  2118. self.start_segments_button.configure(state=tk.NORMAL)
  2119. self._set_test_result(message, "fail")
  2120. self.log(message)
  2121. messagebox.showerror("段表写入失败", message)
  2122. def _write_segment_at(self, index: int) -> None:
  2123. """按参数表每段0x10步长依次写入8个有效寄存器。"""
  2124. params = self.pending_segments
  2125. if params is None:
  2126. return
  2127. if index >= params["total"]:
  2128. # 段表写完后由设备统一校验公共参数和10段表并保存Flash。
  2129. self.client.write_single(
  2130. int(Register.CONTROL),
  2131. int(Command.SAVE_PUBLIC),
  2132. "SAVE_SEGMENT_CONFIGURATION",
  2133. )
  2134. return
  2135. segment = params["segments"][index]
  2136. pulses, frequency, jump, wait_condition, condition_value = segment
  2137. wait_time = condition_value if wait_condition == int(WaitCondition.WAIT_TIME) else 0
  2138. act_time = condition_value if wait_condition == int(WaitCondition.ACT_TIME) else 0
  2139. values = split_u32(frequency) + split_u32(pulses & 0xFFFFFFFF)
  2140. values += [wait_condition, wait_time, act_time, jump]
  2141. self.client.write_multiple(segment_base(index), values, f"SET_SEGMENT_{index}")
  2142. def _update_status(self, status: Dict[str, int]) -> None:
  2143. state = status["state"]
  2144. error = status["error"]
  2145. current = status["current_pulses"]
  2146. current_segment = status["current_segment"]
  2147. display_segment = current_segment
  2148. # 固件在总段数为1时复用单段执行器并上报段号0;只要本次由段表
  2149. # 启动,上位机仍按段表的起始段显示目标频率和段号。
  2150. if current_segment == 0 and self.multi_test_active:
  2151. if self.written_segments is not None:
  2152. display_segment = int(self.written_segments.get("start", 1))
  2153. else:
  2154. display_segment = max(1, int(self.start_segment_var.get()))
  2155. self._set_state_badge(state)
  2156. self.error_var.set(f"{error} · {error_text(error)}")
  2157. self.current_pulses_var.set(str(current))
  2158. self.actual_frequency_var.set(f"{status['current_frequency']} Hz")
  2159. # “设定频率”显示当前段的目标值;“实际频率”直接使用设备状态区
  2160. # 上报的定时器生效值。加减速或运行中改频时,两者可能暂时不同。
  2161. configured_frequency = self.frequency_var.get()
  2162. if 1 <= display_segment <= len(self.segment_frequency_vars):
  2163. try:
  2164. configured_frequency = int(
  2165. self.segment_frequency_vars[display_segment - 1].get()
  2166. )
  2167. if configured_frequency == 0:
  2168. configured_frequency = int(self.frequency_var.get())
  2169. except ValueError:
  2170. pass
  2171. self.current_frequency_var.set(f"{configured_frequency} Hz")
  2172. # 单段运行时固件按协议上报段号0;多段运行上报1~10。
  2173. self.current_segment_var.set(
  2174. str(display_segment) if display_segment else "0(未执行)"
  2175. )
  2176. if display_segment > 0:
  2177. suffix = "(等待)" if state == int(State.WAITING) else ""
  2178. self.running_segment_var.set(f"第 {display_segment} 段{suffix}")
  2179. elif state in (int(State.RUNNING), int(State.PAUSED)):
  2180. suffix = "(暂停)" if state == int(State.PAUSED) else ""
  2181. self.running_segment_var.set(f"单段{suffix}")
  2182. else:
  2183. self.running_segment_var.set("未运行")
  2184. self.cumulative_position_var.set(str(status["cumulative_position"]))
  2185. self.total_pulses_var.set(str(status["total_pulses"]))
  2186. self._update_diagnostic(status)
  2187. # 多段运动可逐段改变正反向,当前段变化后同步显示实际方向。
  2188. if self.multi_test_active and 1 <= display_segment <= len(self.segment_pulse_vars):
  2189. try:
  2190. segment_direction = 1 if int(self.segment_pulse_vars[display_segment - 1].get()) < 0 else 0
  2191. self.direction_status_var.set(self._direction_display_text(segment_direction))
  2192. except ValueError:
  2193. pass
  2194. target = self.expected_pulses if self.test_active and self.expected_pulses else self.display_target
  2195. self.pulse_fraction_var.set(f"{current} / {target}")
  2196. progress = min(100.0, (current * 100.0 / target)) if target else 0.0
  2197. self.progress_var.set(progress)
  2198. running = state == int(State.RUNNING)
  2199. if (
  2200. self.test_active
  2201. and not self.multi_test_active
  2202. and running
  2203. and self.expected_constant_frequency
  2204. and status["current_frequency"] != self.expected_constant_frequency
  2205. ):
  2206. actual_frequency = status["current_frequency"]
  2207. expected_frequency = self.expected_constant_frequency
  2208. self.test_active = False
  2209. self.monitoring_active = False
  2210. self.client.write_single(
  2211. int(Register.CONTROL), int(Command.STOP), "FREQUENCY_MISMATCH_STOP"
  2212. )
  2213. self._set_test_result(
  2214. f"测试失败:设定{expected_frequency}Hz,设备实际运行"
  2215. f"{actual_frequency}Hz,已自动暂停输出。",
  2216. "fail",
  2217. )
  2218. self.log(
  2219. f"频率一致性检查失败:设定{expected_frequency}Hz,"
  2220. f"状态寄存器报告{actual_frequency}Hz,已发送暂停命令。"
  2221. )
  2222. return
  2223. frequency_applied = False
  2224. if self.pending_frequency:
  2225. if error != int(Error.NONE):
  2226. self.log(
  2227. f"动态改频失败:设备错误{error},{error_text(error)}。"
  2228. )
  2229. self.pending_frequency = 0
  2230. elif running:
  2231. self.log(
  2232. f"动态改频已生效:新目标{self.pending_frequency}Hz,"
  2233. f"当前曲线频率{status['current_frequency']}Hz。"
  2234. )
  2235. self._set_test_result(
  2236. f"目标频率已修改为{self.pending_frequency}Hz,曲线立即重新计算。",
  2237. "running",
  2238. )
  2239. self.pending_frequency = 0
  2240. frequency_applied = True
  2241. elif state == int(State.PAUSED):
  2242. self.log(
  2243. f"新频率{self.pending_frequency}Hz已保存,将在继续发送时生效。"
  2244. )
  2245. self._set_test_result(
  2246. f"频率{self.pending_frequency}Hz已保存,继续发送时生效。",
  2247. "neutral",
  2248. )
  2249. self.pending_frequency = 0
  2250. if running:
  2251. self.seen_running = True
  2252. if self.test_active and not frequency_applied:
  2253. self._set_test_result(
  2254. f"运行中:{state_text(state)},已发送{current}个脉冲。",
  2255. "running",
  2256. )
  2257. elif self.test_active and state == int(State.WAITING):
  2258. self._set_test_result("当前段已停止输出,正在等待设定条件。", "running")
  2259. elif self.test_active and state == int(State.COMPLETE):
  2260. elapsed_ok = (time.monotonic() - self.test_started_at) >= 0.05
  2261. diagnostic_flags = status.get("diagnostic_status", 0)
  2262. diagnostic_ok = (
  2263. (diagnostic_flags & int(DiagnosticStatus.COMPLETE)) != 0
  2264. and (diagnostic_flags & (
  2265. int(DiagnosticStatus.COUNT_FAIL)
  2266. | int(DiagnosticStatus.FREQUENCY_FAIL)
  2267. | int(DiagnosticStatus.PROFILE_FAIL)
  2268. )) == 0
  2269. )
  2270. if (
  2271. error == int(Error.NONE)
  2272. and (self.multi_test_active or current == self.expected_pulses)
  2273. and (self.seen_running or elapsed_ok)
  2274. and diagnostic_ok
  2275. ):
  2276. if self._selected_output_mode() == OutputMode.AB_QUADRATURE:
  2277. verification = "请用双通道示波器核对A/B相周期数及90°相位关系。"
  2278. else:
  2279. verification = "请核对脉冲端边沿数以及方向端的稳定电平。"
  2280. self._set_test_result(
  2281. f"测试通过:设备完成并报告{current}个脉冲,"
  2282. f"个数、频率和加减速曲线诊断均通过。{verification}",
  2283. "pass",
  2284. )
  2285. self.log("测试通过:完成状态、脉冲计数、频率和曲线诊断均符合目标。")
  2286. else:
  2287. self._set_test_result(
  2288. f"测试失败:目标{self.expected_pulses},设备报告{current},"
  2289. f"错误码{error};诊断异常数为个数"
  2290. f"{status.get('diagnostic_count_errors', 0)}、频率"
  2291. f"{status.get('diagnostic_frequency_errors', 0)}、曲线"
  2292. f"{status.get('diagnostic_profile_errors', 0)}。",
  2293. "fail",
  2294. )
  2295. self.test_active = False
  2296. self.multi_test_active = False
  2297. self.monitoring_active = False
  2298. self.log("脉冲输出完成,串口保持连接,已停止周期查询。")
  2299. elif self.test_active and (state == int(State.ERROR) or error != int(Error.NONE)):
  2300. self._set_test_result(f"测试失败:{error_text(error)}。", "fail")
  2301. self.test_active = False
  2302. self.monitoring_active = False
  2303. if state != self.last_state or error != self.last_error:
  2304. self.log(f"状态变化:{state_text(state)},错误:{error_text(error)}。")
  2305. self.last_state = state
  2306. self.last_error = error
  2307. def _update_diagnostic(self, status: Dict[str, int]) -> None:
  2308. """把固件诊断位和采样值转换为易读结果。"""
  2309. flags = status.get("diagnostic_status", 0)
  2310. def result_text(pass_flag: DiagnosticStatus,
  2311. fail_flag: DiagnosticStatus) -> str:
  2312. if flags & int(fail_flag):
  2313. return "异常"
  2314. if flags & int(pass_flag):
  2315. return "一致"
  2316. if flags & int(DiagnosticStatus.ACTIVE):
  2317. return "诊断中"
  2318. return "未诊断"
  2319. if flags & int(DiagnosticStatus.ACTIVE):
  2320. overall = "诊断中"
  2321. elif flags & (int(DiagnosticStatus.COUNT_FAIL) |
  2322. int(DiagnosticStatus.FREQUENCY_FAIL) |
  2323. int(DiagnosticStatus.PROFILE_FAIL)):
  2324. overall = "诊断失败"
  2325. elif flags & int(DiagnosticStatus.COMPLETE):
  2326. overall = "诊断通过"
  2327. else:
  2328. overall = "未运行诊断"
  2329. profile_value = status.get("diagnostic_profile_type", 0)
  2330. try:
  2331. profile_name = PROFILE_TEXT[ProfileType(profile_value)]
  2332. except (ValueError, KeyError):
  2333. profile_name = f"未知曲线{profile_value}"
  2334. phase = DIAGNOSTIC_PHASE_TEXT.get(
  2335. status.get("diagnostic_phase", 0), "未知阶段"
  2336. )
  2337. self.diag_overall_var.set(overall)
  2338. self.diag_count_var.set(result_text(
  2339. DiagnosticStatus.COUNT_PASS, DiagnosticStatus.COUNT_FAIL
  2340. ))
  2341. self.diag_frequency_var.set(result_text(
  2342. DiagnosticStatus.FREQUENCY_PASS, DiagnosticStatus.FREQUENCY_FAIL
  2343. ))
  2344. self.diag_profile_var.set(
  2345. f"{result_text(DiagnosticStatus.PROFILE_PASS, DiagnosticStatus.PROFILE_FAIL)}"
  2346. f"({profile_name})"
  2347. )
  2348. self.diag_phase_var.set(phase)
  2349. self.diag_pulses_var.set(
  2350. f"{status.get('diagnostic_observed_pulses', 0)} / "
  2351. f"{status.get('diagnostic_expected_pulses', 0)}"
  2352. )
  2353. self.diag_frequency_values_var.set(
  2354. f"{status.get('diagnostic_actual_frequency', 0)} Hz / "
  2355. f"{status.get('diagnostic_expected_frequency', 0)} Hz"
  2356. )
  2357. self.diag_max_error_var.set(
  2358. f"{status.get('diagnostic_max_frequency_error', 0)} Hz"
  2359. )
  2360. self.diag_error_counts_var.set(
  2361. f"个数{status.get('diagnostic_count_errors', 0)},"
  2362. f"频率{status.get('diagnostic_frequency_errors', 0)},"
  2363. f"曲线{status.get('diagnostic_profile_errors', 0)}"
  2364. )
  2365. def _set_state_badge(self, state: int) -> None:
  2366. if state == int(State.RUNNING):
  2367. colors = ("#DDEEFF", "#15558A")
  2368. elif state == int(State.COMPLETE):
  2369. colors = ("#DDF7EC", "#087A55")
  2370. elif state == int(State.ERROR):
  2371. colors = ("#FDECEC", "#B42318")
  2372. elif state == int(State.PAUSED):
  2373. colors = ("#FFF3D8", "#865600")
  2374. elif state == int(State.WAITING):
  2375. colors = ("#E8E2FF", "#5B35A5")
  2376. else:
  2377. colors = ("#E8EEF5", "#425466")
  2378. self.state_badge.configure(text=state_text(state), bg=colors[0], fg=colors[1])
  2379. def _set_test_result(self, text: str, kind: str) -> None:
  2380. # 主页面不再显示独立的运行提示条;保留最近结果供流程和调试使用。
  2381. self.last_result_text = text
  2382. self.last_result_kind = kind
  2383. def log(self, message: str) -> None:
  2384. if not hasattr(self, "log_text"):
  2385. return
  2386. timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
  2387. self.log_text.configure(state=tk.NORMAL)
  2388. self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
  2389. self.log_text.see(tk.END)
  2390. self.log_text.configure(state=tk.DISABLED)
  2391. def clear_log(self) -> None:
  2392. self.log_text.configure(state=tk.NORMAL)
  2393. self.log_text.delete("1.0", tk.END)
  2394. self.log_text.configure(state=tk.DISABLED)
  2395. self.log("日志已清空。")
  2396. def _load_settings(self) -> None:
  2397. try:
  2398. settings = json.loads(SETTINGS_FILE.read_text(encoding="utf-8"))
  2399. except (OSError, ValueError):
  2400. return
  2401. port = str(settings.get("port", ""))
  2402. if port in self.port_combo["values"]:
  2403. self.port_var.set(port)
  2404. self.slave_var.set(int(settings.get("slave", DEFAULT_SLAVE_ADDRESS)))
  2405. preset_index = max(0, min(int(settings.get("preset", 0)), len(PRESETS) - 1))
  2406. self.preset_var.set(PRESETS[preset_index]["name"])
  2407. self.pulses_var.set(str(settings.get("pulses", self.pulses_var.get())))
  2408. self.frequency_var.set(int(settings.get("frequency", self.frequency_var.get())))
  2409. self.runtime_frequency_var.set(
  2410. int(settings.get("runtime_frequency", self.frequency_var.get()))
  2411. )
  2412. self.maximum_frequency_var.set(
  2413. int(settings.get("maximum_frequency", MAX_FREQUENCY_HZ))
  2414. )
  2415. saved_profile = int(settings.get("profile_type", int(ProfileType.LINEAR)))
  2416. self.profile_var.set(
  2417. PROFILE_TEXT[ProfileType(saved_profile)]
  2418. if saved_profile in [int(item) for item in ProfileType]
  2419. else PROFILE_TEXT[ProfileType.LINEAR]
  2420. )
  2421. self.acceleration_var.set(int(settings.get("acceleration_ms", 1000)))
  2422. self.deceleration_var.set(int(settings.get("deceleration_ms", 1000)))
  2423. legacy_speed_default = min(100, max(1, int(self.frequency_var.get())))
  2424. self.start_frequency_var.set(
  2425. int(settings.get("start_frequency", legacy_speed_default))
  2426. )
  2427. self.end_frequency_var.set(
  2428. int(settings.get("end_frequency", legacy_speed_default))
  2429. )
  2430. self.direction_var.set(
  2431. DIRECTION_TEXT[1 if int(settings.get("direction", 0)) else 0]
  2432. )
  2433. saved_run_mode = int(settings.get("run_mode", int(RunMode.RELATIVE)))
  2434. self.run_mode_var.set(
  2435. MODE_TEXT[RunMode.ABSOLUTE]
  2436. if saved_run_mode == int(RunMode.ABSOLUTE)
  2437. else MODE_TEXT[RunMode.RELATIVE]
  2438. )
  2439. self._on_run_mode_changed()
  2440. saved_send_mode = int(settings.get("send_mode", int(SendMode.COMPLETE)))
  2441. self.send_mode_var.set(
  2442. SEND_MODE_TEXT[SendMode(saved_send_mode)]
  2443. if saved_send_mode in [int(item) for item in SendMode]
  2444. else SEND_MODE_TEXT[SendMode.COMPLETE]
  2445. )
  2446. saved_direction_logic = int(settings.get(
  2447. "direction_logic", int(DirectionLogic.POSITIVE)
  2448. ))
  2449. self.direction_logic_var.set(
  2450. DIRECTION_LOGIC_TEXT[DirectionLogic(saved_direction_logic)]
  2451. if saved_direction_logic in [int(item) for item in DirectionLogic]
  2452. else DIRECTION_LOGIC_TEXT[DirectionLogic.POSITIVE]
  2453. )
  2454. saved_pulse_point = int(settings.get("pulse_point", 0))
  2455. saved_direction_point = int(settings.get(
  2456. "secondary_point",
  2457. settings.get("b_phase_point", settings.get("direction_point", 0)),
  2458. ))
  2459. if saved_pulse_point in PULSE_POINTS:
  2460. self.pulse_point_var.set(PULSE_POINTS[saved_pulse_point])
  2461. if saved_direction_point in SECONDARY_POINTS:
  2462. self.secondary_point_var.set(SECONDARY_POINTS[saved_direction_point])
  2463. saved_output_mode = int(settings.get("output_mode", int(OutputMode.PULSE_DIRECTION)))
  2464. if saved_output_mode in [int(item) for item in OutputMode]:
  2465. self.output_mode_var.set(OUTPUT_MODE_TEXT[OutputMode(saved_output_mode)])
  2466. self._on_output_mode_changed()
  2467. try:
  2468. self.display_target = int(self.pulses_var.get())
  2469. except ValueError:
  2470. self.display_target = 0
  2471. def _save_settings(self) -> None:
  2472. preset_index = next(
  2473. (index for index, item in enumerate(PRESETS) if item["name"] == self.preset_var.get()),
  2474. 0,
  2475. )
  2476. settings = {
  2477. "port": self.port_var.get(),
  2478. "slave": self.slave_var.get(),
  2479. "preset": preset_index,
  2480. "pulses": self.pulses_var.get(),
  2481. "frequency": self.frequency_var.get(),
  2482. "runtime_frequency": self.runtime_frequency_var.get(),
  2483. "maximum_frequency": self.maximum_frequency_var.get(),
  2484. "profile_type": next(
  2485. (int(item) for item, text in PROFILE_TEXT.items()
  2486. if text == self.profile_var.get()),
  2487. int(ProfileType.LINEAR),
  2488. ),
  2489. "acceleration_ms": self.acceleration_var.get(),
  2490. "deceleration_ms": self.deceleration_var.get(),
  2491. "start_frequency": self.start_frequency_var.get(),
  2492. "end_frequency": self.end_frequency_var.get(),
  2493. "direction": 1 if self.direction_var.get() == DIRECTION_TEXT[1] else 0,
  2494. "run_mode": (
  2495. int(RunMode.ABSOLUTE)
  2496. if self.run_mode_var.get() == MODE_TEXT[RunMode.ABSOLUTE]
  2497. else int(RunMode.RELATIVE)
  2498. ),
  2499. "send_mode": int(self._selected_send_mode()),
  2500. "direction_logic": int(self._selected_direction_logic()),
  2501. "pulse_point": point_id_from_label(PULSE_POINTS, self.pulse_point_var.get()),
  2502. "secondary_point": point_id_from_label(
  2503. SECONDARY_POINTS, self.secondary_point_var.get()
  2504. ),
  2505. "output_mode": int(self._selected_output_mode()),
  2506. }
  2507. try:
  2508. SETTINGS_FILE.write_text(
  2509. json.dumps(settings, ensure_ascii=False, indent=2),
  2510. encoding="utf-8",
  2511. )
  2512. except OSError as exc:
  2513. self.log(f"保存界面设置失败:{exc}")
  2514. def close(self) -> None:
  2515. if self.closing:
  2516. return
  2517. self.closing = True
  2518. self._save_settings()
  2519. self.client.close()
  2520. self.root.destroy()
  2521. def main() -> int:
  2522. if sys.platform != "win32":
  2523. print("该工具使用Windows原生串口API,只支持Windows。")
  2524. return 1
  2525. root = tk.Tk()
  2526. PlsrTesterApp(root)
  2527. root.mainloop()
  2528. return 0
  2529. if __name__ == "__main__":
  2530. raise SystemExit(main())