1139 lines
49 KiB
Python
1139 lines
49 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
电动扳手自动拧紧界面程序(前端)
|
||
支持从后端API获取工单、认领工单、提交数据
|
||
"""
|
||
|
||
import tkinter as tk
|
||
from tkinter import ttk, messagebox
|
||
import json
|
||
import threading
|
||
import time
|
||
import requests
|
||
from datetime import datetime
|
||
import sys
|
||
import os
|
||
|
||
# 添加父目录到路径,以便导入wrench_controller
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
from wrench_controller import WrenchController
|
||
from device_manager import DeviceManagerWindow
|
||
from device_manager import DeviceManagerWindow
|
||
|
||
|
||
class WrenchGUI:
|
||
"""电动扳手图形界面(前端)"""
|
||
|
||
@staticmethod
|
||
def translate_status(status):
|
||
"""将英文状态转换为中文"""
|
||
status_map = {
|
||
'pending': '待处理',
|
||
'claimed': '已认领',
|
||
'completed': '已完成'
|
||
}
|
||
return status_map.get(status, status)
|
||
|
||
def __init__(self, root):
|
||
self.root = root
|
||
self.root.title("电动扳手自动拧紧系统")
|
||
# 默认全屏显示
|
||
self.root.state('zoomed') # Windows全屏
|
||
# 如果zoomed不支持,使用geometry设置全屏
|
||
try:
|
||
self.root.state('zoomed')
|
||
except:
|
||
# 获取屏幕尺寸并设置窗口大小
|
||
screen_width = self.root.winfo_screenwidth()
|
||
screen_height = self.root.winfo_screenheight()
|
||
self.root.geometry(f"{screen_width}x{screen_height}")
|
||
|
||
# API配置(从配置文件读取,如果没有则使用默认值)
|
||
self.api_base_url = self.load_api_config()
|
||
self.poll_interval = 3 # 轮询间隔(秒)
|
||
|
||
# 数据
|
||
self.work_order = None
|
||
self.current_bolt_index = 0
|
||
self.wrench = None
|
||
self.selected_device = None # 选中的扳手设备
|
||
self.work_device_sn = None # 工作开始时保存的设备SN
|
||
self.work_device_name = None # 工作开始时保存的设备名称
|
||
self.is_running = False
|
||
self.is_polling = False
|
||
self.thread = None
|
||
self.poll_thread = None
|
||
self.test_mode = False
|
||
|
||
# 工单列表缓存,用于判断是否需要更新
|
||
self.cached_orders = []
|
||
self.selected_trace_id = None
|
||
self.selected_process_id = None
|
||
|
||
# 设备列表
|
||
self.wrench_devices = []
|
||
|
||
# 不再需要追溯号和工序号输入
|
||
|
||
# 创建界面
|
||
self._create_widgets()
|
||
|
||
# 检查测试模式
|
||
self.check_test_mode()
|
||
|
||
# 初始化螺栓列表显示(确保始终可见)
|
||
self.update_bolt_list()
|
||
|
||
# 加载设备列表
|
||
self.load_wrench_devices()
|
||
|
||
# 开始轮询
|
||
self.start_polling()
|
||
|
||
def _create_widgets(self):
|
||
"""创建界面组件"""
|
||
# 使用左右布局:左侧工单列表,右侧其他内容
|
||
self.root.update_idletasks()
|
||
screen_height = self.root.winfo_screenheight()
|
||
|
||
# 配置列权重:左侧40%,右侧60%,支持1920和1600分辨率
|
||
# 使用相对权重,自适应不同分辨率
|
||
self.root.grid_columnconfigure(0, weight=2, minsize=350) # 左侧工单列表和日志,最小350像素
|
||
self.root.grid_columnconfigure(1, weight=3, minsize=450) # 右侧内容区域,最小450像素
|
||
# 配置行权重:从row=2开始的内容区域可扩展
|
||
self.root.grid_rowconfigure(2, weight=1)
|
||
|
||
# 标题(横跨两列)
|
||
title_frame = tk.Frame(self.root, bg="#2c3e50", height=60)
|
||
title_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
|
||
title_frame.grid_propagate(False)
|
||
|
||
title_label = tk.Label(
|
||
title_frame,
|
||
text="电动扳手自动拧紧系统",
|
||
font=("微软雅黑", 20, "bold"),
|
||
fg="white",
|
||
bg="#2c3e50"
|
||
)
|
||
title_label.pack(pady=15)
|
||
|
||
# 测试模式指示器
|
||
self.test_mode_label = tk.Label(
|
||
title_frame,
|
||
text="",
|
||
font=("微软雅黑", 10, "bold"),
|
||
fg="#f39c12",
|
||
bg="#2c3e50"
|
||
)
|
||
self.test_mode_label.place(relx=1.0, rely=0.5, anchor=tk.E, x=-20)
|
||
|
||
# 工单列表状态区域(横跨两列)
|
||
status_frame = tk.Frame(self.root, padx=10, pady=5)
|
||
status_frame.grid(row=1, column=0, columnspan=2, sticky="ew")
|
||
|
||
self.poll_status_label = tk.Label(
|
||
status_frame,
|
||
text="🔄 正在加载工单列表...",
|
||
font=("微软雅黑", 10),
|
||
fg="#3498db"
|
||
)
|
||
self.poll_status_label.pack(side=tk.LEFT)
|
||
|
||
self.auto_refresh_var = tk.BooleanVar(value=True)
|
||
self.auto_refresh_check = tk.Checkbutton(
|
||
status_frame,
|
||
text="自动刷新",
|
||
variable=self.auto_refresh_var,
|
||
font=("微软雅黑", 9),
|
||
command=self.toggle_auto_refresh
|
||
)
|
||
self.auto_refresh_check.pack(side=tk.LEFT, padx=10)
|
||
|
||
self.refresh_button = tk.Button(
|
||
status_frame,
|
||
text="手动刷新",
|
||
font=("微软雅黑", 9),
|
||
bg="#3498db",
|
||
fg="white",
|
||
command=self.query_work_orders
|
||
)
|
||
self.refresh_button.pack(side=tk.RIGHT, padx=5)
|
||
|
||
# ========== 左侧:工单列表和操作控制 ==========
|
||
# 创建左侧主容器(上下布局:工单列表 + 操作控制)
|
||
left_container = tk.Frame(self.root)
|
||
left_container.grid(row=2, column=0, sticky="nsew", padx=(10, 5), pady=5)
|
||
# 配置行权重:工单列表可扩展,操作控制固定不压缩
|
||
left_container.grid_rowconfigure(0, weight=3) # 工单列表权重较大
|
||
left_container.grid_rowconfigure(1, weight=0, minsize=120) # 操作控制固定,最小120像素,不压缩
|
||
left_container.grid_columnconfigure(0, weight=1)
|
||
|
||
# 工单列表区域
|
||
order_list_frame = tk.LabelFrame(left_container, text="可用工单列表", font=("微软雅黑", 12), padx=10, pady=10)
|
||
order_list_frame.grid(row=0, column=0, sticky="nsew", padx=0, pady=(0, 5))
|
||
order_list_frame.grid_rowconfigure(0, weight=1)
|
||
order_list_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
# 创建工单列表表格(使用相对高度,不固定)
|
||
order_columns = ("追溯号", "工序号", "工序名称", "产品", "螺栓数", "状态")
|
||
self.order_tree = ttk.Treeview(order_list_frame, columns=order_columns, show="headings")
|
||
|
||
for col in order_columns:
|
||
self.order_tree.heading(col, text=col)
|
||
self.order_tree.column(col, width=120, anchor=tk.CENTER)
|
||
|
||
order_scrollbar = ttk.Scrollbar(order_list_frame, orient=tk.VERTICAL, command=self.order_tree.yview)
|
||
self.order_tree.configure(yscrollcommand=order_scrollbar.set)
|
||
|
||
self.order_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
order_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
# 操作控制区域(移到左侧,放在工单列表下方)
|
||
button_frame = tk.LabelFrame(left_container, text="操作控制", font=("微软雅黑", 12), padx=10, pady=8)
|
||
button_frame.grid(row=1, column=0, sticky="ew", padx=0, pady=0)
|
||
|
||
# 使用grid布局,将所有按钮放在同一行确保可见
|
||
# 第一列:工单操作
|
||
col1_frame = tk.Frame(button_frame)
|
||
col1_frame.grid(row=0, column=0, padx=(0, 20), sticky=tk.W)
|
||
|
||
tk.Label(col1_frame, text="工单操作", font=("微软雅黑", 9, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 3))
|
||
self.claim_button = tk.Button(
|
||
col1_frame,
|
||
text="认领工单",
|
||
font=("微软雅黑", 10),
|
||
bg="#f39c12",
|
||
fg="white",
|
||
width=10,
|
||
height=1,
|
||
cursor="hand2",
|
||
command=self.claim_work_order
|
||
)
|
||
self.claim_button.pack()
|
||
|
||
# 第二列:设备选择
|
||
col2_frame = tk.Frame(button_frame)
|
||
col2_frame.grid(row=0, column=1, padx=(0, 20), sticky=tk.W)
|
||
|
||
tk.Label(col2_frame, text="设备选择", font=("微软雅黑", 9, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 3))
|
||
device_select_inner = tk.Frame(col2_frame)
|
||
device_select_inner.pack()
|
||
|
||
tk.Label(device_select_inner, text="选择扳手:", font=("微软雅黑", 9)).pack(side=tk.LEFT, padx=(0, 5))
|
||
|
||
# 设备状态指示器(圆点)
|
||
self.device_status_indicator = tk.Label(
|
||
device_select_inner,
|
||
text="●",
|
||
font=("微软雅黑", 12),
|
||
fg="#7f8c8d" # 默认灰色
|
||
)
|
||
self.device_status_indicator.pack(side=tk.LEFT, padx=(0, 3))
|
||
|
||
self.device_combo = ttk.Combobox(device_select_inner, width=18, state="readonly", font=("微软雅黑", 9))
|
||
self.device_combo.pack(side=tk.LEFT, padx=(0, 5))
|
||
self.device_combo.bind("<<ComboboxSelected>>", self.on_device_selected)
|
||
|
||
self.device_manage_button = tk.Button(
|
||
device_select_inner,
|
||
text="设备管理",
|
||
font=("微软雅黑", 8),
|
||
bg="#3498db",
|
||
fg="white",
|
||
width=8,
|
||
height=1,
|
||
command=self.open_device_manager
|
||
)
|
||
self.device_manage_button.pack(side=tk.LEFT)
|
||
|
||
# 第三列:执行控制(放在同一行,确保可见)
|
||
col3_frame = tk.Frame(button_frame)
|
||
col3_frame.grid(row=0, column=2, padx=(0, 0), sticky=tk.W)
|
||
|
||
tk.Label(col3_frame, text="执行控制", font=("微软雅黑", 9, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 3))
|
||
exec_control_inner = tk.Frame(col3_frame)
|
||
exec_control_inner.pack()
|
||
|
||
self.start_button = tk.Button(
|
||
exec_control_inner,
|
||
text="开始拧紧",
|
||
font=("微软雅黑", 10),
|
||
bg="#27ae60",
|
||
fg="white",
|
||
width=10,
|
||
height=1,
|
||
cursor="hand2",
|
||
command=self.start_process,
|
||
state=tk.DISABLED
|
||
)
|
||
self.start_button.pack(side=tk.LEFT, padx=(0, 10))
|
||
|
||
self.stop_button = tk.Button(
|
||
exec_control_inner,
|
||
text="停止",
|
||
font=("微软雅黑", 10),
|
||
bg="#e74c3c",
|
||
fg="white",
|
||
width=10,
|
||
height=1,
|
||
cursor="hand2",
|
||
state=tk.DISABLED,
|
||
command=self.stop_process
|
||
)
|
||
self.stop_button.pack(side=tk.LEFT)
|
||
|
||
# 配置grid列权重
|
||
button_frame.grid_columnconfigure(0, weight=0)
|
||
button_frame.grid_columnconfigure(1, weight=1)
|
||
button_frame.grid_columnconfigure(2, weight=0)
|
||
|
||
# ========== 右侧:内容区域 ==========
|
||
# 创建右侧主容器(上下布局)
|
||
right_container = tk.Frame(self.root)
|
||
right_container.grid(row=2, column=1, sticky="nsew", padx=(5, 10), pady=5)
|
||
# 配置行权重:日志区域可压缩,螺栓列表可压缩
|
||
right_container.grid_rowconfigure(0, weight=0) # 工单信息不扩展
|
||
right_container.grid_rowconfigure(1, weight=0) # 当前螺栓不扩展
|
||
right_container.grid_rowconfigure(2, weight=3) # 螺栓列表可扩展可压缩(主要区域)
|
||
right_container.grid_rowconfigure(3, weight=1, minsize=100) # 日志区域可压缩,最小100像素
|
||
right_container.grid_columnconfigure(0, weight=1)
|
||
|
||
# 工单信息区域(上下布局,第一行)
|
||
info_frame = tk.LabelFrame(right_container, text="当前工单信息", font=("微软雅黑", 12), padx=10, pady=10)
|
||
info_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
|
||
|
||
self.trace_id_label = tk.Label(info_frame, text="追溯号: --", font=("微软雅黑", 10))
|
||
self.trace_id_label.grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
|
||
|
||
self.process_id_label = tk.Label(info_frame, text="工序号: --", font=("微软雅黑", 10))
|
||
self.process_id_label.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
|
||
|
||
self.process_name_label = tk.Label(info_frame, text="工序名称: --", font=("微软雅黑", 10))
|
||
self.process_name_label.grid(row=0, column=2, sticky=tk.W, padx=5, pady=2)
|
||
|
||
self.product_label = tk.Label(info_frame, text="产品: --", font=("微软雅黑", 10))
|
||
self.product_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
|
||
|
||
self.operator_label = tk.Label(info_frame, text="操作员: --", font=("微软雅黑", 10))
|
||
self.operator_label.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
|
||
|
||
self.device_label = tk.Label(info_frame, text="扳手设备: --", font=("微软雅黑", 10), fg="#2c3e50")
|
||
self.device_label.grid(row=1, column=2, sticky=tk.W, padx=5, pady=2)
|
||
|
||
# 当前螺栓信息(上下布局,第二行)
|
||
current_frame = tk.LabelFrame(right_container, text="当前螺栓", font=("微软雅黑", 12), padx=10, pady=10)
|
||
current_frame.grid(row=1, column=0, sticky="ew", padx=5, pady=5)
|
||
|
||
self.current_bolt_label = tk.Label(
|
||
current_frame,
|
||
text="等待开始...",
|
||
font=("微软雅黑", 16, "bold"),
|
||
fg="#2c3e50"
|
||
)
|
||
self.current_bolt_label.pack(pady=5)
|
||
|
||
self.current_torque_label = tk.Label(
|
||
current_frame,
|
||
text="目标扭矩: -- Nm",
|
||
font=("微软雅黑", 12)
|
||
)
|
||
self.current_torque_label.pack(pady=2)
|
||
|
||
self.current_status_label = tk.Label(
|
||
current_frame,
|
||
text="状态: 待机",
|
||
font=("微软雅黑", 12),
|
||
fg="#7f8c8d"
|
||
)
|
||
self.current_status_label.pack(pady=2)
|
||
|
||
# 进度条(放在current_frame内部)
|
||
progress_frame = tk.Frame(current_frame, padx=10, pady=5)
|
||
progress_frame.pack(fill=tk.X, pady=5)
|
||
|
||
self.progress_label = tk.Label(progress_frame, text="总进度: 0/0", font=("微软雅黑", 10))
|
||
self.progress_label.pack(anchor=tk.W)
|
||
|
||
self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate', length=400)
|
||
self.progress_bar.pack(fill=tk.X, pady=5)
|
||
|
||
# 螺栓列表(固定在"当前螺栓"下方、"操作控制"上方,可压缩但始终可见)
|
||
# 创建螺栓列表框架
|
||
list_frame = tk.LabelFrame(right_container, text="螺栓列表", font=("微软雅黑", 12), padx=10, pady=10)
|
||
list_frame.grid(row=2, column=0, sticky="nsew", padx=5, pady=5)
|
||
# 螺栓列表通过right_container的row=2的weight=1来控制,可以压缩
|
||
# 设置最小高度,支持1920和1600分辨率
|
||
self.root.update_idletasks()
|
||
screen_height = self.root.winfo_screenheight()
|
||
# 根据分辨率动态调整最小高度:1920约180像素,1600约150像素
|
||
if screen_height >= 1920:
|
||
min_list_height = max(180, int(screen_height * 0.15))
|
||
elif screen_height >= 1600:
|
||
min_list_height = max(150, int(screen_height * 0.15))
|
||
else:
|
||
min_list_height = max(120, int(screen_height * 0.15))
|
||
right_container.grid_rowconfigure(2, weight=1, minsize=min_list_height)
|
||
|
||
# 保存list_frame引用,确保始终可见
|
||
self.list_frame = list_frame
|
||
|
||
# 创建表格(使用相对布局,不固定高度)
|
||
columns = ("序号", "名称", "扭矩(Nm)", "状态", "实际扭矩", "时间")
|
||
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings")
|
||
|
||
# 设置列
|
||
for col in columns:
|
||
self.tree.heading(col, text=col)
|
||
|
||
self.tree.column("序号", width=60, anchor=tk.CENTER)
|
||
self.tree.column("名称", width=150, anchor=tk.W)
|
||
self.tree.column("扭矩(Nm)", width=100, anchor=tk.CENTER)
|
||
self.tree.column("状态", width=100, anchor=tk.CENTER)
|
||
self.tree.column("实际扭矩", width=120, anchor=tk.CENTER)
|
||
self.tree.column("时间", width=150, anchor=tk.CENTER)
|
||
|
||
# 滚动条
|
||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
|
||
self.tree.configure(yscrollcommand=scrollbar.set)
|
||
|
||
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
# 配置标签颜色
|
||
self.tree.tag_configure("pending", foreground="#7f8c8d")
|
||
self.tree.tag_configure("running", foreground="#3498db", font=("微软雅黑", 10, "bold"))
|
||
self.tree.tag_configure("success", foreground="#27ae60", font=("微软雅黑", 10, "bold"))
|
||
self.tree.tag_configure("failed", foreground="#e74c3c")
|
||
|
||
# 操作日志区域(移到右侧,放在螺栓列表下方)
|
||
log_frame = tk.LabelFrame(right_container, text="操作日志", font=("微软雅黑", 9), padx=5, pady=3)
|
||
log_frame.grid(row=3, column=0, sticky="nsew", padx=5, pady=(0, 5))
|
||
log_frame.grid_rowconfigure(0, weight=1)
|
||
log_frame.grid_columnconfigure(0, weight=1)
|
||
|
||
self.log_text = tk.Text(log_frame, font=("Consolas", 9))
|
||
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||
|
||
log_scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
|
||
self.log_text.configure(yscrollcommand=log_scrollbar.set)
|
||
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||
|
||
def get_config_path(self):
|
||
"""获取配置文件路径(支持开发环境和打包后的环境)"""
|
||
# 如果是打包后的可执行文件,config.json 应该在可执行文件同一目录
|
||
if getattr(sys, 'frozen', False):
|
||
# 打包后的环境
|
||
base_path = os.path.dirname(sys.executable)
|
||
else:
|
||
# 开发环境,在项目根目录
|
||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
return os.path.join(base_path, "config.json")
|
||
|
||
def load_api_config(self):
|
||
"""从配置文件读取API地址,如果没有则使用默认值"""
|
||
default_url = "http://localhost:5000/api"
|
||
try:
|
||
config_path = self.get_config_path()
|
||
if os.path.exists(config_path):
|
||
with open(config_path, "r", encoding="utf-8") as f:
|
||
config = json.load(f)
|
||
api_config = config.get("api", {})
|
||
api_url = api_config.get("base_url", default_url)
|
||
# 确保URL以/api结尾
|
||
if not api_url.endswith("/api"):
|
||
api_url = api_url.rstrip("/") + "/api"
|
||
return api_url
|
||
except Exception as e:
|
||
print(f"Warning: Failed to load API config: {e}, using default: {default_url}")
|
||
return default_url
|
||
|
||
def check_test_mode(self):
|
||
"""检查测试模式"""
|
||
try:
|
||
config_path = self.get_config_path()
|
||
if os.path.exists(config_path):
|
||
with open(config_path, "r", encoding="utf-8") as f:
|
||
config = json.load(f)
|
||
self.test_mode = config.get("test_mode", {}).get("enabled", False)
|
||
|
||
if self.test_mode:
|
||
self.test_mode_label.config(text="⚠️ 测试模式")
|
||
self.log("⚠️ 测试模式已启用:失败也算成功", "WARN")
|
||
else:
|
||
self.test_mode_label.config(text="")
|
||
except Exception as e:
|
||
self.log(f"读取配置失败: {e}", "ERROR")
|
||
|
||
def log(self, message, level="INFO"):
|
||
"""添加日志"""
|
||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||
log_message = f"[{timestamp}] [{level}] {message}\n"
|
||
self.log_text.insert(tk.END, log_message)
|
||
self.log_text.see(tk.END)
|
||
print(log_message.strip())
|
||
|
||
def query_work_orders(self):
|
||
"""查询所有可用工单列表"""
|
||
try:
|
||
url = f"{self.api_base_url}/work-orders"
|
||
# 不传参数,获取所有可用工单
|
||
response = requests.get(url, timeout=5)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("success"):
|
||
orders = data.get("data", [])
|
||
self.update_order_list(orders)
|
||
self.log(f"查询成功,找到 {len(orders)} 个可用工单")
|
||
if len(orders) > 0:
|
||
self.poll_status_label.config(text=f"🔄 已找到 {len(orders)} 个可用工单", fg="#27ae60")
|
||
else:
|
||
self.poll_status_label.config(text="⏸ 暂无可用工单", fg="#7f8c8d")
|
||
else:
|
||
self.log(f"查询失败: {data.get('message')}", "ERROR")
|
||
self.poll_status_label.config(text="❌ 查询失败", fg="#e74c3c")
|
||
else:
|
||
self.log(f"查询失败: HTTP {response.status_code}", "ERROR")
|
||
self.poll_status_label.config(text="❌ 查询失败", fg="#e74c3c")
|
||
except requests.exceptions.RequestException as e:
|
||
self.log(f"查询失败: {e}", "ERROR")
|
||
self.poll_status_label.config(text="❌ 连接失败", fg="#e74c3c")
|
||
if not self.is_polling: # 只在非轮询模式下显示错误对话框
|
||
messagebox.showerror("错误", f"无法连接到后端服务器\n请确保后端服务已启动\n\n错误: {e}")
|
||
|
||
def _orders_equal(self, orders1, orders2):
|
||
"""比较两个工单列表是否相同"""
|
||
if len(orders1) != len(orders2):
|
||
return False
|
||
|
||
# 创建键集合用于比较
|
||
keys1 = set((o.get('trace_id'), o.get('process_id')) for o in orders1)
|
||
keys2 = set((o.get('trace_id'), o.get('process_id')) for o in orders2)
|
||
|
||
return keys1 == keys2
|
||
|
||
def update_order_list(self, orders):
|
||
"""更新工单列表(只在数据变化时更新,并保持选中状态)"""
|
||
# 检查数据是否真的变化了
|
||
if self._orders_equal(orders, self.cached_orders):
|
||
return # 数据没有变化,不更新
|
||
|
||
# 保存当前选中状态
|
||
selected_item = self.order_tree.selection()
|
||
if selected_item:
|
||
item_values = self.order_tree.item(selected_item[0])['values']
|
||
if len(item_values) >= 2:
|
||
self.selected_trace_id = item_values[0]
|
||
self.selected_process_id = item_values[1]
|
||
|
||
# 更新缓存
|
||
self.cached_orders = orders.copy()
|
||
|
||
# 清空并重新填充列表
|
||
self.order_tree.delete(*self.order_tree.get_children())
|
||
for order in orders:
|
||
status = order.get('status', 'pending')
|
||
status_cn = self.translate_status(status)
|
||
self.order_tree.insert("", tk.END, values=(
|
||
order.get('trace_id', '--'),
|
||
order.get('process_id', '--'),
|
||
order.get('process_name', '--'),
|
||
order.get('product_name', '--'),
|
||
order.get('bolt_count', 0),
|
||
status_cn
|
||
))
|
||
|
||
# 恢复选中状态
|
||
if self.selected_trace_id and self.selected_process_id:
|
||
for item in self.order_tree.get_children():
|
||
values = self.order_tree.item(item)['values']
|
||
if len(values) >= 2 and values[0] == self.selected_trace_id and values[1] == self.selected_process_id:
|
||
self.order_tree.selection_set(item)
|
||
self.order_tree.see(item) # 滚动到选中项
|
||
break
|
||
|
||
def toggle_auto_refresh(self):
|
||
"""切换自动刷新状态"""
|
||
if self.auto_refresh_var.get():
|
||
if not self.is_polling:
|
||
self.start_polling()
|
||
else:
|
||
if self.is_polling:
|
||
self.stop_polling()
|
||
|
||
def start_polling(self):
|
||
"""开始轮询工单列表"""
|
||
if self.is_polling:
|
||
return
|
||
|
||
self.is_polling = True
|
||
self.poll_thread = threading.Thread(target=self.poll_work_orders, daemon=True)
|
||
self.poll_thread.start()
|
||
self.poll_status_label.config(text="🔄 正在轮询工单列表...", fg="#3498db")
|
||
self.log("开始轮询工单列表")
|
||
|
||
def stop_polling(self):
|
||
"""停止轮询"""
|
||
self.is_polling = False
|
||
self.poll_status_label.config(text="⏸ 轮询已停止", fg="#7f8c8d")
|
||
self.log("停止轮询工单列表")
|
||
|
||
def poll_work_orders(self):
|
||
"""轮询工单列表(获取所有可用工单)"""
|
||
while self.is_polling:
|
||
try:
|
||
url = f"{self.api_base_url}/work-orders"
|
||
# 不传参数,获取所有可用工单
|
||
response = requests.get(url, timeout=5)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("success"):
|
||
orders = data.get("data", [])
|
||
# 只在数据变化时更新
|
||
self.root.after(0, lambda o=orders: self.update_order_list(o))
|
||
# 更新状态标签
|
||
if len(orders) > 0:
|
||
self.root.after(0, lambda: self.poll_status_label.config(
|
||
text=f"🔄 已找到 {len(orders)} 个可用工单", fg="#27ae60"
|
||
))
|
||
else:
|
||
self.root.after(0, lambda: self.poll_status_label.config(
|
||
text="⏸ 暂无可用工单", fg="#7f8c8d"
|
||
))
|
||
except Exception as e:
|
||
# 静默失败,继续轮询
|
||
pass
|
||
|
||
time.sleep(self.poll_interval)
|
||
|
||
def claim_work_order(self):
|
||
"""认领工单"""
|
||
selected = self.order_tree.selection()
|
||
if not selected:
|
||
messagebox.showwarning("警告", "请先选择要认领的工单")
|
||
return
|
||
|
||
item = self.order_tree.item(selected[0])
|
||
values = item['values']
|
||
trace_id = values[0]
|
||
process_id = values[1]
|
||
|
||
try:
|
||
url = f"{self.api_base_url}/work-orders/claim"
|
||
data = {
|
||
"trace_id": trace_id,
|
||
"process_id": process_id,
|
||
"operator": "操作员" # 可以从配置或输入获取
|
||
}
|
||
response = requests.post(url, json=data, timeout=5)
|
||
|
||
if response.status_code == 200:
|
||
result = response.json()
|
||
if result.get("success"):
|
||
self.work_order = result.get("data")
|
||
self.update_work_order_info()
|
||
self.update_bolt_list()
|
||
self.claim_button.config(state=tk.DISABLED)
|
||
self.start_button.config(state=tk.NORMAL)
|
||
self.log(f"✅ 成功认领工单: {trace_id} - {process_id}", "SUCCESS")
|
||
else:
|
||
messagebox.showerror("错误", result.get("message", "认领失败"))
|
||
self.log(f"认领失败: {result.get('message')}", "ERROR")
|
||
else:
|
||
messagebox.showerror("错误", f"认领失败: HTTP {response.status_code}")
|
||
self.log(f"认领失败: HTTP {response.status_code}", "ERROR")
|
||
except requests.exceptions.RequestException as e:
|
||
messagebox.showerror("错误", f"无法连接到后端服务器\n\n错误: {e}")
|
||
self.log(f"认领失败: {e}", "ERROR")
|
||
|
||
def update_work_order_info(self):
|
||
"""更新工单信息显示"""
|
||
if self.work_order:
|
||
self.trace_id_label.config(text=f"追溯号: {self.work_order.get('trace_id', '--')}")
|
||
self.process_id_label.config(text=f"工序号: {self.work_order.get('process_id', '--')}")
|
||
self.process_name_label.config(text=f"工序名称: {self.work_order.get('process_name', '--')}")
|
||
self.product_label.config(text=f"产品: {self.work_order.get('product_name', '--')}")
|
||
self.operator_label.config(text=f"操作员: {self.work_order.get('operator', '--')}")
|
||
else:
|
||
self.trace_id_label.config(text="追溯号: --")
|
||
self.process_id_label.config(text="工序号: --")
|
||
self.process_name_label.config(text="工序名称: --")
|
||
self.product_label.config(text="产品: --")
|
||
self.operator_label.config(text="操作员: --")
|
||
|
||
# 更新设备显示(无论是否有工单都要更新)
|
||
self.update_device_status_display()
|
||
|
||
def update_device_status_display(self):
|
||
"""更新设备状态显示(带颜色)"""
|
||
if self.selected_device:
|
||
device_name = self.selected_device.get('device_name', '--')
|
||
status = self.selected_device.get('status', 'offline')
|
||
status_text = "在线" if status == 'online' else "离线"
|
||
# 根据状态设置颜色
|
||
if status == 'online':
|
||
status_color = "#27ae60" # 绿色
|
||
else:
|
||
status_color = "#e74c3c" # 红色
|
||
|
||
# 强制更新颜色和文本
|
||
self.device_label.config(text="") # 先清空
|
||
self.device_label.config(
|
||
text=f"扳手设备: {device_name} ({status_text})",
|
||
foreground=status_color # 使用foreground而不是fg
|
||
)
|
||
print(f"[DEBUG] 更新设备状态显示: {device_name}, 状态: {status}, 颜色: {status_color}")
|
||
else:
|
||
self.device_label.config(text="扳手设备: 未选择", foreground="#2c3e50")
|
||
|
||
def update_bolt_list(self):
|
||
"""更新螺栓列表(始终显示,即使没有工单)"""
|
||
# 确保螺栓列表框架始终显示,无论连接状态如何
|
||
if hasattr(self, 'list_frame'):
|
||
self.list_frame.grid() # 强制显示,防止被隐藏
|
||
|
||
# 始终清空表格,确保表格框架始终可见
|
||
self.tree.delete(*self.tree.get_children())
|
||
|
||
if not self.work_order:
|
||
# 没有工单时,显示空表格,但表格框架保持可见
|
||
return
|
||
|
||
# 有工单时,显示螺栓数据
|
||
bolts = self.work_order.get('bolts', [])
|
||
for bolt in bolts:
|
||
self.tree.insert("", tk.END, values=(
|
||
bolt.get('bolt_id'),
|
||
bolt.get('name'),
|
||
bolt.get('target_torque'),
|
||
"待拧紧",
|
||
"--",
|
||
"--"
|
||
), tags=("pending",))
|
||
|
||
total = len(bolts)
|
||
self.progress_label.config(text=f"总进度: 0/{total}")
|
||
self.progress_bar['maximum'] = total
|
||
self.progress_bar['value'] = 0
|
||
self.current_bolt_index = 0
|
||
|
||
def start_process(self):
|
||
"""开始拧紧流程"""
|
||
if not self.work_order:
|
||
messagebox.showwarning("警告", "请先认领工单")
|
||
return
|
||
|
||
if not self.selected_device:
|
||
messagebox.showwarning("警告", "请先选择扳手设备")
|
||
return
|
||
|
||
if self.selected_device.get('status') != 'online':
|
||
result = messagebox.askyesno("警告", f"选中的扳手设备 {self.selected_device.get('device_name')} 当前离线,是否继续?")
|
||
if not result:
|
||
return
|
||
|
||
if self.is_running:
|
||
return
|
||
|
||
self.is_running = True
|
||
self.start_button.config(state=tk.DISABLED)
|
||
self.stop_button.config(state=tk.NORMAL)
|
||
self.claim_button.config(state=tk.DISABLED)
|
||
self.device_combo.config(state=tk.DISABLED)
|
||
|
||
# 启动工作线程
|
||
self.thread = threading.Thread(target=self.work_thread, daemon=True)
|
||
self.thread.start()
|
||
|
||
self.log("开始自动拧紧流程")
|
||
|
||
def stop_process(self):
|
||
"""停止拧紧流程"""
|
||
self.is_running = False
|
||
self.start_button.config(state=tk.NORMAL)
|
||
self.stop_button.config(state=tk.DISABLED)
|
||
self.claim_button.config(state=tk.NORMAL)
|
||
self.device_combo.config(state="readonly")
|
||
self.log("用户停止流程")
|
||
|
||
def work_thread(self):
|
||
"""工作线程"""
|
||
try:
|
||
# 保存工作开始时使用的设备信息(防止后续被状态检测线程覆盖)
|
||
if self.selected_device:
|
||
self.work_device_sn = self.selected_device.get('device_sn')
|
||
self.work_device_name = self.selected_device.get('device_name')
|
||
|
||
# 连接扳手(使用选中的设备配置)
|
||
device_config = {
|
||
"ip_address": self.selected_device.get('ip_address'),
|
||
"port": self.selected_device.get('port', 7888),
|
||
"address_code": self.selected_device.get('address_code', 1)
|
||
}
|
||
self.log(f"正在连接扳手: {device_config['ip_address']}:{device_config['port']}...")
|
||
self.wrench = WrenchController(device_config=device_config)
|
||
|
||
if not self.wrench.connect():
|
||
self.log("❌ 连接扳手失败,请检查IP和端口配置", "ERROR")
|
||
self.root.after(0, lambda: messagebox.showerror(
|
||
"连接失败",
|
||
f"无法连接到扳手\nIP: {self.wrench.host}\n端口: {self.wrench.port}\n\n请检查:\n1. 扳手是否开机\n2. 网络连接是否正常\n3. IP和端口配置是否正确"
|
||
))
|
||
self.stop_process()
|
||
return
|
||
|
||
self.log(f"✅ 已连接到扳手: {self.wrench.host}:{self.wrench.port}", "SUCCESS")
|
||
|
||
# 启用远程控制
|
||
self.log("正在启用远程控制...")
|
||
if not self.wrench.enable_remote_control(True):
|
||
self.log("❌ 启用远程控制失败", "ERROR")
|
||
self.root.after(0, lambda: messagebox.showerror("错误", "启用远程控制失败"))
|
||
self.stop_process()
|
||
return
|
||
|
||
self.log("✅ 已启用远程控制", "SUCCESS")
|
||
|
||
# 遍历所有螺栓
|
||
bolts = self.work_order.get('bolts', [])
|
||
bolt_results = []
|
||
|
||
for index, bolt in enumerate(bolts):
|
||
if not self.is_running:
|
||
break
|
||
|
||
self.current_bolt_index = index
|
||
result = self.process_bolt(bolt, index)
|
||
if result:
|
||
bolt_results.append(result)
|
||
|
||
# 完成,提交数据
|
||
if self.is_running:
|
||
self.log("所有螺栓拧紧完成!", "SUCCESS")
|
||
self.submit_results(bolt_results)
|
||
self.root.after(0, lambda: messagebox.showinfo("完成", "所有螺栓已成功拧紧!"))
|
||
|
||
except Exception as e:
|
||
self.log(f"流程异常: {e}", "ERROR")
|
||
import traceback
|
||
traceback.print_exc()
|
||
finally:
|
||
if self.wrench:
|
||
self.wrench.disconnect()
|
||
# 清空工作设备信息
|
||
self.work_device_sn = None
|
||
self.work_device_name = None
|
||
self.root.after(0, lambda: self.device_combo.config(state="readonly"))
|
||
self.root.after(0, self.stop_process)
|
||
|
||
def process_bolt(self, bolt, index):
|
||
"""处理单个螺栓"""
|
||
bolt_id = bolt.get('bolt_id')
|
||
bolt_name = bolt.get('name')
|
||
target_torque = bolt.get('target_torque')
|
||
|
||
self.log(f"开始拧紧: [{bolt_id}] {bolt_name}, 目标扭矩: {target_torque} Nm")
|
||
|
||
# 更新界面
|
||
self.root.after(0, lambda: self.update_current_bolt(bolt, "拧紧中..."))
|
||
self.root.after(0, lambda: self.update_tree_item(index, "拧紧中", "running"))
|
||
|
||
# 设定参数
|
||
self.log(f"设定参数: 扭矩={target_torque}Nm, 模式=M{bolt.get('mode', 1)}")
|
||
self.wrench.set_torque_parameters(
|
||
target_torque=target_torque,
|
||
mode=bolt.get('mode', 1),
|
||
torque_tolerance=bolt.get('torque_tolerance', 0.10),
|
||
angle_min=bolt.get('angle_min', 1),
|
||
angle_max=bolt.get('angle_max', 360)
|
||
)
|
||
|
||
# 循环尝试,直到成功
|
||
attempt = 0
|
||
result_data = None
|
||
|
||
while self.is_running:
|
||
attempt += 1
|
||
self.log(f"第 {attempt} 次尝试拧紧螺栓 {bolt_id}")
|
||
|
||
# 启动扳手
|
||
self.log("发送启动命令...")
|
||
if not self.wrench.start_wrench(direction=1):
|
||
self.log("❌ 发送启动命令失败", "ERROR")
|
||
time.sleep(1)
|
||
continue
|
||
|
||
# 等待结果
|
||
self.log("等待扳手响应...")
|
||
result = self.wrench.wait_for_result()
|
||
|
||
if result and result.get("success"):
|
||
# 成功
|
||
actual_torque = result.get("actual_torque", 0)
|
||
actual_angle = result.get("actual_angle", 0)
|
||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
self.log(f"✅ 螺栓 {bolt_id} 拧紧成功! 实际扭矩: {actual_torque} Nm", "SUCCESS")
|
||
|
||
# 更新界面
|
||
self.root.after(0, lambda at=actual_torque, ts=timestamp: self.update_tree_item(
|
||
index, "成功", "success", at, ts
|
||
))
|
||
self.root.after(0, lambda: self.update_progress(index + 1))
|
||
|
||
# 保存结果数据
|
||
result_data = {
|
||
"bolt_id": bolt_id,
|
||
"name": bolt_name,
|
||
"target_torque": target_torque,
|
||
"actual_torque": actual_torque,
|
||
"actual_angle": actual_angle,
|
||
"status": "success",
|
||
"timestamp": timestamp,
|
||
"result": result
|
||
}
|
||
|
||
break
|
||
else:
|
||
# 失败,继续尝试
|
||
if result:
|
||
self.log(f"❌ 螺栓 {bolt_id} 拧紧失败: {result.get('status', '未知错误')}, 继续重试...", "WARN")
|
||
else:
|
||
self.log(f"❌ 螺栓 {bolt_id} 无响应, 继续重试...", "WARN")
|
||
|
||
time.sleep(1) # 等待1秒后重试
|
||
|
||
# 完成后等待一下
|
||
time.sleep(0.5)
|
||
return result_data
|
||
|
||
def submit_results(self, bolt_results):
|
||
"""提交结果到后端"""
|
||
if not self.work_order or not bolt_results:
|
||
return
|
||
|
||
try:
|
||
# 优先使用工作开始时保存的设备信息,如果没有则使用当前选中的设备信息
|
||
device_sn = self.work_device_sn
|
||
device_name = self.work_device_name
|
||
|
||
if not device_sn or not device_name:
|
||
# 如果工作开始时没有保存,尝试从当前选中的设备获取
|
||
if self.selected_device:
|
||
device_sn = device_sn or self.selected_device.get('device_sn')
|
||
device_name = device_name or self.selected_device.get('device_name')
|
||
|
||
# 如果还是没有,尝试从设备列表中查找
|
||
if not device_sn or not device_name:
|
||
# 通过IP地址匹配设备(因为工作线程中使用了IP地址)
|
||
if self.wrench and hasattr(self.wrench, 'host'):
|
||
for device in self.wrench_devices:
|
||
if device.get('ip_address') == self.wrench.host:
|
||
device_sn = device_sn or device.get('device_sn')
|
||
device_name = device_name or device.get('device_name')
|
||
break
|
||
|
||
# 记录设备信息(用于调试)
|
||
self.log(f"提交数据 - 设备SN: {device_sn or '(未设置)'}, 设备名称: {device_name or '(未设置)'}")
|
||
|
||
url = f"{self.api_base_url}/work-orders/submit"
|
||
data = {
|
||
"trace_id": self.work_order.get('trace_id'),
|
||
"process_id": self.work_order.get('process_id'),
|
||
"bolts": bolt_results,
|
||
"device_sn": device_sn,
|
||
"device_name": device_name
|
||
}
|
||
|
||
# 调试:打印提交的数据
|
||
print(f"[DEBUG] 提交工单数据:")
|
||
print(f" trace_id: {data['trace_id']}")
|
||
print(f" process_id: {data['process_id']}")
|
||
print(f" device_sn: {device_sn}")
|
||
print(f" device_name: {device_name}")
|
||
print(f" bolts数量: {len(bolt_results)}")
|
||
|
||
response = requests.post(url, json=data, timeout=10)
|
||
|
||
if response.status_code == 200:
|
||
result = response.json()
|
||
if result.get("success"):
|
||
self.log(f"✅ 数据提交成功", "SUCCESS")
|
||
# 清空当前工单
|
||
self.work_order = None
|
||
self.root.after(0, self.update_work_order_info)
|
||
self.root.after(0, lambda: self.tree.delete(*self.tree.get_children()))
|
||
self.claim_button.config(state=tk.NORMAL)
|
||
self.start_button.config(state=tk.DISABLED)
|
||
else:
|
||
self.log(f"❌ 数据提交失败: {result.get('message')}", "ERROR")
|
||
else:
|
||
self.log(f"❌ 数据提交失败: HTTP {response.status_code}", "ERROR")
|
||
except requests.exceptions.RequestException as e:
|
||
self.log(f"❌ 数据提交失败: {e}", "ERROR")
|
||
|
||
def update_current_bolt(self, bolt, status):
|
||
"""更新当前螺栓显示"""
|
||
self.current_bolt_label.config(text=f"[{bolt.get('bolt_id')}] {bolt.get('name')}")
|
||
self.current_torque_label.config(text=f"目标扭矩: {bolt.get('target_torque')} Nm")
|
||
self.current_status_label.config(text=f"状态: {status}")
|
||
|
||
def update_tree_item(self, index, status, tag, actual_torque="--", timestamp="--"):
|
||
"""更新表格项"""
|
||
items = self.tree.get_children()
|
||
if index < len(items):
|
||
item = items[index]
|
||
values = list(self.tree.item(item)['values'])
|
||
values[3] = status
|
||
if actual_torque != "--":
|
||
values[4] = actual_torque
|
||
if timestamp != "--":
|
||
values[5] = timestamp
|
||
self.tree.item(item, values=values, tags=(tag,))
|
||
|
||
def update_progress(self, completed):
|
||
"""更新进度"""
|
||
if not self.work_order:
|
||
return
|
||
total = len(self.work_order.get('bolts', []))
|
||
self.progress_label.config(text=f"总进度: {completed}/{total}")
|
||
self.progress_bar['value'] = completed
|
||
|
||
def load_wrench_devices(self):
|
||
"""加载扳手设备列表"""
|
||
try:
|
||
url = f"{self.api_base_url}/wrench-devices"
|
||
response = requests.get(url, timeout=5)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("success"):
|
||
self.wrench_devices = data.get("data", [])
|
||
self.update_device_combo()
|
||
# 启动设备状态检测
|
||
self.start_device_status_check()
|
||
else:
|
||
self.log(f"加载设备列表失败: {data.get('message')}", "ERROR")
|
||
except Exception as e:
|
||
self.log(f"加载设备列表失败: {e}", "ERROR")
|
||
|
||
def update_device_combo(self):
|
||
"""更新设备下拉框"""
|
||
device_names = []
|
||
for device in self.wrench_devices:
|
||
# 不在下拉框中显示emoji,而是使用状态指示器
|
||
device_names.append(f"{device.get('device_name')} ({device.get('ip_address')})")
|
||
|
||
self.device_combo['values'] = device_names
|
||
if device_names and not self.device_combo.get():
|
||
self.device_combo.current(0)
|
||
self.on_device_selected(None)
|
||
|
||
# 更新状态指示器颜色
|
||
self.update_device_status_indicator()
|
||
|
||
def on_device_selected(self, event):
|
||
"""设备选择事件"""
|
||
selection = self.device_combo.current()
|
||
if selection >= 0 and selection < len(self.wrench_devices):
|
||
self.selected_device = self.wrench_devices[selection]
|
||
self.log(f"已选择扳手设备: {self.selected_device.get('device_name')}")
|
||
self.update_work_order_info()
|
||
# 确保设备状态颜色更新
|
||
self.update_device_status_display()
|
||
# 更新状态指示器
|
||
self.update_device_status_indicator()
|
||
|
||
def update_device_status_indicator(self):
|
||
"""更新设备状态指示器(圆点)颜色"""
|
||
if self.selected_device:
|
||
status = self.selected_device.get('status', 'offline')
|
||
if status == 'online':
|
||
self.device_status_indicator.config(fg="#27ae60") # 绿色
|
||
else:
|
||
self.device_status_indicator.config(fg="#e74c3c") # 红色
|
||
else:
|
||
self.device_status_indicator.config(fg="#7f8c8d") # 灰色
|
||
|
||
def start_device_status_check(self):
|
||
"""启动设备状态检测(后台线程)"""
|
||
def check_status():
|
||
while True:
|
||
try:
|
||
# 首先获取设备列表
|
||
url = f"{self.api_base_url}/wrench-devices"
|
||
response = requests.get(url, timeout=5)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
if data.get("success"):
|
||
devices = data.get("data", [])
|
||
|
||
# 对每个设备进行在线状态检测
|
||
for device in devices:
|
||
device_id = device.get('id')
|
||
if device_id:
|
||
try:
|
||
# 调用后端API检测设备状态
|
||
check_url = f"{self.api_base_url}/wrench-devices/{device_id}/check-status"
|
||
check_response = requests.post(check_url, timeout=3)
|
||
if check_response.status_code == 200:
|
||
check_data = check_response.json()
|
||
if check_data.get("success"):
|
||
# 更新设备状态
|
||
device['status'] = check_data.get('data', {}).get('status', 'offline')
|
||
except:
|
||
# 检测失败,保持原状态
|
||
pass
|
||
|
||
# 更新设备状态
|
||
for device in devices:
|
||
for i, old_device in enumerate(self.wrench_devices):
|
||
if old_device.get('id') == device.get('id'):
|
||
self.wrench_devices[i] = device
|
||
break
|
||
|
||
# 更新下拉框
|
||
self.root.after(0, self.update_device_combo)
|
||
|
||
# 如果当前选中的设备状态变化,更新显示
|
||
if self.selected_device:
|
||
for device in devices:
|
||
if device.get('id') == self.selected_device.get('id'):
|
||
self.selected_device = device
|
||
# 更新设备状态显示(带颜色)
|
||
self.root.after(0, self.update_device_status_display)
|
||
# 更新状态指示器
|
||
self.root.after(0, self.update_device_status_indicator)
|
||
break
|
||
except:
|
||
pass
|
||
|
||
time.sleep(10) # 每10秒检测一次(避免过于频繁)
|
||
|
||
status_thread = threading.Thread(target=check_status, daemon=True)
|
||
status_thread.start()
|
||
|
||
def open_device_manager(self):
|
||
"""打开设备管理窗口"""
|
||
DeviceManagerWindow(self.root, self.api_base_url, self.load_wrench_devices)
|
||
|
||
|
||
def main():
|
||
root = tk.Tk()
|
||
app = WrenchGUI(root)
|
||
|
||
def on_closing():
|
||
app.stop_polling()
|
||
root.destroy()
|
||
|
||
root.protocol("WM_DELETE_WINDOW", on_closing)
|
||
root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|
||
|