Python で実装された小型かつ超軽量の mqtt クライアント ツールの共有 (python+tkinter+paho.mqtt)

mqtt プロトコルをデバッグする場合、クライアント ツールが必要ですが、オンラインで見つかるパッケージはすべて非常に大きく、小さくなく移植性も十分ではありません。そこで、週末に Python を使用してクライアント ツールを構築しました。これは、python+tinker+paho.mqtt を使用して実装されました。ソースコードは非常に小さいですが、機能は弱くなく、非常に軽量です。必要としている友達と共有してください。気に入ったら、クリックして収集してください。

序文

Python を使用してクロスプラットフォームの MQTT クライアント ツールを実装し、Python の MQTT クライアント ライブラリ paho.mqtt の使用を紹介します。ここでのインターフェースはPythonに付属するtkinterを使用しており、あまり使いやすいとは言えませんが、非常に軽量でツールを構築するには十分です。また、ttkbootstrap パッケージを使用すると、インターフェイスを美化したり、スキンを変更したりできるため、非常に優れています。ただし、インターフェイスが特に複雑な場合は、pyqt をお勧めします。

ツールのダウンロード アドレス: https://download.csdn.net/download/qq8864/88351834

インターフェース効果:

環境整備

python3 を使用して実装された mqtt クライアント ツールは非常に軽量です。ソース コードは 1 つのファイルにすぎません。

依存関係パッケージをインストールする必要があります。

pip install ttkbootstrap
pip install -i https://pypi.doubanio.com/simple paho-mqtt

tkinter は Python に付属する標準の GUI ライブラリで、日常的に使用する小さなプログラムを作成するのに非常に適しています。tkinter は他の強力な gui ライブラリ (PyQT、WxPython など) よりもシンプルで便利で、学習がはるかに簡単であるため、小さなツールを作成するのは非常に良いですが、作成されるインターフェイスはあまり見栄えがよくありません。

ttkbootstrap導入

ttkbootstrapは、 に基づく Python ライブラリでありtkinterttk美しいグラフィカル ユーザー インターフェイス (GUI) アプリケーションの作成に使用できる最新のテーマとスタイルのセットを提供します。これはBootstrapフレームワークの設計スタイルに基づいており、tkinterアプリケーションに一貫した外観とユーザー エクスペリエンスを提供します。

公式ドキュメント: ttkbootstrap - ttkbootstrap

ttkbootstrapライブラリの簡単な例、テーマの選択: 

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
root = ttk.Window()
root.geometry("500x400+500+150")  
style = ttk.Style()
theme_names = style.theme_names()#以列表的形式返回多个主题名
theme_selection = ttk.Frame(root, padding=(10, 10, 10, 0))
theme_selection.pack(fill=X, expand=YES)
lbl = ttk.Label(theme_selection, text="选择主题:")
theme_cbo = ttk.Combobox(
        master=theme_selection,
        text=style.theme.name,
        values=theme_names,
)
theme_cbo.pack(padx=10, side=RIGHT)
theme_cbo.current(theme_names.index(style.theme.name))
lbl.pack(side=RIGHT)
def change_theme(event):
    theme_cbo_value = theme_cbo.get()
    style.theme_use(theme_cbo_value)
    theme_selected.configure(text=theme_cbo_value)
    theme_cbo.selection_clear()
theme_cbo.bind('<<ComboboxSelected>>', change_theme)
theme_selected = ttk.Label(
        master=theme_selection,
        text="litera",
        font="-size 24 -weight bold"
)
theme_selected.pack(side=LEFT)
root.mainloop()

ttkbootstrapは使いやすいです

import ttkbootstrap as ttk
#实例化创建应用程序窗口
root = ttk.Window(
        title="窗口名字",        #设置窗口的标题
        themename="litera",     #设置主题
        size=(1066,600),        #窗口的大小
        position=(100,100),     #窗口所在的位置
        minsize=(0,0),          #窗口的最小宽高
        maxsize=(1920,1080),    #窗口的最大宽高
        resizable=None,         #设置窗口是否可以更改大小
        alpha=1.0,              #设置窗口的透明度(0.0完全透明)
        )
# root.place_window_center()    #让显现出的窗口居中
# root.resizable(False,False)   #让窗口不可更改大小
# root.wm_attributes('-topmost', 1)#让窗口位置其它窗口之上
root.mainloop()

ラベル表示:

import ttkbootstrap as ttk
from ttkbootstrap.constants import *
root = ttk.Window()
ttk.Label(root,text="标签1",bootstyle=INFO).pack(side=ttk.LEFT, padx=5, pady=10)
ttk.Label(root,text="标签2",bootstyle="inverse").pack(side=ttk.LEFT, padx=5, pady=10)
ttk.Label(root,text="标签3",bootstyle="inverse-danger").pack(side=ttk.LEFT, padx=5, pady=10)
ttk.Label(root, text="标签4", bootstyle=WARNING, font=("微软雅黑", 15), background='#94a2a4').pack(side=LEFT, padx=5, pady=10)
root.mainloop()
'''
# bootstyle colors
PRIMARY = 'primary'
SECONDARY = 'secondary'
SUCCESS = 'success'
DANGER = 'danger'
WARNING = 'warning'
INFO = 'info'
LIGHT = 'light'
DARK = 'dark'

# bootstyle types
OUTLINE = 'outline'
LINK = 'link'
TOGGLE = 'toggle'
INVERSE = 'inverse'
STRIPED = 'striped'
TOOLBUTTON = 'toolbutton'
ROUND = 'round'
SQUARE = 'square'
'''

 paho-mqtt ライブラリの紹介

