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.
 
 
 
 
 
 

1140 rader
44 KiB

  1. """Tkinter control panel for the 2026 PLSR Modbus register map."""
  2. from __future__ import annotations
  3. import argparse
  4. import queue
  5. import threading
  6. import time
  7. import tkinter as tk
  8. import tkinter.font as tkfont
  9. from tkinter import messagebox, ttk
  10. import serial
  11. from serial.tools import list_ports
  12. # Keep one RTU implementation for the board tests and this operator panel.
  13. from plsr_modbus_product_test import (
  14. COMMAND_CLEAR,
  15. COMMAND_START,
  16. COMMAND_STOP,
  17. COMMON_WORDS,
  18. CONFIG_BASE,
  19. CONTROL,
  20. DIAGNOSTIC_CLEAR,
  21. DIAGNOSTIC_CONTROL,
  22. DIAG_COUNT_CHECKED,
  23. DIAG_COUNT_PASS,
  24. DIAG_CURVE_CHECKED,
  25. DIAG_CURVE_PASS,
  26. DIAG_FAULT_LATCHED,
  27. DIAG_FREQUENCY_CHECKED,
  28. DIAG_FREQUENCY_PASS,
  29. DIAG_MONITORING,
  30. ModbusException,
  31. OUTPUT_AB,
  32. OUTPUT_MODE,
  33. OUTPUT_PULSE_DIR,
  34. RtuClient,
  35. SEGMENT_1_BASE,
  36. SEGMENT_WORDS,
  37. TestFailure,
  38. read_diagnostic,
  39. read_status,
  40. split_i32,
  41. split_u32,
  42. )
  43. BAUD_RATE = 9600
  44. SERIAL_TIMEOUT_SECONDS = 0.8
  45. STATUS_POLL_SECONDS = 0.25
  46. SEGMENT_COUNT = 10
  47. SEGMENT_STRIDE = 0x10
  48. MAX_FREQUENCY_HZ = 100_000
  49. OUTPUT_MODE_OPTIONS = (
  50. ("PULSE/DIR (0)", OUTPUT_PULSE_DIR),
  51. ("AB 正交脉冲 (1)", OUTPUT_AB),
  52. )
  53. PULSE_OPTIONS = (
  54. ("Y0 (0)", 0),
  55. ("Y1 (1)", 1),
  56. ("Y2 (2)", 2),
  57. ("Y3 (3)", 3),
  58. )
  59. DIRECTION_OPTIONS = (
  60. ("Y12 (0)", 0),
  61. ("Y13 (1)", 1),
  62. ("Y14 (2)", 2),
  63. ("Y15 (3)", 3),
  64. )
  65. INPUT_OPTIONS = (("X4 (0)", 0), ("X5 (1)", 1))
  66. SEND_OPTIONS = (("脉冲发送完毕 (0)", 0), ("后续段 (1)", 1))
  67. LOGIC_OPTIONS = (("正逻辑 (0)", 0), ("负逻辑 (1)", 1))
  68. CURVE_OPTIONS = (("直线 (0)", 0), ("S 曲线 (1)", 1), ("正弦曲线 (2)", 2))
  69. POSITION_OPTIONS = (("相对位置 (0)", 0), ("绝对位置 (1)", 1))
  70. WAIT_OPTIONS = (
  71. ("WAIT 时间 (0)", 0),
  72. ("WAIT 信号 (1)", 1),
  73. ("ACT 时间 (2)", 2),
  74. ("EXT 信号 (3)", 3),
  75. ("EXT 或完成 (4)", 4),
  76. )
  77. STATUS_NAMES = {
  78. 0: "未初始化",
  79. 1: "空闲",
  80. 2: "加速",
  81. 3: "运行",
  82. 4: "减速",
  83. 5: "等待",
  84. 6: "暂停",
  85. 7: "完成",
  86. 8: "停止",
  87. 9: "错误",
  88. }
  89. ERROR_NAMES = {
  90. 0: "无错误",
  91. 1: "状态转换非法",
  92. 2: "资源冲突",
  93. 3: "资源非法",
  94. 4: "定时器错误",
  95. 5: "计数错误",
  96. 6: "正限位",
  97. 7: "负限位",
  98. 8: "急停",
  99. 9: "内部错误",
  100. }
  101. DIAGNOSTIC_REASON_NAMES = {
  102. 0: "无故障",
  103. 1: "计数",
  104. 2: "频率",
  105. 3: "曲线",
  106. 4: "无效采样",
  107. }
  108. def option_labels(options):
  109. return tuple(label for label, _value in options)
  110. def option_label(options, value):
  111. for label, candidate in options:
  112. if candidate == value:
  113. return label
  114. return str(value)
  115. def option_value(options, label, field_name):
  116. for candidate_label, value in options:
  117. if candidate_label == label:
  118. return value
  119. raise ValueError("%s 不是有效选项" % field_name)
  120. def parse_integer(text, field_name, minimum, maximum):
  121. raw = text.strip()
  122. if not raw:
  123. raise ValueError("%s 不能为空" % field_name)
  124. try:
  125. value = int(raw, 0)
  126. except ValueError:
  127. try:
  128. value = int(raw, 10)
  129. except ValueError as error:
  130. raise ValueError("%s 必须是整数" % field_name) from error
  131. if value < minimum or value > maximum:
  132. raise ValueError(
  133. "%s 必须在 %d 到 %d 之间" % (field_name, minimum, maximum)
  134. )
  135. return value
  136. def write_common_configuration(client, words, output_mode):
  137. """Order writes so the intermediate configuration is always valid."""
  138. if output_mode == OUTPUT_PULSE_DIR:
  139. client.write_single(OUTPUT_MODE, OUTPUT_PULSE_DIR)
  140. client.write_multiple(CONFIG_BASE, words)
  141. if output_mode == OUTPUT_AB:
  142. client.write_single(OUTPUT_MODE, OUTPUT_AB)
  143. class SerialWorker(threading.Thread):
  144. """Own the serial port and report all results through a queue."""
  145. def __init__(self):
  146. super().__init__(name="plsr-serial", daemon=True)
  147. self.commands = queue.Queue()
  148. self.results = queue.Queue()
  149. self.stop_event = threading.Event()
  150. self.client = None
  151. self.next_status_poll = 0.0
  152. def submit(self, command, **payload):
  153. self.commands.put((command, payload))
  154. def close(self):
  155. self.stop_event.set()
  156. self.commands.put(("shutdown", {}))
  157. def _emit(self, event, **payload):
  158. self.results.put((event, payload))
  159. def _disconnect(self, notify=True, reason=""):
  160. client = self.client
  161. self.client = None
  162. if client is not None:
  163. try:
  164. client.__exit__(None, None, None)
  165. except (OSError, serial.SerialException):
  166. pass
  167. if notify:
  168. self._emit("connection", connected=False, reason=reason)
  169. def _require_client(self):
  170. if self.client is None:
  171. raise RuntimeError("串口尚未连接")
  172. return self.client
  173. def _read_configuration(self):
  174. client = self._require_client()
  175. common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
  176. output_mode = client.read_holding(OUTPUT_MODE, 1)[0]
  177. segments = []
  178. for index in range(SEGMENT_COUNT):
  179. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  180. segments.append(client.read_holding(address, SEGMENT_WORDS))
  181. self._emit(
  182. "configuration",
  183. common=common,
  184. output_mode=output_mode,
  185. segments=segments,
  186. )
  187. def _handle_connection_error(self, operation, error):
  188. self._disconnect(notify=True, reason=str(error))
  189. self._emit("error", operation=operation, message=str(error), modal=True)
  190. def _handle_command(self, command, payload):
  191. if command == "connect":
  192. self._disconnect(notify=False)
  193. try:
  194. client = RtuClient(
  195. payload["port"],
  196. BAUD_RATE,
  197. payload["slave"],
  198. SERIAL_TIMEOUT_SECONDS,
  199. )
  200. client.__enter__()
  201. self.client = client
  202. self.next_status_poll = 0.0
  203. self._emit(
  204. "connection",
  205. connected=True,
  206. port=payload["port"],
  207. slave=payload["slave"],
  208. )
  209. self._read_configuration()
  210. except (OSError, serial.SerialException) as error:
  211. self._handle_connection_error("连接", error)
  212. except (TestFailure, ModbusException, RuntimeError) as error:
  213. self._disconnect(notify=True, reason=str(error))
  214. self._emit(
  215. "error", operation="读取参数", message=str(error), modal=True
  216. )
  217. return
  218. if command == "disconnect":
  219. self._disconnect(notify=True)
  220. return
  221. try:
  222. client = self._require_client()
  223. if command == "read_configuration":
  224. self._read_configuration()
  225. self._emit("operation", message="参数读取完成")
  226. elif command == "write_common":
  227. write_common_configuration(
  228. client, payload["words"], payload["output_mode"]
  229. )
  230. common = client.read_holding(CONFIG_BASE, COMMON_WORDS)
  231. output_mode = client.read_holding(OUTPUT_MODE, 1)[0]
  232. self._emit("common", words=common, output_mode=output_mode)
  233. self._emit("operation", message="公共参数写入并回读完成")
  234. elif command == "write_segments":
  235. for index, words in enumerate(payload["segments"]):
  236. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  237. client.write_multiple(address, words)
  238. segments = []
  239. for index in range(SEGMENT_COUNT):
  240. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  241. segments.append(client.read_holding(address, SEGMENT_WORDS))
  242. self._emit("segments", words=segments)
  243. self._emit("operation", message="10 段参数写入并回读完成")
  244. elif command == "control":
  245. client.write_single(CONTROL, payload["value"])
  246. self._emit("operation", message=payload["message"])
  247. elif command == "clear_diagnostic":
  248. client.write_single(DIAGNOSTIC_CONTROL, DIAGNOSTIC_CLEAR)
  249. self._emit("diagnostic", diagnostic=read_diagnostic(client))
  250. self._emit("operation", message="诊断锁存和统计已清除")
  251. else:
  252. raise RuntimeError("未知后台命令: %s" % command)
  253. except (OSError, serial.SerialException) as error:
  254. self._handle_connection_error(payload.get("operation", "通信"), error)
  255. except (TestFailure, ModbusException, RuntimeError) as error:
  256. self._emit(
  257. "error",
  258. operation=payload.get("operation", "通信"),
  259. message=str(error),
  260. modal=True,
  261. )
  262. def _poll_status(self):
  263. try:
  264. status = read_status(self._require_client())
  265. diagnostic = read_diagnostic(self._require_client())
  266. self._emit("status", status=status)
  267. self._emit("diagnostic", diagnostic=diagnostic)
  268. self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS
  269. except (OSError, serial.SerialException) as error:
  270. self._handle_connection_error("状态轮询", error)
  271. except (TestFailure, ModbusException, RuntimeError) as error:
  272. self._emit(
  273. "error", operation="状态轮询", message=str(error), modal=False
  274. )
  275. self.next_status_poll = time.monotonic() + 1.0
  276. def run(self):
  277. try:
  278. while not self.stop_event.is_set():
  279. try:
  280. command, payload = self.commands.get(timeout=0.05)
  281. except queue.Empty:
  282. command = None
  283. payload = None
  284. if command == "shutdown":
  285. break
  286. if command is not None:
  287. self._handle_command(command, payload)
  288. if (
  289. self.client is not None
  290. and time.monotonic() >= self.next_status_poll
  291. ):
  292. self._poll_status()
  293. finally:
  294. self._disconnect(notify=False)
  295. class PlsrControlPanel:
  296. def __init__(self, root):
  297. self.root = root
  298. self.root.title("PLSR 控制面板")
  299. self.root.geometry("1180x720")
  300. self.root.minsize(980, 650)
  301. self.root.protocol("WM_DELETE_WINDOW", self._on_close)
  302. self.connected = False
  303. self.operation_pending = False
  304. self.worker = SerialWorker()
  305. self.worker.start()
  306. self.port_var = tk.StringVar()
  307. self.slave_var = tk.StringVar(value="1")
  308. self.connection_var = tk.StringVar(value="未连接")
  309. self.footer_var = tk.StringVar(value="请选择串口并连接")
  310. self.status_vars = {
  311. "position": tk.StringVar(value="--"),
  312. "frequency": tk.StringVar(value="--"),
  313. "state": tk.StringVar(value="--"),
  314. "segment": tk.StringVar(value="--"),
  315. "error": tk.StringVar(value="--"),
  316. }
  317. self.diagnostic_vars = {
  318. key: tk.StringVar(value="--")
  319. for key in (
  320. "summary", "reason", "segment", "mode_direction",
  321. "expected_count", "actual_count", "count_error",
  322. "requested_hz", "expected_hz", "active_hz",
  323. "request_error", "sample_count", "mismatch_count",
  324. "maximum_error", "first_mismatch",
  325. )
  326. }
  327. self.common_vars = {}
  328. self.segment_vars = []
  329. self.connection_widgets = []
  330. self.action_buttons = []
  331. self._configure_style()
  332. self._build_ui()
  333. self.refresh_ports()
  334. self._set_connected(False)
  335. self.root.after(50, self._drain_results)
  336. def _configure_style(self):
  337. default_font = tkfont.nametofont("TkDefaultFont")
  338. default_font.configure(family="Microsoft YaHei UI", size=10)
  339. text_font = tkfont.nametofont("TkTextFont")
  340. text_font.configure(family="Microsoft YaHei UI", size=10)
  341. style = ttk.Style(self.root)
  342. style.configure("Connected.TLabel", foreground="#18794e")
  343. style.configure("Disconnected.TLabel", foreground="#5f6368")
  344. style.configure("Error.TLabel", foreground="#b42318")
  345. style.configure("StatusValue.TLabel", font=("Microsoft YaHei UI", 11, "bold"))
  346. def _build_ui(self):
  347. self.root.columnconfigure(0, weight=1)
  348. self.root.rowconfigure(2, weight=1)
  349. connection = ttk.LabelFrame(self.root, text="连接")
  350. connection.grid(row=0, column=0, padx=10, pady=(10, 6), sticky="ew")
  351. connection.columnconfigure(7, weight=1)
  352. ttk.Label(connection, text="串口").grid(row=0, column=0, padx=(8, 4), pady=8)
  353. self.port_box = ttk.Combobox(
  354. connection, textvariable=self.port_var, width=13, state="readonly"
  355. )
  356. self.port_box.grid(row=0, column=1, padx=4, pady=8)
  357. refresh_button = ttk.Button(connection, text="刷新", command=self.refresh_ports)
  358. refresh_button.grid(row=0, column=2, padx=(0, 12), pady=8)
  359. ttk.Label(connection, text="串口参数").grid(row=0, column=3, padx=4, pady=8)
  360. ttk.Label(connection, text="9600, 8E1").grid(row=0, column=4, padx=(4, 12), pady=8)
  361. ttk.Label(connection, text="从站地址").grid(row=0, column=5, padx=4, pady=8)
  362. slave_entry = ttk.Entry(connection, textvariable=self.slave_var, width=7)
  363. slave_entry.grid(row=0, column=6, padx=(4, 12), pady=8)
  364. self.connect_button = ttk.Button(
  365. connection, text="连接", command=self._toggle_connection
  366. )
  367. self.connect_button.grid(row=0, column=8, padx=8, pady=8)
  368. self.connection_label = ttk.Label(
  369. connection,
  370. textvariable=self.connection_var,
  371. style="Disconnected.TLabel",
  372. width=28,
  373. anchor="e",
  374. )
  375. self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e")
  376. self.connection_widgets = [self.port_box, refresh_button, slave_entry]
  377. self._build_runtime_status()
  378. notebook = ttk.Notebook(self.root)
  379. notebook.grid(row=2, column=0, padx=10, pady=6, sticky="nsew")
  380. common_tab = ttk.Frame(notebook, padding=12)
  381. segment_tab = ttk.Frame(notebook, padding=12)
  382. diagnostic_tab = ttk.Frame(notebook, padding=12)
  383. notebook.add(common_tab, text="公共参数 0x1000-0x1013")
  384. notebook.add(segment_tab, text="10 段参数 0x1100-0x1197")
  385. notebook.add(diagnostic_tab, text="输出诊断 0x2100-0x2119")
  386. self._build_common_tab(common_tab)
  387. self._build_segment_tab(segment_tab)
  388. self._build_diagnostic_tab(diagnostic_tab)
  389. footer = ttk.Frame(self.root)
  390. footer.grid(row=3, column=0, padx=10, pady=(4, 10), sticky="ew")
  391. footer.columnconfigure(4, weight=1)
  392. read_button = ttk.Button(footer, text="读取全部参数", command=self._read_all)
  393. read_button.grid(row=0, column=0, padx=(0, 6))
  394. common_button = ttk.Button(
  395. footer, text="写入公共参数", command=self._write_common
  396. )
  397. common_button.grid(row=0, column=1, padx=6)
  398. segment_button = ttk.Button(
  399. footer, text="写入 10 段参数", command=self._write_segments
  400. )
  401. segment_button.grid(row=0, column=2, padx=6)
  402. self.action_buttons.extend([read_button, common_button, segment_button])
  403. self.footer_label = ttk.Label(
  404. footer, textvariable=self.footer_var, anchor="e"
  405. )
  406. self.footer_label.grid(row=0, column=4, padx=(12, 0), sticky="ew")
  407. def _build_runtime_status(self):
  408. frame = ttk.LabelFrame(self.root, text="实时状态 0x2000-0x2006")
  409. frame.grid(row=1, column=0, padx=10, pady=6, sticky="ew")
  410. for column in range(11):
  411. frame.columnconfigure(column, weight=1 if column % 2 else 0)
  412. fields = (
  413. ("累计位置", "position"),
  414. ("当前频率", "frequency"),
  415. ("运行状态", "state"),
  416. ("当前段", "segment"),
  417. ("错误码", "error"),
  418. )
  419. for index, (label, key) in enumerate(fields):
  420. ttk.Label(frame, text=label).grid(
  421. row=0, column=index * 2, padx=(8, 4), pady=9, sticky="e"
  422. )
  423. ttk.Label(
  424. frame, textvariable=self.status_vars[key], style="StatusValue.TLabel"
  425. ).grid(row=0, column=index * 2 + 1, padx=(4, 12), pady=9, sticky="w")
  426. commands = ttk.Frame(frame)
  427. commands.grid(row=0, column=10, padx=8, pady=6, sticky="e")
  428. start_button = ttk.Button(
  429. commands, text="启动", command=lambda: self._send_control(COMMAND_START)
  430. )
  431. stop_button = ttk.Button(
  432. commands, text="停止", command=lambda: self._send_control(COMMAND_STOP)
  433. )
  434. clear_button = ttk.Button(commands, text="清零", command=self._clear_position)
  435. start_button.grid(row=0, column=0, padx=3)
  436. stop_button.grid(row=0, column=1, padx=3)
  437. clear_button.grid(row=0, column=2, padx=3)
  438. self.action_buttons.extend([start_button, stop_button, clear_button])
  439. def _add_entry(self, parent, row, group, key, label, default="0", width=13):
  440. column = group * 2
  441. ttk.Label(parent, text=label).grid(
  442. row=row, column=column, padx=(8, 4), pady=7, sticky="e"
  443. )
  444. variable = tk.StringVar(value=default)
  445. ttk.Entry(parent, textvariable=variable, width=width).grid(
  446. row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w"
  447. )
  448. self.common_vars[key] = variable
  449. def _add_combo(self, parent, row, group, key, label, options):
  450. column = group * 2
  451. ttk.Label(parent, text=label).grid(
  452. row=row, column=column, padx=(8, 4), pady=7, sticky="e"
  453. )
  454. variable = tk.StringVar(value=options[0][0])
  455. ttk.Combobox(
  456. parent,
  457. textvariable=variable,
  458. values=option_labels(options),
  459. state="readonly",
  460. width=18,
  461. ).grid(row=row, column=column + 1, padx=(4, 18), pady=7, sticky="w")
  462. self.common_vars[key] = variable
  463. def _build_common_tab(self, parent):
  464. for column in (1, 3, 5):
  465. parent.columnconfigure(column, weight=1)
  466. self._add_combo(
  467. parent, 0, 0, "pulse_output", "脉冲输出(单逻辑 PLSR)", PULSE_OPTIONS
  468. )
  469. self._add_combo(
  470. parent, 0, 1, "direction_output", "方向输出", DIRECTION_OPTIONS
  471. )
  472. self._add_entry(parent, 0, 2, "direction_delay", "方向延时 ms")
  473. self._add_combo(parent, 1, 0, "wait_input", "WAIT 输入", INPUT_OPTIONS)
  474. self._add_combo(parent, 1, 1, "ext_input", "EXT 输入", INPUT_OPTIONS)
  475. self._add_combo(parent, 1, 2, "send_mode", "发送模式", SEND_OPTIONS)
  476. self._add_combo(parent, 2, 0, "negative_logic", "方向逻辑", LOGIC_OPTIONS)
  477. self._add_combo(parent, 2, 1, "curve_mode", "曲线模式", CURVE_OPTIONS)
  478. self._add_combo(parent, 2, 2, "position_mode", "位置模式", POSITION_OPTIONS)
  479. self._add_entry(parent, 3, 0, "segment_count", "段数", default="1")
  480. self._add_entry(parent, 3, 1, "start_segment", "起始段", default="1")
  481. self._add_entry(parent, 3, 2, "default_speed", "基准速度 Hz", default="1000")
  482. self._add_entry(parent, 4, 0, "start_speed", "启动速度 Hz", default="100")
  483. self._add_entry(parent, 4, 1, "stop_speed", "停止速度 Hz", default="100")
  484. self._add_entry(parent, 4, 2, "acceleration", "加速时间 ms")
  485. self._add_entry(parent, 5, 0, "deceleration", "减速时间 ms")
  486. self._add_combo(
  487. parent, 5, 1, "output_mode", "输出模式 0x1200", OUTPUT_MODE_OPTIONS
  488. )
  489. def _build_diagnostic_tab(self, parent):
  490. parent.columnconfigure(1, weight=1)
  491. parent.columnconfigure(3, weight=1)
  492. parent.columnconfigure(5, weight=1)
  493. fields = (
  494. ("综合结果", "summary"),
  495. ("首个故障", "reason"),
  496. ("段号", "segment"),
  497. ("模式 / 方向", "mode_direction"),
  498. ("期望脉冲", "expected_count"),
  499. ("实测脉冲", "actual_count"),
  500. ("计数误差", "count_error"),
  501. ("请求频率", "requested_hz"),
  502. ("量化频率", "expected_hz"),
  503. ("活动频率", "active_hz"),
  504. ("请求量化误差", "request_error"),
  505. ("曲线采样数", "sample_count"),
  506. ("不匹配数", "mismatch_count"),
  507. ("最大频率误差", "maximum_error"),
  508. ("首个不匹配样本", "first_mismatch"),
  509. )
  510. for index, (label, key) in enumerate(fields):
  511. row = index // 3
  512. group = index % 3
  513. column = group * 2
  514. ttk.Label(parent, text=label).grid(
  515. row=row, column=column, padx=(8, 4), pady=9, sticky="e"
  516. )
  517. ttk.Label(
  518. parent,
  519. textvariable=self.diagnostic_vars[key],
  520. style="StatusValue.TLabel" if key == "summary" else "TLabel",
  521. ).grid(row=row, column=column + 1, padx=(4, 18), pady=9, sticky="w")
  522. clear_button = ttk.Button(
  523. parent, text="清除诊断", command=self._clear_diagnostic
  524. )
  525. clear_button.grid(row=5, column=5, padx=8, pady=(16, 4), sticky="e")
  526. self.action_buttons.append(clear_button)
  527. def _build_segment_tab(self, parent):
  528. headers = (
  529. ("段 / 基址", 15),
  530. ("频率 Hz", 13),
  531. ("脉冲数", 15),
  532. ("等待类型", 19),
  533. ("WAIT ms", 11),
  534. ("ACT ms", 11),
  535. ("跳转段", 9),
  536. )
  537. for column, (label, _width) in enumerate(headers):
  538. parent.columnconfigure(column, weight=1 if column in (1, 2, 3) else 0)
  539. ttk.Label(parent, text=label, anchor="center").grid(
  540. row=0, column=column, padx=4, pady=(0, 6), sticky="ew"
  541. )
  542. for index in range(SEGMENT_COUNT):
  543. address = SEGMENT_1_BASE + index * SEGMENT_STRIDE
  544. ttk.Label(parent, text="%d / 0x%04X" % (index + 1, address)).grid(
  545. row=index + 1, column=0, padx=(2, 8), pady=4, sticky="e"
  546. )
  547. variables = {
  548. "frequency": tk.StringVar(value="1000"),
  549. "pulses": tk.StringVar(value="0"),
  550. "wait_type": tk.StringVar(value=WAIT_OPTIONS[0][0]),
  551. "wait_ms": tk.StringVar(value="0"),
  552. "act_ms": tk.StringVar(value="0"),
  553. "jump": tk.StringVar(value="0"),
  554. }
  555. ttk.Entry(parent, textvariable=variables["frequency"], width=13).grid(
  556. row=index + 1, column=1, padx=4, pady=4, sticky="ew"
  557. )
  558. ttk.Entry(parent, textvariable=variables["pulses"], width=15).grid(
  559. row=index + 1, column=2, padx=4, pady=4, sticky="ew"
  560. )
  561. ttk.Combobox(
  562. parent,
  563. textvariable=variables["wait_type"],
  564. values=option_labels(WAIT_OPTIONS),
  565. state="readonly",
  566. width=19,
  567. ).grid(row=index + 1, column=3, padx=4, pady=4, sticky="ew")
  568. ttk.Entry(parent, textvariable=variables["wait_ms"], width=11).grid(
  569. row=index + 1, column=4, padx=4, pady=4
  570. )
  571. ttk.Entry(parent, textvariable=variables["act_ms"], width=11).grid(
  572. row=index + 1, column=5, padx=4, pady=4
  573. )
  574. ttk.Entry(parent, textvariable=variables["jump"], width=9).grid(
  575. row=index + 1, column=6, padx=4, pady=4
  576. )
  577. self.segment_vars.append(variables)
  578. def refresh_ports(self):
  579. ports = [item.device for item in list_ports.comports()]
  580. self.port_box["values"] = ports
  581. if ports and self.port_var.get() not in ports:
  582. self.port_var.set(ports[0])
  583. elif not ports:
  584. self.port_var.set("")
  585. def _set_connected(self, connected, description=""):
  586. self.connected = connected
  587. self.connect_button.configure(
  588. text="断开" if connected else "连接", state="normal"
  589. )
  590. connection_state = "disabled" if connected else "normal"
  591. for widget in self.connection_widgets:
  592. if widget is self.port_box:
  593. widget.configure(state="disabled" if connected else "readonly")
  594. else:
  595. widget.configure(state=connection_state)
  596. if connected:
  597. self.connection_var.set(description or "已连接")
  598. self.connection_label.configure(style="Connected.TLabel")
  599. else:
  600. self.connection_var.set(description or "未连接")
  601. self.connection_label.configure(
  602. style="Error.TLabel" if description else "Disconnected.TLabel"
  603. )
  604. for variable in self.status_vars.values():
  605. variable.set("--")
  606. for variable in self.diagnostic_vars.values():
  607. variable.set("--")
  608. self._update_action_state()
  609. def _update_action_state(self):
  610. state = "normal" if self.connected and not self.operation_pending else "disabled"
  611. for button in self.action_buttons:
  612. button.configure(state=state)
  613. def _start_operation(self, message):
  614. if not self.connected:
  615. messagebox.showerror("未连接", "请先连接 PLSR 从站")
  616. return False
  617. if self.operation_pending:
  618. return False
  619. self.operation_pending = True
  620. self.footer_var.set(message)
  621. self._update_action_state()
  622. return True
  623. def _finish_operation(self, message):
  624. self.operation_pending = False
  625. self.footer_var.set(message)
  626. self.footer_label.configure(style="TLabel")
  627. self._update_action_state()
  628. def _toggle_connection(self):
  629. if self.connected:
  630. self.operation_pending = False
  631. self.footer_var.set("正在断开")
  632. self.worker.submit("disconnect")
  633. return
  634. port = self.port_var.get().strip()
  635. if not port:
  636. messagebox.showerror("连接参数", "请选择可用串口")
  637. return
  638. try:
  639. slave = parse_integer(self.slave_var.get(), "从站地址", 1, 247)
  640. except ValueError as error:
  641. messagebox.showerror("连接参数", str(error))
  642. return
  643. self.connect_button.configure(state="disabled")
  644. self.operation_pending = True
  645. self._update_action_state()
  646. self.connection_var.set("正在连接 %s" % port)
  647. self.footer_var.set("正在打开串口并读取参数")
  648. self.worker.submit("connect", port=port, slave=slave)
  649. def _common_words(self):
  650. values = self.common_vars
  651. segment_count = parse_integer(values["segment_count"].get(), "段数", 1, 10)
  652. start_segment = parse_integer(
  653. values["start_segment"].get(), "起始段", 1, 10
  654. )
  655. if start_segment > segment_count:
  656. raise ValueError("起始段不能大于段数")
  657. words = [0] * COMMON_WORDS
  658. words[0] = option_value(
  659. PULSE_OPTIONS, values["pulse_output"].get(), "脉冲输出"
  660. )
  661. words[1] = option_value(
  662. DIRECTION_OPTIONS, values["direction_output"].get(), "方向输出"
  663. )
  664. words[2] = option_value(INPUT_OPTIONS, values["wait_input"].get(), "WAIT 输入")
  665. words[3] = option_value(INPUT_OPTIONS, values["ext_input"].get(), "EXT 输入")
  666. words[4] = option_value(SEND_OPTIONS, values["send_mode"].get(), "发送模式")
  667. words[5] = parse_integer(
  668. values["direction_delay"].get(), "方向延时", 0, 0xFFFF
  669. )
  670. words[6] = option_value(
  671. LOGIC_OPTIONS, values["negative_logic"].get(), "方向逻辑"
  672. )
  673. words[7] = option_value(CURVE_OPTIONS, values["curve_mode"].get(), "曲线模式")
  674. words[8] = option_value(
  675. POSITION_OPTIONS, values["position_mode"].get(), "位置模式"
  676. )
  677. words[9] = segment_count
  678. words[10] = start_segment
  679. words[11:13] = split_u32(
  680. parse_integer(
  681. values["default_speed"].get(), "基准速度", 1, MAX_FREQUENCY_HZ
  682. )
  683. )
  684. words[13:15] = split_u32(
  685. parse_integer(
  686. values["start_speed"].get(), "启动速度", 0, MAX_FREQUENCY_HZ
  687. )
  688. )
  689. words[15] = 0
  690. words[16:18] = split_u32(
  691. parse_integer(
  692. values["stop_speed"].get(), "停止速度", 0, MAX_FREQUENCY_HZ
  693. )
  694. )
  695. words[18] = parse_integer(values["acceleration"].get(), "加速时间", 0, 0xFFFF)
  696. words[19] = parse_integer(values["deceleration"].get(), "减速时间", 0, 0xFFFF)
  697. return words
  698. def _segment_words(self):
  699. segment_count = parse_integer(
  700. self.common_vars["segment_count"].get(), "段数", 1, SEGMENT_COUNT
  701. )
  702. encoded = []
  703. for index, variables in enumerate(self.segment_vars):
  704. name = "第 %d 段" % (index + 1)
  705. frequency = parse_integer(
  706. variables["frequency"].get(), name + "频率", 1, MAX_FREQUENCY_HZ
  707. )
  708. pulses = parse_integer(
  709. variables["pulses"].get(), name + "脉冲数", -(1 << 31), (1 << 31) - 1
  710. )
  711. wait_type = option_value(
  712. WAIT_OPTIONS, variables["wait_type"].get(), name + "等待类型"
  713. )
  714. wait_ms = parse_integer(
  715. variables["wait_ms"].get(), name + " WAIT 时间", 0, 0xFFFF
  716. )
  717. act_ms = parse_integer(
  718. variables["act_ms"].get(), name + " ACT 时间", 0, 0xFFFF
  719. )
  720. jump = parse_integer(variables["jump"].get(), name + "跳转段", 0, 10)
  721. if index < segment_count and jump > segment_count:
  722. raise ValueError(
  723. "%s跳转段必须为 0 或不大于脉冲总段数 %d"
  724. % (name, segment_count)
  725. )
  726. words = split_u32(frequency) + split_i32(pulses)
  727. words.extend((wait_type, wait_ms, act_ms, jump))
  728. encoded.append(words)
  729. return encoded
  730. def _set_common_words(self, words):
  731. if len(words) != COMMON_WORDS:
  732. raise ValueError("公共参数回读长度错误")
  733. values = self.common_vars
  734. values["pulse_output"].set(option_label(PULSE_OPTIONS, words[0]))
  735. values["direction_output"].set(option_label(DIRECTION_OPTIONS, words[1]))
  736. values["wait_input"].set(option_label(INPUT_OPTIONS, words[2]))
  737. values["ext_input"].set(option_label(INPUT_OPTIONS, words[3]))
  738. values["send_mode"].set(option_label(SEND_OPTIONS, words[4]))
  739. values["direction_delay"].set(str(words[5]))
  740. values["negative_logic"].set(option_label(LOGIC_OPTIONS, words[6]))
  741. values["curve_mode"].set(option_label(CURVE_OPTIONS, words[7]))
  742. values["position_mode"].set(option_label(POSITION_OPTIONS, words[8]))
  743. values["segment_count"].set(str(words[9]))
  744. values["start_segment"].set(str(words[10]))
  745. values["default_speed"].set(str(words[11] | (words[12] << 16)))
  746. values["start_speed"].set(str(words[13] | (words[14] << 16)))
  747. values["stop_speed"].set(str(words[16] | (words[17] << 16)))
  748. values["acceleration"].set(str(words[18]))
  749. values["deceleration"].set(str(words[19]))
  750. def _set_segment_words(self, segments):
  751. if len(segments) != SEGMENT_COUNT:
  752. raise ValueError("段参数回读数量错误")
  753. for index, words in enumerate(segments):
  754. if len(words) != SEGMENT_WORDS:
  755. raise ValueError("第 %d 段回读长度错误" % (index + 1))
  756. variables = self.segment_vars[index]
  757. frequency = words[0] | (words[1] << 16)
  758. pulse_bits = words[2] | (words[3] << 16)
  759. pulses = pulse_bits - (1 << 32) if pulse_bits & 0x80000000 else pulse_bits
  760. variables["frequency"].set(str(frequency))
  761. variables["pulses"].set(str(pulses))
  762. variables["wait_type"].set(option_label(WAIT_OPTIONS, words[4]))
  763. variables["wait_ms"].set(str(words[5]))
  764. variables["act_ms"].set(str(words[6]))
  765. variables["jump"].set(str(words[7]))
  766. def _read_all(self):
  767. if self._start_operation("正在读取公共参数和 10 段参数"):
  768. self.worker.submit("read_configuration", operation="读取参数")
  769. def _write_common(self):
  770. try:
  771. words = self._common_words()
  772. output_mode = option_value(
  773. OUTPUT_MODE_OPTIONS,
  774. self.common_vars["output_mode"].get(),
  775. "输出模式",
  776. )
  777. if output_mode == OUTPUT_AB and words[0] not in (0, 2):
  778. raise ValueError("AB 模式的脉冲输出只能选择 Y0/Y1 或 Y2/Y3")
  779. except ValueError as error:
  780. messagebox.showerror("参数错误", str(error))
  781. return
  782. if self._start_operation("正在写入公共参数"):
  783. self.worker.submit(
  784. "write_common",
  785. words=words,
  786. output_mode=output_mode,
  787. operation="写入公共参数",
  788. )
  789. def _write_segments(self):
  790. try:
  791. segments = self._segment_words()
  792. except ValueError as error:
  793. messagebox.showerror("参数错误", str(error))
  794. return
  795. if self._start_operation("正在写入 10 段参数"):
  796. self.worker.submit(
  797. "write_segments", segments=segments, operation="写入段参数"
  798. )
  799. def _send_control(self, command):
  800. messages = {
  801. COMMAND_START: "启动命令已发送",
  802. COMMAND_STOP: "停止命令已发送",
  803. COMMAND_CLEAR: "清零命令已发送",
  804. }
  805. if self._start_operation("正在发送控制命令"):
  806. self.worker.submit(
  807. "control",
  808. value=command,
  809. message=messages[command],
  810. operation="控制命令",
  811. )
  812. def _clear_position(self):
  813. if messagebox.askyesno("确认清零", "确认将累计位置清零?"):
  814. self._send_control(COMMAND_CLEAR)
  815. def _clear_diagnostic(self):
  816. if self._start_operation("正在清除诊断锁存和统计"):
  817. self.worker.submit(
  818. "clear_diagnostic", operation="清除诊断"
  819. )
  820. def _show_status(self, status):
  821. self.status_vars["position"].set(str(status.position))
  822. self.status_vars["frequency"].set("%d Hz" % status.frequency_hz)
  823. self.status_vars["state"].set(
  824. "%s (%d)" % (STATUS_NAMES.get(status.state, "未知"), status.state)
  825. )
  826. self.status_vars["segment"].set(str(status.segment))
  827. self.status_vars["error"].set(
  828. "%s (%d)" % (ERROR_NAMES.get(status.error, "未知"), status.error)
  829. )
  830. def _show_diagnostic(self, diagnostic):
  831. def check_name(checked_bit, pass_bit):
  832. if not diagnostic.flags & checked_bit:
  833. return "未检查"
  834. return "通过" if diagnostic.flags & pass_bit else "失败"
  835. checks = "计数 %s / 频率 %s / 曲线 %s" % (
  836. check_name(DIAG_COUNT_CHECKED, DIAG_COUNT_PASS),
  837. check_name(DIAG_FREQUENCY_CHECKED, DIAG_FREQUENCY_PASS),
  838. check_name(DIAG_CURVE_CHECKED, DIAG_CURVE_PASS),
  839. )
  840. if diagnostic.flags & DIAG_MONITORING:
  841. checks = "监测中 / " + checks
  842. if diagnostic.flags & DIAG_FAULT_LATCHED:
  843. checks = "故障锁存 / " + checks
  844. direction = "正向" if diagnostic.direction_positive else "反向"
  845. mode = "AB" if diagnostic.output_mode == OUTPUT_AB else "PULSE/DIR"
  846. first_mismatch = (
  847. "无" if diagnostic.first_mismatch_sample == 0xFFFFFFFF
  848. else str(diagnostic.first_mismatch_sample)
  849. )
  850. values = self.diagnostic_vars
  851. values["summary"].set(checks)
  852. values["reason"].set(
  853. "%s (%d)" % (
  854. DIAGNOSTIC_REASON_NAMES.get(diagnostic.reason, "未知"),
  855. diagnostic.reason,
  856. )
  857. )
  858. values["segment"].set(str(diagnostic.segment))
  859. values["mode_direction"].set("%s / %s" % (mode, direction))
  860. values["expected_count"].set(str(diagnostic.expected_pulses))
  861. values["actual_count"].set(str(diagnostic.actual_pulses))
  862. values["count_error"].set(str(diagnostic.count_error))
  863. values["requested_hz"].set("%d Hz" % diagnostic.requested_hz)
  864. values["expected_hz"].set("%d Hz" % diagnostic.expected_timer_hz)
  865. values["active_hz"].set("%d Hz" % diagnostic.active_timer_hz)
  866. values["request_error"].set("%d Hz" % diagnostic.request_error_hz)
  867. values["sample_count"].set(str(diagnostic.sample_count))
  868. values["mismatch_count"].set(str(diagnostic.mismatch_count))
  869. values["maximum_error"].set(
  870. "%d Hz" % diagnostic.maximum_frequency_error_hz
  871. )
  872. values["first_mismatch"].set(first_mismatch)
  873. def _handle_result(self, event, payload):
  874. if event == "connection":
  875. if payload["connected"]:
  876. description = "%s / 从站 %d" % (payload["port"], payload["slave"])
  877. self._set_connected(True, description)
  878. self.footer_var.set("已连接,正在读取参数")
  879. else:
  880. reason = payload.get("reason", "")
  881. self.operation_pending = False
  882. self.connect_button.configure(state="normal")
  883. self._set_connected(False, reason)
  884. self.footer_var.set("连接已断开" if not reason else "通信错误")
  885. elif event == "configuration":
  886. self._set_common_words(payload["common"])
  887. self.common_vars["output_mode"].set(
  888. option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"])
  889. )
  890. self._set_segment_words(payload["segments"])
  891. self._finish_operation("全部参数已读取")
  892. elif event == "common":
  893. self._set_common_words(payload["words"])
  894. self.common_vars["output_mode"].set(
  895. option_label(OUTPUT_MODE_OPTIONS, payload["output_mode"])
  896. )
  897. elif event == "segments":
  898. self._set_segment_words(payload["words"])
  899. elif event == "status":
  900. self._show_status(payload["status"])
  901. elif event == "diagnostic":
  902. self._show_diagnostic(payload["diagnostic"])
  903. elif event == "operation":
  904. self._finish_operation(payload["message"])
  905. elif event == "error":
  906. message = "%s失败:%s" % (payload["operation"], payload["message"])
  907. self._finish_operation(message)
  908. self.footer_label.configure(style="Error.TLabel")
  909. if payload.get("modal", False):
  910. messagebox.showerror("PLSR 通信错误", message)
  911. def _drain_results(self):
  912. try:
  913. while True:
  914. event, payload = self.worker.results.get_nowait()
  915. try:
  916. self._handle_result(event, payload)
  917. except (KeyError, ValueError) as error:
  918. self._finish_operation("界面数据错误:%s" % error)
  919. except queue.Empty:
  920. pass
  921. self.root.after(50, self._drain_results)
  922. def _on_close(self):
  923. self.worker.close()
  924. self.root.destroy()
  925. class _SelfTestVariable:
  926. def __init__(self, value):
  927. self.value = str(value)
  928. def get(self):
  929. return self.value
  930. def set(self, value):
  931. self.value = str(value)
  932. class _SelfTestClient:
  933. def __init__(self):
  934. self.calls = []
  935. def write_single(self, address, value):
  936. self.calls.append(("single", address, value))
  937. def write_multiple(self, address, values):
  938. self.calls.append(("multiple", address, list(values)))
  939. def self_test():
  940. assert BAUD_RATE == 9600
  941. assert INPUT_OPTIONS == (("X4 (0)", 0), ("X5 (1)", 1))
  942. assert [SEGMENT_1_BASE + index * SEGMENT_STRIDE
  943. for index in range(SEGMENT_COUNT)] == [
  944. 0x1100, 0x1110, 0x1120, 0x1130, 0x1140,
  945. 0x1150, 0x1160, 0x1170, 0x1180, 0x1190,
  946. ]
  947. assert parse_integer("0x10", "测试值", 0, 16) == 16
  948. try:
  949. parse_integer("17", "测试值", 0, 16)
  950. except ValueError:
  951. pass
  952. else:
  953. raise AssertionError("range validation did not reject 17")
  954. panel = PlsrControlPanel.__new__(PlsrControlPanel)
  955. panel.common_vars = {
  956. "pulse_output": _SelfTestVariable(PULSE_OPTIONS[3][0]),
  957. "direction_output": _SelfTestVariable(DIRECTION_OPTIONS[3][0]),
  958. "wait_input": _SelfTestVariable(INPUT_OPTIONS[1][0]),
  959. "ext_input": _SelfTestVariable(INPUT_OPTIONS[0][0]),
  960. "send_mode": _SelfTestVariable(SEND_OPTIONS[1][0]),
  961. "direction_delay": _SelfTestVariable(25),
  962. "negative_logic": _SelfTestVariable(LOGIC_OPTIONS[1][0]),
  963. "curve_mode": _SelfTestVariable(CURVE_OPTIONS[2][0]),
  964. "position_mode": _SelfTestVariable(POSITION_OPTIONS[1][0]),
  965. "segment_count": _SelfTestVariable(10),
  966. "start_segment": _SelfTestVariable(2),
  967. "default_speed": _SelfTestVariable(100000),
  968. "start_speed": _SelfTestVariable(0),
  969. "stop_speed": _SelfTestVariable(65536),
  970. "acceleration": _SelfTestVariable(1000),
  971. "deceleration": _SelfTestVariable(2000),
  972. "output_mode": _SelfTestVariable(OUTPUT_MODE_OPTIONS[1][0]),
  973. }
  974. common = panel._common_words()
  975. assert common[:11] == [3, 3, 1, 0, 1, 25, 1, 2, 1, 10, 2]
  976. assert common[11:20] == [34464, 1, 0, 0, 0, 0, 1, 1000, 2000]
  977. assert option_value(
  978. OUTPUT_MODE_OPTIONS,
  979. panel.common_vars["output_mode"].get(),
  980. "输出模式",
  981. ) == OUTPUT_AB
  982. assert OUTPUT_MODE == 0x1200
  983. assert DIAGNOSTIC_CONTROL == 0x3100
  984. client = _SelfTestClient()
  985. write_common_configuration(client, common, OUTPUT_PULSE_DIR)
  986. assert client.calls[0] == ("single", OUTPUT_MODE, OUTPUT_PULSE_DIR)
  987. assert client.calls[1][0:2] == ("multiple", CONFIG_BASE)
  988. client = _SelfTestClient()
  989. write_common_configuration(client, common, OUTPUT_AB)
  990. assert client.calls[0][0:2] == ("multiple", CONFIG_BASE)
  991. assert client.calls[1] == ("single", OUTPUT_MODE, OUTPUT_AB)
  992. panel.segment_vars = []
  993. for index in range(SEGMENT_COUNT):
  994. panel.segment_vars.append({
  995. "frequency": _SelfTestVariable(1000 + index),
  996. "pulses": _SelfTestVariable(-1 if index == 0 else index),
  997. "wait_type": _SelfTestVariable(WAIT_OPTIONS[4][0]),
  998. "wait_ms": _SelfTestVariable(index),
  999. "act_ms": _SelfTestVariable(index + 1),
  1000. "jump": _SelfTestVariable(0),
  1001. })
  1002. segments = panel._segment_words()
  1003. assert len(segments) == SEGMENT_COUNT
  1004. assert all(len(words) == SEGMENT_WORDS for words in segments)
  1005. assert segments[0] == [1000, 0, 0xFFFF, 0xFFFF, 4, 0, 1, 0]
  1006. panel.common_vars["segment_count"].set(1)
  1007. panel.segment_vars[0]["jump"].set(2)
  1008. try:
  1009. panel._segment_words()
  1010. except ValueError:
  1011. pass
  1012. else:
  1013. raise AssertionError("active segment jump exceeded segment count")
  1014. print("PLSR control panel self-test passed; no GUI or serial port was opened")
  1015. def parse_arguments(arguments=None):
  1016. parser = argparse.ArgumentParser(description=__doc__)
  1017. parser.add_argument(
  1018. "--self-test",
  1019. action="store_true",
  1020. help="validate register encoding without opening the GUI or serial port",
  1021. )
  1022. return parser.parse_args(arguments)
  1023. def main(arguments=None):
  1024. args = parse_arguments(arguments)
  1025. if args.self_test:
  1026. self_test()
  1027. return 0
  1028. root = tk.Tk()
  1029. PlsrControlPanel(root)
  1030. root.mainloop()
  1031. return 0
  1032. if __name__ == "__main__":
  1033. raise SystemExit(main())