python3+tkinter实践历程(四)——模仿CRT完成基于socket通信与tkinter的TCP串口客户端

python3+tkinter实践历程(四)——基于socket通信与tkinter的TCP串口客户端(仿CRT)


系列文章目录

python3+tkinter实践历程(一)——基于requests与tkinter的API工具
python3+tkinter实践历程(二)——基于tkinter的日志检索工具
python3+tkinter实践历程(三)——基于requests与tkinter的模拟web登录工具
python3+tkinter实践历程(四)——模仿CRT完成基于socket通信与tkinter的TCP串口客户端

分享背景

①分享意图在于帮助新入门的朋友,提供思路,里面详细的注释多多少少能解决一些问题。欢迎大佬指点跟交流。
②2021年8月,开始陆续有需求制作一些工具,因为python语言跟tkinter工具相对简单,所以就基于这些做了好几个不同用处的工具。
③分享从完成的第一个工具开始分享,分享到最新完成的工具,对于tkinter的理解也从一开始的摸索入门,到后来逐渐熟练,完成速度也越来越快,所用到的tk的功能点也越来越多。
④这是发布前做的最后一个工具,也是最难、最复杂、要求最多、耗时最长的一个。

制作背景

① 研发部自己做一个串口服务器,硬件部解决板子问题,开发将软件写入板子。最终服务器可以用特制的线连通24个设备的串口,开启了TCP服务端口,服务器驱动将24个设备的日志实时的通过socket套接字将发送给已连接的TCP客户端。
② 本人所做的就是这个接收、处理串口服务器传过来的日志的TCP客户端。
③ 功能需求如最终功能所示,完全实现了所有预期需求。

最终功能

① 提供界面输入-TCP服务器的IP、端口
② 提供界面选项-保存日志的目录,成功连接TCP服务器后,自动将所有接收到的日志保存,按日期划分文件夹,按串口划分log文件。
③ 提供界面打印界面,自主选择串口打印,且与保存日志功能完全分离,互不干扰。
④ 打印日志的界面支持增、删、重命名。
⑤ 工具支持读取界面配置、记录历史配置
⑥ 支持给不同串口发送命令,发送命令框支持回车发送+按钮发送
⑦ 快速发送按钮支持读取配置
⑧ 提供选项-滚动条是否自动跟随新打印的日志,支持实时改变

工具截图展示

在这里插入图片描述
在这里插入图片描述

代码详解

import tkinter as tk
from tkinter import ttk
import tkinter.font as tf
import threading
import os
import datetime
import socket
import re
import time
import json
from tkinter.filedialog import askdirectory