paho-mqtt は、Python で最も一般的に使用される MQTT クライアント ライブラリであり、Python 2.7 または 3.x のクライアント クラスに対して MQTT v3.1 および v3.1.1 のサポートを提供します。また、MQTT サーバーへのメッセージのパブリッシュを非常に簡単にするいくつかのヘルパー関数も提供します。 

paho-mqtt の使い方は簡単です。

# -*- coding: utf-8 -*-# -*- coding: utf-8 -*-
 
 
import paho.mqtt.client as mqtt
import time
 
 
def on_connect(client, userdata, flags, rc):
    print "链接"
    print("Connected with result code: " + str(rc))
 
 
def on_message(client, userdata, msg):
    print "消息内容"
    print(msg.topic + " " + str(msg.payload))
 
 
#   订阅回调
def on_subscribe(client, userdata, mid, granted_qos):
    print "订阅"
    print("On Subscribed: qos = %d" % granted_qos)
    pass
 
 
#   取消订阅回调
def on_unsubscribe(client, userdata, mid, granted_qos):
    print "取消订阅"
    print("On unSubscribed: qos = %d" % granted_qos)
    pass
 
 
#   发布消息回调
def on_publish(client, userdata, mid):
    print "发布消息"
    print("On onPublish: qos = %d" % mid)
    pass
 
 
#   断开链接回调
def on_disconnect(client, userdata, rc):
    print "断开链接"
    print("Unexpected disconnection rc = " + str(rc))
    pass
 
 
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.on_publish = on_publish
client.on_disconnect = on_disconnect
client.on_unsubscribe = on_unsubscribe
client.on_subscribe = on_subscribe
client.connect('127.0.0.1', 1883, 600) # 600为keepalive的时间间隔
while True:
    client.publish(topic='mqtt11', payload='amazing', qos=0, retain=False)
    time.sleep(2)

パラメータの説明:

  • keepalive => ハートビート間隔、単位は秒です。この期間中にブローカーとクライアントが通信しない場合、クライアントはブローカーに ping メッセージを送信します。
  • 保持  => True に設定すると、このメッセージは保持メッセージとして設定されます。 
  • payload   => メッセージの内容、文字列型。None に設定すると、長さ 0 のメッセージが送信されます。int または float 型の値が設定されている場合、文字列として送信されます。実際の int または float 値を送信したい場合は、struct.pack() を使用してメッセージを生成する必要があります。MQTT のパブリッシュは None のみをサポートします。 string, int. float 型データの場合、json 型データを送信する必要がある場合は、json.dumps() を通じてデータを変換して送信することができ、受信側は on_message 内の json.loads() を通じてデータを解析できます。 () コールバック関数。
  • topic => このメッセージが属するトピック
  • qos   => メッセージ セキュリティ レベル Qos の詳細な紹介
  • qos=0    QoS0、最大 1 回、最大 1 回。
  • QoS0 は、送信者が送信したメッセージを受信者が最大 1 回受信できることを表します。つまり、送信者はメッセージを受信者に送信するために最善を尽くします。送信に失敗した場合は、送信を忘れてください。
  • qos=1    QoS1、少なくとも 1 回、少なくとも 1 回。
  • QoS1 は、Sender が送信したメッセージを Receiver が少なくとも 1 回受信できることを意味します。これは、Sender が Receiver にメッセージを送信することを意味します。送信に失敗した場合、Receiver がメッセージを受信するまで再試行を続けます。ただし、再送信により、受信者は重複メッセージを受信する可能性があります。
  • qos=2    QoS2、1 回だけ、1 回だけ保証
  • QoS2 は、送信者によって送信されたメッセージが受信可能であり、一度だけ受信できることを受信者が保証することを表します。つまり、送信者は受信者にメッセージを送信するために最善を尽くします。送信が失敗した場合は、送信が失敗するまで再試行を続けます。受信者がメッセージを受信しないようにしながら、受信者はメッセージを受信します。メッセージの再送信により、重複したメッセージが受信されます。

ソースコードの実装

 ソース コードの実装は比較的単純ですが、これは主にいくつかの注意事項があるためです。たとえば、画像の表示に関しては、tkinter で画像を表示できないという問題がインターネット上で多く報告されています。絶対に実現可能な方法をいくつか紹介します。

ラベルのイメージ表示

from PIL import Image, ImageTk       
#.....
        img = Image.open("me.jpg")  # 替换为你的图片路径
        img = img.resize((80,80))
        #self._img = ImageTk.PhotoImage(file = "me.jpg")    
        self._img = ImageTk.PhotoImage(img)     
        self.about = Label(self.fr1) 
        self.about.image = self._img
        self.about.configure(image=self._img)
        self.about.place(x=65,y=0,width=80,height=80)

16進数かどうかを判断する

def ISHEX(data):        #判断输入字符串是否为十六进制
    if len(data)%2:
        return False
    for item in data:
        if item not in '0123456789ABCDEFabcdef': #循环判断数字和字符
            return False
    return True

文書を保存する

