diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f09620b --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +python/*/build/* +python/*/dist/* +python/*/*.spec diff --git a/python/hfc/com_config.txt b/python/hfc/com_config.txt new file mode 100644 index 0000000..4202bc4 --- /dev/null +++ b/python/hfc/com_config.txt @@ -0,0 +1 @@ +COM3 \ No newline at end of file diff --git a/python/hfc/hfc.py b/python/hfc/hfc.py new file mode 100644 index 0000000..707dbe9 --- /dev/null +++ b/python/hfc/hfc.py @@ -0,0 +1,552 @@ +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() \ No newline at end of file diff --git a/python/hfc/logo.png b/python/hfc/logo.png new file mode 100644 index 0000000..6489aa9 Binary files /dev/null and b/python/hfc/logo.png differ diff --git a/python/hfc/packet.cmd b/python/hfc/packet.cmd new file mode 100644 index 0000000..61dcf4d --- /dev/null +++ b/python/hfc/packet.cmd @@ -0,0 +1 @@ +python.exe -m PyInstaller --onefile --windowed --name HFC .\hfc.py