class SocketServer(object):
    def __init__(self, ip, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        print('开始连接串口服务器%s:%d' % (ip, port))
        self.sock.connect((ip, port))
        print('连接完成')

    def get_serial_data(self):
        recv_info = self.sock.recv(7168).decode("utf-8", "replace")       # 服务器传过来的是bytes类型,收到数据后编码变成str
        # print(recv_info)
        return recv_info

    def send_data(self, data):
        self.sock.send(data)


class Application(tk.Frame):
    def __init__(self, master=None):
        tk.Frame.__init__(self, master)
        self.grid()
        self.save_log_dir = ''
        self.is_running = ''
        self.LOG_LINE_NUM = 0
        self.see_log = True
        self.button_column = 0      # 用于记录button的最大值
        self.top = ''               # 重命名窗口
        self.tab = {}       # TAB集合
        self.active_tab = ''    # 当前置顶
        self.create_widgets(master)

    def create_widgets(self, master):
        """创建图形化界面"""
        # 菜单栏
        menus = tk.Menu(self)
        num1 = tk.Menu(menus, tearoff=False)
        menus.add_cascade(label='菜单栏', menu=num1)
        num1.add_command(label='邮件')
        master['menu'] = menus
        # 第一行
        row1 = tk.Frame(self)
        row1.grid(row=0, column=0, sticky='w', padx=(15, 0), pady=10)

        # IP端口输入框
        server_url = ttk.Label(row1, text="串口服务器地址(IP:端口)")
        server_url.grid(row=0, column=1)
        server_url = tk.StringVar()
        server_url.set("10.100.10.180:9036")         # set的作用的预置的文字(在标签内可变的文本)
        self.server_url = tk.Entry(row1, textvariable=server_url, width=20)
        self.server_url.grid(row=0, column=2, padx=2, pady=2)
        # 日志存放目录
        ttk.Label(row1, text="", width=1).grid(row=0, column=3)
        ttk.Label(row1, text="日志存放地址").grid(row=0, column=4)
        self.dir = tk.StringVar()
        self.dir.set("")
        self.log_dir = ttk.Entry(row1, textvariable=self.dir, width=35)
        self.log_dir.grid(row=0, column=5)
        ttk.Button(row1, text="选择日志文件夹", command=self.selectPath).grid(row=0, column=6)
        # 开始连接服务器并接收日志按钮
        ttk.Label(row1, text="", width=2).grid(row=0, column=7)
        start_btn = ttk.Button(row1, text="连接串口服务器", command=self.start_socket_conn).grid(row=0, column=8)
        # 停止按钮
        stop_btn = ttk.Button(row1, text="停止连接", command=self.start_stop_task).grid(row=0, column=9, padx=(10, 0))

        self.see_log_value = tk.IntVar()
        self.see_log_value.set(1)
        ttk.Checkbutton(row1, text="滚动条跟随日志", variable=self.see_log_value, command=self.change_see_log).grid(row=0, column=14, padx=20)

        ttk.Button(row1, text='+', command=self.add_log_text, width=2).grid(row=0, column=1000, padx=2)
        ttk.Button(row1, text='-', command=self.del_log_text, width=2).grid(row=0, column=1001, padx=2)
        ttk.Button(row1, text='重命名', command=self.start_tab_rename, width=5).grid(row=0, column=1002, padx=2)

        # 第二行 tab页  非固定行
        self.row2 = ttk.Notebook(self)
        self.row2.grid(row=1, column=0, sticky='w', padx=(15, 0))

        # 读取第二第三行的内容
        interface_data = self.work_for_data('tab', 'read')
        if not interface_data:
            interface_data = {'log1': {'name_str': 'log1', 'serial_value': ''}}
        for name in interface_data:
            column = self.button_column + 1
            self.tab[name] = dict()
            self.tab[name]['Frame'] = tk.Frame(self, relief='ridge')
            self.tab[name]['Frame'].grid(row=0, column=column)
            self.tab[name]['name_str'] = interface_data[name]['name_str']
            self.row2.add(self.tab[name]['Frame'], text=interface_data[name]['name_str'])

            ft = tf.Font(size=10)
            self.tab[name]['text'] = tk.Text(self.tab[name]['Frame'], width=180, height=50, font=ft, padx=5, pady=5,
                                             relief='sunken')
            log_slide_bar = tk.Scrollbar(self.tab[name]['Frame'], command=self.tab[name]['text'].yview,
                                         orient="vertical")
            log_slide_bar.grid(row=0, column=1, sticky='ns')
            self.tab[name]['text'].grid(row=0, column=0)
            self.tab[name]['text'].config(font=ft, yscrollcommand=log_slide_bar.set)
            ttk.Label(self.tab[name]['Frame'], text="当前串口").grid(row=0, column=2, sticky='n', padx=(0, 5))
            self.tab[name]['serial'] = ttk.Combobox(self.tab[name]['Frame'], state='readonly', width=10)
            self.tab[name]['serial'].grid(row=0, column=3, sticky='n')
            self.tab[name]['serial_value'] = interface_data[name]['serial_value']
            self.tab[name]['serial']['value'] = (
                                            'serial01', 'serial02', 'serial03', 'serial04', 'serial05', 'serial06',
                                            'serial07', 'serial08', 'serial09', 'serial10', 'serial11', 'serial12',
                                            'serial13', 'serial14', 'serial15', 'serial16', 'serial17', 'serial18',
                                            'serial19', 'serial20', 'serial21', 'serial22', 'serial23', 'serial24')
            self.tab[name]['serial'].bind('<<ComboboxSelected>>', self.start_change_serial)
            if interface_data[name]['serial_value']:
                self.tab[name]['serial'].set(interface_data[name]['serial_value'])

        row4 = tk.Frame(self)
        row4.grid(row=3, column=0, sticky='w', padx=(15, 0), pady=(15, 0))

        # 数据输入框
        ft = tf.Font(size=10)
        self.log_input = tk.Text(row4, width=100, height=4, font=ft)
        self.log_input.grid(row=0, column=1)
        self.log_input.bind('<Return>', self.start_send_data)       # text框绑定回车键
        ttk.Button(row4, text="发送", command=self.start_send_data).grid(row=0, column=2, padx=10)

        # 第四行
        row5 = tk.Frame(self)
        row5.grid(row=4, column=0, sticky='w', padx=(15, 0), pady=(15, 0))

        # 快捷输入按钮
        quick_send_data = self.work_for_data('quick_send')
        if quick_send_data:
            print(quick_send_data)
            column = 0
            for data in quick_send_data:
                ttk.Button(row5, text=data['name'], command=lambda data2=data: self.start_quick_print(data2)).grid(row=0, column=column, padx=(0, 10))
                column = column + 1

    def add_log_text(self):
        """增加多个tab页"""
        now_log_text_num = 0
        for ii in range(1, 1000):
            if 'log' + str(ii) not in self.tab:
                now_log_text_num = ii
                break
            else:
                now_log_text_num = 1000
        column = self.button_column + 1
        name = 'log' + str(now_log_text_num)
        self.tab[name] = dict()
        self.tab[name]['Frame'] = tk.Frame(self, relief='ridge')
        self.tab[name]['Frame'].grid(row=0, column=column)
        self.tab[name]['name_str'] = name
        self.row2.add(self.tab[name]['Frame'], text=name)

        ft = tf.Font(size=10)
        self.tab[name]['text'] = tk.Text(self.tab[name]['Frame'], width=180, height=50, font=ft, padx=5, pady=5,
                                         relief='sunken')
        log_slide_bar = tk.Scrollbar(self.tab[name]['Frame'], command=self.tab[name]['text'].yview,
                                     orient="vertical")
        log_slide_bar.grid(row=0, column=1, sticky='ns')
        self.tab[name]['text'].grid(row=0, column=0)
        self.tab[name]['text'].config(font=ft, yscrollcommand=log_slide_bar.set)
        ttk.Label(self.tab[name]['Frame'], text="当前串口").grid(row=0, column=2, sticky='n', padx=(0, 5))
        self.tab[name]['serial'] = ttk.Combobox(self.tab[name]['Frame'], state='readonly', width=10)
        self.tab[name]['serial'].grid(row=0, column=3, sticky='n')
        self.tab[name]['serial_value'] = ''
        self.tab[name]['serial']['value'] = (
            'serial01', 'serial02', 'serial03', 'serial04', 'serial05', 'serial06',
            'serial07', 'serial08', 'serial09', 'serial10', 'serial11', 'serial12',
            'serial13', 'serial14', 'serial15', 'serial16', 'serial17', 'serial18',
            'serial19', 'serial20', 'serial21', 'serial22', 'serial23', 'serial24')
        self.tab[name]['serial'].bind('<<ComboboxSelected>>', self.start_change_serial)
        self.active_tab = name
        self.row2.select(self.tab[name]['Frame'])
        # 存放tab信息
        result = self.work_for_data('tab', 'write')
        if result:
            print('存放tab信息与interface_data.txt完成')

    def del_log_text(self):
        """删除当前置顶的tab页"""
        if len(self.tab) == 1:
            print('只有一个tab页,不可销毁')
            return
        self.get_now_active_tab()
        self.row2.forget(self.row2.select())
        del self.tab[self.active_tab]
        # 存放tab信息
        result = self.work_for_data('tab', 'write')
        if result:
            print('存放tab信息与interface_data.txt完成')

    def tab_rename(self):
        """给tab页面重命名"""
        try:
            if self.top:
                self.top.destroy()
                self.top = ''
            self.top = tk.Toplevel()
            self.top.title('重命名')
            self.top.transient(self)
            self.top.resizable(0, 0)
            # 居中
            width = self.winfo_screenwidth()
            height = self.winfo_screenheight()
            ww = 220
            wh = 80
            x = (width-ww)/2
            y = (height-wh)/2
            self.top.geometry("%dx%d+%d+%d" % (ww, wh, x, y))  # 自适应居中
            self.top.grid()
            self.get_now_active_tab()
            # 重命名输入框
            name = tk.StringVar()
            name.set(self.tab[self.active_tab]['name_str'])
            self.rename_entry = tk.Entry(self.top, text=name, width=27)
            self.rename_entry.grid(row=0, column=0, columnspan=2, ipady=5, pady=(10, 5))
            # 确定按钮
            self.rename_sure_btn = ttk.Button(self.top, text='确定', command=lambda : self.control_rename_btn('确定'))
            self.rename_sure_btn.grid(row=1, column=0, padx=10, pady=5)
            # 取消按钮
            self.rename_cancel_btn = ttk.Button(self.top, text='取消', command=lambda : self.control_rename_btn('取消'))
            self.rename_cancel_btn.grid(row=1, column=1, padx=10, pady=5)
        except Exception as e:
            print('初始化重命名界面异常:%s' % e)

    def control_rename_btn(self, operate):
        """控制重命名的确定取消按钮"""
        if operate == '确定':
            new_name = self.rename_entry.get()
            if new_name:
                for i in self.tab:
                    if self.tab[i]['name_str'] == new_name:
                        print('有重名')
                        self.top.destroy()
                        return
                self.tab[self.active_tab]['name_str'] = new_name
                self.row2.add(self.tab[self.active_tab]['Frame'], text=new_name)
                result = self.work_for_data('tab', 'write')
                if result:
                    print('存放tab信息与interface_data.txt完成')
            self.top.destroy()
        if operate == '取消':
            self.top.destroy()

    def socket_conn(self):
        """与串口服务器建立TCP连接,无限循环获取数据"""
        ip_port = self.server_url.get().split(':')
        ip = ip_port[0]
        port = int(ip_port[1])
        global sock_conn
        sock_conn = SocketServer(ip, port)
        self.is_running = True
        while True:
            if self.is_running:
                try:
                    recv_info = sock_conn.get_serial_data()
                except Exception as e:
                    print('获取数据异常:%s' % e)
                    continue
                try:
                    parse_rule = re.compile(r'\*<\[(\d{2})]>\*')
                    parse_result = parse_rule.search(recv_info)
                    if parse_result:
                        log_info = recv_info.replace(parse_result.group(0), '')
                        serial = 'serial' + parse_result.group(1)
                    else:
                        print('串口标识识别失败:%s' % recv_info)
                        continue
                except Exception as e:
                    print('解析数据异常:%s,异常数据:%s' % (e, recv_info))
                    continue
                try:
                    self.save_log(log_info, serial)
                except Exception as e:
                    print('保存日志异常:%s' % e)
                    continue
                try:
                    self.output_log(log_info, serial)
                except Exception as e:
                    print('输出日志异常:%s' % e)
                    continue
            else:
                print('stopped')
                break

    def selectPath(self):
        """把获取到的串口目录传入Entry"""
        dir_ = askdirectory()
        self.dir.set(str(dir_))
        self.save_log_dir = self.log_dir.get()

    def save_log(self, log_info, serial_port):
        """把收到的日志分别传入不同的log文本中"""
        if not self.save_log_dir:
            # print('无传入日志目录')
            return
        # print('获取到的日志目录为:%s' % self.save_log_dir)
        now_day = datetime.datetime.now().strftime('%Y-%m-%d')
        # print('当前日期为:%s' % now_day)
        log_folder = os.path.join(self.save_log_dir, now_day)
        # print('存放今天日志目录为:%s' % log_folder)
        if not os.path.exists(log_folder):
            # print('不存在%s文件夹,进行新建' % log_folder)
            os.mkdir(log_folder)
        log_file = os.path.join(log_folder, '%s_%s.log' % (serial_port, now_day))
        log_info = log_info.rstrip('\n')
        with open(log_file, 'a+', errors='ignore', newline='') as f:
            f.write(log_info)

    def change_serial(self):
        """更改输出日志的串口"""
        self.get_now_active_tab()
        print('当前tab为%s' % self.active_tab)
        if self.tab[self.active_tab]['serial_value'] != self.tab[self.active_tab]['serial'].get():
            self.tab[self.active_tab]['serial_value'] = self.tab[self.active_tab]['serial'].get()
            self.tab[self.active_tab]['text'].delete(0.0, 'end')
            print('更改%s的输出串口为:%s' % (self.active_tab, self.tab[self.active_tab]['serial_value']))
            # 存放tab信息
            result = self.work_for_data('tab', 'write')
            if result:
                print('存放tab信息与interface_data.txt完成')

    def get_now_active_tab(self):
        active_name = self.row2.tab(self.row2.select(), "text")
        for i in self.tab:
            if self.tab[i]['name_str'] == active_name:
                self.active_tab = i
                break

    def work_for_data(self, key, work=''):
        """存/取tab字典,取快捷输入数据"""
        try:
            if key == 'tab':
                if work == 'write':
                    self.save_tab = {}
                    for i in self.tab:
                        self.save_tab[i] = {}
                        self.save_tab[i]['name_str'] = self.tab[i]['name_str']
                        self.save_tab[i]['serial_value'] = self.tab[i]['serial_value']
                    with open('interface_data.txt', 'w') as f:
                        data = json.dumps(self.save_tab)
                        f.write(data)
                        return True
                if work == 'read':
                    with open('interface_data.txt', 'r') as f:
                        data = f.read()
                        data = json.loads(data)
                        print('读取到的tab:%s' % data)
                        return data
            if key == 'quick_send':
                data_list = []
                with open('quick_send_data.txt', 'r') as f:
                    for i in f:
                        if '*start-' in i:
                            info = {}
                            l = i.replace('*start-', '')
                            name = l[0:l.rfind('=')]
                            l = l[l.rfind('=') + 1:]
                            if '\enter' in l:
                                enter = True
                                l = l.replace('\enter', '')
                            else:
                                enter = False
                            if '\n' in l:
                                l = l.replace('\n', '')
                            l = l.split(';')
                            info['data'] = l
                            info['name'] = name
                            info['enter'] = enter
                            data_list.append(info)
                return data_list
        except Exception as e:
            print('对%s进行%s操作异常:%s' % (key, work, e))

    def output_log(self, log_info, serial_port):
        """实时输出日志"""
        for i in self.tab:
            if self.tab[i]['serial_value'] == serial_port:
                self.write_log_to_Text(i, log_info)
                break

    def stop_recv_data(self):
        self.is_running = False
        print('stopping')

    def send_data_to_server(self, data=''):
        """发送数据给服务器"""
        try:
            self.get_now_active_tab()
            if not self.tab[self.active_tab]['serial_value']:
                print('无指定串口')
                return
            if data:
                print('有指定数据:%s' % data)
            else:
                data = self.log_input.get(0.0, 'end')
                self.log_input.delete(0.0, 'end')
                if not data:
                    print('无数据')
                    return
            if data[-1] not in ['\n', '\r']:
                print('给数据加回车')
                data = data + '\n'
            print('给%s发送%s指令' % (self.tab[self.active_tab]['serial_value'], data))
            data = '*<[%s]>*' % self.tab[self.active_tab]['serial_value'][-2:] + data
            sock_conn.send_data(bytes(data, encoding='utf8'))
        except Exception as e:
            print('发送数据异常:%s' % e)

    def quick_print(self, data):
        if data:
            if len(data['data']) == 1:      # 只有一条数据
                if data['enter']:   # 自动发送
                    self.start_send_data(data=data['data'][0])
                else:               # 打印出来,不自动发送
                    self.log_input.insert('end', data['data'][0])
            else:               # 大于1条数据,需要发送多次
                for i in data['data']:
                    if i == 'p':
                        print('sleep(1)')
                        time.sleep(1)
                    else:
                        if data['enter']:
                            self.start_send_data(data=i)
                        else:
                            self.log_input.insert('end', i)

    def change_see_log(self):
        if self.see_log_value.get() in ['1', 1]:
            self.see_log = True
            print('切换成跟随日志')
        else:
            self.see_log = False
            print('切换成不跟随日志')

    def write_log_to_Text(self, interface, log_msg):
        """日志动态打印"""
        self.LOG_LINE_NUM = int(self.tab[interface]['text'].index('end').split('.')[0]) - 1
        if self.LOG_LINE_NUM > 3001:
            print('log超过3000行,%s' % self.LOG_LINE_NUM)
            del_target = float(self.LOG_LINE_NUM - 3000 - 1)
            self.tab[interface]['text'].delete(1.0, del_target)
        logmsg_in = "%s\n" % log_msg
        self.tab[interface]['text'].insert('end', logmsg_in)
        if self.active_tab == interface and self.see_log:
            self.tab[interface]['text'].see('end')

    def thread_it(self, func, *args):
        """将函数打包进线程"""
        # 创建
        t = threading.Thread(target=func, args=args)
        # 守护 !!!
        t.setDaemon(True)
        # 启动
        t.start()

    def start_socket_conn(self, *args):
        self.thread_it(self.socket_conn)

    def start_save_log(self, *args):
        self.thread_it(self.save_log)

    def start_change_serial(self, *args):
        self.thread_it(self.change_serial)

    def start_output_log(self, *args):
        self.thread_it(self.output_log)

    def start_send_data(self, ev=None, data=''):
        self.thread_it(self.send_data_to_server, data)
        return 'break'

    def start_stop_task(self, *args):
        self.thread_it(self.stop_recv_data)

    def start_quick_print(self, data=''):
        self.thread_it(self.quick_print, data)

    def start_write_log(self, log_msg):
        self.thread_it(self.write_log_to_Text, log_msg)

    def start_tab_rename(self, *args):
        self.thread_it(self.tab_rename)


root = tk.Tk()
app = Application(master=root)
root.withdraw()
root.update()
root.title("串口收发客户端")
root.resizable(False, False)

sw = root.winfo_screenwidth()   # 得到屏幕宽度
sh = root.winfo_screenheight()  # 得到屏幕高度
# 窗口宽高为100
ww = 1500
wh = 900
x = (sw-ww) / 2
y = (sh-wh) / 2
root.geometry("%dx%d+%d+%d" % (ww, wh, x, y))       # 自适应居中


root.deiconify()        # 显示窗口
app.mainloop()          # 进入消息循环

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值