import tkinter as tk
from tkinter import ttk, messagebox, font
import serial
import serial.tools.list_ports
import threading
import time
import struct
import paho.mqtt.client as mqtt
from datetime import datetime
import json
import logging
import binascii

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

class SensorMQTTApp:
    def __init__(self, root):
        self.root = root
        self.root.title("气体浓度监测系统")
        self.root.geometry("800x600")
        self.root.configure(bg='#f0f0f0')
        
        # 变量初始化
        self.serial_port = None
        self.is_collecting = False
        self.is_uploading = False
        self.sensor_responded = False
        self.upload_success = False
        self.data_points = []
        self.sample_interval = 5
        self.current_concentration = 0
        
        # MQTT客户端
        self.mqtt_client = None
        self.mqtt_testing = False
        
        self.setup_ui()
        self.refresh_serial_ports()
    
    def setup_ui(self):
        # 设置主题样式
        style = ttk.Style()
        style.theme_use('clam')
        
        # 主框架
        main_frame = ttk.Frame(self.root, padding="8")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 标题
        title_label = tk.Label(main_frame, text="气体浓度实时监测系统", 
                              font=('Arial', 16, 'bold'), fg='#2c3e50', bg='#f0f0f0')
        title_label.grid(row=0, column=0, columnspan=3, pady=(0, 10))
        
        # 配置区域（三列布局）
        config_frame = ttk.Frame(main_frame)
        config_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N), pady=(0, 10))
        
        # 第一列：串口设置
        serial_frame = ttk.LabelFrame(config_frame, text="串口设置", padding="8")
        serial_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
        
        # 串口号
        ttk.Label(serial_frame, text="串口号:", font=('Arial', 9)).grid(row=0, column=0, sticky=tk.W, pady=3)
        self.port_var = tk.StringVar()
        self.port_combo = ttk.Combobox(serial_frame, textvariable=self.port_var, width=15, state='readonly')
        self.port_combo.grid(row=0, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # 波特率
        ttk.Label(serial_frame, text="波特率:", font=('Arial', 9)).grid(row=1, column=0, sticky=tk.W, pady=3)
        self.baudrate_var = tk.StringVar(value="19200")
        baudrate_combo = ttk.Combobox(serial_frame, textvariable=self.baudrate_var, 
                                    values=["9600", "19200", "38400", "57600", "115200"], 
                                    width=15, state='readonly')
        baudrate_combo.grid(row=1, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # 设备地址
        ttk.Label(serial_frame, text="设备地址:", font=('Arial', 9)).grid(row=2, column=0, sticky=tk.W, pady=3)
        self.address_var = tk.StringVar(value="1")
        address_spin = ttk.Spinbox(serial_frame, from_=0, to=255, textvariable=self.address_var, width=15)
        address_spin.grid(row=2, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # 采样间隔
        ttk.Label(serial_frame, text="采样间隔(秒):", font=('Arial', 9)).grid(row=3, column=0, sticky=tk.W, pady=3)
        self.interval_var = tk.StringVar(value="5")
        interval_spin = ttk.Spinbox(serial_frame, from_=1, to=3600, textvariable=self.interval_var, width=15)
        interval_spin.grid(row=3, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # 第二列：云服务设置（加宽文本框）
        mqtt_frame = ttk.LabelFrame(config_frame, text="云服务设置", padding="8")
        mqtt_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 5))
        
        # MQTT服务器地址
        ttk.Label(mqtt_frame, text="服务器地址:", font=('Arial', 9)).grid(row=0, column=0, sticky=tk.W, pady=3)
        self.mqtt_broker_var = tk.StringVar(value="122.112.229.121")
        ttk.Entry(mqtt_frame, textvariable=self.mqtt_broker_var, width=20).grid(row=0, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # MQTT端口
        ttk.Label(mqtt_frame, text="端口:", font=('Arial', 9)).grid(row=1, column=0, sticky=tk.W, pady=3)
        self.mqtt_port_var = tk.StringVar(value="1883")
        ttk.Entry(mqtt_frame, textvariable=self.mqtt_port_var, width=20).grid(row=1, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # MQTT主题（加宽文本框）
        ttk.Label(mqtt_frame, text="主题:", font=('Arial', 9)).grid(row=2, column=0, sticky=tk.W, pady=3)
        self.mqtt_topic_var = tk.StringVar(value="xiaofang/test")
        ttk.Entry(mqtt_frame, textvariable=self.mqtt_topic_var, width=20).grid(row=2, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # MQTT用户名
        ttk.Label(mqtt_frame, text="用户名:", font=('Arial', 9)).grid(row=3, column=0, sticky=tk.W, pady=3)
        self.mqtt_username_var = tk.StringVar(value="xiaofang")
        ttk.Entry(mqtt_frame, textvariable=self.mqtt_username_var, width=20).grid(row=3, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # MQTT密码
        ttk.Label(mqtt_frame, text="密码:", font=('Arial', 9)).grid(row=4, column=0, sticky=tk.W, pady=3)
        self.mqtt_password_var = tk.StringVar(value="xiaofang@qwer")
        ttk.Entry(mqtt_frame, textvariable=self.mqtt_password_var, show="*", width=20).grid(row=4, column=1, sticky=tk.W, pady=3, padx=(5, 0))
        
        # 第三列：按钮容器和系统状态
        right_column_frame = ttk.Frame(config_frame)
        right_column_frame.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 5))
        
        # 按钮容器（上下顺序排列）
        button_container = ttk.LabelFrame(right_column_frame, text="操作控制", padding="8")
        button_container.grid(row=0, column=2, sticky=(tk.W, tk.E, tk.N), padx=(5, 0))
        
        # 测试MQTT连接按钮
        self.test_mqtt_btn = ttk.Button(button_container, text="测试连接", command=self.start_mqtt_test, width=15)
        self.test_mqtt_btn.pack(pady=5)
        
        # 数据采集按钮
        self.collect_btn = ttk.Button(button_container, text="开始采集", command=self.toggle_collection, width=15)
        self.collect_btn.pack(pady=5)
        
        # 数据上传按钮
        self.upload_btn = ttk.Button(button_container, text="开始上传", command=self.toggle_upload, width=15)
        self.upload_btn.pack(pady=5)
        
        # 系统状态（上下排列）
        status_frame = ttk.LabelFrame(right_column_frame, text="系统状态", padding="8")
        status_frame.grid(row=0, column=3, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 5))
        
        # 传感器状态（上下排列）
        sensor_status_frame = ttk.Frame(status_frame)
        sensor_status_frame.pack(fill=tk.X, pady=3)
        ttk.Label(sensor_status_frame, text="传感器:", font=('Arial', 9)).pack(side=tk.LEFT)
        self.sensor_status = tk.Label(sensor_status_frame, text="●", fg="red", font=("Arial", 12, "bold"))
        self.sensor_status.pack(side=tk.LEFT, padx=(5, 5))
        self.sensor_status_text = ttk.Label(sensor_status_frame, text="未连接", font=('Arial', 9))
        self.sensor_status_text.pack(side=tk.LEFT)
        
        # 云服务状态（上下排列）
        upload_status_frame = ttk.Frame(status_frame)
        upload_status_frame.pack(fill=tk.X, pady=3)
        ttk.Label(upload_status_frame, text="云服务:", font=('Arial', 9)).pack(side=tk.LEFT)
        self.upload_status = tk.Label(upload_status_frame, text="●", fg="red", font=("Arial", 12, "bold"))
        self.upload_status.pack(side=tk.LEFT, padx=(5, 5))
        self.upload_status_text = ttk.Label(upload_status_frame, text="未连接", font=('Arial', 9))
        self.upload_status_text.pack(side=tk.LEFT)
        
        # 网络状态（上下排列）
        network_status_frame = ttk.Frame(status_frame)
        network_status_frame.pack(fill=tk.X, pady=3)
        ttk.Label(network_status_frame, text="网    络:", font=('Arial', 9)).pack(side=tk.LEFT)
        self.mqtt_test_status = tk.Label(network_status_frame, text="●", fg="gray", font=("Arial", 12, "bold"))
        self.mqtt_test_status.pack(side=tk.LEFT, padx=(5, 5))
        self.mqtt_test_status_text = ttk.Label(network_status_frame, text="未测试", font=('Arial', 9))
        self.mqtt_test_status_text.pack(side=tk.LEFT)
        
        # 浓度显示区域（跨三列，和数据记录一样宽）
        display_frame = ttk.LabelFrame(main_frame, text="实时浓度监测", padding="12")
        display_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 10))
        
        # 浓度值显示（超大字体，宽度与数据记录一致）
        self.concentration_var = tk.StringVar(value="0")
        concentration_label = tk.Label(display_frame, textvariable=self.concentration_var, 
                                      font=('Arial', 36, 'bold'), fg='#2c3e50', bg='white',
                                      relief='sunken', bd=2, width=25, height=2)
        concentration_label.pack(pady=10)
        
        # 单位标签和更新时间
        info_frame = ttk.Frame(display_frame)
        info_frame.pack()
        
        unit_label = tk.Label(info_frame, text="ppm", font=('Arial', 14), fg='#7f8c8d')
        unit_label.pack(side=tk.LEFT, padx=(0, 20))
        
        self.update_time_var = tk.StringVar(value="未更新")
        time_label = tk.Label(info_frame, textvariable=self.update_time_var, 
                             font=('Arial', 9), fg='#95a5a6')
        time_label.pack(side=tk.LEFT)
        
        # 历史数据区域（跨三列，和浓度显示一样宽）
        history_frame = ttk.LabelFrame(main_frame, text="最近数据记录", padding="6")
        history_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # 创建历史数据文本框（宽度与浓度显示一致）
        self.history_text = tk.Text(history_frame, height=4, width=60, font=('Consolas', 9),
                                   bg='#f8f9fa', relief='sunken', bd=1)
        scrollbar = ttk.Scrollbar(history_frame, orient=tk.VERTICAL, command=self.history_text.yview)
        self.history_text.configure(yscrollcommand=scrollbar.set)
        
        self.history_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 配置权重
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        
        main_frame.columnconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.columnconfigure(2, weight=1)
        main_frame.rowconfigure(2, weight=1)  # 浓度显示区域
        main_frame.rowconfigure(3, weight=1)  # 历史数据区域
        
        config_frame.columnconfigure(0, weight=1)
        config_frame.columnconfigure(1, weight=2)  # 云服务设置更宽
        config_frame.columnconfigure(2, weight=1)
        
        serial_frame.columnconfigure(1, weight=1)
        mqtt_frame.columnconfigure(1, weight=1)
        right_column_frame.columnconfigure(0, weight=1)
        
        display_frame.columnconfigure(0, weight=1)
        history_frame.columnconfigure(0, weight=1)
        history_frame.rowconfigure(0, weight=1)
        
    def refresh_serial_ports(self):
        """刷新可用串口列表（只在启动时调用一次）"""
        ports = serial.tools.list_ports.comports()
        port_list = [port.device for port in ports]
        self.port_combo['values'] = port_list
        if port_list:
            self.port_combo.current(0)
    
    def calculate_crc16(self, data):
        """计算Modbus CRC16校验"""
        crc = 0xFFFF
        for byte in data:
            crc ^= byte
            for _ in range(8):
                if crc & 0x0001:
                    crc = (crc >> 1) ^ 0xA001
                else:
                    crc = crc >> 1
        return crc
    
    def start_mqtt_test(self):
        """启动MQTT连接测试"""
        if self.mqtt_testing:
            return
            
        self.mqtt_testing = True
        self.test_mqtt_btn.config(state="disabled", text="测试中...")
        self.mqtt_test_status.config(fg="yellow")
        self.mqtt_test_status_text.config(text="测试中")
        
        test_thread = threading.Thread(target=self.test_mqtt_connection_thread, daemon=True)
        test_thread.start()
    
    def test_mqtt_connection_thread(self):
        """在单独线程中测试MQTT连接"""
        try:
            client = mqtt.Client()
            connected = False
            
            def on_connect(client, userdata, flags, rc):
                nonlocal connected
                connected = (rc == 0)
            
            client.on_connect = on_connect
            
            username = self.mqtt_username_var.get()
            password = self.mqtt_password_var.get()
            if username and password:
                client.username_pw_set(username, password)
            
            client.connect(self.mqtt_broker_var.get(), int(self.mqtt_port_var.get()), 5)
            client.loop_start()
            
            timeout = 5
            start_time = time.time()
            while not connected and time.time() - start_time < timeout:
                time.sleep(0.1)
            
            client.loop_stop()
            client.disconnect()
            
            if connected:
                self.root.after(0, self.on_mqtt_test_success)
            else:
                self.root.after(0, lambda: self.on_mqtt_test_failure("连接失败"))
                
        except Exception as e:
            self.root.after(0, lambda: self.on_mqtt_test_failure(str(e)))
    
    def on_mqtt_test_success(self):
        """MQTT测试成功回调"""
        self.mqtt_testing = False
        self.test_mqtt_btn.config(state="normal", text="测试连接")
        self.mqtt_test_status.config(fg="green")
        self.mqtt_test_status_text.config(text="已连接")
        messagebox.showinfo("成功", "MQTT连接测试成功！")
    
    def on_mqtt_test_failure(self, error_msg):
        """MQTT测试失败回调"""
        self.mqtt_testing = False
        self.test_mqtt_btn.config(state="normal", text="测试连接")
        self.mqtt_test_status.config(fg="red")
        self.mqtt_test_status_text.config(text="连接失败")
        messagebox.showerror("错误", f"MQTT连接测试失败: {error_msg}")
    
    def build_modbus_command(self, address):
        """构建Modbus读取命令"""
        command = bytearray([
            address, 0x04, 0x05, 0x20, 0x00, 0x02
        ])
        crc = self.calculate_crc16(command)
        command.extend([crc & 0xFF, (crc >> 8) & 0xFF])
        return command
    
    def parse_sensor_data(self, response):
        """解析传感器返回数据"""
        try:
            if len(response) >= 9:
                if response[0] == int(self.address_var.get()) and response[1] == 0x04:
                    data_bytes = response[3:7]
                    concentration = struct.unpack('>i', data_bytes)[0]
                    return concentration
            return None
        except Exception as e:
            logger.error(f"解析传感器数据错误: {e}")
            return None
    
    def read_sensor_data(self):
        """读取传感器数据"""
        try:
            if not self.serial_port or not self.serial_port.is_open:
                return None
                
            address = int(self.address_var.get())
            command = self.build_modbus_command(address)
            
            self.serial_port.reset_input_buffer()
            self.serial_port.write(command)
            time.sleep(0.1)
            
            if self.serial_port.in_waiting > 0:
                response = self.serial_port.read(self.serial_port.in_waiting)
                concentration = self.parse_sensor_data(response)
                
                if concentration is not None:
                    self.current_concentration = concentration
                    self.sensor_responded = True
                    self.sensor_status.config(fg="green")
                    self.sensor_status_text.config(text="已连接")
                    
                    # 更新显示
                    self.update_display(concentration)
                    
                    # 添加到历史数据
                    self.add_to_history(concentration)
                    
                    # 如果上传功能开启，上传数据
                    if self.is_uploading and self.mqtt_client:
                        self.upload_to_mqtt(concentration)
                    
                    return concentration
                else:
                    self.sensor_responded = False
                    self.sensor_status.config(fg="red")
                    self.sensor_status_text.config(text="无响应")
            else:
                self.sensor_responded = False
                self.sensor_status.config(fg="red")
                self.sensor_status_text.config(text="无响应")
                
        except Exception as e:
            logger.error(f"读取传感器数据错误: {e}")
            self.sensor_responded = False
            self.sensor_status.config(fg="red")
            self.sensor_status_text.config(text="错误")
        
        return None
    
    def update_display(self, concentration):
        """更新显示"""
        self.concentration_var.set(str(concentration))
        current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.update_time_var.set(f"最后更新: {current_time}")
    
    def add_to_history(self, concentration):
        """添加到历史记录"""
        current_time = datetime.now().strftime("%H:%M:%S")
        history_entry = f"{current_time} - {concentration} ppm\n"
        
        # 添加到文本框开头
        self.history_text.insert('1.0', history_entry)
        
        # 限制历史记录条数
        lines = self.history_text.get('1.0', 'end').count('\n')
        if lines > 10:
            self.history_text.delete('10.0', 'end')
    
    def upload_to_mqtt(self, concentration):
        """上传数据到MQTT"""
        try:
            if self.mqtt_client:
                result = self.mqtt_client.publish(
                    self.mqtt_topic_var.get(),
                    str(concentration)
                )
                
                if result.rc == mqtt.MQTT_ERR_SUCCESS:
                    self.upload_success = True
                    self.upload_status.config(fg="green")
                    self.upload_status_text.config(text="已连接")
                else:
                    self.upload_success = False
                    self.upload_status.config(fg="red")
                    self.upload_status_text.config(text="发送失败")
                    
        except Exception as e:
            logger.error(f"MQTT上传错误: {e}")
            self.upload_success = False
            self.upload_status.config(fg="red")
            self.upload_status_text.config(text="错误")
    
    def connect_mqtt(self):
        """连接MQTT服务器"""
        try:
            self.mqtt_client = mqtt.Client()
            self.mqtt_client.username_pw_set(
                self.mqtt_username_var.get(),
                self.mqtt_password_var.get()
            )
            
            def on_connect(client, userdata, flags, rc):
                if rc == 0:
                    logger.info("MQTT连接成功")
                    self.upload_status.config(fg="green")
                    self.upload_status_text.config(text="已连接")
                else:
                    logger.error(f"MQTT连接失败，错误代码: {rc}")
                    self.upload_status.config(fg="red")
                    self.upload_status_text.config(text="连接失败")
            
            self.mqtt_client.on_connect = on_connect
            self.mqtt_client.connect(
                self.mqtt_broker_var.get(),
                int(self.mqtt_port_var.get()),
                60
            )
            self.mqtt_client.loop_start()
            
        except Exception as e:
            logger.error(f"MQTT连接错误: {e}")
            messagebox.showerror("错误", f"MQTT连接失败: {str(e)}")
    
    def disconnect_mqtt(self):
        """断开MQTT连接"""
        if self.mqtt_client:
            self.mqtt_client.loop_stop()
            self.mqtt_client.disconnect()
            self.mqtt_client = None
            logger.info("MQTT连接已断开")
            self.upload_status.config(fg="red")
            self.upload_status_text.config(text="未连接")
    
    def toggle_collection(self):
        """切换数据采集状态"""
        if not self.is_collecting:
            try:
                port = self.port_var.get()
                if not port:
                    messagebox.showwarning("警告", "请选择串口号")
                    return
                    
                baudrate = int(self.baudrate_var.get())
                self.serial_port = serial.Serial(port, baudrate, timeout=1)
                self.is_collecting = True
                self.collect_btn.config(text="停止采集")
                
                # 更新状态显示
                self.sensor_status.config(fg="green")
                self.sensor_status_text.config(text="已连接")
                
                # 启动采集线程
                self.collection_thread = threading.Thread(target=self.collection_loop, daemon=True)
                self.collection_thread.start()
                
            except Exception as e:
                messagebox.showerror("错误", f"打开串口失败: {str(e)}")
                self.is_collecting = False
                self.collect_btn.config(text="开始采集")
                self.sensor_status.config(fg="red")
                self.sensor_status_text.config(text="错误")
        else:
            self.is_collecting = False
            self.collect_btn.config(text="开始采集")
            if self.serial_port:
                self.serial_port.close()
                self.serial_port = None
            
            # 更新状态显示
            self.sensor_status.config(fg="red")
            self.sensor_status_text.config(text="未连接")
    
    def toggle_upload(self):
        """切换数据上传状态"""
        if not self.is_uploading:
            try:
                self.connect_mqtt()
                self.is_uploading = True
                self.upload_btn.config(text="停止上传")
                
            except Exception as e:
                messagebox.showerror("错误", f"启动上传失败: {str(e)}")
                self.is_uploading = False
                self.upload_btn.config(text="开始上传")
        else:
            self.is_uploading = False
            self.upload_btn.config(text="开始上传")
            self.disconnect_mqtt()
    
    def collection_loop(self):
        """数据采集循环"""
        while self.is_collecting:
            try:
                self.read_sensor_data()
                interval = int(self.interval_var.get())
                time.sleep(interval)
            except Exception as e:
                logger.error(f"采集循环错误: {e}")
                time.sleep(1)
    
    def on_closing(self):
        """程序关闭时的清理工作"""
        self.is_collecting = False
        self.is_uploading = False
        
        if self.serial_port:
            self.serial_port.close()
        
        if self.mqtt_client:
            self.disconnect_mqtt()
        
        self.root.destroy()

def main():
    root = tk.Tk()
    app = SensorMQTTApp(root)
    root.protocol("WM_DELETE_WINDOW", app.on_closing)
    root.mainloop()

if __name__ == "__main__":
    main()