diff --git a/HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc b/HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc index 89f8c73..670fa65 100644 Binary files a/HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc and b/HostComputer/__pycache__/plsr_control_panel.cpython-314.pyc differ diff --git a/HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc b/HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc index e4a3419..c9337d1 100644 Binary files a/HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc and b/HostComputer/__pycache__/plsr_modbus_product_test.cpython-314.pyc differ diff --git a/HostComputer/plsr_control_panel.py b/HostComputer/plsr_control_panel.py index 4f5e3a8..751f3f3 100644 --- a/HostComputer/plsr_control_panel.py +++ b/HostComputer/plsr_control_panel.py @@ -6,6 +6,7 @@ import argparse import queue import threading import time +from datetime import datetime import tkinter as tk import tkinter.font as tkfont from tkinter import messagebox, ttk @@ -48,7 +49,9 @@ from plsr_modbus_product_test import ( BAUD_RATE = 9600 SERIAL_TIMEOUT_SECONDS = 0.8 -STATUS_POLL_SECONDS = 0.25 +DEFAULT_STATUS_POLL_MS = 250 +MIN_STATUS_POLL_MS = 1 +MAX_STATUS_POLL_MS = 3_600_000 SEGMENT_COUNT = 10 SEGMENT_STRIDE = 0x10 MAX_FREQUENCY_HZ = 100_000 @@ -171,10 +174,25 @@ class SerialWorker(threading.Thread): self.stop_event = threading.Event() self.client = None self.next_status_poll = 0.0 + self.polling_enabled = True + self.poll_interval_seconds = DEFAULT_STATUS_POLL_MS / 1000.0 + self.log_callback = None def submit(self, command, **payload): self.commands.put((command, payload)) + def configure_polling(self, enabled, interval_ms): + self.polling_enabled = bool(enabled) + self.poll_interval_seconds = interval_ms / 1000.0 + self.next_status_poll = time.monotonic() + self.poll_interval_seconds + + def set_log_callback(self, callback): + self.log_callback = callback + + def _log(self, message): + if self.log_callback is not None: + self.log_callback("%s %s" % (datetime.now().strftime("%H:%M:%S.%f")[:-3], message)) + def close(self): self.stop_event.set() self.commands.put(("shutdown", {})) @@ -228,6 +246,7 @@ class SerialWorker(threading.Thread): SERIAL_TIMEOUT_SECONDS, ) client.__enter__() + client.log_callback = self._log self.client = client self.next_status_poll = 0.0 self._emit( @@ -298,14 +317,14 @@ class SerialWorker(threading.Thread): diagnostic = read_diagnostic(self._require_client()) self._emit("status", status=status) self._emit("diagnostic", diagnostic=diagnostic) - self.next_status_poll = time.monotonic() + STATUS_POLL_SECONDS + self.next_status_poll = time.monotonic() + self.poll_interval_seconds except (OSError, serial.SerialException) as error: self._handle_connection_error("状态轮询", error) except (TestFailure, ModbusException, RuntimeError) as error: self._emit( "error", operation="状态轮询", message=str(error), modal=False ) - self.next_status_poll = time.monotonic() + 1.0 + self.next_status_poll = time.monotonic() + self.poll_interval_seconds def run(self): try: @@ -322,7 +341,8 @@ class SerialWorker(threading.Thread): self._handle_command(command, payload) if ( - self.client is not None + self.polling_enabled + and self.client is not None and time.monotonic() >= self.next_status_poll ): self._poll_status() @@ -341,10 +361,15 @@ class PlsrControlPanel: self.connected = False self.operation_pending = False self.worker = SerialWorker() + self.worker.set_log_callback(self._queue_log) self.worker.start() + self.log_window = None + self.log_text = None self.port_var = tk.StringVar() self.slave_var = tk.StringVar(value="1") + self.polling_enabled_var = tk.BooleanVar(value=True) + self.poll_interval_var = tk.StringVar(value=str(DEFAULT_STATUS_POLL_MS)) self.connection_var = tk.StringVar(value="未连接") self.footer_var = tk.StringVar(value="请选择串口并连接") self.status_vars = { @@ -410,6 +435,21 @@ class PlsrControlPanel: connection, text="连接", command=self._toggle_connection ) self.connect_button.grid(row=0, column=8, padx=8, pady=8) + + polling = ttk.LabelFrame(connection, text="状态轮询") + polling.grid(row=1, column=0, columnspan=9, padx=8, pady=(0, 6), sticky="ew") + ttk.Checkbutton( + polling, text="启动轮询", variable=self.polling_enabled_var, + command=self._apply_polling_settings, + ).grid(row=0, column=0, padx=(8, 4), pady=6) + ttk.Label(polling, text="轮询间隔").grid(row=0, column=1, padx=(12, 4), pady=6) + ttk.Entry(polling, textvariable=self.poll_interval_var, width=9).grid( + row=0, column=2, padx=4, pady=6 + ) + ttk.Label(polling, text="ms(最小 1 ms)").grid( + row=0, column=3, padx=(4, 8), pady=6 + ) + self.connection_label = ttk.Label( connection, textvariable=self.connection_var, @@ -420,6 +460,10 @@ class PlsrControlPanel: self.connection_label.grid(row=0, column=7, padx=8, pady=8, sticky="e") self.connection_widgets = [self.port_box, refresh_button, slave_entry] + ttk.Button(connection, text="通信日志", command=self._show_log_window).grid( + row=0, column=9, padx=8, pady=8 + ) + self._build_runtime_status() notebook = ttk.Notebook(self.root) @@ -648,6 +692,44 @@ class PlsrControlPanel: elif not ports: self.port_var.set("") + def _queue_log(self, message): + self.worker.results.put(("log", {"message": message})) + + def _show_log_window(self): + if self.log_window is not None and self.log_window.winfo_exists(): + self.log_window.deiconify() + self.log_window.lift() + return + self.log_window = tk.Toplevel(self.root) + self.log_window.title("Modbus 通信日志") + self.log_window.geometry("760x420") + self.log_text = tk.Text(self.log_window, wrap="none", state="disabled") + self.log_text.pack(fill="both", expand=True, padx=8, pady=8) + ttk.Button(self.log_window, text="清空日志", command=self._clear_log).pack(pady=(0, 8)) + + def _clear_log(self): + if self.log_text is not None: + self.log_text.configure(state="normal") + self.log_text.delete("1.0", "end") + self.log_text.configure(state="disabled") + + def _polling_settings(self): + interval_ms = parse_integer( + self.poll_interval_var.get(), "轮询间隔", MIN_STATUS_POLL_MS, MAX_STATUS_POLL_MS + ) + return self.polling_enabled_var.get(), interval_ms + + def _apply_polling_settings(self): + try: + enabled, interval_ms = self._polling_settings() + except ValueError as error: + messagebox.showerror("轮询设置", str(error)) + return + self.worker.configure_polling(enabled, interval_ms) + self.footer_var.set( + "轮询已启动,间隔 %d ms" % interval_ms if enabled else "轮询已停止" + ) + def _set_connected(self, connected, description=""): self.connected = connected self.connect_button.configure( @@ -715,6 +797,15 @@ class PlsrControlPanel: self._update_action_state() self.connection_var.set("正在连接 %s" % port) self.footer_var.set("正在打开串口并读取参数") + try: + enabled, interval_ms = self._polling_settings() + except ValueError as error: + self.connect_button.configure(state="normal") + self.operation_pending = False + self._update_action_state() + messagebox.showerror("轮询设置", str(error)) + return + self.worker.configure_polling(enabled, interval_ms) self.worker.submit("connect", port=port, slave=slave) def _common_words(self): @@ -985,6 +1076,12 @@ class PlsrControlPanel: self._show_status(payload["status"]) elif event == "diagnostic": self._show_diagnostic(payload["diagnostic"]) + elif event == "log": + if self.log_text is not None and self.log_text.winfo_exists(): + self.log_text.configure(state="normal") + self.log_text.insert("end", payload["message"] + "\n") + self.log_text.see("end") + self.log_text.configure(state="disabled") elif event == "operation": self._finish_operation(payload["message"]) elif event == "error": diff --git a/HostComputer/plsr_modbus_product_test.py b/HostComputer/plsr_modbus_product_test.py index 2712939..ff3c33d 100644 --- a/HostComputer/plsr_modbus_product_test.py +++ b/HostComputer/plsr_modbus_product_test.py @@ -504,6 +504,7 @@ class RtuClient: self.frame_gap_seconds = 3.5 * self.character_seconds self.serial_port = None self.last_request_finished = 0.0 + self.log_callback = None def __enter__(self): self.serial_port = serial.Serial( @@ -534,6 +535,8 @@ class RtuClient: self.serial_port.reset_input_buffer() self.serial_port.write(request) self.serial_port.flush() + if hasattr(self, "log_callback") and self.log_callback is not None: + self.log_callback("TX %s" % request.hex(" ")) response = bytearray() deadline = time.monotonic() + self.timeout @@ -564,6 +567,8 @@ class RtuClient: expected_length = 8 self.last_request_finished = time.monotonic() + if self.log_callback is not None: + self.log_callback("RX %s" % bytes(response).hex(" ")) if not response: raise RtuTimeout("no Modbus response to %s" % request.hex(" ")) if expected_length is None: