用Tkinter实现一个串口调试助手

2020年元旦,武汉出现了不明原因肺炎,搞得人心惶惶,只好宅在家里。闲着也是闲着,于是把很久之前用python写的一个串口调试助手再修改一下。

简单说明一下:

  1. 以前是准备在debian系统里调试单片机用的,基于python 2.6,GUI是用自带Tkinter做的,不用安装第三方库。
  2. 修改了之后,可以跨linux、windows平台(我没有MacOS系统,只能放弃),python 2.6/2.7/3.7都简单测试了一下,暂时只能说都可以用。
  3. 只实现了基本功能,没有自动发送、发送文件、保存等功能,这些以后有时间再增加。
  4. 需要第三方pyserial库,请“pip install pyserial”自行安装。

 

先上一个截图

用Tkinter实现一个串口调试助手

下面是代码,供大家参考(我比较懒,没添加注释)。

serialassistant.py:

# -*- coding: utf-8 -*-
import sys
import serial
import serial.tools.list_ports
import threading
import signal
import binascii

import strings_zh as strings

if sys.version_info.major == 2:
    import Tkinter as tk
    import tkMessageBox as msgbox
elif sys.version_info.major == 3:
    import tkinter as tk
    from tkinter import messagebox as msgbox


WINDOWSIZE = '710x510+50+50'
# Common baudrate
BAUDRATES = (1200, 2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200)
BYTESIZES = (5, 6, 7, 8)
PARITIES = {'None': 'N', 'Even': 'E', 'Odd': 'O', 'Mark': 'M', 'Space': 'S'}
STOPBITS = (1, 1.5, 2)
BYTETYPES = ('1 Byte', '2 Bytes', '4 Bytes')

TIMEOUT = 0.015

font = ("宋体", 10, "normal")
font_text = ("Courier", 9, "normal")


def format_data(data):
        li = data.split(' ')
        result = []
        k = 0
        for each in li:
            if len(each) <= 2:
                result.append(each)

            if len(each) > 2:
                while k < len(each):
                    result.append(each[k: k + 2])
                    k = k + 2
                k = 0

        for i in range(len(result)):
            if len(result[i]) == 1:
                result[i] = '0' + result[i]

        return result


def getAvailabelSerialPort():
    available_ports = []
    coms = serial.tools.list_ports.comports()
    if coms is not None:
        for com in coms:
            if sys.platform == 'win32':
                available_ports.append(com.device)
            elif sys.platform == 'linux2':
                if com[2] != 'n/a':
                    available_ports.append(com[0])
    return tuple(available_ports)


if sys.platform == 'win32':
    PORTS = sorted(getAvailabelSerialPort(), key=lambda n: int(n[3:]))
else:
    PORTS = getAvailabelSerialPort()


class SerialAssistantGUI(object):
    def __init__(self):
        self.root = tk.Tk()
        self.root.title('%s v%s by %s' % (strings.APP_NAME, strings.APP_VER,
                                          strings.AUTHOR))
        self.root.geometry(WINDOWSIZE)
        self.root.resizable(width=False, height=False)

        self.__recv_area()
        self.__send_area()
        self.__cmd_area()
        self.__opt_area()

        self.root.mainloop()

    def __recv_area(self):
        recv_lframe = tk.LabelFrame(self.root,
                                    text=strings.recv_buf_text,
                                    height=250)
        recv_lframe.pack(fill=tk.X, padx=5)

        recv_optframe = tk.Frame(recv_lframe)
        recv_txtframe = tk.Frame(recv_lframe)
        recv_optframe.pack(fill=tk.Y, side=tk.LEFT, padx=5, pady=5)
        recv_txtframe.pack(fill=tk.Y, side=tk.RIGHT, padx=5, pady=5)

        self.recv_mode = tk.IntVar()
        self.recv_mode.set(0)
        recv_radbtn1 = tk.Radiobutton(recv_optframe,
                                      text=strings.text_mode_text,
                                      font=font,
                                      variable=self.recv_mode, value=0)
        recv_radbtn2 = tk.Radiobutton(recv_optframe,
                                      text=strings.hex_mode_text,
                                      font=font,
                                      variable=self.recv_mode, value=1)
        recv_clrbtn = tk.Button(recv_optframe, width=15,
                                text=strings.clear_recv_text,
                                font=font,
                                command=self.clear_received)
        recv_radbtn1.pack(anchor=tk.W)
        recv_radbtn2.pack(anchor=tk.W)
        recv_clrbtn.pack(fill=tk.X)

        self.recv_txtarea = tk.Text(recv_txtframe, height=15, width=74,
                                    font=font_text)
        self.recv_txtarea.pack(side=tk.LEFT, fill=tk.X)
        recv_scrbar = tk.Scrollbar(recv_txtframe)
        recv_scrbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.recv_txtarea['yscrollcommand'] = recv_scrbar.set
        recv_scrbar['command'] = self.recv_txtarea.yview

    def __send_area(self):
        send_lframe = tk.LabelFrame(self.root,
                                    text=strings.send_buf_text,
                                    height=100)
        send_lframe.pack(fill=tk.X, padx=5)

        send_optframe = tk.Frame(send_lframe)
        send_txtframe = tk.Frame(send_lframe)
        send_optframe.pack(fill=tk.Y, side=tk.LEFT, padx=5, pady=5)
        send_txtframe.pack(fill=tk.Y, side=tk.RIGHT, padx=5, pady=5)

        self.send_mode = tk.IntVar()
        self.send_mode.set(0)
        send_radbtn1 = tk.Radiobutton(send_optframe,
                                      text=strings.text_mode_text,
                                      font=font,
                                      variable=self.send_mode, value=0)
        send_radbtn2 = tk.Radiobutton(send_optframe,
                                      text=strings.hex_mode_text,
                                      font=font,
                                      variable=self.send_mode, value=1)
        self.linebreak = tk.IntVar()
        if sys.platform == 'win32':
            self.linebreak.set(1)
        elif sys.platform == 'linux2':
            self.linebreak.set(0)
        send_chkbtn = tk.Checkbutton(send_optframe,
                                     text=strings.line_break_text,
                                     font=font,
                                     variable=self.linebreak)
        send_clrbtn = tk.Button(send_optframe,
                                text=strings.clear_send_text,
                                font=font,
                                width=15,
                                command=self.clear_sent)
        send_radbtn1.pack(anchor=tk.W)
        send_radbtn2.pack(anchor=tk.W)
        send_chkbtn.pack(anchor=tk.W)
        send_clrbtn.pack(fill=tk.X)

        self.send_txtarea = tk.Text(send_txtframe, height=7, width=74,
                                    font=font_text)
        self.send_txtarea.pack(side=tk.LEFT, fill=tk.X)
        send_scrbar = tk.Scrollbar(send_txtframe)
        send_scrbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.send_txtarea['yscrollcommand'] = send_scrbar.set
        send_scrbar['command'] = self.send_txtarea.yview

    def __cmd_area(self):
        cmd_frame = tk.Frame(self.root)
        cmd_frame.pack(fill=tk.X, padx=5, pady=5)
        cmd_btn = tk.Button(cmd_frame,
                            text=strings.send_btn_text,
                            font=font,
                            command=self.send_data)
        cmd_btn.pack(side=tk.LEFT)

    def __opt_area(self):
        opt_frame = tk.Frame(self.root)
        opt_frame.pack(fill=tk.X, padx=5, pady=5)

        # Serial port setting
        port_label = tk.Label(opt_frame,
                              text=strings.port_lbl_text,
                              font=font)

        self.port_var = tk.StringVar()
        self.port_var.set(strings.select_port)

        if len(PORTS) == 0:
            port_opmenu = tk.OptionMenu(opt_frame, self.port_var, '')
        else:
            port_opmenu = tk.OptionMenu(opt_frame, self.port_var, *PORTS)
        port_opmenu.config(anchor=tk.W,
                           width=10,
                           font=font)
        port_label.pack(side=tk.LEFT)
        port_opmenu.pack(side=tk.LEFT)

        # Baudrate setting
        brt_label = tk.Label(opt_frame,
                             text=strings.baud_lbl_text,
                             font=font)
        self.brt_var = tk.StringVar()
        self.brt_var.set(BAUDRATES[3])
        brt_opmenu = tk.OptionMenu(opt_frame, self.brt_var, *BAUDRATES)
        brt_opmenu.config(anchor=tk.W,
                          width=3,
                          font=font)
        brt_label.pack(side=tk.LEFT)
        brt_opmenu.pack(side=tk.LEFT)

        # Bytesize setting
        size_label = tk.Label(opt_frame,
                              text=strings.size_lbl_text,
                              font=font)
        self.size_var = tk.StringVar()
        self.size_var.set(BYTESIZES[3])
        size_opmenu = tk.OptionMenu(opt_frame, self.size_var, *BYTESIZES)
        size_opmenu.config(anchor=tk.W,
                           width=3,
                           font=font)
        size_label.pack(side=tk.LEFT)
        size_opmenu.pack(side=tk.LEFT)

        # Parity setting
        parity_label = tk.Label(opt_frame,
                                text=strings.parity_lbl_text,
                                font=font)
        self.parity_var = tk.StringVar()
        self.parity_var.set('None')
        parity_opmenu = tk.OptionMenu(opt_frame, self.parity_var,
                                      *PARITIES)
        parity_opmenu.config(anchor=tk.W,
                             width=3,
                             font=font)
        parity_label.pack(side=tk.LEFT)
        parity_opmenu.pack(side=tk.LEFT)

        # Stopbit setting
        stop_label = tk.Label(opt_frame,
                              text=strings.stopbit_lbl_text,
                              font=font)
        self.stop_var = tk.StringVar()
        self.stop_var.set(STOPBITS[0])
        stop_opmenu = tk.OptionMenu(opt_frame, self.stop_var, *STOPBITS)
        stop_opmenu.config(anchor=tk.W,
                           width=3,
                           font=font)
        stop_label.pack(side=tk.LEFT)
        stop_opmenu.pack(side=tk.LEFT)

        # Set buttons
        control_frame = tk.Frame(self.root, width=300)
        status_frame = tk.Frame(self.root, width=300)
        control_frame.pack(side=tk.LEFT)
        status_frame.pack(side=tk.RIGHT)

        start_btn = tk.Button(control_frame,
                              text=strings.start_port_text,
                              width=12,
                              command=self.start_port)
        close_btn = tk.Button(control_frame,
                              text=strings.close_port_text,
                              width=12,
                              command=self.close_port)
        start_btn.pack(side=tk.LEFT, padx=5)
        close_btn.pack(side=tk.LEFT, padx=5)
        self.state_lbl = tk.Label(control_frame, text='')
        self.state_lbl.pack(side=tk.LEFT, padx=5)

        # Status frame widgets
        send_cnt_label = tk.Label(status_frame,
                                  text=strings.send_lbl_text,
                                  font=font)
        self.send_cnt = tk.StringVar()
        self.send_cnt.set(self.TX)
        send_cnt_entry = tk.Entry(status_frame,
                                  textvariable=self.send_cnt, width=10,
                                  font=font,
                                  relief=tk.SUNKEN,
                                  state=tk.DISABLED,
                                  justify=tk.RIGHT)
        send_cnt_label.pack(side=tk.LEFT)
        send_cnt_entry.pack(side=tk.LEFT)

        recv_cnt_label = tk.Label(status_frame,
                                  text=strings.recv_lbl_text,
                                  font=font)
        self.recv_cnt = tk.StringVar()
        self.recv_cnt.set(self.RX)
        recv_cnt_entry = tk.Entry(status_frame,
                                  textvariable=self.recv_cnt, width=10,
                                  font=font,
                                  relief=tk.SUNKEN,
                                  state=tk.DISABLED,
                                  justify=tk.RIGHT)
        recv_cnt_label.pack(side=tk.LEFT)
        recv_cnt_entry.pack(side=tk.LEFT)

        clr_btn = tk.Button(status_frame,
                            text=strings.clear_btn_text,
                            font=font,
                            command=self.clear_count)
        clr_btn.pack()

    def clear_received(self):
        self.recv_txtarea.delete(0.0, tk.END)

    def clear_sent(self):
        self.send_txtarea.delete(0.0, tk.END)

    def clear_count(self):
        pass

    def start_port(self):
        pass

    def send_data(self):
        pass

    def recv_data(self):
        pass


class SerialAssistant(SerialAssistantGUI):

    portisopen = 0
    TX = 0
    RX = 0

    def __init__(self):
        super(SerialAssistant, self).__init__()

    def clear_count(self):
        self.RX = 0
        self.TX = 0
        self.send_cnt.set(self.RX)
        self.recv_cnt.set(self.TX)

    def start_port(self):
        port = self.port_var.get()
        baudrate = int(self.brt_var.get())
        bytesize = int(self.size_var.get())
        parity = PARITIES[self.parity_var.get()]
        stopbits = float(self.stop_var.get())

        try:
            self.s = serial.Serial(port=port,
                                   baudrate=baudrate,
                                   bytesize=bytesize,
                                   parity=parity,
                                   stopbits=stopbits,
                                   timeout=TIMEOUT)
        except serial.SerialException as e:
            msgbox.showerror("OpenError", e)
        except serial.SerialTimeoutException as e:
            msgbox.showerror("OpenError", e)
        else:
            self.portisopen = 1
            self.state_lbl.config(background='green',
                                  text=port + strings.opening)
            self.th = threading.Thread(target=self.recv_data)
            self.th.daemon = True
            self.th.start()

    def send_data(self):
        if self.portisopen:
            data_fmt = []
            data = self.send_txtarea.get(0.0, tk.END)

            if len(data) == 1:
                return

            if self.send_mode.get():
                if data[-1] == '\x0a':
                    data = data[0:-1]
                data_fmt = format_data(data)
                for each in data_fmt:
                    try:
                        x = binascii.a2b_hex(each)
                    except TypeError:
                        x = '\x00'

                    self.s.write(x)
                    self.TX += 1
            else:
                if data[-1] == '\n':
                    data = data[0:-1]
                if self.linebreak.get():
                    data = data.replace('\n', '\r\n')

                self.s.write(data.encode('utf-8'))
                self.TX += len(data)
            self.send_cnt.set(self.TX)
        else:
            msgbox.showerror("Error", "Port NOT open!")
            return

    def recv_data(self):
        while self.portisopen:
            data = self.s.read()
            if len(data) != 0:
                if self.recv_mode.get() == 1:
                    data = binascii.b2a_hex(data)
                    if sys.version_info.major == 3:
                        data += b' '
                    elif sys.version_info.major == 2:
                        data += ' '
                    self.RX += 1
                else:
                    self.RX += len(data)
            self.recv_cnt.set(self.RX)
            if data != '\x0d':
                self.recv_txtarea.insert(tk.END, data)
                self.recv_txtarea.see(tk.END)
        return

    def close_port(self):
        if self.portisopen:
            self.portisopen = 0
            self.s.close()
            self.state_lbl.config(background='red',
                                  text=self.port_var.get() + strings.closed)
        else:
            return

    def handler(self, signum, frame):
        self.portisopen = 0
        self.th.join()

    def __del__(self):
        self.close_port()
        signal.signal(signal.SIGINT, self.handler)


def main():
    SerialAssistant()


if __name__ == '__main__':
    main()

strings_zh.py:

# -*- coding: utf-8 -*-
APP_NAME = '串口调试助手'
APP_VER = '0.3'
AUTHOR = 'cbdeng'

recv_buf_text = "接收缓冲区"
text_mode_text = "文本模式"
hex_mode_text = "16进制模式"
clear_recv_text = "清空接收"
send_buf_text = "发送缓冲区"
line_break_text = "换行符:'\\r\\n'"
clear_send_text = "清空发送"
send_btn_text = "发送数据"
port_lbl_text = "串口:"
baud_lbl_text = "波特率:"
size_lbl_text = "数据位:"
parity_lbl_text = "校验位:"
stopbit_lbl_text = "停止位:"
send_lbl_text = "SEND"
recv_lbl_text = "RECV"
clear_btn_text = "清除"
opening = "已打开"
closed = "已关闭"
select_port = "请选择串口"
start_port_text = "打开串口"
close_port_text = "关闭串口"
上一篇:linux – EBADF在epoll_wait之后的recv


下一篇:Linux-TCP之深入浅出send和recv【转】