You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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