python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python Slint网络调试助手

基于Python与Slint实现的网络调试助手源码分享

作者:小灰灰搞电子

本文分享基于Python与Slint实现的网络调试助手源码,支持TCP客户端/服务器、UDP协议的连接与通信,通过多线程处理数据收发,实时显示连接状态、收发计数及消息历史,界面友好,功能完整,适用于网络开发与调试场景,需要的朋友可以参考下

一、效果展示

二、源码分享

1、工程结构

2、main.py

# -*- coding: utf-8 -*-

import os
import sys
import socket
import threading
import queue
from datetime import datetime, timedelta

import slint

# ═══════════════════════════════════════════════════════
# 全局变量
# ═══════════════════════════════════════════════════════
ui = None
connections = []  # 内部连接数据,包含 socket
tx_count = 0
rx_count = 0
send_history = []
max_history = 5
message_queue = queue.Queue()
server_socket = None
server_thread = None
server_running = False

# ═══════════════════════════════════════════════════════
# 连接管理
# ═══════════════════════════════════════════════════════
def create_connection(protocol_idx: int, ip_addr: str, port_str: str) -> bool:
    """创建新连接"""
    global server_socket, server_thread, server_running

    try:
        port = int(port_str)
        if port < 1 or port > 65535:
            add_message("[错误] 端口号必须在 1-65535 之间", False)
            return False

        if protocol_idx == 0:  # TCP 客户端
            return create_tcp_client(ip_addr, port)
        elif protocol_idx == 1:  # TCP 服务器
            return create_tcp_server(ip_addr, port)
        elif protocol_idx == 2:  # UDP
            return create_udp(ip_addr, port)
        else:
            add_message("[错误] 未知的协议类型", False)
            return False

    except Exception as e:
        add_message(f"[错误] 创建连接失败: {str(e)}", False)
        return False

def create_tcp_client(ip_addr: str, port: int) -> bool:
    """创建 TCP 客户端连接"""
    try:
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        client_socket.settimeout(5.0)
        client_socket.connect((ip_addr, port))
        client_socket.settimeout(None)

        conn_info = {
            "name": f"TCP Client",
            "description": f"{ip_addr}:{port}",
            "connected": True,
            "socket": client_socket,
            "mode": "tcp_client",
            "addr": (ip_addr, port)
        }
        connections.append(conn_info)
        conn_index = len(connections) - 1
        update_connections_ui()

        thread = threading.Thread(
            target=tcp_client_read_loop,
            args=(client_socket, conn_index),
            daemon=True
        )
        thread.start()

        ui.is_connected = True
        ui.status_text = f"TCP 客户端已连接到 {ip_addr}:{port}"
        add_message(f"[系统] 已连接到 {ip_addr}:{port}", False)
        return True

    except Exception as e:
        add_message(f"[错误] TCP 客户端连接失败: {str(e)}", False)
        return False

def create_tcp_server(ip_addr: str, port: int) -> bool:
    """创建 TCP 服务器"""
    global server_socket, server_thread, server_running

    try:
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind((ip_addr, port))
        server_socket.listen(5)
        server_socket.settimeout(1.0)

        server_running = True

        server_thread = threading.Thread(
            target=tcp_server_accept_loop,
            daemon=True
        )
        server_thread.start()

        conn_info = {
            "name": f"TCP Server",
            "description": f"监听 {ip_addr}:{port}",
            "connected": True,
            "socket": server_socket,
            "mode": "tcp_server"
        }
        connections.append(conn_info)
        update_connections_ui()

        ui.is_connected = True
        ui.status_text = f"TCP 服务器正在监听 {ip_addr}:{port}"
        add_message(f"[系统] 服务器已启动,监听 {ip_addr}:{port}", False)
        return True

    except Exception as e:
        add_message(f"[错误] TCP 服务器启动失败: {str(e)}", False)
        if server_socket:
            server_socket.close()
            server_socket = None
        return False

def create_udp(ip_addr: str, port: int) -> bool:
    """创建 UDP 连接"""
    try:
        udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        udp_socket.bind(('', port))
        udp_socket.settimeout(1.0)

        target_addr = (ip_addr, port)

        conn_info = {
            "name": f"UDP",
            "description": f"绑定:{port} 目标:{ip_addr}:{port}",
            "connected": True,
            "socket": udp_socket,
            "mode": "udp",
            "addr": target_addr
        }
        connections.append(conn_info)
        conn_index = len(connections) - 1
        update_connections_ui()

        thread = threading.Thread(
            target=udp_read_loop,
            args=(udp_socket, conn_index),
            daemon=True
        )
        thread.start()

        ui.is_connected = True
        ui.status_text = f"UDP 绑定端口 {port}, 目标 {ip_addr}:{port}"
        add_message(f"[系统] UDP 已绑定端口 {port}, 目标 {ip_addr}:{port}", False)
        return True

    except Exception as e:
        add_message(f"[错误] UDP 绑定失败: {str(e)}", False)
        return False

