main.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  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. import os
  7. TEMPLATE_FILE = "templates.txt"
  8. SRC_FILE = "src_addrs.txt"
  9. class SerialAssistant:
  10. def __init__(self, master):
  11. self.master = master
  12. self.master.title("Python 帧格式串口助手")
  13. self.master.geometry("900x650")
  14. self.master.minsize(750, 500)
  15. self.master.columnconfigure(0, weight=1)
  16. self.master.rowconfigure(1, weight=1)
  17. self.ser = None
  18. self.is_running = False
  19. self.hex_display = tk.BooleanVar(value=True)
  20. # ------------------ 串口设置 ------------------
  21. frame_top = ttk.LabelFrame(master, text="串口设置", padding=10)
  22. frame_top.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
  23. for i in range(7):
  24. frame_top.columnconfigure(i, weight=1)
  25. ttk.Label(frame_top, text="端口:").grid(row=0, column=0, sticky="w")
  26. self.combo_port = ttk.Combobox(frame_top, width=10, values=self.list_serial_ports())
  27. self.combo_port.grid(row=0, column=1, padx=5, sticky="w")
  28. if self.combo_port["values"]:
  29. self.combo_port.current(0)
  30. ttk.Label(frame_top, text="波特率:").grid(row=0, column=2, sticky="w")
  31. self.combo_baud = ttk.Combobox(frame_top, width=10, values=[9600, 19200, 38400, 57600, 115200])
  32. self.combo_baud.current(4)
  33. self.combo_baud.grid(row=0, column=3, padx=5, sticky="w")
  34. self.btn_open = ttk.Button(frame_top, text="打开串口", command=self.open_serial)
  35. self.btn_open.grid(row=0, column=4, padx=5)
  36. self.btn_close = ttk.Button(frame_top, text="关闭串口", command=self.close_serial, state=tk.DISABLED)
  37. self.btn_close.grid(row=0, column=5, padx=5)
  38. ttk.Checkbutton(frame_top, text="16进制显示", variable=self.hex_display).grid(row=0, column=6, padx=10, sticky="w")
  39. # ------------------ 接收区 ------------------
  40. frame_rx = ttk.LabelFrame(master, text="接收数据", padding=10)
  41. frame_rx.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
  42. frame_rx.columnconfigure(0, weight=1)
  43. frame_rx.rowconfigure(0, weight=1)
  44. self.text_rx = scrolledtext.ScrolledText(frame_rx, wrap=tk.WORD)
  45. self.text_rx.grid(row=0, column=0, sticky="nsew")
  46. # ------------------ 发送帧区 ------------------
  47. frame_tx = ttk.LabelFrame(master, text="帧发送设置", padding=10)
  48. frame_tx.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
  49. for i in range(5):
  50. frame_tx.columnconfigure(i, weight=1)
  51. # 命令模板
  52. ttk.Label(frame_tx, text="命令模板:").grid(row=0, column=0, sticky="w")
  53. self.combo_frame = ttk.Combobox(frame_tx, width=50, state="normal")
  54. self.combo_frame.grid(row=0, column=1, padx=5, sticky="w")
  55. frame_template_btn = ttk.Frame(frame_tx)
  56. frame_template_btn.grid(row=0, column=2, padx=5, sticky="w")
  57. ttk.Button(frame_template_btn, text="保存模板", command=self.save_template).pack(side="left", padx=2)
  58. ttk.Button(frame_template_btn, text="删除模板", command=self.delete_template).pack(side="left", padx=2)
  59. # ------------------ 源地址 ------------------
  60. ttk.Label(frame_tx, text="源地址(2B):").grid(row=1, column=0, sticky="w")
  61. self.combo_src = ttk.Combobox(frame_tx, width=10)
  62. self.combo_src.grid(row=1, column=1, padx=5, sticky="w")
  63. ttk.Label(frame_tx, text="新增源地址:").grid(row=2, column=0, sticky="w")
  64. self.entry_new_src = ttk.Entry(frame_tx, width=10)
  65. self.entry_new_src.grid(row=2, column=1, padx=5, sticky="w")
  66. frame_src_btn = ttk.Frame(frame_tx)
  67. frame_src_btn.grid(row=2, column=2, columnspan=3, sticky="w")
  68. ttk.Button(frame_src_btn, text="添加源地址", command=self.add_src).pack(side="left", padx=2)
  69. ttk.Button(frame_src_btn, text="删除选中源地址", command=self.delete_src).pack(side="left", padx=2)
  70. # 发送帧按钮
  71. ttk.Button(frame_tx, text="发送帧", command=self.send_frame).grid(row=1, column=2, padx=10)
  72. # 状态栏
  73. self.status = ttk.Label(master, text="状态: 未连接", relief="sunken", anchor="w")
  74. self.status.grid(row=3, column=0, sticky="ew")
  75. # ------------------ 加载永久存储 ------------------
  76. self.load_templates()
  77. self.load_src_addrs()
  78. # ------------------ 串口相关 ------------------
  79. def list_serial_ports(self):
  80. ports = [port.device for port in serial.tools.list_ports.comports()]
  81. return ports if ports else ["无可用端口"]
  82. def open_serial(self):
  83. port = self.combo_port.get()
  84. baud = self.combo_baud.get()
  85. try:
  86. self.ser = serial.Serial(port, baudrate=int(baud), timeout=0)
  87. self.is_running = True
  88. self.btn_open["state"] = tk.DISABLED
  89. self.btn_close["state"] = tk.NORMAL
  90. self.status["text"] = f"状态: 已连接 {port} ({baud}bps)"
  91. self.text_rx.insert(tk.END, f"[系统] 成功打开串口 {port}\n")
  92. threading.Thread(target=self.read_serial, daemon=True).start()
  93. except Exception as e:
  94. messagebox.showerror("错误", f"无法打开串口: {e}")
  95. def close_serial(self):
  96. self.is_running = False
  97. if self.ser and self.ser.is_open:
  98. self.ser.close()
  99. self.btn_open["state"] = tk.NORMAL
  100. self.btn_close["state"] = tk.DISABLED
  101. self.status["text"] = "状态: 未连接"
  102. self.text_rx.insert(tk.END, "[系统] 串口已关闭\n")
  103. # ------------------ 接收数据 ------------------
  104. def read_serial(self):
  105. while self.is_running and self.ser and self.ser.is_open:
  106. try:
  107. count = self.ser.in_waiting
  108. if count:
  109. raw_data = self.ser.read(count)
  110. display_data = " ".join(f"{b:02X}" for b in raw_data) if self.hex_display.get() else raw_data.decode("utf-8", errors="ignore")
  111. self.text_rx.insert(tk.END, display_data + "\n")
  112. self.text_rx.see(tk.END)
  113. except Exception:
  114. break
  115. # ------------------ 源地址管理 ------------------
  116. def add_src(self):
  117. addr = self.entry_new_src.get().strip()
  118. if addr and addr not in self.combo_src["values"]:
  119. values = list(self.combo_src["values"])
  120. values.append(addr)
  121. self.combo_src["values"] = values
  122. self.entry_new_src.delete(0, tk.END)
  123. self.save_src_file(values)
  124. def delete_src(self):
  125. addr = self.combo_src.get().strip()
  126. values = list(self.combo_src["values"])
  127. if addr in values:
  128. values.remove(addr)
  129. self.combo_src["values"] = values
  130. if values:
  131. self.combo_src.current(0)
  132. self.save_src_file(values)
  133. def save_src_file(self, values):
  134. with open(SRC_FILE, "w") as f:
  135. for a in values:
  136. f.write(a + "\n")
  137. def load_src_addrs(self):
  138. if os.path.exists(SRC_FILE):
  139. with open(SRC_FILE, "r") as f:
  140. addrs = [line.strip() for line in f if line.strip()]
  141. if addrs:
  142. self.combo_src["values"] = addrs
  143. self.combo_src.current(0)
  144. else:
  145. self.combo_src["values"] = []
  146. else:
  147. self.combo_src["values"] = []
  148. # ------------------ 命令模板管理 ------------------
  149. def save_template(self):
  150. new_template = self.combo_frame.get().strip()
  151. if not new_template:
  152. messagebox.showwarning("提示", "命令模板不能为空!")
  153. return
  154. values = list(self.combo_frame["values"])
  155. if new_template not in values:
  156. values.append(new_template)
  157. self.combo_frame["values"] = values
  158. self.save_template_file(values)
  159. messagebox.showinfo("提示", f"模板已保存: {new_template}")
  160. else:
  161. messagebox.showinfo("提示", "模板已存在,无需重复添加")
  162. def delete_template(self):
  163. template = self.combo_frame.get().strip()
  164. values = list(self.combo_frame["values"])
  165. if template in values:
  166. values.remove(template)
  167. self.combo_frame["values"] = values
  168. if values:
  169. self.combo_frame.current(0)
  170. else:
  171. self.combo_frame.set('')
  172. self.save_template_file(values)
  173. messagebox.showinfo("提示", f"模板已删除: {template}")
  174. else:
  175. messagebox.showwarning("提示", "模板不存在或未选择")
  176. def save_template_file(self, values):
  177. with open(TEMPLATE_FILE, "w") as f:
  178. for t in values:
  179. f.write(t + "\n")
  180. def load_templates(self):
  181. if os.path.exists(TEMPLATE_FILE):
  182. with open(TEMPLATE_FILE, "r") as f:
  183. templates = [line.strip() for line in f if line.strip()]
  184. if templates:
  185. self.combo_frame["values"] = templates
  186. self.combo_frame.current(0)
  187. else:
  188. self.combo_frame["values"] = []
  189. else:
  190. self.combo_frame["values"] = []
  191. # ------------------ 发送帧 ------------------
  192. def send_frame(self):
  193. if not (self.ser and self.ser.is_open):
  194. messagebox.showwarning("警告", "请先打开串口!")
  195. return
  196. try:
  197. template = self.combo_frame.get().strip().replace(" ", "")
  198. src_addr = self.combo_src.get().replace(" ", "")
  199. if len(src_addr) != 4:
  200. raise ValueError("源地址长度非法")
  201. if len(template) < 2:
  202. raise ValueError("模板太短,至少需要命令码一个字节")
  203. # 命令码是模板的第一个字节
  204. cmd_code = template[:2]
  205. rest_template = template[2:] # 剩余模板内容
  206. # 组帧: 55 BB [命令码] [源地址] [剩余模板]
  207. frame = bytearray()
  208. frame.extend(b'\x55\xBB')
  209. frame.extend(bytes.fromhex(cmd_code))
  210. frame.extend(bytes.fromhex(src_addr))
  211. if rest_template:
  212. frame.extend(bytes.fromhex(rest_template))
  213. self.ser.write(frame)
  214. self.text_rx.insert(tk.END, f"[发送] {' '.join(f'{b:02X}' for b in frame)}\n")
  215. self.text_rx.see(tk.END)
  216. except ValueError as e:
  217. messagebox.showerror("输入错误", str(e))
  218. if __name__ == "__main__":
  219. root = tk.Tk()
  220. app = SerialAssistant(root)
  221. root.mainloop()