import tkinter as tk from tkinter import ttk, messagebox import serial import serial.tools.list_ports import threading import time from datetime import datetime import struct from PIL import Image, ImageTk, ImageDraw, ImageFont import os class ModbusGasAnalyzer: def __init__(self, root): self.root = root self.root.title("HFC-RapidScan多通道灭火剂气体快检仪") self.custom_blue = "#0161DA" # 这是你的RGB颜色 # 使用PNG图片作为图标 try: img = Image.open("logo.png") photo = ImageTk.PhotoImage(img) self.root.iconphoto(True, photo) # False表示不用于子窗口 # 保存引用防止垃圾回收 self.icon_image = photo except Exception as e: print(f"设置图标失败: {e}") # 设置黄金比例窗口大小 (800x495) window_width = 800 window_height = 495 self.root.geometry(f"{window_width}x{window_height}") self.root.configure(bg=self.custom_blue) # Modbus通信参数 self.serial_port = None self.is_connected = False self.is_testing = False # 测试数据 self.concentration = 0.0 self.start_time = None self.test_duration = 0 self.last_concentrations = [] # 用于检测浓度变化 # 用户信息 - 初始化空值 self.user_info = { "测试人员": "", "测试时间": "", "样品信息": "", "出厂日期": "", "产品型号": "", "编号": "" } # 创建页面 self.create_page1() def create_page1(self): """创建第一个页面""" self.clear_window() # 主框架 main_frame = tk.Frame(self.root, bg=self.custom_blue) main_frame.pack(fill='both', expand=True, padx=20, pady=20) # 左侧区域 left_frame = tk.Frame(main_frame, bg=self.custom_blue) left_frame.pack(side='left', fill='both', expand=True, padx=(0, 10)) # 右侧区域 right_frame = tk.Frame(main_frame, bg=self.custom_blue) right_frame.pack(side='right', fill='both', expand=True, padx=(10, 0)) # 左侧内容 # 测试人员 tk.Label(left_frame, text="测试人员:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.tester_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.tester_entry.pack(fill='x', pady=5) # 保留之前填写的信息 if self.user_info["测试人员"]: self.tester_entry.insert(0, self.user_info["测试人员"]) # 测试时间 tk.Label(left_frame, text="测试时间:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.test_time_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.test_time_entry.pack(fill='x', pady=5) # 自动填入当前时间,但如果已有信息则保留 if self.user_info["测试时间"]: self.test_time_entry.insert(0, self.user_info["测试时间"]) else: current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") self.test_time_entry.insert(0, current_time) # 样品信息 tk.Label(left_frame, text="样品信息:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.sample_info_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.sample_info_entry.pack(fill='x', pady=5) if self.user_info["样品信息"]: self.sample_info_entry.insert(0, self.user_info["样品信息"]) # 出厂日期 tk.Label(left_frame, text="出厂日期:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.production_date_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.production_date_entry.pack(fill='x', pady=5) if self.user_info["出厂日期"]: self.production_date_entry.insert(0, self.user_info["出厂日期"]) # 产品型号 tk.Label(left_frame, text="产品型号:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.model_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.model_entry.pack(fill='x', pady=5) if self.user_info["产品型号"]: self.model_entry.insert(0, self.user_info["产品型号"]) # 编号 tk.Label(left_frame, text="编号:", font=('Arial', 12), bg=self.custom_blue).pack(anchor='w', pady=5) self.serial_number_entry = tk.Entry(left_frame, font=('Arial', 12), width=20) self.serial_number_entry.pack(fill='x', pady=5) if self.user_info["编号"]: self.serial_number_entry.insert(0, self.user_info["编号"]) # 右侧内容 - 标题 title_label = tk.Label(right_frame, text="HFC-RapidScan多通道\n灭火剂气体快检仪", font=('Arial', 18, 'bold'), bg=self.custom_blue, justify='center') title_label.pack(pady=40) # 开始测试按钮 self.start_button = tk.Button(right_frame, text="开始测试", font=('Arial', 16, 'bold'), bg='green', fg='white', width=15, height=2, command=self.start_test) self.start_button.pack(pady=40) def create_page2(self): """创建第二个页面""" self.clear_window() main_frame = tk.Frame(self.root, bg=self.custom_blue) main_frame.pack(fill='both', expand=True, padx=20, pady=20) # 标题 title_label = tk.Label(main_frame, text="HFC-RapidScan多通道灭火剂气体快检仪", font=('Arial', 16, 'bold'), bg=self.custom_blue) title_label.pack(pady=10) # 通道信息 channel_frame = tk.Frame(main_frame, bg=self.custom_blue) channel_frame.pack(fill='x', pady=20) tk.Label(channel_frame, text="通道一:七氟丙烷", font=('Arial', 20, 'bold'), bg=self.custom_blue).pack() # 浓度显示 concentration_frame = tk.Frame(main_frame, bg=self.custom_blue) concentration_frame.pack(fill='both', expand=True) self.concentration_label = tk.Label(concentration_frame, text="0.00%", font=('Arial', 48, 'bold'), bg=self.custom_blue, fg='red') self.concentration_label.pack(expand=True) # 返回按钮 back_button = tk.Button(main_frame, text="返回", font=('Arial', 12), command=self.back_to_page1, bg='orange') back_button.pack(pady=10) # 开始Modbus通信 self.start_modbus_communication() def create_page3(self): """创建第三个页面 - 结果汇总""" self.clear_window() main_frame = tk.Frame(self.root, bg=self.custom_blue) main_frame.pack(fill='both', expand=True, padx=20, pady=20) # 标题 title_label = tk.Label(main_frame, text="HFC-RapidScan多通道灭火剂气体快检仪", font=('Arial', 16, 'bold'), bg=self.custom_blue) title_label.pack(pady=10) # 结果显示框架 result_frame = tk.Frame(main_frame, bg='white', relief='raised', bd=2) result_frame.pack(fill='both', expand=True, padx=20, pady=10) # 创建滚动框架以适应所有内容 canvas = tk.Canvas(result_frame, bg='white') scrollbar = tk.Scrollbar(result_frame, orient="vertical", command=canvas.yview) scrollable_frame = tk.Frame(canvas, bg='white') scrollable_frame.bind( "", lambda e: canvas.configure(scrollregion=canvas.bbox("all")) ) canvas.create_window((0, 0), window=scrollable_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) # 左侧信息框架 info_frame = tk.Frame(scrollable_frame, bg='white') info_frame.pack(side='left', fill='both', expand=True, padx=20, pady=20) # 显示所有用户信息 info_items = [ ("测试人员:", self.user_info["测试人员"]), ("测试时间:", self.user_info["测试时间"]), ("样品信息:", self.user_info["样品信息"]), ("出厂日期:", self.user_info["出厂日期"]), ("产品型号:", self.user_info["产品型号"]), ("编号:", self.user_info["编号"]) ] for label, value in info_items: label_frame = tk.Frame(info_frame, bg='white') label_frame.pack(fill='x', pady=8) tk.Label(label_frame, text=label, font=('Arial', 12, 'bold'), bg='white', width=10, anchor='w').pack(side='left') tk.Label(label_frame, text=value, font=('Arial', 12), bg='white', anchor='w').pack(side='left', fill='x', expand=True) # 右侧结果框架 result_right_frame = tk.Frame(scrollable_frame, bg='white') result_right_frame.pack(side='right', fill='both', expand=True, padx=20, pady=20) tk.Label(result_right_frame, text="最终浓度", font=('Arial', 16, 'bold'), bg='white').pack(pady=10) final_concentration = max(0.0, min(self.concentration, 99.6)) concentration_label = tk.Label(result_right_frame, text=f"{final_concentration:.2f}%", font=('Arial', 36, 'bold'), bg='white', fg='blue') concentration_label.pack(pady=20) tk.Label(result_right_frame, text=f"测试时长: {self.test_duration}秒", font=('Arial', 12), bg='white').pack(pady=10) # 打包canvas和scrollbar canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # 按钮框架 button_frame = tk.Frame(main_frame, bg=self.custom_blue) button_frame.pack(fill='x', pady=10) tk.Button(button_frame, text="重新测试", font=('Arial', 12), command=self.back_to_page1, bg='green', fg='white').pack(side='left', padx=20) tk.Button(button_frame, text="保存报告", font=('Arial', 12), command=self.save_report, bg='blue', fg='white').pack(side='left', padx=20) tk.Button(button_frame, text="退出", font=('Arial', 12), command=self.root.quit, bg='red', fg='white').pack(side='right', padx=20) # 自动保存报告 self.save_report() def save_report(self): """保存汇总信息为JPG图片""" try: # 创建图片 - 增大高度以容纳所有信息 img_width = 1000 img_height = 800 img = Image.new('RGB', (img_width, img_height), color=self.custom_blue) draw = ImageDraw.Draw(img) # 尝试加载字体 try: title_font = ImageFont.truetype("simhei.ttf", 36) # 黑体 header_font = ImageFont.truetype("simhei.ttf", 24) content_font = ImageFont.truetype("simhei.ttf", 18) small_font = ImageFont.truetype("simhei.ttf", 16) except: # 如果系统没有中文字体,使用默认字体 title_font = ImageFont.load_default() header_font = ImageFont.load_default() content_font = ImageFont.load_default() small_font = ImageFont.load_default() # 绘制标题 title = "HFC-RapidScan多通道灭火剂气体快检仪" draw.text((img_width//2 - 250, 35), title, fill='black', font=title_font) # 绘制分隔线 draw.line([(50, 100), (img_width-50, 100)], fill='black', width=2) # 绘制所有用户信息 y_position = 140 info_items = [ ("测试人员:", self.user_info["测试人员"]), ("测试时间:", self.user_info["测试时间"]), ("样品信息:", self.user_info["样品信息"]), ("出厂日期:", self.user_info["出厂日期"]), ("产品型号:", self.user_info["产品型号"]), ("编号:", self.user_info["编号"]) ] for label, value in info_items: # 标签 draw.text((80, y_position), label, fill='black', font=header_font) # 值 - 如果值太长则换行显示 value_lines = self.wrap_text(value, content_font, img_width - 300) for line in value_lines: draw.text((200, y_position), line, fill='darkblue', font=content_font) y_position += 30 y_position += 10 # 绘制浓度结果 result_y = 140 final_concentration = max(0.0, min(self.concentration, 99.6)) draw.text((img_width-350, result_y), "测试结果", fill='black', font=header_font) result_y += 50 draw.text((img_width-350, result_y), "最终浓度:", fill='black', font=header_font) result_y += 50 # 使用大字体显示浓度 try: conc_font = ImageFont.truetype("simhei.ttf", 48) except: conc_font = ImageFont.load_default() draw.text((img_width-350, result_y), f"{final_concentration:.2f}%", fill='blue', font=conc_font) result_y += 80 draw.text((img_width-350, result_y), f"测试时长: {self.test_duration}秒", fill='black', font=content_font) # 保存图片 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"测试报告_{timestamp}.jpg" img.save(filename, quality=95) messagebox.showinfo("成功", f"报告已保存为: {filename}") except Exception as e: messagebox.showerror("错误", f"保存报告失败: {str(e)}") def wrap_text(self, text, font, max_width): """文本换行处理""" lines = [] words = text.split(' ') current_line = [] for word in words: # 估算文本宽度 test_line = ' '.join(current_line + [word]) bbox = font.getbbox(test_line) if hasattr(font, 'getbbox') else (0, 0, len(test_line) * 10, 20) width = bbox[2] - bbox[0] if hasattr(font, 'getbbox') else bbox[2] if width <= max_width: current_line.append(word) else: if current_line: lines.append(' '.join(current_line)) current_line = [word] if current_line: lines.append(' '.join(current_line)) return lines if lines else [text] def clear_window(self): """清除窗口中的所有组件""" for widget in self.root.winfo_children(): widget.destroy() def start_test(self): """开始测试""" # 保存用户信息 self.user_info = { "测试人员": self.tester_entry.get(), "测试时间": self.test_time_entry.get(), "样品信息": self.sample_info_entry.get(), "出厂日期": self.production_date_entry.get(), "产品型号": self.model_entry.get(), "编号": self.serial_number_entry.get() } # 切换到第二个页面 self.create_page2() def back_to_page1(self): """返回到第一个页面""" self.stop_modbus_communication() self.create_page1() def get_com_port(self): """获取COM端口""" try: # 尝试从配置文件读取COM口 if os.path.exists("com_config.txt"): with open("com_config.txt", "r", encoding='utf-8') as f: com_port = f.read().strip() # 验证COM口是否存在 ports = [port.device for port in serial.tools.list_ports.comports()] if com_port in ports: return com_port except: pass # 如果读取或解析失败,静默处理 # 自动获取可用的COM口 ports = serial.tools.list_ports.comports() if ports: return ports[0].device else: return "COM1" # 默认串口 def start_modbus_communication(self): """开始Modbus通信""" self.is_testing = True self.start_time = time.time() self.last_concentrations = [] # 尝试连接串口 try: com_port = self.get_com_port() self.serial_port = serial.Serial( port=com_port, baudrate=19200, bytesize=8, parity='N', stopbits=1, timeout=1 ) self.is_connected = True except Exception as e: messagebox.showerror("错误", f"无法连接串口: {str(e)}") self.is_connected = False return # 启动通信线程 self.communication_thread = threading.Thread(target=self.modbus_communication_loop) self.communication_thread.daemon = True self.communication_thread.start() # 启动UI更新线程 self.ui_update_thread = threading.Thread(target=self.ui_update_loop) self.ui_update_thread.daemon = True self.ui_update_thread.start() def stop_modbus_communication(self): """停止Modbus通信""" self.is_testing = False if self.serial_port and self.serial_port.is_open: self.serial_port.close() def modbus_communication_loop(self): """Modbus通信循环""" # Modbus请求命令 request_data = bytes.fromhex('01 04 05 20 00 02 70 CD') while self.is_testing and self.is_connected: try: # 清空输入和输出缓冲区 self.serial_port.reset_input_buffer() # 清空接收缓冲区 self.serial_port.reset_output_buffer() # 清空发送缓冲区 # 发送请求 self.serial_port.write(request_data) # 读取响应 response = self.serial_port.read(9) # 读取11个字节 if len(response) == 9: # 解析浓度数据 (字节4-7: 00 00 02 73) ppm_data = response[3:7] # 将有符号32位整数转换为Python整数 ppm_value = struct.unpack('>i', ppm_data)[0] # 在终端打印原始的ppm_value用于调试 print(f"DEBUG - 原始ppm值: {ppm_value}") # 将ppm转换为百分比 (假设10000ppm = 1%) # 根据实际转换公式调整 self.concentration = ppm_value / 10000.0 # 限制浓度范围 if self.concentration < 0: self.concentration = 0.0 elif self.concentration > 99.6: self.concentration = 99.6 # 记录浓度用于变化检测 current_time = time.time() self.last_concentrations.append((current_time, self.concentration)) # 只保留最近10秒的数据 self.last_concentrations = [(t, c) for t, c in self.last_concentrations if current_time - t <= 10] # 更新测试时间 self.test_duration = int(time.time() - self.start_time) # 检查停止条件 (20秒或浓度稳定) if self.check_stop_conditions(): self.root.after(0, self.create_page3) break time.sleep(1) # 每秒通信一次 except Exception as e: print(f"Modbus通信错误: {e}") time.sleep(1) def ui_update_loop(self): """UI更新循环""" while self.is_testing: try: # 更新浓度显示 concentration_display = max(0.0, min(self.concentration, 99.6)) concentration_text = f"{concentration_display:.2f}%" # 在主线程中更新UI self.root.after(0, self.update_concentration_display, concentration_text) time.sleep(0.5) # 每0.5秒更新一次UI except: break def update_concentration_display(self, concentration_text): """更新浓度显示""" try: if hasattr(self, 'concentration_label'): self.concentration_label.config(text=concentration_text) except: pass def check_stop_conditions(self): """检查停止条件""" # 条件1: 测试时间达到120秒 if self.test_duration >= 120: print(f"DEBUG - 检测时间到") return True # 条件2: 10秒内浓度变化小于2% if len(self.last_concentrations) >= 10: recent_concentrations = [c for t, c in self.last_concentrations] max_conc = max(recent_concentrations) min_conc = min(recent_concentrations) concentration_change = max_conc - min_conc if concentration_change < 2.0: print(f"DEBUG - 浓度变化小于2%") return True return False def main(): root = tk.Tk() app = ModbusGasAnalyzer(root) root.mainloop() if __name__ == "__main__": main()