def disconnect_all():
    """断开所有连接"""
    global server_socket, server_running

    if server_socket:
        server_running = False
        try:
            server_socket.close()
        except:
            pass
        server_socket = None

    for conn in connections:
        if conn.get("socket"):
            try:
                conn["socket"].close()
            except:
                pass
        conn["connected"] = False

    connections.clear()
    update_connections_ui()

    ui.is_connected = False
    ui.status_text = "已断开所有连接"
    ui.selected_conn_index = -1
    add_message("[系统] 已断开所有连接", False)

# ═══════════════════════════════════════════════════════
# 数据发送
# ═══════════════════════════════════════════════════════
def send_data(data: str, is_hex: bool, add_newline: bool) -> bool:
    """发送数据 - 从选中的连接发送"""
    global tx_count

    if not ui.is_connected:
        add_message("[错误] 未建立连接", False)
        return False

    selected_index = int(ui.selected_conn_index)
    if selected_index < 0 or selected_index >= len(connections):
        add_message("[错误] 未选择连接,请先在左侧列表中选择", False)
        return False

    conn = connections[selected_index]
    if not conn.get("connected") or not conn.get("socket"):
        add_message("[错误] 选中的连接已断开", False)
        return False

    sock = conn["socket"]
    mode = conn.get("mode")

    try:
        if is_hex:
            hex_str = data.replace(" ", "").replace("-", "")
            if len(hex_str) % 2 != 0:
                add_message("[错误] HEX 数据长度必须为偶数", False)
                return False
            send_bytes = bytes.fromhex(hex_str)
        else:
            send_bytes = data.encode('utf-8')
            if add_newline:
                send_bytes += b'\r\n'

        if mode == "tcp_client":
            sock.send(send_bytes)
        elif mode == "tcp_server":
            add_message("[错误] TCP 服务器不能直接发送,请选择具体的客户端连接", False)
            return False
        elif mode == "tcp_server_client":
            sock.send(send_bytes)
        elif mode == "udp":
            target_addr = conn.get("addr")
            if target_addr:
                sock.sendto(send_bytes, target_addr)
            else:
                add_message("[错误] UDP 未设置目标地址", False)
                return False
        else:
            add_message("[错误] 未知的连接模式", False)
            return False

        tx_count += len(send_bytes)
        ui.tx_count = tx_count

        if is_hex:
            display_text = " ".join(f"{b:02X}" for b in send_bytes)
        else:
            display_text = data + ("\r\n" if add_newline else "")
        add_message(f"[{conn['name']}] {display_text}", True)
        add_history(data)

        return True

    except ValueError as e:
        add_message(f"[错误] 无效的 HEX 数据: {data}", False)
        return False
    except Exception as e:
        add_message(f"[发送错误] {str(e)}", False)
        return False

# ═══════════════════════════════════════════════════════
# 消息队列处理
# ═══════════════════════════════════════════════════════
def process_message_queue():
    """处理消息队列(在主线程中调用)"""
    global rx_count, tx_count

    while not message_queue.empty():
        try:
            msg = message_queue.get_nowait()
            msg_type = msg[0]

            if msg_type == "rx":
                data, source, rx, tx = msg[1], msg[2], msg[3], msg[4]
                rx_count = rx
                tx_count = tx
                ui.rx_count = rx_count
                ui.tx_count = tx_count

                if ui.recv_hex_mode:
                    display_text = " ".join(f"{b:02X}" for b in data)
                else:
                    try:
                        display_text = data.decode('utf-8', errors='replace')
                    except:
                        display_text = data.decode('latin-1', errors='replace')

                if source:
                    add_message(f"[{source}] {display_text}", False)
                else:
                    add_message(display_text, False)

            elif msg_type == "client_connected":
                client_addr, client_count = msg[1], msg[2]
                add_message(f"[系统] 客户端已连接: {client_addr} (当前 {client_count} 个客户端)", False)
                ui.status_text = f"TCP 服务器 - {client_count} 个客户端已连接"
                update_connections_ui()

            elif msg_type == "client_disconnected":
                client_addr, client_count = msg[1], msg[2]
                add_message(f"[系统] 客户端已断开: {client_addr} (剩余 {client_count} 个客户端)", False)
                ui.status_text = f"TCP 服务器 - {client_count} 个客户端已连接"

            elif msg_type == "conn_closed":
                conn_idx = msg[1]
                if conn_idx < len(connections):
                    connections[conn_idx]["connected"] = False
                    update_connections_ui()
                    has_connected = any(c.get("connected") for c in connections)
                    ui.is_connected = has_connected
                    if not has_connected:
                        ui.status_text = "所有连接已断开"

            elif msg_type == "error":
                text, rx, tx = msg[1], msg[2], msg[3]
                rx_count = rx
                tx_count = tx
                ui.rx_count = rx_count
                ui.tx_count = tx_count
                add_message(f"[错误] {text}", False)

        except queue.Empty:
            break

