|
|
|
@@ -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": |
|
|
|
|