import tkinter as tk
from tkinter import ttk, messagebox, scrolledtext, filedialog
import serial
import serial.tools.list_ports
import threading
import time
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import openpyxl
from openpyxl.chart import LineChart, Reference
from openpyxl.chart.label import DataLabelList
from openpyxl.styles import Font, Alignment
from datetime import datetime
import queue
import json
import winsound # Windows提示音(非Windows可注释)
# --- 优化字体配置 ---
plt.rcParams["font.family"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans", "Arial Unicode MS"]
plt.rcParams['axes.unicode_minus'] = False
class USBWeightMonitor:
def __init__(self, root):
self.root = root
self.root.title("USB称重变送器 + 机械臂控制系统")
self.root.geometry("1600x900")
self.root.resizable(True, True)
# ========== 称重核心变量 ==========
self.ser = None
self.is_connected = False
self.raw_data = [] # 存储所有原始采样点 [(time_str, weight_g)]
self.peak_valley_log = [] # 存储所有识别出的峰值/谷值 [(time_str, weight_g, 'peak'/'valley')]
self.sample_interval = 500 # 默认采样间隔 (ms)
self.unit = "g"
self.recording = False
self.data_queue = queue.Queue()
self.lock = threading.Lock()
self.max_data_points = 1000 # 用于绘图的缓冲区大小
self.max_weight = None
self.min_weight = None
self.max_time = None
self.min_time = None
# 用于峰值检测的变量
self.last_weight = None
self.peak_detection_buffer = [] # 临时缓冲区,用于峰值检测
self.peak_detection_window = 5 # 用于峰值检测的滑动窗口大小
# 峰值检测的辅助变量
self.last_peak_time = None
self.last_valley_time = None
self.last_peak_weight = None
self.last_valley_weight = None
# 用于GUI更新的临时列表
self.new_peak_valley_events = [] # 存储新检测到的峰值/谷值事件 [(time_str, weight_g, 'peak'/'valley')]
# 峰值记录开关
self.record_peaks_var = tk.BooleanVar(value=True) # 默认开启
# ========== 机械臂相关变量 ==========
self.arm_ser = None
self.arm_is_connected = False
self.move_step = tk.DoubleVar(value=1.0)
self.gcode_content = ""
self.running_gcode = False
self.loop_count = tk.IntVar(value=1)
self.current_loop = 0
# ========== 创建UI ==========
self.create_widgets()
# 初始化
self.refresh_com_ports()
self.refresh_arm_ports()
self.update_plot_periodic()
self.process_data_queue()
self.update_peak_log_table_periodically() # 启动GUI更新循环
def create_widgets(self):
# 主容器,使用 Grid
main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
main_container.grid_columnconfigure(0, weight=1)
main_container.grid_columnconfigure(1, weight=1)
main_container.grid_rowconfigure(1, weight=1) # 曲线图区域占用最大空间
main_container.grid_rowconfigure(2, weight=0) # 日志区域占用较小空间
# 1. 顶部:称重 & 机械臂 串口配置(一行两列)
top_frame = ttk.Frame(main_container)
top_frame.grid(row=0, column=0, columnspan=2, sticky=tk.EW, padx=5, pady=5)
top_frame.grid_columnconfigure(0, weight=1)
top_frame.grid_columnconfigure(1, weight=1)
# 称重设备配置
self.top_weight_frame = ttk.LabelFrame(top_frame, text="称重设备串口配置")
self.top_weight_frame.grid(row=0, column=0, sticky=tk.EW, padx=(0, 5), pady=5)
self.top_weight_frame.grid_columnconfigure(4, weight=1)
ttk.Label(self.top_weight_frame, text="COM口:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.port_combo = ttk.Combobox(self.top_weight_frame, state="readonly")
self.port_combo.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
ttk.Button(self.top_weight_frame, text="刷新端口", command=self.refresh_com_ports).grid(row=0, column=2, padx=5, pady=5)
ttk.Label(self.top_weight_frame, text="波特率:").grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
self.baud_combo = ttk.Combobox(self.top_weight_frame, values=["38400", "115200"], state="readonly")
self.baud_combo.current(0)
self.baud_combo.grid(row=0, column=4, padx=5, pady=5, sticky=tk.W)
self.connect_btn = ttk.Button(self.top_weight_frame, text="连接设备", command=self.toggle_connection)
self.connect_btn.grid(row=0, column=5, padx=10, pady=5)
self.unit_btn = ttk.Button(self.top_weight_frame, text="切换到N", command=self.toggle_unit)
self.unit_btn.grid(row=0, column=6, padx=10, pady=5)
# 机械臂配置
self.top_arm_frame = ttk.LabelFrame(top_frame, text="机械臂串口配置")
self.top_arm_frame.grid(row=0, column=1, sticky=tk.EW, padx=(5, 0), pady=5)
self.top_arm_frame.grid_columnconfigure(4, weight=1)
ttk.Label(self.top_arm_frame, text="COM口:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
self.arm_port_combo = ttk.Combobox(self.top_arm_frame, state="readonly", width=10)
self.arm_port_combo.grid(row=0, column=1, padx=5, pady=5)
ttk.Button(self.top_arm_frame, text="刷新", command=self.refresh_arm_ports).grid(row=0, column=2, padx=5, pady=5)
ttk.Label(self.top_arm_frame, text="波特率:").grid(row=0, column=3, padx=5, pady=5, sticky=tk.W)
self.arm_baud_combo = ttk.Combobox(self.top_arm_frame, values=["9600", "19200", "38400", "115200"], state="readonly", width=10)
self.arm_baud_combo.set("115200")
self.arm_baud_combo.grid(row=0, column=4, padx=5, pady=5)
self.arm_connect_btn = ttk.Button(self.top_arm_frame, text="连接机械臂", command=self.toggle_arm_connection)
self.arm_connect_btn.grid(row=0, column=5, padx=10, pady=5)
# 2. 中部:重量显示 + 曲线图 (左侧) & 机械臂控制 + 峰值日志 (右侧)
mid_container = ttk.Frame(main_container)
mid_container.grid(row=1, column=0, columnspan=2, sticky=tk.NSEW, padx=5, pady=5)
mid_container.grid_columnconfigure(0, weight=2)
mid_container.grid_columnconfigure(1, weight=1)
mid_container.grid_rowconfigure(0, weight=1)
# 左侧:重量显示 + 曲线图
left_panel = ttk.Frame(mid_container)
left_panel.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 5))
# 重量显示
self.weight_display_frame = ttk.LabelFrame(left_panel, text="重量监控")
self.weight_display_frame.pack(fill=tk.X, padx=5, pady=5)
self.weight_label = ttk.Label(self.weight_display_frame, text="0.00 g", font=("Arial", 48, "bold"))
self.weight_label.pack(pady=10)
self.stats_frame = ttk.Frame(self.weight_display_frame)
self.stats_frame.pack(pady=5)
self.max_label = ttk.Label(self.stats_frame, text="最大值: -- g", font=("Arial", 12), foreground="red")
self.max_label.grid(row=0, column=0, padx=20)
self.min_label = ttk.Label(self.stats_frame, text="最小值: -- g", font=("Arial", 12), foreground="blue")
self.min_label.grid(row=0, column=1, padx=20)
# 曲线图
self.plot_frame = ttk.LabelFrame(left_panel, text="实时重力变化曲线")
self.plot_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.fig, self.ax = plt.subplots(figsize=(10, 4))
self.ax.set_xlabel("时间")
self.ax.set_ylabel("重量 (g)")
self.ax.set_title("实时重力变化曲线", fontsize=14, fontweight='bold')
self.line, = self.ax.plot([], [], color="red", linewidth=1, label='Raw Data')
self.peak_line, = self.ax.plot([], [], 'o', color="blue", markersize=4, label='Peaks')
self.valley_line, = self.ax.plot([], [], 'v', color="green", markersize=4, label='Valleys')
self.ax.legend()
self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 右侧:机械臂控制 + 峰值日志
right_panel = ttk.Frame(mid_container)
right_panel.grid(row=0, column=1, sticky=tk.NSEW, padx=(5, 0))
# 机械臂控制
self.move_frame = ttk.LabelFrame(right_panel, text="手动移动 (X轴)")
self.move_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(self.move_frame, text="步长 (mm):").grid(row=0, column=0, padx=5, pady=5)
ttk.Entry(self.move_frame, textvariable=self.move_step, width=8).grid(row=0, column=1, padx=5, pady=5)
self.x_left_btn = ttk.Button(self.move_frame, text="↓ X-1", command=lambda: self.move_arm("X", -1), state=tk.DISABLED)
self.x_left_btn.grid(row=0, column=2, padx=5, pady=5)
self.x_right_btn = ttk.Button(self.move_frame, text="X+1 ↑", command=lambda: self.move_arm("X", 1), state=tk.DISABLED)
self.x_right_btn.grid(row=0, column=3, padx=5, pady=5)
# G-code 控制
self.gcode_frame = ttk.LabelFrame(right_panel, text="G-code 控制")
self.gcode_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(self.gcode_frame, text="G-code:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.NW)
self.gcode_text = tk.Text(self.gcode_frame, height=4, width=40)
self.gcode_text.grid(row=0, column=1, padx=5, pady=5, columnspan=3)
self.gcode_text.insert(tk.END, "G91\nG0 X-1 F500\nG0 X1 F500\n")
ttk.Label(self.gcode_frame, text="循环次数:").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
ttk.Spinbox(self.gcode_frame, from_=1, to=100, textvariable=self.loop_count, width=8).grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)
btn_frame = ttk.Frame(self.gcode_frame)
btn_frame.grid(row=1, column=2, columnspan=2, sticky=tk.E)
self.send_gcode_btn = ttk.Button(btn_frame, text="发送G-code", command=self.send_gcode, state=tk.DISABLED)
self.send_gcode_btn.pack(side=tk.LEFT, padx=5)
self.stop_gcode_btn = ttk.Button(btn_frame, text="停止", command=self.stop_gcode, state=tk.DISABLED)
self.stop_gcode_btn.pack(side=tk.LEFT, padx=5)
# 峰值日志表格
self.peak_log_frame = ttk.LabelFrame(right_panel, text="峰值/谷值日志")
self.peak_log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
self.peak_log_tree = ttk.Treeview(self.peak_log_frame, columns=("Time", "Weight", "Type"), show="headings", height=10)
self.peak_log_tree.heading("Time", text="时间")
self.peak_log_tree.heading("Weight", text="重量 (g)")
self.peak_log_tree.heading("Type", text="类型")
self.peak_log_tree.column("Time", width=120, anchor="center")
self.peak_log_tree.column("Weight", width=80, anchor="center")
self.peak_log_tree.column("Type", width=60, anchor="center")
tree_scrollbar = ttk.Scrollbar(self.peak_log_frame, orient=tk.VERTICAL, command=self.peak_log_tree.yview)
self.peak_log_tree.configure(yscrollcommand=tree_scrollbar.set)
self.peak_log_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 3. 底部:操作控制
self.bottom_frame = ttk.LabelFrame(main_container, text="操作控制")
self.bottom_frame.grid(row=2, column=0, columnspan=2, sticky=tk.EW, padx=5, pady=5)
ttk.Label(self.bottom_frame, text="采样间隔(ms):").grid(row=0, column=0, padx=5, pady=10, sticky=tk.W)
self.interval_spin = ttk.Spinbox(self.bottom_frame, from_=50, to=5000, increment=50, value=500)
self.interval_spin.grid(row=0, column=1, padx=5, pady=10, sticky=tk.W)
ttk.Button(self.bottom_frame, text="应用间隔", command=self.set_sample_interval).grid(row=0, column=2, padx=5, pady=10)
# 新增峰值记录复选框
self.record_peaks_check = ttk.Checkbutton(self.bottom_frame, text="记录峰值/谷值", variable=self.record_peaks_var)
self.record_peaks_check.grid(row=0, column=3, padx=10, pady=10)
ttk.Button(self.bottom_frame, text="零点标定", command=self.calibrate_zero).grid(row=0, column=4, padx=10, pady=10)
ttk.Button(self.bottom_frame, text="砝码标定", command=self.calibrate_weight).grid(row=0, column=5, padx=10, pady=10)
ttk.Button(self.bottom_frame, text="清零最值", command=self.reset_extremes).grid(row=0, column=6, padx=10, pady=10)
ttk.Button(self.bottom_frame, text="导出峰值日志", command=self.export_peak_valley_log_to_excel).grid(row=0, column=7, padx=10, pady=10)
# 4. 日志区(放在底部,不占用主空间)
self.log_frame = ttk.LabelFrame(main_container, text="运行日志")
self.log_frame.grid(row=3, column=0, columnspan=2, sticky=tk.EW, padx=5, pady=5)
self.log_text = scrolledtext.ScrolledText(self.log_frame, height=6, state=tk.DISABLED)
self.log_text.pack(fill=tk.X, padx=5, pady=5)
# ========== 称重功能(核心逻辑修改)==========
def add_log(self, content):
self.log_text.config(state=tk.NORMAL)
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_text.insert(tk.END, f"[{current_time}] {content}\n")
self.log_text.see(tk.END)
self.log_text.config(state=tk.DISABLED)
def refresh_com_ports(self):
ports = [p.device for p in serial.tools.list_ports.comports()]
self.port_combo["values"] = ports
if ports:
self.port_combo.current(0)
else:
self.add_log("未检测到可用称重设备COM口")
def toggle_connection(self):
if not self.is_connected:
port = self.port_combo.get()
baud = int(self.baud_combo.get())
if not port:
messagebox.showerror("错误", "请选择COM口")
return
try:
self.ser = serial.Serial(port, baud, parity=serial.PARITY_NONE, stopbits=serial.STOPBITS_ONE, timeout=1)
self.is_connected = True
self.connect_btn.config(text="断开设备")
self.recording = True
self.reset_extremes()
threading.Thread(target=self.read_weight_data, daemon=True).start()
self.add_log(f"称重设备已连接:{port} @ {baud}")
except Exception as e:
messagebox.showerror("连接失败", str(e))
self.add_log(f"称重连接失败:{e}")
else:
if self.ser and self.ser.is_open:
self.ser.close()
self.is_connected = False
self.recording = False
self.connect_btn.config(text="连接设备")
self.add_log("称重设备已断开")
def read_weight_data(self):
while self.recording and self.ser and self.ser.is_open:
try:
read_cmd = bytes.fromhex("010300000002C40B")
self.ser.write(read_cmd)
time.sleep(0.1) # 发送命令后等待响应
response = self.ser.read(9)
if len(response) == 9 and response[0] == 0x01 and response[1] == 0x03:
raw_data = response[3:7]
high_word = (raw_data[2] << 8) | raw_data[3]
low_word = (raw_data[0] << 8) | raw_data[1]
raw_value = (high_word << 16) | low_word
if raw_value > 0x7FFFFFFF:
raw_value -= 0x100000000
actual_weight_g = raw_value
current_time = datetime.now().strftime("%H:%M:%S.%f")[:-3]
with self.lock:
# 将当前点加入检测缓冲区
self.peak_detection_buffer.append((current_time, actual_weight_g))
# 如果缓冲区满了,进行一次峰值检测
if len(self.peak_detection_buffer) >= self.peak_detection_window:
self.detect_peaks_in_buffer()
# 添加到原始数据队列
self.data_queue.put((current_time, actual_weight_g))
self.last_weight = actual_weight_g
else:
self.add_log(f"称重响应异常:{response.hex()}")
except Exception as e:
self.add_log(f"称重读取错误:{e}")
# 固定采样间隔
time.sleep(self.sample_interval / 1000)
def detect_peaks_in_buffer(self):
"""在缓冲区中检测峰值和谷值"""
# 检查峰值记录开关
if not self.record_peaks_var.get():
# 如果开关关闭,清空缓冲区并返回,不进行任何峰值检测
self.peak_detection_buffer = []
return
if len(self.peak_detection_buffer) < 3:
return
# 获取当前缓冲区的值
times = [t for t, w in self.peak_detection_buffer]
weights = [w for t, w in self.peak_detection_buffer]
# 寻找峰值和谷值
# 峰值:中间点大于两边
for i in range(1, len(weights) - 1):
if weights[i] > weights[i-1] and weights[i] > weights[i+1]:
# 检查是否比当前已知的最近峰值还大
if self.last_peak_weight is None or weights[i] > self.last_peak_weight:
time_str = times[i]
weight_g = weights[i]
# 检查是否与上一个记录的峰值时间不同,避免重复
if self.last_peak_time != time_str:
with self.lock:
self.peak_valley_log.append((time_str, weight_g, 'peak'))
self.new_peak_valley_events.append((time_str, weight_g, 'peak'))
self.last_peak_time = time_str
self.last_peak_weight = weight_g
self.add_log(f"检测到波峰: {weight_g:.2f}g @ {time_str}")
# 找到一个峰值就退出本次检测
break
# 谷值:中间点小于两边
for i in range(1, len(weights) - 1):
if weights[i] < weights[i-1] and weights[i] < weights[i+1]:
# 检查是否比当前已知的最近谷值还小
if self.last_valley_weight is None or weights[i] < self.last_valley_weight:
time_str = times[i]
weight_g = weights[i]
# 检查是否与上一个记录的谷值时间不同,避免重复
if self.last_valley_time != time_str:
with self.lock:
self.peak_valley_log.append((time_str, weight_g, 'valley'))
self.new_peak_valley_events.append((time_str, weight_g, 'valley'))
self.last_valley_time = time_str
self.last_valley_weight = weight_g
self.add_log(f"检测到波谷: {weight_g:.2f}g @ {time_str}")
# 找到一个谷值就退出本次检测
break
# 保留缓冲区最后的几个点,用于下一次检测的连续性
self.peak_detection_buffer = self.peak_detection_buffer[-2:]
def process_data_queue(self):
try:
new_data = []
while True:
try:
data = self.data_queue.get_nowait()
new_data.append(data)
except queue.Empty:
break
if new_data:
with self.lock:
for time_str, weight_g in new_data:
# 添加到原始数据,用于绘图
self.raw_data.append((time_str, weight_g))
if len(self.raw_data) > self.max_data_points:
self.raw_data.pop(0)
# 更新统计信息(使用原始数据)
if self.max_weight is None or weight_g > self.max_weight:
self.max_weight = weight_g
self.max_time = time_str
if self.min_weight is None or weight_g < self.min_weight:
self.min_weight = weight_g
self.min_time = time_str
self.update_weight_display()
except Exception as e:
self.add_log(f"处理数据队列时出错:{e}")
self.root.after(100, self.process_data_queue)
def update_peak_log_table_periodically(self):
"""定期更新峰值日志表格,避免频繁更新导致卡顿"""
# 检查峰值记录开关
if not self.record_peaks_var.get():
# 如果开关关闭,清空待处理列表并返回
with self.lock:
self.new_peak_valley_events.clear()
return
with self.lock:
events_to_process = self.new_peak_valley_events[:]
self.new_peak_valley_events.clear() # 清空临时列表
for time_str, weight_g, pv_type in events_to_process:
# 确保单位转换
display_weight = weight_g if self.unit == "g" else weight_g * 0.0098
unit_str = "g" if self.unit == "g" else "N"
self.peak_log_tree.insert("", "end", values=(time_str, f"{display_weight:.4f}", pv_type))
# 滚动到最后
self.peak_log_tree.see(self.peak_log_tree.get_children()[-1])
# 优化:如果事件队列很长,可以增加更新频率,反之降低
if len(events_to_process) > 10:
self.root.after(50, self.update_peak_log_table_periodically) # 高频更新
else:
self.root.after(200, self.update_peak_log_table_periodically) # 低频更新
def update_weight_display(self):
if self.raw_data:
last_time, last_weight = self.raw_data[-1]
if self.unit == "g":
self.weight_label.config(text=f"{last_weight:.2f} g")
self.max_label.config(text=f"最大值: {self.max_weight:.2f} g")
self.min_label.config(text=f"最小值: {self.min_weight:.2f} g")
else:
display_N = last_weight * 0.0098
self.weight_label.config(text=f"{display_N:.4f} N")
self.max_label.config(text=f"最大值: {self.max_weight*0.0098:.4f} N")
self.min_label.config(text=f"最小值: {self.min_weight*0.0098:.4f} N")
else:
if self.unit == "g":
self.weight_label.config(text="0.00 g")
self.max_label.config(text="最大值: -- g")
self.min_label.config(text="最小值: -- g")
else:
self.weight_label.config(text="0.00 N")
self.max_label.config(text="最大值: -- N")
self.min_label.config(text="最小值: -- N")
def update_plot_periodic(self):
with self.lock:
self.ax.set_ylabel(f"重量 ({self.unit})")
self.ax.set_title(f"实时重力变化曲线 ({self.unit})", fontsize=14, fontweight='bold')
# 绘制原始数据
if self.raw_data:
plot_raw_weight = [w if self.unit == "g" else w * 0.0098 for _, w in self.raw_data]
time_labels = [t for t, _ in self.raw_data]
if len(plot_raw_weight) > 100:
plot_raw_weight = plot_raw_weight[-100:]
time_labels = time_labels[-100:]
x_data = range(len(plot_raw_weight))
# 修复 xlim 警告
if len(plot_raw_weight) == 1:
self.ax.set_xlim(-0.5, 0.5)
else:
self.ax.set_xlim(0, max(len(plot_raw_weight)-1, 0))
self.line.set_data(x_data, plot_raw_weight)
if plot_raw_weight:
y_min, y_max = min(plot_raw_weight), max(plot_raw_weight)
y_margin = (y_max - y_min) * 0.1 if len(plot_raw_weight) > 1 else 0.5
self.ax.set_ylim(y_min - y_margin, y_max + y_margin)
step = max(1, len(time_labels) // 10)
x_ticks = range(0, len(time_labels), step)
x_labels = [time_labels[i] for i in x_ticks]
self.ax.set_xticks(x_ticks)
self.ax.set_xticklabels(x_labels, rotation=45, fontsize=8)
else:
self.ax.set_ylim(0, 1)
self.ax.set_xlim(0, 1)
else:
self.line.set_data([], [])
self.ax.set_xlim(0, 1)
self.ax.set_ylim(0, 1)
# 绘制峰值和谷值
peak_times = []
peak_weights = []
valley_times = []
valley_weights = []
if self.peak_valley_log and self.raw_data:
# 创建一个时间到索引的映射,提高查找效率
time_to_idx_map = {t: i for i, (t, w) in enumerate(self.raw_data)}
for pt, pw, ppv in self.peak_valley_log:
idx = time_to_idx_map.get(pt, -1)
if idx != -1:
if ppv == 'peak':
peak_times.append(idx)
peak_weights.append(pw if self.unit == "g" else pw * 0.0098)
elif ppv == 'valley':
valley_times.append(idx)
valley_weights.append(pw if self.unit == "g" else pw * 0.0098)
self.peak_line.set_data(peak_times, peak_weights)
self.valley_line.set_data(valley_times, valley_weights)
self.fig.tight_layout()
self.canvas.draw()
self.root.after(200, self.update_plot_periodic)
def toggle_unit(self):
with self.lock:
if self.unit == "g":
self.unit = "N"
self.unit_btn.config(text="切换到g")
# 更新表格单位
for item in self.peak_log_tree.get_children():
values = self.peak_log_tree.item(item, "values")
if values[2] in ['peak', 'valley']:
new_weight = float(values[1]) * 0.0098
self.peak_log_tree.item(item, values=(values[0], f"{new_weight:.4f}", values[2]))
else:
self.unit = "g"
self.unit_btn.config(text="切换到N")
# 更新表格单位
for item in self.peak_log_tree.get_children():
values = self.peak_log_tree.item(item, "values")
if values[2] in ['peak', 'valley']:
new_weight = float(values[1]) / 0.0098
self.peak_log_tree.item(item, values=(values[0], f"{new_weight:.2f}", values[2]))
self.update_weight_display()
self.add_log(f"单位已切换为:{self.unit}")
def set_sample_interval(self):
try:
interval = int(self.interval_spin.get())
if 50 <= interval <= 5000:
self.sample_interval = interval
self.add_log(f"采样间隔已设置为:{interval}ms")
else:
messagebox.showerror("错误", "采样间隔需在50-5000ms之间")
except ValueError:
messagebox.showerror("错误", "请输入有效数字")
def calibrate_zero(self):
if not self.is_connected:
messagebox.showerror("错误", "未连接称重设备")
return
try:
zero_cmd = bytes.fromhex("010600120001E80F")
self.ser.write(zero_cmd)
time.sleep(0.5)
response = self.ser.read(8)
if len(response) == 8 and response[0] == 0x01 and response[1] == 0x06:
self.add_log("零点标定成功")
messagebox.showinfo("成功", "零点标定已完成")
else:
messagebox.showerror("失败", "零点标定响应异常")
except Exception as e:
messagebox.showerror("失败", f"零点标定错误:{e}")
def calibrate_weight(self):
if not self.is_connected:
messagebox.showerror("错误", "未连接称重设备")
return
weight_dialog = tk.Toplevel(self.root)
weight_dialog.title("砝码标定")
weight_dialog.geometry("300x150")
weight_dialog.transient(self.root)
ttk.Label(weight_dialog, text="请输入砝码重量(g):").pack(pady=10)
weight_entry = ttk.Entry(weight_dialog)
weight_entry.pack(pady=5)
weight_entry.focus()
def confirm_calib():
try:
weight_g = float(weight_entry.get())
if weight_g <= 0:
raise ValueError("重量需大于0")
cal_value = int(weight_g)
low_word = cal_value & 0xFFFF
high_word = (cal_value >> 16) & 0xFFFF
cal_cmd = bytes([0x01, 0x10, 0x00, 0x08, 0x00, 0x02, 0x04,
low_word >> 8, low_word & 0xFF,
high_word >> 8, high_word & 0xFF])
crc = self.calculate_crc(cal_cmd[:-2])
cal_cmd += crc.to_bytes(2, byteorder="little")
self.ser.write(cal_cmd)
time.sleep(0.5)
response = self.ser.read(8)
if len(response) == 8 and response[0] == 0x01 and response[1] == 0x10:
trigger_cmd = bytes.fromhex("010600120002E80A")
self.ser.write(trigger_cmd)
time.sleep(0.5)
self.add_log(f"砝码标定成功(砝码重量:{weight_g}g)")
messagebox.showinfo("成功", f"砝码标定已完成(砝码:{weight_g}g)")
else:
messagebox.showerror("失败", "砝码值写入异常")
weight_dialog.destroy()
except Exception as e:
messagebox.showerror("错误", str(e))
weight_dialog.destroy()
ttk.Button(weight_dialog, text="确认", command=confirm_calib).pack(pady=10)
def calculate_crc(self, data):
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 0x0001:
crc >>= 1
crc ^= 0xA001
else:
crc >>= 1
return crc
def reset_extremes(self):
# 在主线程中调用,需要获取锁
with self.lock:
self.max_weight = None
self.min_weight = None
self.max_time = None
self.min_time = None
# 保留原始数据的清空逻辑,但确保在加锁时进行
self.raw_data = []
self.peak_valley_log = []
self.new_peak_valley_events = []
# 清空表格
for item in self.peak_log_tree.get_children():
self.peak_log_tree.delete(item)
# 重置峰值检测状态
self.peak_detection_buffer = []
# 重置辅助变量
self.last_peak_time = None
self.last_valley_time = None
self.last_peak_weight = None
self.last_valley_weight = None
# 重置最后权重
self.last_weight = None
# 在锁释放后更新显示
self.update_weight_display()
self.add_log("最值记录、数据缓冲区、峰值检测状态已清零")
def export_peak_valley_log_to_excel(self):
with self.lock:
if not self.peak_valley_log:
messagebox.showerror("错误", "无峰值/谷值日志数据可导出")
return
save_path = filedialog.asksaveasfilename(
defaultextension=".xlsx",
filetypes=[("Excel文件", "*.xlsx")],
title="选择Excel保存路径"
)
if not save_path:
return
# 在新线程中执行耗时的Excel导出操作,避免阻塞GUI
threading.Thread(target=self._export_peak_valley_log_to_excel_threaded, args=(save_path,), daemon=True).start()
def _export_peak_valley_log_to_excel_threaded(self, save_path):
try:
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "峰值谷值日志"
headers = ["序号", "时间", "重量(g)", "重量(N)", "类型 (Peak/Valley)"]
for col, header in enumerate(headers, 1):
cell = ws.cell(row=1, column=col, value=header)
cell.font = Font(bold=True)
cell.alignment = Alignment(horizontal="center")
for row, (time_str, weight_g, pv_type) in enumerate(self.peak_valley_log, 2):
ws.cell(row=row, column=1, value=row-1)
ws.cell(row=row, column=2, value=time_str)
ws.cell(row=row, column=3, value=round(weight_g, 2))
ws.cell(row=row, column=4, value=round(weight_g * 0.0098, 4))
ws.cell(row=row, column=5, value=pv_type)
# 统计信息
peaks = [w for _, w, t in self.peak_valley_log if t == 'peak']
valleys = [w for _, w, t in self.peak_valley_log if t == 'valley']
start_row = len(self.peak_valley_log) + 3
if peaks:
ws.cell(row=start_row, column=1, value="峰值统计")
ws.cell(row=start_row + 1, column=1, value="最大峰值")
ws.cell(row=start_row + 1, column=3, value=round(max(peaks), 2))
if valleys:
ws.cell(row=start_row + 2, column=1, value="谷值统计")
ws.cell(row=start_row + 3, column=1, value="最小谷值")
ws.cell(row=start_row + 3, column=3, value=round(min(valleys), 2))
# 图表
chart = LineChart()
chart.title = "峰值谷值变化曲线"
chart.x_axis.title = "序号"
chart.y_axis.title = "重量 (g)"
x_data = Reference(ws, min_col=1, min_row=2, max_row=len(self.peak_valley_log)+1)
y_data = Reference(ws, min_col=3, min_row=1, max_row=len(self.peak_valley_log)+1)
chart.add_data(y_data, titles_from_data=True)
chart.set_categories(x_data)
for series in chart.series:
series.dataLabels = DataLabelList()
series.dataLabels.showVal = True
ws.add_chart(chart, "G2")
wb.save(save_path)
# 在主线程中更新日志和弹窗
self.root.after(0, lambda: self.add_log(f"峰值/谷值日志数据已成功导出至:{save_path}"))
self.root.after(0, lambda: messagebox.showinfo("成功", f"峰值/谷值日志数据导出完成!\n路径:{save_path}"))
except Exception as e:
# 在主线程中处理错误
self.root.after(0, lambda: messagebox.showerror("失败", f"Excel导出错误:{e}"))
self.root.after(0, lambda: self.add_log(f"Excel导出失败:{e}"))
# ========== 机械臂功能(保持不变)==========
def refresh_arm_ports(self):
ports = [p.device for p in serial.tools.list_ports.comports()]
self.arm_port_combo["values"] = ports
if ports:
self.arm_port_combo.current(0)
def toggle_arm_connection(self):
if not self.arm_is_connected:
port = self.arm_port_combo.get()
baud = int(self.arm_baud_combo.get())
if not port:
messagebox.showerror("错误", "请选择机械臂COM口")
return
try:
self.arm_ser = serial.Serial(port, baud, timeout=1)
self.arm_is_connected = True
self.arm_connect_btn.config(text="断开机械臂")
self.send_gcode_btn.config(state=tk.NORMAL)
self.x_left_btn.config(state=tk.NORMAL)
self.x_right_btn.config(state=tk.NORMAL)
self.add_log(f"机械臂已连接:{port} @ {baud}")
except Exception as e:
messagebox.showerror("连接失败", str(e))
self.add_log(f"机械臂连接失败:{e}")
else:
if self.arm_ser and self.arm_ser.is_open:
self.arm_ser.close()
self.arm_is_connected = False
self.arm_connect_btn.config(text="连接机械臂")
self.send_gcode_btn.config(state=tk.DISABLED)
self.x_left_btn.config(state=tk.DISABLED)
self.x_right_btn.config(state=tk.DISABLED)
self.add_log("机械臂已断开")
def move_arm(self, axis, direction):
if not self.arm_is_connected:
return
step = self.move_step.get()
cmd = f"G91\nG0 {axis}{direction * step:.3f}\n"
try:
self.arm_ser.write(cmd.encode())
self.add_log(f"手动移动: {cmd.strip()}")
except Exception as e:
self.add_log(f"移动失败: {e}")
def send_gcode(self):
if not self.arm_is_connected or self.running_gcode:
return
self.gcode_content = self.gcode_text.get("1.0", tk.END).strip()
if not self.gcode_content:
messagebox.showwarning("警告", "G-code 为空!")
return
self.running_gcode = True
self.send_gcode_btn.config(state=tk.DISABLED)
self.stop_gcode_btn.config(state=tk.NORMAL)
self.current_loop = 0
threading.Thread(target=self._send_gcode_lines, args=([line.strip() for line in self.gcode_content.splitlines() if line.strip() and not line.startswith(';')],), daemon=True).start()
def _send_gcode_lines(self, lines):
for line in lines:
if not self.running_gcode:
break
try:
self.arm_ser.write((line + '\n').encode())
time.sleep(0.1)
except Exception as e:
self.add_log(f"G-code 发送错误: {e}")
self.running_gcode = False
break
self.root.after(500, self.run_gcode_loop)
def run_gcode_loop(self):
if not self.running_gcode or self.current_loop >= self.loop_count.get():
self.finish_gcode_run()
return
self.current_loop += 1
self.add_log(f"▶ 开始第 {self.current_loop} 次循环...")
threading.Thread(target=self._send_gcode_lines, args=([line.strip() for line in self.gcode_content.splitlines() if line.strip() and not line.startswith(';')],), daemon=True).start()
def stop_gcode(self):
self.running_gcode = False
self.add_log("⏹ G-code 运行已停止")
def finish_gcode_run(self):
self.running_gcode = False
self.send_gcode_btn.config(state=tk.NORMAL)
self.stop_gcode_btn.config(state=tk.DISABLED)
try:
winsound.Beep(1000, 300)
except:
pass # 非Windows忽略
self.add_log("✅ G-code 循环运行完成!")
def export_gcode_config(self):
config = {
"gcode": self.gcode_text.get("1.0", tk.END).strip(),
"loop_count": self.loop_count.get(),
"move_step": self.move_step.get()
}
path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON 文件", "*.json")],
title="导出G-code配置"
)
if path:
with open(path, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=2)
self.add_log(f"G-code 配置已导出:{path}")
def import_gcode_config(self):
path = filedialog.askopenfilename(
filetypes=[("JSON 文件", "*.json")],
title="导入G-code配置"
)
if path:
try:
with open(path, 'r', encoding='utf-8') as f:
config = json.load(f)
self.gcode_text.delete("1.0", tk.END)
self.gcode_text.insert("1.0", config.get("gcode", ""))
self.loop_count.set(config.get("loop_count", 1))
self.move_step.set(config.get("move_step", 1.0))
self.add_log(f"G-code 配置已导入:{path}")
except Exception as e:
messagebox.showerror("导入失败", str(e))
self.add_log(f"配置导入失败:{e}")
if __name__ == "__main__":
root = tk.Tk()
app = USBWeightMonitor(root)
root.mainloop()
帮我分析这个代码为什么一采样就卡死
最新发布