# ═══════════════════════════════════════════════════════
# 接收线程
# ═══════════════════════════════════════════════════════
def tcp_client_read_loop(client_socket, conn_index):
    """TCP 客户端接收线程"""
    global rx_count

    while True:
        try:
            data = client_socket.recv(4096)
            if data:
                rx_count += len(data)
                message_queue.put(("rx", data, "", rx_count, tx_count))
            else:
                message_queue.put(("conn_closed", conn_index))
                message_queue.put(("error", "连接已关闭", rx_count, tx_count))
                break
        except Exception as e:
            message_queue.put(("conn_closed", conn_index))
            message_queue.put(("error", str(e), rx_count, tx_count))
            break

def tcp_server_accept_loop():
    """TCP 服务器接受连接线程"""
    global rx_count, tx_count

    while server_running and server_socket:
        try:
            client_socket, client_addr = server_socket.accept()
            client_socket.settimeout(None)

            conn_info = {
                "name": f"Client {client_addr[0]}:{client_addr[1]}",
                "description": f"来自 {client_addr[0]}:{client_addr[1]}",
                "connected": True,
                "socket": client_socket,
                "mode": "tcp_server_client",
                "addr": client_addr
            }
            connections.append(conn_info)
            conn_index = len(connections) - 1

            client_count = len([c for c in connections if c.get("mode") == "tcp_server_client"])
            message_queue.put(("client_connected", f"{client_addr[0]}:{client_addr[1]}", client_count))

            thread = threading.Thread(
                target=tcp_server_client_read_loop,
                args=(client_socket, client_addr, conn_index),
                daemon=True
            )
            thread.start()

        except socket.timeout:
            continue
        except Exception as e:
            if server_running:
                message_queue.put(("error", str(e), rx_count, tx_count))
            break

def tcp_server_client_read_loop(client_socket, client_addr, conn_index):
    """TCP 服务器客户端接收线程"""
    global rx_count

    while server_running:
        try:
            data = client_socket.recv(4096)
            if data:
                rx_count += len(data)
                message_queue.put(("rx", data, f"{client_addr[0]}:{client_addr[1]}", rx_count, tx_count))
            else:
                break
        except:
            break

    try:
        client_socket.close()
    except:
        pass

    client_count = len([c for c in connections if c.get("mode") == "tcp_server_client" and c.get("connected")]) - 1
    message_queue.put(("conn_closed", conn_index))
    message_queue.put(("client_disconnected", f"{client_addr[0]}:{client_addr[1]}", client_count))

def udp_read_loop(udp_socket, conn_index):
    """UDP 接收线程"""
    global rx_count

    while True:
        try:
            data, addr = udp_socket.recvfrom(4096)
            if data:
                rx_count += len(data)
                message_queue.put(("rx", data, f"{addr[0]}:{addr[1]}", rx_count, tx_count))
        except socket.timeout:
            continue
        except Exception as e:
            message_queue.put(("conn_closed", conn_index))
            message_queue.put(("error", str(e), rx_count, tx_count))
            break

# ═══════════════════════════════════════════════════════
# UI 更新
# ═══════════════════════════════════════════════════════
def update_connections_ui():
    """更新连接列表 UI"""
    ui_data = []
    for conn in connections:
        ui_data.append({
            "name": str(conn.get("name", "")),
            "description": str(conn.get("description", "")),
            "connected": bool(conn.get("connected", False))
        })
    ui.connections = slint.ListModel(ui_data)

