main.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import serial
  2. import serial.tools.list_ports
  3. import threading
  4. import tkinter as tk
  5. from tkinter import ttk, scrolledtext, messagebox
  6. class SerialAssistant:
  7. def __init__(self, master):
  8. self.master = master
  9. self.master.title("Python 帧格式串口助手")
  10. self.master.geometry("800x550")
  11. self.master.resizable(False, False)
  12. self.ser = None
  13. self.is_running = False
  14. self.hex_display = tk.BooleanVar(value=True)
  15. # 串口设置
  16. frame_top = ttk.LabelFrame(master, text="串口设置", padding=10)
  17. frame_top.pack(fill="x")
  18. ttk.Label(frame_top, text="端口:").grid(row=0, column=0)
  19. self.combo_port = ttk.Combobox(frame_top, width=10, values=self.list_serial_ports())
  20. self.combo_port.grid(row=0, column=1, padx=5)
  21. if self.combo_port["values"]:
  22. self.combo_port.current(0)
  23. ttk.Label(frame_top, text="波特率:").grid(row=0, column=2)
  24. self.combo_baud = ttk.Combobox(frame_top, width=10, values=[9600, 19200, 38400, 57600, 115200])
  25. self.combo_baud.current(4)
  26. self.combo_baud.grid(row=0, column=3, padx=5)
  27. self.btn_open = ttk.Button(frame_top, text="打开串口", command=self.open_serial)
  28. self.btn_open.grid(row=0, column=4, padx=5)
  29. self.btn_close = ttk.Button(frame_top, text="关闭串口", command=self.close_serial, state=tk.DISABLED)
  30. self.btn_close.grid(row=0, column=5, padx=5)
  31. ttk.Checkbutton(frame_top, text="16进制显示", variable=self.hex_display).grid(row=0, column=6, padx=10)
  32. # 接收区
  33. frame_rx = ttk.LabelFrame(master, text="接收数据", padding=10)
  34. frame_rx.pack(fill="both", expand=True)
  35. self.text_rx = scrolledtext.ScrolledText(frame_rx, wrap=tk.WORD, height=15)
  36. self.text_rx.pack(fill="both", expand=True)
  37. # 发送帧区
  38. frame_tx = ttk.LabelFrame(master, text="帧发送设置", padding=10)
  39. frame_tx.pack(fill="x")
  40. # 命令模板下拉
  41. ttk.Label(frame_tx, text="命令模板:").grid(row=0, column=0)
  42. self.combo_frame = ttk.Combobox(frame_tx, width=50, state="readonly", values=[
  43. "0A 00 00 00", # 保留字段部分固定
  44. "08 00 00 00",
  45. "03 0A 00 D0",
  46. "07 03 00 00",
  47. "05 00 00 14",
  48. "06 00 00 14",
  49. "05 00 00 14"
  50. ])
  51. self.combo_frame.grid(row=0, column=1, padx=5)
  52. self.combo_frame.current(0)
  53. # 源地址下拉
  54. ttk.Label(frame_tx, text="源地址(2B):").grid(row=1, column=0)
  55. self.combo_src = ttk.Combobox(frame_tx, width=10, values=[
  56. "29 8A", "FF FF", "2A 75"
  57. ])
  58. self.combo_src.grid(row=1, column=1, padx=5)
  59. self.combo_src.current(0)
  60. # 目标地址下拉
  61. ttk.Label(frame_tx, text="目标地址(2B):").grid(row=1, column=2)
  62. self.combo_dst = ttk.Combobox(frame_tx, width=10, values=[
  63. "B4 60", "50 05", "60 06" , "FF FF"
  64. ])
  65. self.combo_dst.grid(row=1, column=3, padx=5)
  66. self.combo_dst.current(0)
  67. ttk.Button(frame_tx, text="发送帧", command=self.send_frame).grid(row=1, column=4, padx=10)
  68. # 状态栏
  69. self.status = ttk.Label(master, text="状态: 未连接", relief="sunken", anchor="w")
  70. self.status.pack(fill="x", side="bottom")
  71. def list_serial_ports(self):
  72. ports = [port.device for port in serial.tools.list_ports.comports()]
  73. return ports if ports else ["无可用端口"]
  74. def open_serial(self):
  75. port = self.combo_port.get()
  76. baud = self.combo_baud.get()
  77. try:
  78. self.ser = serial.Serial(port, baudrate=int(baud), timeout=0)
  79. self.is_running = True
  80. self.btn_open["state"] = tk.DISABLED
  81. self.btn_close["state"] = tk.NORMAL
  82. self.status["text"] = f"状态: 已连接 {port} ({baud}bps)"
  83. self.text_rx.insert(tk.END, f"[系统] 成功打开串口 {port}\n")
  84. threading.Thread(target=self.read_serial, daemon=True).start()
  85. except Exception as e:
  86. messagebox.showerror("错误", f"无法打开串口: {e}")
  87. def close_serial(self):
  88. self.is_running = False
  89. if self.ser and self.ser.is_open:
  90. self.ser.close()
  91. self.btn_open["state"] = tk.NORMAL
  92. self.btn_close["state"] = tk.DISABLED
  93. self.status["text"] = "状态: 未连接"
  94. self.text_rx.insert(tk.END, "[系统] 串口已关闭\n")
  95. def read_serial(self):
  96. while self.is_running and self.ser and self.ser.is_open:
  97. try:
  98. count = self.ser.in_waiting
  99. if count:
  100. raw_data = self.ser.read(count)
  101. display_data = " ".join(f"{b:02X}" for b in raw_data) if self.hex_display.get() else raw_data.decode("utf-8", errors="ignore")
  102. self.text_rx.insert(tk.END, display_data + "\n")
  103. self.text_rx.see(tk.END)
  104. except Exception:
  105. break
  106. def send_frame(self):
  107. if not (self.ser and self.ser.is_open):
  108. messagebox.showwarning("警告", "请先打开串口!")
  109. return
  110. try:
  111. cmd_template = self.combo_frame.get().replace(" ", "")
  112. src_addr = self.combo_src.get().replace(" ", "")
  113. dst_addr = self.combo_dst.get().replace(" ", "")
  114. if len(cmd_template) % 2 != 0:
  115. raise ValueError("命令模板长度非法")
  116. if len(src_addr) != 4 or len(dst_addr) != 4:
  117. raise ValueError("地址长度非法")
  118. # 组帧: 55 BB [命令模板] [源地址] [目标地址] FF EE
  119. frame = bytearray()
  120. frame.extend(b'\x55\xBB')
  121. frame.extend(bytes.fromhex(cmd_template))
  122. frame.extend(bytes.fromhex(src_addr))
  123. # 保留字段 3B, 可在模板里也可固定
  124. if len(cmd_template) < 4:
  125. frame.extend(b'\x00\x00\x00')
  126. frame.extend(bytes.fromhex(dst_addr))
  127. frame.extend(b'\xFF\xEE')
  128. self.ser.write(frame)
  129. self.text_rx.insert(tk.END, f"[发送] {' '.join(f'{b:02X}' for b in frame)}\n")
  130. self.text_rx.see(tk.END)
  131. except ValueError as e:
  132. messagebox.showerror("输入错误", str(e))
  133. if __name__ == "__main__":
  134. root = tk.Tk()
  135. app = SerialAssistant(root)
  136. root.mainloop()