208 lines
7.6 KiB
Python
208 lines
7.6 KiB
Python
import serial
|
||
import sys
|
||
import time
|
||
import threading
|
||
import tkinter as tk
|
||
from tkinter import ttk, scrolledtext
|
||
|
||
# ==========================================================
|
||
# 串口配置
|
||
# ==========================================================
|
||
SERIAL_PORT = "/dev/ttyS3"
|
||
BAUD_RATE = 115200
|
||
TIMEOUT = 1
|
||
|
||
class SerialHexViewer:
|
||
def __init__(self):
|
||
self.ser = None
|
||
self.running = False
|
||
self.rx_thread = None
|
||
|
||
# ---- 主窗口 ----
|
||
self.root = tk.Tk()
|
||
self.root.title("串口 HEX 接收器 - {}".format(SERIAL_PORT))
|
||
self.root.geometry("700x500")
|
||
|
||
# ---- 顶部配置栏 ----
|
||
top = tk.Frame(self.root)
|
||
top.pack(fill='x', padx=8, pady=6)
|
||
|
||
tk.Label(top, text="串口:").pack(side='left')
|
||
self.port_entry = tk.Entry(top, width=12, justify='center')
|
||
self.port_entry.insert(0, SERIAL_PORT)
|
||
self.port_entry.pack(side='left', padx=4)
|
||
|
||
tk.Label(top, text="波特率:").pack(side='left')
|
||
self.baud_cb = ttk.Combobox(top, width=10,
|
||
values=["9600","19200","38400","57600","115200","230400","460800","921600"])
|
||
self.baud_cb.set(str(BAUD_RATE))
|
||
self.baud_cb.pack(side='left', padx=4)
|
||
|
||
tk.Label(top, text="数据位:").pack(side='left')
|
||
self.data_cb = ttk.Combobox(top, width=4, values=["5","6","7","8"])
|
||
self.data_cb.set("8")
|
||
self.data_cb.pack(side='left', padx=2)
|
||
|
||
tk.Label(top, text="停止位:").pack(side='left')
|
||
self.stop_cb = ttk.Combobox(top, width=4, values=["1","1.5","2"])
|
||
self.stop_cb.set("1")
|
||
self.stop_cb.pack(side='left', padx=2)
|
||
|
||
tk.Label(top, text="校验:").pack(side='left')
|
||
self.parity_cb = ttk.Combobox(top, width=6, values=["None","Odd","Even"])
|
||
self.parity_cb.set("None")
|
||
self.parity_cb.pack(side='left', padx=2)
|
||
|
||
# ---- 按钮 ----
|
||
btn_frame = tk.Frame(self.root)
|
||
btn_frame.pack(fill='x', padx=8, pady=2)
|
||
|
||
self.btn_open = tk.Button(btn_frame, text="打开串口", bg="#2ecc71", fg="white",
|
||
width=12, command=self.toggle_port)
|
||
self.btn_open.pack(side='left', padx=4)
|
||
|
||
tk.Button(btn_frame, text="清空显示", bg="#3498db", fg="white",
|
||
width=12, command=self.clear_display).pack(side='left', padx=4)
|
||
|
||
tk.Button(btn_frame, text="退出", bg="#e74c3c", fg="white",
|
||
width=8, command=self.shutdown).pack(side='right', padx=4)
|
||
|
||
# 显示格式选择
|
||
self.show_ascii = tk.BooleanVar(value=True)
|
||
tk.Checkbutton(btn_frame, text="同时显示 ASCII",
|
||
variable=self.show_ascii).pack(side='left', padx=8)
|
||
|
||
ttk.Separator(self.root, orient='horizontal').pack(fill='x')
|
||
|
||
# ---- HEX 显示区 ----
|
||
self.text_area = scrolledtext.ScrolledText(
|
||
self.root, font=("Courier New", 11),
|
||
bg="#1e1e1e", fg="#00ff00",
|
||
insertbackground="white",
|
||
state='disabled'
|
||
)
|
||
self.text_area.pack(fill='both', expand=True, padx=8, pady=6)
|
||
|
||
# ---- 状态栏 ----
|
||
self.status_var = tk.StringVar(value="状态: 未连接")
|
||
tk.Label(self.root, textvariable=self.status_var,
|
||
anchor='w', fg="gray").pack(fill='x', padx=8, pady=2)
|
||
|
||
self.rx_count = 0
|
||
|
||
# ----------------------------------------------------------
|
||
def toggle_port(self):
|
||
if not self.running:
|
||
self.open_port()
|
||
else:
|
||
self.close_port()
|
||
|
||
def open_port(self):
|
||
port = self.port_entry.get().strip()
|
||
baud = int(self.baud_cb.get())
|
||
data = int(self.data_cb.get())
|
||
stop = float(self.stop_cb.get())
|
||
parity_map = {"None": serial.PARITY_NONE,
|
||
"Odd": serial.PARITY_ODD,
|
||
"Even": serial.PARITY_EVEN}
|
||
parity = parity_map.get(self.parity_cb.get(), serial.PARITY_NONE)
|
||
|
||
try:
|
||
self.ser = serial.Serial(
|
||
port=port, baudrate=baud,
|
||
bytesize=data, stopbits=stop,
|
||
parity=parity, timeout=TIMEOUT
|
||
)
|
||
self.running = True
|
||
self.rx_count = 0
|
||
self.btn_open.config(text="关闭串口", bg="#e74c3c")
|
||
self.status_var.set("状态: 已连接 {} @ {} bps".format(port, baud))
|
||
|
||
self.rx_thread = threading.Thread(target=self._read_loop, daemon=True)
|
||
self.rx_thread.start()
|
||
except Exception as e:
|
||
self.status_var.set("状态: 打开失败 - {}".format(e))
|
||
|
||
def close_port(self):
|
||
self.running = False
|
||
if self.ser and self.ser.is_open:
|
||
self.ser.close()
|
||
self.btn_open.config(text="打开串口", bg="#2ecc71")
|
||
self.status_var.set("状态: 已断开 | 累计接收: {} 字节".format(self.rx_count))
|
||
|
||
# ----------------------------------------------------------
|
||
def _read_loop(self):
|
||
"""独立线程:持续读取串口数据"""
|
||
buf = bytearray()
|
||
last_flush = time.time()
|
||
|
||
while self.running:
|
||
try:
|
||
waiting = self.ser.in_waiting
|
||
if waiting > 0:
|
||
data = self.ser.read(waiting)
|
||
buf.extend(data)
|
||
self.rx_count += len(data)
|
||
|
||
# 每满 16 字节 或 超过 50ms 未刷新 则输出一行
|
||
now = time.time()
|
||
while len(buf) >= 16:
|
||
self._append_hex_line(bytes(buf[:16]))
|
||
buf = buf[16:]
|
||
if buf and (now - last_flush) >= 0.05:
|
||
self._append_hex_line(bytes(buf))
|
||
buf.clear()
|
||
last_flush = now
|
||
else:
|
||
# 无数据时刷新剩余缓冲
|
||
if buf and (time.time() - last_flush) >= 0.1:
|
||
self._append_hex_line(bytes(buf))
|
||
buf.clear()
|
||
last_flush = time.time()
|
||
time.sleep(0.005)
|
||
except Exception as e:
|
||
if self.running:
|
||
self.root.after(0, self.status_var.set,
|
||
"状态: 读取错误 - {}".format(e))
|
||
break
|
||
|
||
def _append_hex_line(self, data: bytes):
|
||
"""格式化为 HEX 行并追加到显示区(线程安全)"""
|
||
ts = time.strftime("%H:%M:%S")
|
||
hex_str = " ".join("{:02X}".format(b) for b in data)
|
||
# 对齐:16字节 = 47字符宽
|
||
hex_padded = "{:<47}".format(hex_str)
|
||
|
||
if self.show_ascii.get():
|
||
ascii_str = "".join(chr(b) if 32 <= b < 127 else '.' for b in data)
|
||
line = "[{}] {} |{}|".format(ts, hex_padded, ascii_str)
|
||
else:
|
||
line = "[{}] {}".format(ts, hex_str)
|
||
|
||
self.root.after(0, self._insert_text, line)
|
||
self.root.after(0, self.status_var.set,
|
||
"状态: 接收中 | 累计: {} 字节".format(self.rx_count))
|
||
|
||
def _insert_text(self, line):
|
||
self.text_area.config(state='normal')
|
||
self.text_area.insert('end', line)
|
||
self.text_area.see('end') # 自动滚动到最新
|
||
self.text_area.config(state='disabled')
|
||
|
||
def clear_display(self):
|
||
self.text_area.config(state='normal')
|
||
self.text_area.delete('1.0', 'end')
|
||
self.text_area.config(state='disabled')
|
||
|
||
def shutdown(self):
|
||
self.close_port()
|
||
self.root.destroy()
|
||
|
||
def run(self):
|
||
self.root.mainloop()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
viewer = SerialHexViewer()
|
||
viewer.run()
|