def add_message(text: str, is_sent: bool):
    """添加消息到接收区"""
    timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
    messages = list(ui.messages)
    messages.append({
        "text": text,
        "is_sent": is_sent,
        "timestamp": timestamp
    })
    max_messages = 500
    if len(messages) > max_messages:
        messages = messages[-max_messages:]
    ui.messages = slint.ListModel(messages)

def add_history(text: str):
    """添加到发送历史"""
    global send_history
    if text and text not in send_history:
        send_history.insert(0, text)
        if len(send_history) > max_history:
            send_history = send_history[:max_history]
        update_history_ui()

def update_history_ui():
    """更新历史记录 UI"""
    history_list = [{"text": h} for h in send_history]
    ui.send_history = slint.ListModel(history_list)

# ═══════════════════════════════════════════════════════
# 主程序
# ═══════════════════════════════════════════════════════
def main():
    global ui

    SLINT_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "main.slint")

    try:
        ui_module = slint.load_file(SLINT_PATH)
        MainWindow = ui_module.MainWindow
        ConnectionDialog = ui_module.ConnectionDialog

        ui = MainWindow()

        def on_new_connection():
            dialog = ConnectionDialog()

            def on_connect():
                protocol_idx = dialog.protocol_index
                ip_addr = dialog.ip_address
                port_str = dialog.port_number

                success = create_connection(protocol_idx, ip_addr, port_str)
                if success:
                    dialog.hide()

            def on_cancel():
                dialog.hide()

            dialog.connect_clicked = on_connect
            dialog.cancel_clicked = on_cancel
            dialog.show()

        def on_conn_double_clicked(conn_index):
            conn_index = int(conn_index)
            if conn_index < 0 or conn_index >= len(connections):
                return

            conn = connections[conn_index]
            if conn.get("connected") and conn.get("socket"):
                try:
                    conn["socket"].close()
                except:
                    pass
                conn["connected"] = False
                update_connections_ui()
                add_message(f"[系统] 已断开 {conn['name']}", False)

                has_connected = any(c.get("connected") for c in connections)
                ui.is_connected = has_connected
                if not has_connected:
                    ui.status_text = "所有连接已断开"

        def on_send_message():
            send_text = ui.send_text
            if not send_text.strip():
                return

            selected_idx = int(ui.selected_conn_index)
            if 0 <= selected_idx < len(connections):
                conn = connections[selected_idx]
                ui.status_text = f"正在发送: {conn['name']}"

            send_data(send_text, ui.send_hex_mode, ui.add_newline)

        def on_clear_messages():
            ui.messages = slint.ListModel([])
            ui.tx_count = 0
            ui.rx_count = 0

        def on_disconnect():
            disconnect_all()

        def on_history_selected(index):
            index = int(index)
            if 0 <= index < len(send_history):
                ui.send_text = send_history[index]

        ui.new_connection = on_new_connection
        ui.conn_double_clicked = on_conn_double_clicked
        ui.send_message = on_send_message
        ui.clear_messages = on_clear_messages
        ui.disconnect_conn = on_disconnect
        ui.history_selected = on_history_selected

        ui.messages = slint.ListModel([])
        ui.connections = slint.ListModel([])
        update_history_ui()

        def process_queue():
            process_message_queue()

        queue_timer = slint.Timer()
        queue_timer.start(slint.TimerMode.Repeated, timedelta(milliseconds=50), process_queue)


        ui.run()

    except Exception as e:
        print(f"启动失败: {e}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()




4、main.slint

import { ComboBox, LineEdit, ScrollView } from "std-widgets.slint";

export struct ConnectionInfo { name: string, description: string, connected: bool }
export struct MessageItem { text: string, is-sent: bool, timestamp: string }
export struct HistoryItem { text: string }

component ConnectionItem inherits Rectangle {
    in property <string> conn-name;
    in property <string> conn-desc;
    in property <bool> is-connected;
    in property <bool> is-selected;
    callback clicked();
    callback double-clicked();
    background: item-touch.pressed ? #1e293b : (item-touch.has-hover ? #0e1520 : #0c1420);
    border-radius: 8px;
    border-width: 1px;
    border-color: is-selected ? #00e5ff : #1a2332;
    item-touch := TouchArea {
        clicked => { root.clicked(); }
        double-clicked => { root.double-clicked(); }
    }
    HorizontalLayout {
        padding-left: 12px; padding-right: 12px;
        Rectangle {y:parent.height/2 - self.height/2; width: 10px; height: 10px; border-radius: 5px; background: is-connected ? #00ff88 : #4a5568; }
        VerticalLayout {
            padding-left: 10px; alignment: center;
            Text { text: conn-name; font-size: 13px; font-weight: 600; color: is-connected ? #00e5ff : #e2e8f0; }
            Text { text: conn-desc; font-size: 10px; color: #64748b; overflow: elide; }
        }
    }
}

export component ConnectionDialog inherits Window {
    title: "连接参数";
    preferred-width: 380px; preferred-height: 450px; background: #0a0e1a;
    in-out property <string> title-text: "新建连接";
    callback connect-clicked(); callback cancel-clicked();
    out property <int> protocol-index: protocol-combo-idx;
    out property <string> ip-address: ip-input.text;
    out property <string> port-number: port-input.text;
    property <int> protocol-combo-idx: 1;
    VerticalLayout {
        padding: 20px; spacing: 15px;
        Rectangle { height: 50px; background: #1e293b; border-radius: 8px;
            Text { text: title-text; font-size: 16px; font-weight: 700; color: #00e5ff; horizontal-alignment: center; vertical-alignment: center; }
        }
        Rectangle { vertical-stretch: 1; background: #0c1420; border-radius: 8px;
            VerticalLayout { padding: 20px; spacing: 12px;
                HorizontalLayout { height: 35px; Text { text: "协议:"; width: 80px; color: #94a3b8; vertical-alignment: center; }
                    ComboBox { horizontal-stretch: 1; model: ["TCP 客户端", "TCP 服务器", "UDP"]; current-index <=> protocol-combo-idx; } }
                HorizontalLayout { height: 35px; Text { text: "IP 地址:"; width: 80px; color: #94a3b8; vertical-alignment: center; }
                    ip-input := LineEdit { horizontal-stretch: 1; placeholder-text: "127.0.0.1"; text: "127.0.0.1"; } }
                HorizontalLayout { height: 35px; Text { text: "端口:"; width: 80px; color: #94a3b8; vertical-alignment: center; }
                    port-input := LineEdit { horizontal-stretch: 1; placeholder-text: "8080"; text: "8080"; } }
            }
        }
        HorizontalLayout { height: 45px; alignment: end; spacing: 12px;
            Rectangle { width: 100px; height: 38px; border-radius: 8px; background: #1f2937; border-width: 1px; border-color: #4b5563;
                cancel-touch := TouchArea { clicked => { root.cancel-clicked(); } }
                Text { text: "取消"; color: #9ca3af; horizontal-alignment: center; vertical-alignment: center; } }
            Rectangle { width: 100px; height: 38px; border-radius: 8px; background: #00e5ff;
                connect-touch := TouchArea { clicked => { root.connect-clicked(); } }
                Text { text: "连接"; color: #0a0e1a; font-weight: 600; horizontal-alignment: center; vertical-alignment: center; } }
        }
    }
}

export component MainWindow inherits Window {
    title: "网络调试助手";
    preferred-width: 1100px; preferred-height: 750px;
    min-width: 1000px; min-height: 700px;
    background: #0a0e1a;

    in-out property <[ConnectionInfo]> connections;
    in-out property <int> selected-conn-index: -1;
    in-out property <[MessageItem]> messages:[
        { text: "欢迎使用网络调试助手", is-sent: false, timestamp: "14:55:22" },
        { text: "支持 TCP 客户端、TCP 服务器、UDP 模式", is-sent: false, timestamp: "14:55:22" },
        { text: "点击右下角新建连接按钮开始", is-sent: false, timestamp: "14:55:22" },
    ];
    in-out property <[HistoryItem]> send-history:[];
    in-out property <string> send-text: "";
    in-out property <bool> send-hex-mode: false;
    in-out property <bool> recv-hex-mode: false;
    in-out property <bool> show-timestamp: true;
    in-out property <bool> add-newline: true;
    in-out property <int> tx-count: 0;
    in-out property <int> rx-count: 0;
    in-out property <string> status-text: "就绪";
    in-out property <bool> is-connected: false;

    callback new-connection();
    callback conn-double-clicked(int);
    callback send-message();
    callback clear-messages();
    callback disconnect-conn();
    callback history-selected(int);

    HorizontalLayout {
        padding: 12px; spacing: 12px;

        // 左侧面板
        Rectangle {
            width: 250px;
            background: #0c1420;
            border-radius: 12px;
            VerticalLayout {
                padding: 12px; spacing: 10px;
                HorizontalLayout {
                    height: 32px;
                    Text { text: "连接列表"; font-size: 14px; font-weight: 700; color: #00e5ff; }
                    Rectangle { width: 30px; height: 30px; border-radius: 6px; background: #0e1520;
                        new-touch := TouchArea { clicked => { root.new-connection(); } }
                        Text { text: "+"; font-size: 18px; color: #64748b; horizontal-alignment: center; vertical-alignment: center; }
                    }
                }
                Rectangle { vertical-stretch: 1; background: #080c14; border-radius: 8px;
                    ScrollView {
                        VerticalLayout { spacing: 6px; padding: 8px;
                            for conn[idx] in connections: ConnectionItem {
                                height: 54px; conn-name: conn.name; conn-desc: conn.description;
                                is-connected: conn.connected; is-selected: idx == root.selected-conn-index;
                                clicked => { root.selected-conn-index = idx; }
                                double-clicked => { root.selected-conn-index = idx; root.conn-double-clicked(idx); }
                            }
                        }
                    }

                }
                Rectangle {
                    height: 48px; background: #0e1520; border-radius: 8px;
                    border-width: 1px; border-color: is-connected ? #00ff88 : #1a2332;
                    HorizontalLayout {
                        padding: 10px;
                        width: 100%; height: 100%;
                        Rectangle {
                            y:parent.height/2 - self.height/2;
                            width: 4px;
                            border-radius: 4px;
                            background: is-connected ? #00ff88 : #4a5568;
                        }
                        VerticalLayout { padding-left: 10px; alignment: center;
                            Text { text: is-connected ? "已连接" : "未连接"; font-size: 12px; color: is-connected ? #00ff88 : #64748b; }
                        }
                        if is-connected: Rectangle {
                            width: 55px; height: 26px;
                            border-radius: 6px;
                            background: #dc2626;
                            disc-touch := TouchArea { clicked => { root.disconnect-conn(); } }
                            Text {
                                text: "断开"; font-size: 11px; color: #fff;
                                horizontal-alignment: center; vertical-alignment: center;
                            }
                        }
                    }
                }
            }
        }
        Rectangle {
            VerticalLayout {
                Rectangle {
                    height: 30px;
                    HorizontalLayout {
                        spacing: 5px;
                        padding-left: 10px;
                        padding-right: 10px;
                        Text { text: "接收数据"; font-size: 14px; font-weight: 700; color: #00e5ff;vertical-alignment: center; }
                        Rectangle {
                            y:parent.height/2 - self.height/2; width: 55px; height: 26px; border-radius: 6px; background: recv-hex-mode ? #0891b2 : #1e293b;
                            TouchArea { clicked => { root.recv-hex-mode = !root.recv-hex-mode; } }
                                Text { text: root.recv-hex-mode ? "HEX" : "ASCII"; font-size: 10px; color: #fff; horizontal-alignment: center; vertical-alignment: center; }
                        }
                        Rectangle {
                            y:parent.height/2 - self.height/2; width: 28px; height: 26px; border-radius: 6px; background: show-timestamp ? #0891b2 : #1e293b;
                            TouchArea { clicked => { root.show-timestamp = !root.show-timestamp; } }
                            Text { text: "T"; font-size: 11px; color: #fff; horizontal-alignment: center; vertical-alignment: center; }
                        }
                        Rectangle {
                            y:parent.height/2 - self.height/2; width: 45px; height: 26px; border-radius: 6px; background: #1f2937;
                            clear-touch := TouchArea { clicked => { root.clear-messages(); } }
                            Text { text: "清空"; font-size: 10px; color: #9ca3af; horizontal-alignment: center; vertical-alignment: center; }
                        }
                    }
                }
                Rectangle {
                    ScrollView {
                        viewport-height: recv.preferred-height;
                        viewport-y: max(0px, self.viewport-height - self.height)*-1;
                        recv := VerticalLayout {
                            spacing: 3px;
                            padding: 8px;
                            for msg in messages: Rectangle {
                                height: 25px; background: msg.is-sent ? #0f1f2e : #0a1018; border-radius: 4px;
                                HorizontalLayout { padding-left: 8px; padding-right: 8px; spacing: 8px;
                                    if show-timestamp: Text {
                                        text: msg.timestamp; font-size: 12px;
                                        color: #4a5568; vertical-alignment: center;
                                        font-weight: 700;
                                    }
                                    Text { text: msg.text; font-size: 11px;
                                        color: msg.is-sent ? #22d3ee : #a7f3d0;
                                        vertical-alignment: center; horizontal-stretch: 1;
                                        overflow: elide;
                                    }
                                }
                            }
                        }
                    }
                }
                //发送区
                Rectangle {
                    height: 120px;
                    background: #0c1420; border-radius: 12px;
                    VerticalLayout {
                        spacing: 5px;
                        HorizontalLayout { height: 28px;
                            Text { text: "发送数据"; font-size: 14px; font-weight: 700; color: #00e5ff; }
                            HorizontalLayout { horizontal-stretch: 1; alignment: end; spacing: 8px;
                                Rectangle { width: 55px; height: 24px; border-radius: 6px; background: send-hex-mode ? #0891b2 : #1e293b;
                                    TouchArea { clicked => { root.send-hex-mode = !root.send-hex-mode; } }
                                    Text { text: root.send-hex-mode ? "HEX" : "ASCII"; font-size: 10px; color: #fff; horizontal-alignment: center; vertical-alignment: center; }
                                }
                                Rectangle { width: 45px; height: 24px; border-radius: 6px; background: add-newline ? #0891b2 : #1e293b;
                                    TouchArea { clicked => { root.add-newline = !root.add-newline; } }
                                    Text { text: "换行"; font-size: 10px; color: #fff; horizontal-alignment: center; vertical-alignment: center; }
                                }
                            }
                        }
                        HorizontalLayout { height: 38px; spacing: 8px;
                            Rectangle {
                                border-radius: 8px;
                                border-color: #4a5568; border-width: 1px;
                                TextInput {
                                    x:5px;
                                    color: white; font-size: 12px;
                                    text <=> root.send-text;
                                    vertical-alignment: center;
                                    accepted => { root.send-message(); }
                                }
                            }
                            Rectangle { width: 70px; height: 38px; border-radius: 8px; background: is-connected ? #00e5ff : #2d3748;
                                send-touch := TouchArea { clicked => { if is-connected { root.send-message(); } } }
                                Text { text: "发送"; font-size: 12px; font-weight: 600; color: is-connected ? #0a0e1a : #4a5568; horizontal-alignment: center; vertical-alignment: center; }
                            }
                        }
                        ScrollView {
                            viewport-width: send.preferred-width;
                            viewport-x: max(0px, self.viewport-width - self.width)*-1;
                            send := HorizontalLayout {
                                height: 30px; spacing: 6px;
                                for h[hi] in send-history: Rectangle {
                                    height: 26px;
                                    background: #172d52; border-radius: 6px;
                                    TouchArea { clicked => { root.history-selected(hi); } }
                                    Text { text: h.text; font-size: 10px;
                                         color: #64748b; horizontal-alignment: center;
                                         vertical-alignment: center; overflow: elide;
                                    }
                                }
                            }
                        }
                    }
                }
                // 状态栏
                Rectangle {
                    height: 32px; background: #152338; border-radius: 8px;
                    HorizontalLayout {
                        padding-left: 14px; padding-right: 14px;
                        spacing: 10px;
                        Rectangle {y:parent.height/2 - self.height/2; width: 6px; height: 6px; border-radius: 3px; background: is-connected ? #00ff88 : #4a5568; }
                        Text { text: status-text; font-size: 11px; color: #94a3b8; vertical-alignment: center; }
                        HorizontalLayout { horizontal-stretch: 1; }
                        Text { text: "TX: " + tx-count; font-size: 12px; color: #22d3ee; vertical-alignment: center; }
                        Text { text: "  RX: " + rx-count; font-size: 12px; color: #a7f3d0; vertical-alignment: center; }
                    }
                }
            }
        }
    }
}


三、实现原理

本工具基于 Python + Slint 构建,核心思路是:Python 负责网络通信与业务逻辑,Slint 负责界面渲染与交互,两者通过 Slint 提供的绑定机制进行数据同步。下面从几个关键模块逐一拆解实现原理。

1、整体架构

程序启动后,main() 函数会加载 main.slint 中定义的 MainWindow 组件,并建立 Python 与 UI 之间的双向绑定。UI 上的按钮点击、输入框内容变化等事件,会通过回调(callback)触发 Python 侧的函数;而 Python 侧的网络状态、收发字节数、消息列表等数据,则通过属性(property)同步回 UI 实时刷新。

整体流程如下:

2、连接管理原理

2.1 三种协议的统一抽象

create_connection() 根据用户在 UI 上选择的协议索引(0=TCP 客户端、1=TCP 服务器、2=UDP),分别调用对应的创建函数。所有连接信息统一存入 connections 列表,每条记录包含:

这种统一抽象的好处是:发送数据时只需根据 mode 分支处理,接收线程也只需按模式启动对应的读取循环,代码结构清晰且易于扩展。

2.2 TCP 客户端

create_tcp_client() 创建 socket.socket(AF_INET, SOCK_STREAM),设置 5 秒连接超时后调用 connect() 建立连接。连接成功后:

  1. 将连接信息追加到 connections 列表;
  2. 调用 update_connections_ui() 刷新左侧连接列表;
  3. 启动 tcp_client_read_loop 后台线程持续接收数据。

2.3 TCP 服务器

create_tcp_server() 创建监听 Socket,绑定地址和端口后调用 listen(5) 开始监听,并设置 1 秒超时以便定期检查 server_running 标志。tcp_server_accept_loop 线程循环调用 accept() 接受新客户端:

2.4 UDP

UDP 是无连接协议,create_udp() 创建 SOCK_DGRAM Socket 并绑定本地端口,同时记录目标地址 (ip_addr, port)。udp_read_loop 线程循环调用 recvfrom() 接收数据,发送时通过 sendto() 指定目标地址。

3、数据收发原理

3.1 发送流程

send_data() 是发送数据的核心入口,流程如下:

  1. 校验状态:检查是否已连接、是否选中了有效连接;
  2. 数据编码:若为 HEX 模式,将十六进制字符串转为字节;否则按 UTF-8 编码,可选追加 \r\n 换行;
  3. 按模式发送:TCP 客户端/服务器客户端直接 send(),UDP 使用 sendto() 指定目标地址;
  4. 统计与回显:累加 tx_count 并同步到 UI,同时将发送内容加入消息列表和发送历史。

3.2 接收流程与线程模型

接收数据采用**「后台线程 + 消息队列 + 主线程刷新」**的经典模型,这是本工具的关键设计:

为什么用消息队列而不是直接在线程里改 UI? 因为 Slint 的 UI 属性只能在主线程中安全修改。如果直接在接收线程里操作 ui.messages,会造成线程竞争和界面卡顿。通过消息队列将数据投递到主线程处理,既保证了线程安全,又避免了 UI 阻塞。

4、UI 与 Python 的绑定机制

4.1 属性绑定

main.slint 中通过 in-out property 声明了与 Python 交互的属性,例如:

Python 侧通过 ui.xxx = value 直接赋值即可驱动 UI 刷新,例如 ui.tx_count = tx_count 会立即更新状态栏的 TX 显示。

4.2 回调机制

UI 上的交互事件通过 callback 暴露给 Python,例如:

Python 侧通过 ui.on_xxx(callback) 注册处理函数,实现 UI 事件到业务逻辑的流转。

4.3 列表数据同步

Slint 的列表属性需要使用 slint.ListModel 包装。Python 侧每次更新列表时,都会构造新的 ListModel 并赋值给 UI 属性,例如 update_connections_ui() 和 add_message() 中的做法。这种「整体替换」的方式虽然简单,但对 500 条消息以内的场景性能完全足够。

5、关键设计要点总结

设计点实现方式解决的问题
线程安全消息队列 + 主线程轮询避免多线程直接操作 UI 导致崩溃
多协议支持统一连接抽象 + 模式分支一套 UI 同时支持 TCP/UDP 三种模式
数据格式ASCII / HEX 双模式满足文本与二进制调试需求
发送历史最近 5 条去重记录方便重复发送常用指令
消息上限最多保留 500 条防止长时间运行内存无限增长
服务器多客户端每客户端独立线程支持同时与多个客户端通信

6、运行流程

程序启动后的完整流程如下:

  1. main() 加载 Slint UI,注册回调函数;
  2. 用户点击「+」新建连接,选择协议并填写 IP/端口;
  3. Python 创建对应 Socket,启动接收线程,更新 UI 状态;
  4. 用户输入数据点击发送,send_data() 编码并发送,更新 TX 计数;
  5. 接收线程收到数据放入队列,主线程定时取出并刷新消息区;
  6. 点击「断开」或关闭程序时,disconnect_all() 关闭所有 Socket 并清理资源。

以上就是基于Python与Slint实现的网络调试助手源码分享的详细内容,更多关于Python Slint网络调试助手的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文