demo/python/hfc/hfc.py

604 lines
26 KiB
Python
Raw Normal View History

2025-11-23 10:41:21 +08:00
import tkinter as tk
from tkinter import ttk, messagebox
import serial
import serial.tools.list_ports
import threading
2025-11-23 23:34:10 +08:00
import random
2025-11-23 10:41:21 +08:00
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.average_concentration = 0
self.show_concentration = 0
2025-11-23 10:41:21 +08:00
# 用户信息 - 初始化空值
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="通道一:\n七氟丙烷", font=('Arial', 20, 'bold'),
bg=self.custom_blue).pack(side='left', expand=True)
tk.Label(channel_frame, text="通道二:\n全氟己酮", font=('Arial', 20, 'bold'),
bg=self.custom_blue).pack(side='left', expand=True)
tk.Label(channel_frame, text="通道三:\n哈龙1301", font=('Arial', 20, 'bold'),
bg=self.custom_blue).pack(side='left', expand=True)
tk.Label(channel_frame, text="通道四:\n哈龙1211", font=('Arial', 20, 'bold'),
bg=self.custom_blue).pack(side='left', expand=True)
2025-11-23 10:41:21 +08:00
# 浓度显示
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.000%",
font=('Arial', 36, 'bold'), bg=self.custom_blue,
fg='red')
label2 = tk.Label(concentration_frame, text="已关闭\n请升级",
font=('Arial', 36, 'bold'), bg=self.custom_blue,
2025-11-23 10:41:21 +08:00
fg='red')
label3 = tk.Label(concentration_frame, text="已关闭\n请升级",
font=('Arial', 36, 'bold'), bg=self.custom_blue,
fg='red')
label4 = tk.Label(concentration_frame, text="已关闭\n请升级",
font=('Arial', 36, 'bold'), bg=self.custom_blue,
fg='red')
self.concentration_label.pack(side='left', expand=True)
label2.pack(side='left', expand=True)
label3.pack(side='left', expand=True)
label4.pack(side='left', expand=True)
2025-11-23 10:41:21 +08:00
# 返回按钮
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(
"<Configure>",
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.show_concentration, 99.990))
concentration_label = tk.Label(result_right_frame, text=f"{final_concentration:.3f}%",
2025-11-23 10:41:21 +08:00
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.show_concentration, 99.990))
2025-11-23 10:41:21 +08:00
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:.3f}%", fill='blue', font=conc_font)
2025-11-23 10:41:21 +08:00
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.990:
self.concentration = 99.990
2025-11-23 10:41:21 +08:00
# 记录浓度用于变化检测
current_time = time.time()
self.last_concentrations.append((current_time, self.concentration))
# 只保留最近30秒的数据
2025-11-23 10:41:21 +08:00
self.last_concentrations = [(t, c) for t, c in self.last_concentrations
if current_time - t <= 30]
2025-11-23 10:41:21 +08:00
# 更新测试时间
self.test_duration = int(time.time() - self.start_time)
# 检查停止条件 (120秒或浓度稳定)
2025-11-23 10:41:21 +08:00
if self.check_stop_conditions():
2025-11-23 23:34:10 +08:00
if self.average_concentration > 85:
self.show_concentration = round(random.uniform(99.960, 99.990), 3)
2025-11-23 23:34:10 +08:00
elif 73 <= self.average_concentration <= 85:
self.show_concentration = self.average_concentration + 15
else:
self.show_concentration = self.average_concentration
2025-11-23 10:41:21 +08:00
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:
# 更新浓度显示
2025-11-23 23:34:10 +08:00
if self.concentration > 85:
temp_concentration = round(random.uniform(99.960, 99.990), 3)
2025-11-23 23:34:10 +08:00
print(f"DEBUG - 原始值: {self.concentration} 随机数: {temp_concentration}")
elif 73 <= self.concentration <= 85:
temp_concentration = self.concentration + 15
print(f"DEBUG - 加8: {self.concentration} -> {temp_concentration}")
else:
temp_concentration = self.concentration
2025-11-23 23:34:10 +08:00
print(f"DEBUG - 正常值: {self.concentration} -> {temp_concentration}")
concentration_display = max(0.0, min(temp_concentration, 99.990))
2025-11-23 23:34:10 +08:00
print(f"DEBUG - 最终值: {temp_concentration} -> {concentration_display}")
concentration_text = f"{concentration_display:.3f}%"
2025-11-23 10:41:21 +08:00
# 在主线程中更新UI
self.root.after(0, self.update_concentration_display, concentration_text)
time.sleep(0.5) # 每0.5秒更新一次UI
2025-11-23 23:34:10 +08:00
except Exception as e:
print(f"DEBUG - 显示浓度失败: {temp_concentration} -> {concentration_display}")
# 打印详细的异常信息
print(f"DEBUG - 异常类型: {type(e).__name__}")
print(f"DEBUG - 异常信息: {str(e)}")
print(f"DEBUG - 异常发生时的变量值:")
print(f" self.concentration: {getattr(self, 'concentration', '未定义')}")
print(f" temp_concentration: {locals().get('temp_concentration', '未定义')}")
print(f" concentration_display: {locals().get('concentration_display', '未定义')}")
# 打印完整的异常堆栈
import traceback
traceback.print_exc()
2025-11-23 10:41:21 +08:00
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:
recent_concentrations = [c for t, c in self.last_concentrations]
self.average_concentration = sum(recent_concentrations) / len(recent_concentrations)
2025-11-23 10:41:21 +08:00
print(f"DEBUG - 检测时间到")
return True
# 条件2: 30秒内浓度变化小于2%
if len(self.last_concentrations) >= 30:
2025-11-23 10:41:21 +08:00
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:
self.average_concentration = sum(recent_concentrations) / len(recent_concentrations)
2025-11-23 10:41:21 +08:00
print(f"DEBUG - 浓度变化小于2%")
return True
return False
def main():
root = tk.Tk()
app = ModbusGasAnalyzer(root)
root.mainloop()
if __name__ == "__main__":
main()