def savefiles(self):   #保存日志TXT文本
        try:
            with open('log.txt','a') as file:       #a方式打开 文本追加模式
                file.write(self.txt_rx.get(0.0,'end'))
                messagebox.showinfo('提示', '保存成功')
        except:
            messagebox.showinfo('错误', '保存日志文件失败!')

 最後まで自動的にスクロールします

def appendTxt(self,msg):
        current_t = datetime.now()
        current_ = current_t.strftime("%Y-%m-%d %H:%M:%S ")
        self.txt_rx.insert(END,current_)
        self.txt_rx.insert(END,msg)
        self.txt_rx.insert(END,"\n")  
        #滚动到末尾
        self.txt_rx.see(END)
        self.txt_rx.update_idletasks()      

MQTTの使用

def connect(self,addr,port,alive=60):
        self.client.connect(addr, port,alive)
        self.client.loop_start()

    def disconnect(self):
        self.client.loop_stop()
        self.client.disconnect()
        print("disconnect!")

    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            print("Connected to MQTT Broker ok!\n")
            self.appendTxt("Connected to MQTT Broker ok!\n")
            self.var_bt1.set("断开")
            self.isConnect = True
        else:
            print("Failed to connect, return code %d\n", rc)
            self.appendTxt(f"Failed to connect, return code: {rc}\n")
            self.isConnect = False

    def on_message(self, client, userdata, msg):
        self.tx_rx_cnt(1,0)
        print("Received message: " + msg.payload.decode())
        self.appendTxt(f"Received message:\n[topic]:{msg.topic}\n{msg.payload.decode()}\n")

    def subscribe(self, topic):
        #item = Entry(self.subitem).get()
        if topic in self.subitem.get(0, END):
            print("item already exists.")
        else:
            self.appendTxt(f"[订阅topic]:{topic}\n")
            self.client.subscribe(topic)
            self.subitem.insert(END, topic)

    def publish(self, topic, message):
        self.client.publish(topic, message)
        self.appendTxt(f"[发布topic]:{topic}\n{message}\n")

exeプログラムとしてパッケージ化

Python スクリプトを exe にパッケージ化する方法はたくさんあります。一般的に使用されるのは pyinstaller で、使い方は簡単ですが、パッケージ サイズが大きく、起動速度が遅いなど、多くの欠点があります。したがって、ここでは推奨されません。nuitka を使用することをお勧めします。

pyinstallerのパッケージ化

#pyinstaller方式打包
pyinstaller -F -w -i chengzi.ico py_word.py 打包指定exe图标打包

注意:上記は簡易的なパッケージ化ですので、画像やファイルリソースを含めた場合、パッケージ化後の実行も失敗します。 

画像リソースを使用したパッケージ化方法:

pyi-makespec -F .\mqttclienttool.py

上記のコマンドを実行すると、編集可能な mqttclienttool.spec ファイルが生成されます。このファイルを開いて編集し、

data オプションに追加: datas=[('me.jpg', '.')]、括弧内の記述方法、1 つ目はファイル名、2 つ目はパスです。もう 1 つ注意すべき点は、コード内の参照パスは次のような絶対パスである必要があることです。

img_path = os.path.join(os.path.dirname(__file__), 'me.jpg')
img = Image.open(img_path) 
# -*- mode: python ; coding: utf-8 -*-


block_cipher = None


