# pip install pyserial PyQt5

import sys
import re
import serial
import serial.tools.list_ports
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
                             QLabel, QComboBox, QPushButton, QTextEdit, QLineEdit,
                             QSpinBox, QCheckBox, QMessageBox, QGroupBox, QSplitter,
                             QScrollArea, QSizePolicy, QFrame)
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDateTime, QTimer
from PyQt5.QtGui import QTextCursor, QColor, QTextCharFormat, QFont


class SerialThread(QThread):
    data_received = pyqtSignal(str, str)  # data, timestamp

    def __init__(self, serial_port):
        super().__init__()
        self.serial_port = serial_port
        self.running = True

    def run(self):
        while self.running and self.serial_port.is_open:
            try:
                if self.serial_port.in_waiting:
                    data = self.serial_port.read(self.serial_port.in_waiting).decode('utf-8', errors='ignore')
                    timestamp = QDateTime.currentDateTime().toString("hh:mm:ss.zzz")
                    self.data_received.emit(data, timestamp)
            except Exception as e:
                print(f"Serial read error: {e}")
                break

    def stop(self):
        self.running = False


class SerialDebugger(QMainWindow):
    def __init__(self):
        super().__init__()
        self.serial = None
        self.serial_thread = None
        self.auto_send_timer = None
        self.send_count = 0
        self.receive_count = 0
        self.init_ui()
        self.refresh_ports()

    def init_ui(self):
        self.setWindowTitle('高级串口调试助手')
        self.setGeometry(100, 100, 1200, 800)

        # 主窗口部件
        main_widget = QWidget()
        self.setCentralWidget(main_widget)
        main_layout = QHBoxLayout(main_widget)
        main_layout.setContentsMargins(5, 5, 5, 5)

        # 左侧控制面板
        control_panel = QWidget()
        control_panel.setMaximumWidth(300)
        control_layout = QVBoxLayout(control_panel)
        control_layout.setContentsMargins(0, 0, 0, 0)

        # 串口设置区域
        port_group = QGroupBox("串口设置")
        port_layout = QVBoxLayout()
        
        # 串口选择
        port_select_layout = QHBoxLayout()
        self.port_label = QLabel('串口:')
        self.port_combo = QComboBox()
        self.refresh_btn = QPushButton('刷新')
        self.refresh_btn.clicked.connect(self.refresh_ports)
        port_select_layout.addWidget(self.port_label)
        port_select_layout.addWidget(self.port_combo)
        port_select_layout.addWidget(self.refresh_btn)
        port_layout.addLayout(port_select_layout)

        # 波特率选择
        baud_layout = QHBoxLayout()
        self.baud_label = QLabel('波特率:')
        self.baud_combo = QComboBox()
        common_baudrates = ['9600', '19200', '38400', '57600', '115200', 
                          '230400', '460800', '500000', '576000', '750000',
                          '921600', '1000000', '1152000', '1500000', '2000000']
        self.baud_combo.addItems(common_baudrates)
        self.baud_combo.setCurrentText('2000000')
        self.baud_combo.setEditable(True)
        baud_layout.addWidget(self.baud_label)
        baud_layout.addWidget(self.baud_combo)
        port_layout.addLayout(baud_layout)

        # 其他串口参数
        param_layout = QHBoxLayout()
        self.data_bits_label = QLabel('数据位:')
        self.data_bits_combo = QComboBox()
        self.data_bits_combo.addItems(['5', '6', '7', '8'])
        self.data_bits_combo.setCurrentText('8')
        
        self.stop_bits_label = QLabel('停止位:')
        self.stop_bits_combo = QComboBox()
        self.stop_bits_combo.addItems(['1', '1.5', '2'])
        self.stop_bits_combo.setCurrentText('1')
        
        param_layout.addWidget(self.data_bits_label)
        param_layout.addWidget(self.data_bits_combo)
        param_layout.addWidget(self.stop_bits_label)
        param_layout.addWidget(self.stop_bits_combo)
        port_layout.addLayout(param_layout)

        # 校验位
        parity_layout = QHBoxLayout()
        self.parity_label = QLabel('校验位:')
        self.parity_combo = QComboBox()
        self.parity_combo.addItems(['无', '奇校验', '偶校验', '标记', '空格'])
        self.parity_combo.setCurrentText('无')
        parity_layout.addWidget(self.parity_label)
        parity_layout.addWidget(self.parity_combo)
        port_layout.addLayout(parity_layout)

        # 控制按钮
        ctrl_btn_layout = QHBoxLayout()
        self.open_btn = QPushButton('打开串口')
        self.open_btn.clicked.connect(self.toggle_serial)
        self.dtr_check = QCheckBox('DTR')
        self.dtr_check.stateChanged.connect(self.set_dtr)
        self.rts_check = QCheckBox('RTS')
        self.rts_check.stateChanged.connect(self.set_rts)
        
        ctrl_btn_layout.addWidget(self.open_btn)
        ctrl_btn_layout.addWidget(self.dtr_check)
        ctrl_btn_layout.addWidget(self.rts_check)
        port_layout.addLayout(ctrl_btn_layout)

        port_group.setLayout(port_layout)
        control_layout.addWidget(port_group)

        # 接收选项
        rx_group = QGroupBox("接收设置")
        rx_layout = QVBoxLayout()
        
        self.hex_display = QCheckBox('十六进制显示')
        self.hex_display.stateChanged.connect(self.update_receive_display)
        
        self.timestamp_check = QCheckBox('显示时间戳')
        # self.timestamp_check.setChecked(True)
        
        self.terminal_mode = QCheckBox('自动滚动')
        self.terminal_mode.setChecked(True)
        self.terminal_mode.stateChanged.connect(self.toggle_terminal_mode)
        
        self.clear_rx_btn = QPushButton('清空接收区')
        self.clear_rx_btn.clicked.connect(self.clear_receive)
        
        rx_layout.addWidget(self.hex_display)
        rx_layout.addWidget(self.timestamp_check)
        rx_layout.addWidget(self.terminal_mode)
        rx_layout.addWidget(self.clear_rx_btn)
        rx_group.setLayout(rx_layout)
        control_layout.addWidget(rx_group)

        # 发送选项
        tx_group = QGroupBox("发送设置")
        tx_layout = QVBoxLayout()
        
        self.hex_send = QCheckBox('十六进制发送')
        
        self.auto_send_check = QCheckBox('自动发送')
        self.auto_send_check.stateChanged.connect(self.toggle_auto_send)
        self.auto_send_interval = QSpinBox()
        self.auto_send_interval.setRange(100, 10000)
        self.auto_send_interval.setValue(1000)
        self.auto_send_interval.setSuffix('ms')
        
        auto_send_layout = QHBoxLayout()
        auto_send_layout.addWidget(self.auto_send_check)
        auto_send_layout.addWidget(self.auto_send_interval)
        
        self.clear_tx_btn = QPushButton('清空发送区')
        self.clear_tx_btn.clicked.connect(lambda: self.send_text.clear())
        
        tx_layout.addWidget(self.hex_send)
        tx_layout.addLayout(auto_send_layout)
        tx_layout.addWidget(self.clear_tx_btn)
        tx_group.setLayout(tx_layout)
        control_layout.addWidget(tx_group)

        # 自定义命令区域（可折叠）
        self.cmd_group = QGroupBox("自定义命令 ▼")
        self.cmd_group.setCheckable(True)
        self.cmd_group.setChecked(True)
        self.cmd_group.toggled.connect(self.toggle_cmd_panel)
        cmd_layout = QVBoxLayout()
        
        # 命令列表
        self.cmd_scroll = QScrollArea()
        self.cmd_scroll.setWidgetResizable(True)
        self.cmd_scroll_widget = QWidget()
        self.cmd_scroll_layout = QVBoxLayout(self.cmd_scroll_widget)
        self.cmd_scroll_layout.setContentsMargins(2, 2, 2, 2)
        self.cmd_scroll.setWidget(self.cmd_scroll_widget)
        
        # 添加命令控件
        add_cmd_layout = QHBoxLayout()
        self.new_cmd_input = QLineEdit()
        self.new_cmd_input.setPlaceholderText("输入新命令")
        self.add_cmd_btn = QPushButton('添加')
        self.add_cmd_btn.clicked.connect(self.add_custom_command)
        add_cmd_layout.addWidget(self.new_cmd_input)
        add_cmd_layout.addWidget(self.add_cmd_btn)
        
        cmd_layout.addWidget(self.cmd_scroll)
        cmd_layout.addLayout(add_cmd_layout)
        self.cmd_group.setLayout(cmd_layout)
        control_layout.addWidget(self.cmd_group)

        # 添加一些示例命令
        self.add_example_commands()
        
        control_layout.addStretch()
        main_layout.addWidget(control_panel)

        # 右侧通信区域
        comm_panel = QWidget()
        comm_layout = QVBoxLayout(comm_panel)
        comm_layout.setContentsMargins(5, 0, 0, 0)

        # 接收区
        rx_frame = QFrame()
        rx_frame.setFrameShape(QFrame.StyledPanel)
        rx_frame_layout = QVBoxLayout(rx_frame)
        rx_frame_layout.setContentsMargins(0, 0, 0, 0)
        
        self.receive_text = QTextEdit()
        self.receive_text.setReadOnly(True)
        self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
        font = QFont("Courier New", 10)
        self.receive_text.setFont(font)
        
        rx_frame_layout.addWidget(self.receive_text)
        comm_layout.addWidget(rx_frame)

        # 发送区
        tx_frame = QFrame()
        tx_frame.setFrameShape(QFrame.StyledPanel)
        tx_frame_layout = QVBoxLayout(tx_frame)
        tx_frame_layout.setContentsMargins(0, 0, 0, 0)
        
        self.send_text = QTextEdit()
        self.send_text.setFont(font)
        
        send_btn_layout = QHBoxLayout()
        self.send_btn = QPushButton('发送')
        self.send_btn.clicked.connect(self.send_data)
        send_btn_layout.addStretch()
        send_btn_layout.addWidget(self.send_btn)
        
        tx_frame_layout.addWidget(self.send_text)
        tx_frame_layout.addLayout(send_btn_layout)
        comm_layout.addWidget(tx_frame)

        main_layout.addWidget(comm_panel, stretch=1)

        # 状态栏
        self.status_bar = self.statusBar()
        self.status_label = QLabel("发送: 0 字节 | 接收: 0 字节")
        self.status_bar.addPermanentWidget(self.status_label)
        self.status_bar.showMessage('就绪')

        # 颜色格式
        self.default_format = QTextCharFormat()
        self.default_format.setForeground(QColor('black'))
        
        self.error_format = QTextCharFormat()
        self.error_format.setForeground(QColor('red'))
        
        self.warning_format = QTextCharFormat()
        self.warning_format.setForeground(QColor('orange'))
        
        self.success_format = QTextCharFormat()
        self.success_format.setForeground(QColor('green'))
        
        self.info_format = QTextCharFormat()
        self.info_format.setForeground(QColor('blue'))

    def add_example_commands(self):
        """添加一些示例命令"""
        example_commands = [
            ("55 55 55 55 55 55", True),
            ("BOUFFALOLAB5555DTR1", False),
            ("BOUFFALOLAB5555DTR0", False),
            ("BOUFFALOLAB5555RTS0", False),
            ("BOUFFALOLAB5555RTS1", False)
        ]
        
        for cmd, is_hex in example_commands:
            self.add_command_to_panel(cmd, is_hex)

    def add_command_to_panel(self, cmd, is_hex=False):
        """添加一个命令到命令面板"""
        cmd_widget = QWidget()
        cmd_layout = QHBoxLayout(cmd_widget)
        cmd_layout.setContentsMargins(0, 0, 0, 0)
        
        cmd_label = QLabel(cmd)
        cmd_label.setWordWrap(True)
        
        hex_check = QCheckBox('HEX')
        hex_check.setChecked(is_hex)
        
        send_btn = QPushButton('发送')
        send_btn.setMaximumWidth(60)
        send_btn.clicked.connect(lambda _, c=cmd, h=hex_check: self.send_custom_command(c, h.isChecked()))
        
        del_btn = QPushButton('×')
        del_btn.setMaximumWidth(30)
        del_btn.clicked.connect(lambda _, w=cmd_widget: self.remove_command(w))
        
        cmd_layout.addWidget(cmd_label, stretch=1)
        cmd_layout.addWidget(hex_check)
        cmd_layout.addWidget(send_btn)
        cmd_layout.addWidget(del_btn)
        
        self.cmd_scroll_layout.addWidget(cmd_widget)

    def remove_command(self, widget):
        """从命令面板移除一个命令"""
        self.cmd_scroll_layout.removeWidget(widget)
        widget.deleteLater()

    def toggle_cmd_panel(self, checked):
        """切换命令面板的展开/折叠状态"""
        if checked:
            self.cmd_group.setTitle("自定义命令 ▼")
        else:
            self.cmd_group.setTitle("自定义命令 ►")

    def refresh_ports(self):
        self.port_combo.clear()
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_combo.addItem(port.device)
        if not ports:
            self.port_combo.addItem('无可用串口')

    def toggle_serial(self):
        if self.serial and self.serial.is_open:
            self.close_serial()
            self.open_btn.setText('打开串口')
            self.status_bar.showMessage('串口已关闭')
        else:
            if self.open_serial():
                self.open_btn.setText('关闭串口')
                self.status_bar.showMessage('串口已打开')

    def open_serial(self):
        port = self.port_combo.currentText()
        if not port or port == '无可用串口':
            QMessageBox.warning(self, '警告', '没有可用的串口!')
            return False

        try:
            baudrate = int(self.baud_combo.currentText())
        except ValueError:
            QMessageBox.warning(self, '警告', '请输入有效的波特率!')
            return False

        # 获取其他串口参数
        bytesize = {
            '5': serial.FIVEBITS,
            '6': serial.SIXBITS,
            '7': serial.SEVENBITS,
            '8': serial.EIGHTBITS
        }.get(self.data_bits_combo.currentText(), serial.EIGHTBITS)
        
        stopbits = {
            '1': serial.STOPBITS_ONE,
            '1.5': serial.STOPBITS_ONE_POINT_FIVE,
            '2': serial.STOPBITS_TWO
        }.get(self.stop_bits_combo.currentText(), serial.STOPBITS_ONE)
        
        parity = {
            '无': serial.PARITY_NONE,
            '奇校验': serial.PARITY_ODD,
            '偶校验': serial.PARITY_EVEN,
            '标记': serial.PARITY_MARK,
            '空格': serial.PARITY_SPACE
        }.get(self.parity_combo.currentText(), serial.PARITY_NONE)

        try:
            self.serial = serial.Serial(
                port=port,
                baudrate=baudrate,
                bytesize=bytesize,
                parity=parity,
                stopbits=stopbits,
                timeout=1
            )
            
            # 设置初始DTR/RTS状态
            self.serial.dtr = self.dtr_check.isChecked()
            self.serial.rts = self.rts_check.isChecked()
            
            self.serial_thread = SerialThread(self.serial)
            self.serial_thread.data_received.connect(self.receive_data)
            self.serial_thread.start()
            
            return True
        except Exception as e:
            QMessageBox.critical(self, '错误', f'无法打开串口: {e}')
            return False

    def close_serial(self):
        self.toggle_auto_send(False)
        
        if self.serial_thread:
            self.serial_thread.stop()
            self.serial_thread.wait()
            self.serial_thread = None
        
        if self.serial and self.serial.is_open:
            self.serial.close()
            self.serial = None

    def receive_data(self, data, timestamp):
        self.receive_count += len(data.encode('utf-8'))
        self.update_status()
        
        cursor = self.receive_text.textCursor()
        cursor.movePosition(QTextCursor.End)
        
        if self.timestamp_check.isChecked():
            timestamp_str = f"[{timestamp}] "
            cursor.insertText(timestamp_str, self.default_format)
        
        # 智能颜色判断
        fmt = self.determine_text_format(data)
        
        if self.hex_display.isChecked():
            hex_data = ' '.join([f'{ord(c):02X}' for c in data])
            cursor.insertText(hex_data, fmt)
        else:
            cursor.insertText(data, fmt)
        
        if self.terminal_mode.isChecked():
            cursor.movePosition(QTextCursor.End)
            self.receive_text.setTextCursor(cursor)

    def determine_text_format(self, text):
        # 根据内容判断使用哪种颜色格式
        text_lower = text.lower()
        '''
        if re.search(r'error|fail|failed|err|非法|错误', text_lower):
            return self.error_format
        elif re.search(r'warn|alert|警告|注意', text_lower):
            return self.warning_format
        elif re.search(r'success|ok|完成|成功', text_lower):
            return self.success_format
        elif re.search(r'info|version|信息|版本', text_lower):
            return self.info_format
        else:
            return self.default_format
        '''
        return self.default_format

    def send_data(self):
        if not self.serial or not self.serial.is_open:
            QMessageBox.warning(self, '警告', '请先打开串口!')
            return

        text = self.send_text.toPlainText()
        if not text:
            return

        try:
            if self.hex_send.isChecked():
                # 处理十六进制发送
                text = text.strip()
                hex_bytes = []
                for part in text.split():
                    try:
                        hex_bytes.append(int(part, 16))
                    except ValueError:
                        QMessageBox.warning(self, '警告', f'无效的十六进制数据: {part}')
                        return
                data = bytes(hex_bytes)
            else:
                data = text.encode('utf-8')
            
            self.serial.write(data)
            self.send_count += len(data)
            self.update_status()
            self.status_bar.showMessage(f'已发送 {len(data)} 字节', 2000)
            
        except Exception as e:
            QMessageBox.critical(self, '错误', f'发送失败: {e}')

    def update_receive_display(self):
        # 当切换十六进制显示时，更新显示内容
        if self.serial and self.serial.is_open:
            current_text = self.receive_text.toPlainText()
            if current_text:
                if self.hex_display.isChecked():
                    hex_data = ' '.join([f'{ord(c):02X}' for c in current_text if not c.isspace()])
                    self.receive_text.setPlainText(hex_data)
                else:
                    try:
                        # 尝试将十六进制数据转换回字符
                        bytes_data = bytes.fromhex(current_text.replace(' ', ''))
                        text = bytes_data.decode('utf-8', errors='ignore')
                        self.receive_text.setPlainText(text)
                    except:
                        # 如果不是有效的十六进制数据，保持原样
                        pass

    def toggle_terminal_mode(self, state):
        if state:
            self.receive_text.setLineWrapMode(QTextEdit.NoWrap)
            cursor = self.receive_text.textCursor()
            cursor.movePosition(QTextCursor.End)
            self.receive_text.setTextCursor(cursor)
        else:
            self.receive_text.setLineWrapMode(QTextEdit.WidgetWidth)

    def set_dtr(self, state):
        if self.serial and self.serial.is_open:
            self.serial.dtr = state == Qt.Checked

    def set_rts(self, state):
        if self.serial and self.serial.is_open:
            self.serial.rts = state == Qt.Checked

    def toggle_auto_send(self, state):
        if state:
            interval = self.auto_send_interval.value()
            self.auto_send_timer = QTimer()
            self.auto_send_timer.timeout.connect(self.send_data)
            self.auto_send_timer.start(interval)
        else:
            if self.auto_send_timer:
                self.auto_send_timer.stop()
                self.auto_send_timer = None

    def add_custom_command(self):
        cmd = self.new_cmd_input.text().strip()
        if cmd:
            self.add_command_to_panel(cmd)
            self.new_cmd_input.clear()

    def send_custom_command(self, cmd, is_hex):
        if not self.serial or not self.serial.is_open:
            QMessageBox.warning(self, '警告', '请先打开串口!')
            return
            
        # 保存当前状态
        current_hex = self.hex_send.isChecked()
        
        # 使用命令的HEX设置
        self.hex_send.setChecked(is_hex)
        
        # 设置发送文本并发送
        self.send_text.setPlainText(cmd)
        self.send_data()
        
        # 恢复原状态
        self.hex_send.setChecked(current_hex)

    def clear_receive(self):
        self.receive_text.clear()
        self.receive_count = 0
        self.update_status()

    def update_status(self):
        self.status_label.setText(f"发送: {self.send_count} 字节 | 接收: {self.receive_count} 字节")

    def closeEvent(self, event):
        self.close_serial()
        event.accept()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    debugger = SerialDebugger()
    debugger.show()
    sys.exit(app.exec_())