a = Analysis(
    ['mqttclienttool.py'],
    pathex=[],
    binaries=[],
    datas=[('me.jpg', '.')],
    hiddenimports=[],
    hookspath=[],
    hooksconfig={},
    runtime_hooks=[],
    excludes=[],
    win_no_prefer_redirects=False,
    win_private_assemblies=False,
    cipher=block_cipher,
    noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

最後に以下のコマンドを実行します。

 pyinstaller  .\mqttclienttool.spec

nuitkaのインストール

  • pip を使用して直接インストールできます。pip install Nuitka

  • (MSVS) や MinGW64 などの C++ コンパイラが必要です。

nuitkaの使用プロセス

多くのサードパーティ依存関係を持つプロジェクト (torch、tensorflow、cv2、numpy、pandas、geopy など) をパッケージ化する最善の方法は、これらの大規模なサードパーティに関係なく、独自のコードのみを C++ に変換することです。パッケージ!

使用例:

nuitka --standalone --show-memory --show-progress --nofollow-imports --plugin-enable=qt-plugins --follow-import-to=utils,src --output-dir=out --windows-icon-from-ico=./logo.ico ./demo.py

上記の nuitka コマンドを簡単に紹介します。

  • --standalone: Pythonをインストールする必要がなく、他のマシンへの移植に便利です

  • --show-memory --show-progress: インストールプロセス全体の進行状況を表示します。

  • --nofollow-imports: keras、numpy などのコード内のすべてのインポートをコンパイルしないでください。

  • --plugin-enable=qt-plugins: pyqt5 がインターフェースとして使用されている場合、nuitka には対応するプラグインがあります。

  • --plugin-enable=qt-plugins

  • --follow-import-to=utils,src: ここでは、C++ コードにコンパイルする必要があるソース コードを含む 2 つの指定されたフォルダーが分離に,使用されます。

  • --output-dir=out: 出力結果のパスを out に指定します。

  • --windows-icon-from-ico=./logo.ico:生成されたexeのアイコンをlogo.icoとして指定します ここでは画像をico形式ファイルに変換してくれるサイト(Bit Chong)をオススメします。

  • --windows-disable-console: exe を実行してポップアップ ボックスをキャンセルします。DOS ウィンドウをポップアップ表示して印刷ログを表示できるようにするには、まだデバッグする必要があるため、ここには含まれていません。

完全な実装

# -*- coding: utf-8 -*-
# @Time : 2023/09/17 12:49
# @Author : yangyongzhen
# @Email : [email protected]
# @File : mqttclienttool.py
# @Project : study
import time
import os
from tkinter.ttk import *
from tkinter import *
from datetime import datetime
import time
import threading
from tkinter import messagebox
from ttkbootstrap import Style
import paho.mqtt.client as mqtt
from PIL import Image, ImageTk

global gui           #全局型式保存GUI句柄

tx_cnt=0 #发送条数统计
rx_cnt=0 #接收条数统计

def ISHEX(data):        #判断输入字符串是否为十六进制
    if len(data)%2:
        return False
    for item in data:
        if item not in '0123456789ABCDEFabcdef': #循环判断数字和字符
            return False
    return True

'''GUI'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
class GUI:
    def __init__(self):
        self.root = Tk()
        self.root.title('MQTT调试助手-author:blog.csdn.net/qq8864')             #窗口名称
        self.root.geometry("820x560+500+150")         #尺寸位置
        self.root.resizable(False, False)
        self.interface()
        Style(theme='pulse') #主题修改 可选['cyborg', 'journal', 'darkly', 'flatly' 'solar', 'minty', 'litera', 'united', 'pulse', 'cosmo', 'lumen', 'yeti', 'superhero','sandstone']
        #self.client.on_log = self.log_callback
        self.isConnect = False
        self._img = None
    def interface(self):
        """"界面编写位置"""
        #--------------------------------操作区域-----------------------------#
        self.fr1=Frame(self.root)
        self.fr1.place(x=0,y=0,width=220,height=600)     #区域1位置尺寸
        img_path = os.path.join(os.path.dirname(__file__), 'me.jpg')
        img = Image.open(img_path)  # 替换为你的图片路径
        img = img.resize((80,80))
        #self._img = ImageTk.PhotoImage(file = "me.jpg")    
        self._img = ImageTk.PhotoImage(img)     
        self.about = Label(self.fr1) 
        self.about.image = self._img
        self.about.configure(image=self._img)
        self.about.place(x=65,y=0,width=80,height=80)
        pos = 80
        self.lb_server =Label(self.fr1, text='地址:',anchor="e",fg='red')  #点击可刷新
        self.lb_server.place(x=0,y=pos,width=50,height=35)
        self.txt_server = Text(self.fr1)
        self.txt_server.place(x=65,y=pos,width=155,height=26)
        self.txt_server.insert("1.0", "127.0.0.1")
        
        self.lb1 =Label(self.fr1, text='端口:',anchor="e",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=pos+40,width=50,height=35)
        self.txt_port = Text(self.fr1)
        self.txt_port.place(x=65,y=pos+40,width=155,height=26)
        self.txt_port.insert("1.0", 1883)
        
        self.lb1 =Label(self.fr1, text='clientID:',anchor="e",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=pos+80,width=50,height=35)
        self.txt_id = Text(self.fr1)
        self.txt_id.place(x=65,y=pos+80,width=155,height=26)
        self.txt_id.insert("1.0", "mqtt-client")
        
        self.lb1 =Label(self.fr1, text='用户名:',anchor="e",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=pos+120,width=50,height=35)
        self.txt_name = Text(self.fr1)
        self.txt_name.place(x=65,y=pos+120,width=155,height=26)


        self.lb1 =Label(self.fr1, text='密码 :',anchor="e",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=pos+160,width=50,height=35)
        self.txt_pwd = Text(self.fr1)
        self.txt_pwd.place(x=65,y=pos+160,width=155,height=26)
        
        self.lb1 =Label(self.fr1, text='心跳 :',anchor="e",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=pos+200,width=50,height=35)
        self.txt_heart = Text(self.fr1)
        self.txt_heart.place(x=65,y=pos+200,width=155,height=26)
        self.txt_heart.insert("1.0", 60)

        self.var_bt1 = StringVar()
        self.var_bt1.set("连接")
        self.btn1 = Button(self.fr1,textvariable=self.var_bt1,command=self.btn_connect) #绑定 btn_connect 方法
        self.btn1.place(x=170,y=pos+240,width=50,height=30)


        self.lb_s =Label(self.fr1, text='订阅主题',bg="yellow",anchor='w') #字节统计
        self.lb_s.place(x=5,y=340,width=90,height=28)
        
        self.txt_sub = Text(self.fr1)
        self.txt_sub.place(x=5,y=368,width=155,height=28)
        self.btn5 = Button(self.fr1, text='订阅',command=self.btn_sub) #测试用
        self.btn5.place(x=170,y=368,width=50,height=28)
    

        self.subitem = Listbox(self.fr1)
        self.subitem.place(x=5,y=402,width=215,height=85)
        #self.subitem.insert(END, "This is a read-only Text widget.")
        self.subitem.bind("<Button-3>", self.on_right_click)
        

        #-------------------------------文本区域-----------------------------#
        self.fr2=Frame(self.root)          #区域1 容器  relief   groove=凹  ridge=凸
        self.fr2.place(x=220,y=0,width=620,height=560)     #区域1位置尺寸

        self.txt_rx = Text(self.fr2)
        self.txt_rx.place(relheight=0.6,relwidth=0.9,relx=0.05,rely=0.01) #比例计算控件尺寸和位置
        
        self.scrollbar = Scrollbar(self.txt_rx)
        self.scrollbar.pack(side=RIGHT, fill=Y)
        self.txt_rx.config(yscrollcommand=self.scrollbar.set)
        self.scrollbar.config(command=self.txt_rx.yview)
        self.txt_rx.bind("<Configure>", self.check_scrollbar)

        self.lb_t =Label(self.fr2, text='发布主题',bg="yellow",anchor='w') #字节统计
        self.lb_t.place(relheight=0.04,relwidth=0.2,relx=0.05,rely=0.62)
        
        self.lb_qos =Label(self.fr2, text='QoS:',bg="yellow",anchor='w') #字节统计
        self.lb_qos.place(relheight=0.04,relwidth=0.15,relx=0.15,rely=0.62)
        
        self.var_cb1 = IntVar()
        self.comb1 = Combobox(self.fr2,textvariable=self.var_cb1)
        self.comb1['values'] = [0,1,2] #列出可用等级
        self.comb1.current(0)  # 设置默认选项 0开始
        self.comb1.place(relheight=0.04,relwidth=0.08,relx=0.22,rely=0.615)
        
        self.txt_topic = Text(self.fr2)
        self.txt_topic.place(relheight=0.05,relwidth=0.9,relx=0.05,rely=0.66) #比例计算控件尺寸位置
        
        self.txt_tx = Text(self.fr2)
        self.txt_tx.place(relheight=0.15,relwidth=0.9,relx=0.05,rely=0.72) #比例计算控件尺寸位置

        self.btn6 = Button(self.fr2, text='发送',command=self.btn_send)  #绑定发送方法
        self.btn6.place(relheight=0.06,relwidth=0.11,relx=0.84,rely=0.88)
        
        self.btn3 = Button(self.fr2, text='清空',command = self.txt_clr) #绑定清空方法
        self.btn4 = Button(self.fr2, text='保存',command=self.savefiles) #绑定保存方法
        self.btn3.place(relheight=0.06,relwidth=0.11,relx=0.05,rely=0.88)
        self.btn4.place(relheight=0.06,relwidth=0.11,relx=0.18,rely=0.88)
        
        self.lb3 =Label(self.fr2, text='接收:0    发送:0',bg="yellow",anchor='w') #字节统计
        self.lb3.place(relheight=0.05,relwidth=0.3,relx=0.045,rely=0.945)

        self.lb4 = Label(self.fr2, text=' ', anchor='w',relief=GROOVE)  #时钟
        self.lb4.place(relheight=0.05, relwidth=0.11, relx=0.84, rely=0.945)
#------------------------------------------方法-----------------------------------------------
    def check_scrollbar(self,*args):
        if self.txt_rx.yview() == (0.0, 1.0):
            self.scrollbar.pack_forget()
        else:
            self.scrollbar.place(RIGHT, fill=Y)
            
    def on_right_click(self,w):
        idx = self.subitem.curselection()
        print("Right-Clicked idx:", idx)
        if idx == ():
            return
        selected_item = self.subitem.get(idx)
        print("Right-Clicked on:", selected_item,idx)
        ret = messagebox.askyesno('取消订阅', "取消订阅:\n"+selected_item)
        if ret:
            self.subitem.delete(idx)
            self.client.unsubscribe(selected_item)
            self.appendTxt("取消订阅:"+selected_item)
        
    def gettim(self):#获取时间 未用
            timestr = time.strftime("%H:%M:%S")  # 获取当前的时间并转化为字符串
            self.lb4.configure(text=timestr)  # 重新设置标签文本
            # tim_str = str(datetime.datetime.now()) + '\n'
            # self.lb4['text'] = tim_str
            #self.lb3['text'] = '接收:'+str(rx_cnt),'发送:'+str(tx_cnt)
            self.txt_rx.after(1000, self.gettim)     # 每隔1s调用函数 gettime 自身获取时间 GUI自带的定时函数

    def txt_clr(self):#清空显示
        self.txt_rx.delete(0.0, 'end')  # 清空文本框
        self.txt_tx.delete(0.0, 'end')  # 清空文本框

    def ascii_hex_get(self):#获取单选框状态
        if(self.var_cs.get()):
            return False
        else:
            return True

    def tx_rx_cnt(self,rx=0,tx=0):  #发送接收统计
        global tx_cnt
        global rx_cnt

        rx_cnt += rx
        tx_cnt += tx
        self.lb3['text'] = '接收:'+str(rx_cnt),'发送:'+str(tx_cnt)

    def savefiles(self):   #保存日志TXT文本
        try:
            with open('log.txt','a') as file:       #a方式打开 文本追加模式
                file.write(self.txt_rx.get(0.0,'end'))
                messagebox.showinfo('提示', '保存成功')
        except:
            messagebox.showinfo('错误', '保存日志文件失败!')
    
    def log_callback(self,client, userdata, level, buf):
        print(buf)
        
    def appendTxt(self,msg,flag = None):
        current_t = datetime.now()
        current_ = current_t.strftime("%Y-%m-%d %H:%M:%S ")
        self.txt_rx.insert(END,current_)
        self.txt_rx.insert(END,msg)
        self.txt_rx.insert(END,"\n")  
        #滚动到末尾
        self.txt_rx.see(END)
        self.txt_rx.update_idletasks()      
                
    def connect(self,addr,port,alive=60):
        self.client.connect(addr, port,alive)
        self.client.loop_start()

    def disconnect(self):
        self.client.loop_stop()
        self.client.disconnect()
        print("disconnect!")

    def on_connect(self, client, userdata, flags, rc):
        if rc == 0:
            print("Connected to MQTT Broker ok!\n")
            self.appendTxt("Connected to MQTT Broker ok!\n")
            self.var_bt1.set("断开")
            self.isConnect = True
        else:
            print("Failed to connect, return code %d\n", rc)
            self.appendTxt(f"Failed to connect, return code: {rc}\n")
            self.isConnect = False

    def on_message(self, client, userdata, msg):
        self.tx_rx_cnt(1,0)
        print("Received message: " + msg.payload.decode())
        self.appendTxt(f"Received message:\n[topic]:{msg.topic}\n{msg.payload.decode()}\n","RECV")

    def subscribe(self, topic):
        #item = Entry(self.subitem).get()
        if topic in self.subitem.get(0, END):
            print("item already exists.")
        else:
            self.appendTxt(f"[订阅topic]:{topic}\n")
            self.client.subscribe(topic)
            self.subitem.insert(END, topic)

    def publish(self, topic, message,qos=0):
        self.client.publish(topic, message,qos)
        self.appendTxt(f"[发布topic]:{topic}\n{message}\n")
        
    def btn_connect(self):#连接
        global isConnect
        if self.var_bt1.get() == '连接':
            server = self.txt_server.get("1.0",END).strip()
            port = self.txt_port.get("1.0",END).strip()
            alive = self.txt_heart.get("1.0",END).strip()
            user = self.txt_name.get("1.0",END).strip()
            psd = self.txt_pwd.get("1.0",END).strip()
            cid = self.txt_id.get("1.0",END).strip()
            #用户名密码设置
            if len(user) !=0 :
                self.client.username_pw_set(user, psd)
                
            self.client = mqtt.Client(cid) #MQTT
            self.client.on_connect = self.on_connect
            self.client.on_message = self.on_message
            print("btn connect click: "+server+","+port+",QoS:"+self.comb1.get())
            self.appendTxt(f"连接 {server},port:{port}\n")
            self.connect(server,int(port),int(alive))
        else:
            self.disconnect()
            self.var_bt1.set("连接")
            self.isConnect = False
            self.appendTxt(f"断开连接!\n")
        
    def btn_sub(self):#订阅
        if self.isConnect:
            sub = self.txt_sub.get("1.0",END).strip()
            print("btn sub click,topic: "+sub)
            self.subscribe(sub)
        else:
            messagebox.showinfo('提示', '服务器未连接!')
        
    def btn_send(self):#发布
        if self.isConnect:
            pub_topic = self.txt_topic.get("1.0",END).strip()
            payload = self.txt_tx.get("1.0",END).strip()
            print("btn pub click,topic: "+pub_topic)
            self.publish(pub_topic,payload,int(self.comb1.get()))
            self.tx_rx_cnt(0,1)
        else:
            messagebox.showinfo('提示', '请连接服务器!')

if __name__ == '__main__':
    print('Start...')
    gui = GUI()
    gui.gettim()  #开启时钟
    gui.root.mainloop()
    print('End...')

最後に、Python で実装されたシリアル ポート デバッグ アシスタントのソース コードが添付されています。

import time

from tkinter.ttk import *
from tkinter import *
import datetime
import serial  # 导入模块
import serial.tools.list_ports
import threading
from tkinter import messagebox
from ttkbootstrap import Style



global UART          #全局型式保存串口句柄
global RX_THREAD     #全局型式保存串口接收函数
global gui           #全局型式保存GUI句柄

tx_cnt=0 #发送字符数统计
rx_cnt=0 #接收字符数统计


def ISHEX(data):        #判断输入字符串是否为十六进制
    if len(data)%2:
        return False
    for item in data:
        if item not in '0123456789ABCDEFabcdef': #循环判断数字和字符
            return False
    return True


def uart_open_close(fun,com,bund):  #串口打开关闭控制
    global UART
    global RX_THREAD

    if fun==1:#打开串口
        try:
           UART = serial.Serial(com, bund, timeout=0.2)  # 提取串口号和波特率并打开串口
           if UART.isOpen(): #判断是否打开成功
               lock = threading.Lock()
               RX_THREAD = UART_RX_TREAD('URX1',lock)  #开启数据接收进程
               RX_THREAD.setDaemon(True)               #开启守护进程 主进程结束后接收进程也关闭 会报警告 不知道咋回事
               RX_THREAD.start()
               RX_THREAD.resume()
               return True
        except:
            return False
        return False
    else:                   #关闭串口
        print("关闭串口")
        RX_THREAD.pause()
        UART.close()

def uart_tx(data,isHex=False):          #串口发送数据
    global UART

    try:
        if  UART.isOpen():  #发送前判断串口状态 避免错误
            print("uart_send=" + data)
            gui.tx_rx_cnt(tx=len(data)) #发送计数
            if isHex:   #十六进制发送
                data_bytes = bytes.fromhex(data)
                return UART.write(bytes(data_bytes))
            else:      #字符发送
                return UART.write(data.encode('gb2312'))
    except:#错误返回
        messagebox.showinfo('错误', '发送失败')


class UART_RX_TREAD(threading.Thread):          #数据接收进程 部分重构
    global gui

    def __init__(self, name, lock):
        threading.Thread.__init__(self)
        self.mName = name
        self.mLock = lock
        self.mEvent = threading.Event()

    def run(self): #主函数
        print('开启数据接收\r')
        while True:
            self.mEvent.wait()
            self.mLock.acquire()
            if UART.isOpen():
                rx_buf =  UART.read()
                if len(rx_buf) >0:
                    rx_buf += UART.readall()  #有延迟但不易出错
                    gui.tx_rx_cnt(rx=len(rx_buf))
                    if gui.ascii_hex_get() == False:
                        print('收到hex数据', rx_buf.hex().upper())
                        gui.txt_rx.insert(END,  rx_buf.hex().upper())
                    else:
                        str_data = str(rx_buf, encoding='gb2312')
                        print("串口收到消息:", len(rx_buf), str_data)
                        gui.txt_rx.insert(END,str_data)
                        # self.txt_rx.insert(END,str_data)
            self.mLock.release()
           #time.sleep(3)
    def pause(self): #暂停
        self.mEvent.clear()

    def resume(self):#恢复
        self.mEvent.set()


'''GUI'''''''''''''''''''''''''''''''''''''''''''''''''''''''''
class GUI:
    def __init__(self):
        self.root = Tk()
        self.root.title('梵德觅串口调试助手')             #窗口名称
        self.root.geometry("800x360+500+150")         #尺寸位置
        self.interface()
        Style(theme='pulse') #主题修改 可选['cyborg', 'journal', 'darkly', 'flatly' 'solar', 'minty', 'litera', 'united', 'pulse', 'cosmo', 'lumen', 'yeti', 'superhero','sandstone']


    def interface(self):
        """"界面编写位置"""
        #--------------------------------操作区域-----------------------------#
        self.fr1=Frame(self.root)
        self.fr1.place(x=0,y=0,width=180,height=360)     #区域1位置尺寸

        self.lb1 =Label(self.fr1, text='端口号:',font="微软雅黑",fg='red')  #点击可刷新
        self.lb1.place(x=0,y=5,width=100,height=35)

        self.var_cb1 = StringVar()
        self.comb1 = Combobox(self.fr1,textvariable=self.var_cb1)
        self.comb1['values'] = list(serial.tools.list_ports.comports()) #列出可用串口
        self.comb1.current(0)  # 设置默认选项 0开始
        self.comb1.place(x=10,y=40,width=150,height=30)
        com=list(serial.tools.list_ports.comports())

        print('**********可用串口***********')
        for i in range(0, len(com)):
            print(com[i])
        print('***************************')

        self.lb2 = Label(self.fr1, text='波特率:')
        self.comb2 = Combobox(self.fr1,values=['2400','9600','57600','115200'])
        self.comb2.current(3)                               #设置默认选项 115200
        self.lb2.place(x=5,y=75,width=60,height=20)
        self.comb2.place(x=10,y=100,width=100,height=25)

        self.var_bt1 = StringVar()
        self.var_bt1.set("打开串口")
        self.btn1 = Button(self.fr1,textvariable=self.var_bt1,command=self.uart_opn_close) #绑定 uart_opn_close 方法
        self.btn1.place(x=10,y=140,width=60,height=30)



        self.var_cs = IntVar()  #定义返回类型
        self.rd1 = Radiobutton(self.fr1,text="Ascii",variable=self.var_cs,value=0,command = self.txt_clr) #选择后清除显示内容
        self.rd2 = Radiobutton(self.fr1,text="Hex",variable=self.var_cs,value=1,command = self.txt_clr)
        self.rd1.place(x=5,y=180,width=60,height=30)
        self.rd2.place(x=5,y=210,width=60,height=30)


        self.btn3 = Button(self.fr1, text='清空',command = self.txt_clr) #绑定清空方法
        self.btn4 = Button(self.fr1, text='保存',command=self.savefiles) #绑定保存方法
        self.btn3.place(x=10,y=260,width=60,height=30)
        self.btn4.place(x=100,y=260,width=60,height=30)

        self.btn5 = Button(self.fr1, text='功能',command=self.ascii_hex_get) #测试用
        self.btn6 = Button(self.fr1, text='发送',command=self.uart_send)  #绑定发送方法
        self.btn5.place(x=10,y=315,width=60,height=30)
        self.btn6.place(x=100,y=315,width=60,height=30)

        #-------------------------------文本区域-----------------------------#
        self.fr2=Frame(self.root)          #区域1 容器  relief   groove=凹  ridge=凸
        self.fr2.place(x=180,y=0,width=620,height=360)     #区域1位置尺寸

        self.txt_rx = Text(self.fr2)
        self.txt_rx.place(relheight=0.6,relwidth=0.9,relx=0.05,rely=0.01) #比例计算控件尺寸和位置

        self.txt_tx = Text(self.fr2)
        self.txt_tx.place(relheight=0.25,relwidth=0.9,relx=0.05,rely=0.66) #比例计算控件尺寸位置

        self.lb3 =Label(self.fr2, text='接收:0    发送:0',bg="yellow",anchor='w') #字节统计
        self.lb3.place(relheight=0.05,relwidth=0.3,relx=0.045,rely=0.925)

        self.lb4 = Label(self.fr2, text=' ', anchor='w',relief=GROOVE)  #时钟
        self.lb4.place(relheight=0.05, relwidth=0.1, relx=0.85, rely=0.935)
#------------------------------------------方法-----------------------------------------------
    def gettim(self):#获取时间 未用
            timestr = time.strftime("%H:%M:%S")  # 获取当前的时间并转化为字符串
            self.lb4.configure(text=timestr)  # 重新设置标签文本
            # tim_str = str(datetime.datetime.now()) + '\n'
            # self.lb4['text'] = tim_str
            self.txt_rx.after(1000, self.gettim)     # 每隔1s调用函数 gettime 自身获取时间 GUI自带的定时函数

    def txt_clr(self):#清空显示
        self.txt_rx.delete(0.0, 'end')  # 清空文本框
        self.txt_tx.delete(0.0, 'end')  # 清空文本框

    def ascii_hex_get(self):#获取单选框状态
        if(self.var_cs.get()):
            return False
        else:
            return True

    def uart_opn_close(self):#打开关闭串口
        if(self.var_bt1.get() == '打开串口'):
          if(uart_open_close(1,str(self.comb1.get())[0:5],self.comb2.get())==True): #传递下拉框选择的参数 COM号+波特率  【0:5】表示只提取COM号字符
             self.var_bt1.set('关闭串口')                             #改变按键内容
             self.txt_rx.insert(0.0, self.comb1.get() + ' 打开成功\r\n')  # 开头插入
          else:
             print("串口打开失败")
             messagebox.showinfo('错误','串口打开失败')
        else:
            uart_open_close(0, 'COM1', 115200) #关闭时参数无效
            self.var_bt1.set('打开串口')

    def uart_send(self): #发送数据
        send_data = self.txt_tx.get(0.0, 'end').strip()
        if self.ascii_hex_get():    #字符发送
            uart_tx(send_data)
        else:
            send_data = send_data.replace(" ", "").replace("\n", "0A").replace("\r", "0D") #替换空格和回车换行
            if(ISHEX(send_data)==False):
                messagebox.showinfo('错误', '请输入十六进制数')
                return
            uart_tx(send_data,True)

    def tx_rx_cnt(self,rx=0,tx=0):  #发送接收统计
        global tx_cnt
        global rx_cnt

        rx_cnt += rx
        tx_cnt += tx
        self.lb3['text'] = '接收:'+str(rx_cnt),'发送:'+str(tx_cnt)

    def savefiles(self):   #保存日志TXT文本
        try:
            with open('log.txt','a') as file:       #a方式打开 文本追加模式
                file.write(self.txt_rx.get(0.0,'end'))
                messagebox.showinfo('提示', '保存成功')
        except:
            messagebox.showinfo('错误', '保存日志文件失败!')


if __name__ == '__main__':
    print('Star...')
    gui = GUI()
    gui.gettim()  #开启时钟
    gui.root.mainloop()
    UART.close()   #结束关闭 避免下次打开错误
    print('End...')

その他のリソース

[Python] tkinter の美しさ - ttkbootstrap: tkinter をより美しくする - Zhihu

Python_python で MQTT を使用する方法 mqtt_liming89 のブログ - CSDN ブログ

Python GUI の tkinter スキン (ttkbootstrap) がウィンドウの美しさを作り出す_tkinter の美しい GUI インターフェイス テンプレート_Qing & Light Blog-CSDN Blog

Python を使用してデータを EMQX に送信する mqtt クライアントを作成するサーバー_python mqtt client_TMS320VC5257H のブログ - CSDN ブログ

Python での MQTT の使用法の詳細な説明 mqtt-paho (簡単な例、コールバック関数、コールバック パラメーター、QOS セキュリティ レベル) とコールバック関数の正しい使用法_mqtt python-CSDN ブログ

Python での MQTT の使用法の詳細な説明 mqtt-paho (簡単な例、コールバック関数、コールバック パラメーター、QOS セキュリティ レベル) とコールバック関数の正しい使用法_mqtt python-CSDN ブログ

Nuitka パッケージング チュートリアル_Cangqionzhiyue のブログ-CSDN ブログ

nuitka を使用して Python コードを exe 実行可能ファイルにパッケージ化する Program_ha_lee のブログ - CSDN ブログ

Python パッケージング ツール入門ガイド Nuitka_スプーン一杯のハチミツ〜のブログ-CSDN ブログ

Nuitka よくある質問集 - Dugu Nine Swords のバグ解消スタイル - Zhihu

Python パッケージングの王様 exe-Nuitka - Zhihu

WinLibs - Windows 用 GCC+MinGW-w64 コンパイラー

Nuitka パッケージング 1、インストールの依存関係-Alibaba Cloud 開発者コミュニティ

Nuitka を使用して py ファイルをパッケージ化する_https://github.com/ccache/ccache/releases/download-CSDN ブログ

おすすめ

転載: blog.csdn.net/qq8864/article/details/132919813