diff --git a/.idea/NetworkAnalog.iml b/.idea/NetworkAnalog.iml index 8fb85ed..c444878 100644 --- a/.idea/NetworkAnalog.iml +++ b/.idea/NetworkAnalog.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index ebb0faf..a2e120d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/NetworkAnalog/NetworkAnalog.py b/NetworkAnalog/NetworkAnalog.py index 7622f79..3ab7bcc 100644 --- a/NetworkAnalog/NetworkAnalog.py +++ b/NetworkAnalog/NetworkAnalog.py @@ -1,226 +1,36 @@ import ipaddress import sys import threading -import ttkbootstrap as tk -from ttkbootstrap import * -from ttkbootstrap import ttk -from tkinter import messagebox -import re -from PIL import ImageTk, Image +from tkinter import filedialog + +from SimUtil import * from SimObjs import SimPacket, SimHost, AllSimConnect, SimRouter, SimSwitch, SimHub, SimBase from dbUtil import search, execute_sql, delete_obj, truncate_db +from PIL import Image as imim -def round_rectangle(cv, x1, y1, x2, y2, radius=30, **kwargs): - """ - 绘制圆角矩形 - :param cv: canvas对象 - :param radius: 圆角值 - :return: - """ - points = [x1 + radius, y1, - x1 + radius, y1, - x2 - radius, y1, - x2 - radius, y1, - x2, y1, - x2, y1 + radius, - x2, y1 + radius, - x2, y2 - radius, - x2, y2 - radius, - x2, y2, - x2 - radius, y2, - x2 - radius, y2, - x1 + radius, y2, - x1 + radius, y2, - x1, y2, - x1, y2 - radius, - x1, y2 - radius, - x1, y1 + radius, - x1, y1 + radius, - x1, y1] - - return cv.create_polygon(points, **kwargs, smooth=True) - -def validate_ip_address(ip_address): - """ - 匹配ip地址格式是否规范 - :param ip_address: IP地址 - :return: Boolean - """ - # 定义IP地址的正则表达式模式 - pattern_with_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$' - pattern_without_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' - # 使用re模块进行匹配 - match_with_subnet = re.match(pattern_with_subnet, ip_address) - match_without_subnet = re.match(pattern_without_subnet, ip_address) - if match_with_subnet: - # 带有子网掩码的IP地址 - # 检查每个组件的取值范围是否在 0-255 之间 - for group in match_with_subnet.groups()[:4]: - if not (0 <= int(group) <= 255): - return False - # 检查子网掩码的取值范围是否在 0-32 之间 - subnet_mask = int(match_with_subnet.groups()[4]) - if not (0 <= subnet_mask <= 32): - return False - return True - elif match_without_subnet: - # 不带子网掩码的IP地址 - # 检查每个组件的取值范围是否在 0-255 之间 - for group in match_without_subnet.groups(): - if not (0 <= int(group) <= 255): - return False - return True - else: - # IP地址格式不正确 - return False - - -class RouterConfigWindow(tk.Toplevel): - def __init__(self, parent, router_obj): - super().__init__(parent) - self.geometry("435x433+350+200") - self.title(f"{router_obj.ObjLabel}路由表配置") - self.router_obj = router_obj - self.interface_entries = [] - self.router_table = {} - self.create_interface_inputs() - self.create_router_table() - - def create_interface_inputs(self): - label_text = ["接口1", "接口2", "接口3", "接口4"] - for i in range(4): - label = tk.Label(self, text=label_text[i], font=("黑体", 16)) - label.grid(row=i, column=0, padx=10, pady=5, sticky="w") - entry = tk.Entry(self, width=20, font=("黑体", 16),) - entry.grid(row=i, column=1, padx=10, pady=5, sticky="w") - self.interface_entries.append(entry) - button = tk.Button(self, text="添加", font=("黑体", 16), command=lambda index=i: self.add_router_entry(index)) - button.grid(row=i, column=2, padx=10, pady=5) - lab = LabelFrame(self, text="示例") - lab.grid(row=4, column=0, columnspan=3, sticky=W, padx=20) - Label(lab, text="10.1.2.0/24 或者 10.1.2.12" if self.router_obj.ObjType == 2 else "MAC11", font=("黑体", 16)).pack() - - def create_router_table(self): - def on_right_click(event): - row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 - if row: - self.router_treeview.selection_set(row) # 选中该行 - delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 - - def delete_row(): - selected_items = self.router_treeview.selection() # 获取选中的行 - for item in selected_items: - ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] - self.router_obj.delete_config(ifs, network) - self.router_treeview.delete(item) - self.router_table_frame = tk.Frame(self) - self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) - style = ttk.Style() - style.configure("Custom.Treeview.Heading", font=("宋体", 15)) - style.configure("Custom.Treeview", rowheight=30, font=("宋体", 15)) - self.router_treeview = ttk.Treeview(self.router_table_frame, style="Custom.Treeview", columns=("Interface", "Route"), show="headings") - self.router_treeview.heading("Interface", text="接口") - self.router_treeview.heading("Route", text="网段") - self.router_treeview.pack(side="left", fill="both") - scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) - scrollbar.pack(side="right", fill="y") - self.router_treeview.configure(yscrollcommand=scrollbar.set) - self.router_table = self.router_obj.router_table - self.router_treeview.bind("", on_right_click) - # 创建删除菜单 - delete_menu = tk.Menu(root, tearoff=False) - delete_menu.add_command(label="删除", command=delete_row) - self.update_router_table() - - def add_router_entry(self, index): - entry_text = self.interface_entries[index].get() - try: - ipaddress.ip_network(entry_text) - if isinstance(self.router_obj, SimRouter): - if not validate_ip_address(entry_text): - messagebox.showerror("注意", message="添加的网段信息格式不合格") - self.interface_entries[index].delete(0, tk.END) - self.focus_set() - return - if entry_text: - if index + 1 in self.router_table: - self.router_table[index + 1].append(entry_text) - else: - self.router_table[index + 1] = [entry_text] - self.interface_entries[index].delete(0, tk.END) - self.router_obj.add_config(entry_text, index + 1) - self.update_router_table() - except: - messagebox.showerror("注意", message="网段格式错误!网段示例如下:\n10.1.2.0/24\n10.1.2.12") - return +class Message(Canvas): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.message = [] + self.master = master + self.scrollbar = tk.Scrollbar(master, orient="vertical", command=self.yview) + self.scrollbar.pack(side="right", fill=BOTH) + self.config(yscrollcommand=self.scrollbar.set) - def update_router_table(self): - self.router_treeview.delete(*self.router_treeview.get_children()) - for i, entrys in self.router_table.items(): - for entry in entrys: - self.router_treeview.insert("", "end", values=(f"接口{i}", entry)) - - -class SwitchConfigWindow(RouterConfigWindow): - def __init__(self, parent, router_obj): - super().__init__(parent, router_obj) - self.geometry("435x433+350+200") - self.title(f"{router_obj.ObjLabel}交换表配置") - self.router_obj = router_obj - self.interface_entries = [] - self.router_table = {} - self.create_interface_inputs() - self.create_router_table() - - def create_router_table(self): - def on_right_click(event): - row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 - if row: - self.router_treeview.selection_set(row) # 选中该行 - delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 - - def delete_row(): - selected_items = self.router_treeview.selection() # 获取选中的行 - for item in selected_items: - ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] - self.router_obj.delete_config(ifs, network) - self.router_treeview.delete(item) - - self.router_table_frame = tk.Frame(self) - self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) - self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") - self.router_treeview.heading("Interface", text="接口") - self.router_treeview.heading("Route", text="mac") - self.router_treeview.pack(side="left", fill="both") - scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) - scrollbar.pack(side="right", fill="y") - self.router_treeview.configure(yscrollcommand=scrollbar.set) - self.router_table = self.router_obj.mac_table - self.router_treeview.bind("", on_right_click) - # 创建删除菜单 - delete_menu = tk.Menu(root, tearoff=False) - delete_menu.add_command(label="删除", command=delete_row) - self.update_router_table() - - - def add_router_entry(self, index): - entry_text = self.interface_entries[index].get() - if isinstance(self.router_obj, SimRouter): - if not validate_ip_address(entry_text): - messagebox.showerror("注意", message="添加的网段信息格式不合格") - self.interface_entries[index].delete(0, tk.END) - self.focus_set() - return - if entry_text: - if index + 1 in self.router_table: - self.router_table[index + 1].append(entry_text) - else: - self.router_table[index + 1] = [entry_text] - self.interface_entries[index].delete(0, tk.END) - self.router_obj.add_config(entry_text, index + 1) - self.update_router_table() + def show_message(self, message, color="#5fa8fe"): + self.message.append({"message": message, "color": color}) + num = 0 + self.delete("message") + for function in self.message: + self.create_text(20, 25 * num + 10, anchor="nw", text=function["message"], font=("", 15), + fill=function["color"], tags="message") + num += 1 + self.update() + self.configure(scrollregion=self.bbox("all")) + self.scrollbar.set(1.0, 1.0) + self.yview_moveto(1.0) class NetWorkAnalog(Canvas): @@ -228,17 +38,39 @@ class NetWorkAnalog(Canvas): super().__init__(master, **kwargs) self.master = master self.router_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/路由器.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/路由器@2x.png").resize((40, 40))) self.switch_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/交换机.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/交换机@2x.png").resize((40, 40))) self.hub_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/集线器.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/集线器@2x.png").resize((40, 40))) self.host_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/主机.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/主机@2x.png").resize((40, 40))) + self.pack_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/packet.png").resize((30, 30))) + self.true_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/true.png").resize((30, 30))) + self.false_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/false.png").resize((30, 30))) self.width = int(self.cget("width")) self.height = int(self.cget("height")) + self.canvas_size = [150, 0, int(self.width * 0.8), int(self.height * 0.8)] + self.label_top_img = ImageTk.PhotoImage(imim.open("../datas/images/右上框@2x.png").resize((int(self.width - self.width * 0.85), int(self.canvas_size[3] * 0.4)))) + self.label_bottom_img = ImageTk.PhotoImage(imim.open("../datas/images/右下框@2x.png").resize((int(self.width - self.width * 0.85), int(self.canvas_size[3] * 0.45)))) + self.message_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/日志消息展示框@2x.png").resize((self.width - 60, self.height - self.canvas_size[3] - 30)) + ) + message_frame = Frame(self, width=self.width - 60, height=self.height - self.canvas_size[3] - 30) + message_frame.place(x=15, y=self.canvas_size[3] + 15, anchor=NW) + self.message = Message(message_frame, width=self.width - 60, height=self.height - self.canvas_size[3] - 30) + self.message.pack() + self.message.configure(bg="#edf8ff") + self.configure(bg="#ddeafe") + self.filename = ImageTk.PhotoImage(imim.open("../datas/images/背景@2x.png").resize((self.width, self.canvas_size[3]))) + self.check_filename = ImageTk.PhotoImage(Image.open("../datas/images/背景@2x.png").resize((self.width, self.height))) + self.create_image(0, 0, image=self.filename, anchor=NW) self.chose = self.host_img self.AllSimObjs = {} + self.receive = None self.conns = [] self.drawLine = True # 画线标志 self.line_start_obj = None @@ -248,6 +80,7 @@ class NetWorkAnalog(Canvas): self.chose_obj = None self.show_label_flag = True self.show_interface_flag = True + self.check_error_obj = None self.create_widget() def is_chose(self): @@ -256,8 +89,11 @@ class NetWorkAnalog(Canvas): :return: """ self.delete("rectangle") - self.create_rectangle(self.chose_obj.ObjX - 25, self.chose_obj.ObjY - 25, self.chose_obj.ObjX + 25, - self.chose_obj.ObjY + 25, outline="red", tags="rectangle") + self.create_rectangle(self.chose_obj.ObjX - 35, self.chose_obj.ObjY - 35, self.chose_obj.ObjX + 15, + self.chose_obj.ObjY + 15, outline="red", tags="rectangle") + self.show_network_config() + if self.chose_obj.ObjType == 2 or self.chose_obj.ObjType == 3: + self.show_router_config() def tag_bind_event(self): """ @@ -323,6 +159,7 @@ class NetWorkAnalog(Canvas): if not flag: self.line_start_obj.connections[self.line_start_ifs - 1] = conn self.line_end_obj.connections[self.line_end_ifs - 1] = conn + self.message.show_message(f"{self.line_start_obj.ObjLabel}连接{self.line_end_obj.ObjLabel}成功!") conn.save() self.conns.append(conn) init() @@ -344,15 +181,15 @@ class NetWorkAnalog(Canvas): if self.drawLine: self.delete("Line") x, y = event.x, event.y - if not (0 < x < self.width * 0.8 and 0 < y < self.height * 0.85): + if not (0 < x < self.canvas_size[2] and 0 < y < self.canvas_size[3]): if x < 0: x = 0 - elif x > self.width * 0.8: - x = self.width * 0.8 + elif x > self.canvas_size[2]: + x = self.canvas_size[2] if y < 0: y = 0 - elif y > self.height * 0.85: - y = self.height * 0.85 + elif y > self.canvas_size[3]: + y = self.canvas_size[3] self.create_line(self.line_start_obj.ObjX, self.line_start_obj.ObjY, x + 8, y + 8, fill="#5b9bd5", width=1, tags="Line") @@ -380,7 +217,7 @@ class NetWorkAnalog(Canvas): 鼠标左键松开事件 :param event: 事件对象 """ - if 20 < event.x < self.width * 0.8 + 20 and 20 < event.y < self.height * 0.85 - 20: # 在方框内,无变化 + if 20 + self.canvas_size[0] < event.x < self.canvas_size[2] + 20 and 20 + self.canvas_size[1] < event.y < self.canvas_size[3] - 20: # 在方框内,无变化 if name == "路由器": tag = SimRouter(self, event.x, event.y) elif name == "集线器": @@ -392,6 +229,7 @@ class NetWorkAnalog(Canvas): tag.create_img() self.AllSimObjs[tag.ObjID] = tag tag.save() + self.message.show_message(f"组件 {tag.ObjLabel} 创建成功!") self.tag_bind_event() else: self.delete("L") @@ -410,7 +248,7 @@ class NetWorkAnalog(Canvas): else: self.chose = self.host_img self.delete("L") - if 20 < event.x < self.width * 0.8 + 20 and 20 < event.y < self.height * 0.85 - 20: # 在方框内,无变化 + if 20 + self.canvas_size[0] < event.x < self.canvas_size[2] + 20 and 20 + self.canvas_size[1] < event.y < self.canvas_size[3] - 20: # 在方框内,无变化 pass else: # 在方框外,显示禁止放置标识 self.create_oval(event.x - 10, event.y - 45, event.x + 10, event.y - 25, outline="red", width=2, @@ -471,7 +309,6 @@ class NetWorkAnalog(Canvas): self.AllSimObjs = AllSimObj self.conns = conns for key, sim_obj in self.AllSimObjs.items(): - print(key) sim_obj.create_img() for conn in self.conns: conn.draw_line() @@ -498,6 +335,7 @@ class NetWorkAnalog(Canvas): execute_sql(delete_sql) execute_sql(delete_config_sql) delete_obj(self.chose_obj.ObjID) + self.message.show_message(f"组件 {self.chose_obj.ObjLabel} 删除成功!") self.delete("rectangle") self.reload_data() @@ -565,6 +403,7 @@ class NetWorkAnalog(Canvas): self.AllSimObjs[data[0]].connections[data[1] - 1] = None child_d.destroy() messagebox.showinfo("提示", f"连接线{value.get()}删除成功!") + self.message.show_message(f"连接线{value.get()}删除成功!") btn_yes = tk.Button(child_d, text='确定', font=('黑体', 12), height=1, command=del_line) btn_yes.place(x=240, y=20) @@ -601,6 +440,7 @@ class NetWorkAnalog(Canvas): conn.update_info(self.chose_obj.ObjID) child1.destroy() messagebox.showinfo("提示", message="修改成功!") + self.message.show_message(f"{name} 修改名称成功!") tk.Button(child1, text='确定', font=('黑体', 16), height=1, command=update_tag).grid(row=2, column=0, sticky='e', pady=10) @@ -677,6 +517,7 @@ class NetWorkAnalog(Canvas): addr_en.grid(row=1, column=3, padx=10) def commit(): self.chose_obj.config(datas) + self.message.show_message(f"组件 {self.chose_obj} 网络配置成功!") child_r.destroy() tk.Button(ifs_frame_YN, text='确定', font=('黑体', 16), height=1, command=commit).grid(row=0, column=0, @@ -767,9 +608,9 @@ class NetWorkAnalog(Canvas): messagebox.showerror("注意", message="请选择路由器/交换机对象!") return self.delete("routerSet") - self.create_text(self.width * 0.82 + 120, self.height * 0.85 / 2 + 50, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 14, 'bold'), + self.create_text(self.width * 0.82 + 120, self.canvas_size[3] / 2 + 50, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 14, 'bold'), fill="#7030a0", tags="routerSet") - self.create_text(self.width * 0.82 + 20, self.height * 0.85 / 2 + 50 + 25, text=self.chose_obj.get_table_config(), + self.create_text(self.width * 0.82 + 20, self.canvas_size[3] / 2 + 50 + 25, text=self.chose_obj.get_table_config(), anchor="nw", font=('宋体', 14), tags="routerSet") def send_packet(self): @@ -793,12 +634,9 @@ class NetWorkAnalog(Canvas): tk.Label(child2, text='目的IP:', font=('黑体', 16)).grid(row=0, column=0, columnspan=2, sticky='w', pady=10) packet_ip = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) packet_ip.grid(row=0, column=1, pady=10) - tk.Label(child2, text='目的MAC:', font=('黑体', 16)).grid(row=1, column=0, sticky='w', pady=10) # ,sticky='w'靠左显示 - packet_mac = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) - packet_mac.grid(row=1, column=1, pady=10) - tk.Label(child2, text='消息:', font=('黑体', 16)).grid(row=2, column=0, sticky='w', pady=10) # ,sticky='w'靠左显示 + tk.Label(child2, text='消息:', font=('黑体', 16)).grid(row=1, column=0, sticky='w', pady=10) # ,sticky='w'靠左显示 packet_message = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) - packet_message.grid(row=2, column=1, pady=10) + packet_message.grid(row=1, column=1, pady=10) def send(): """ @@ -811,16 +649,16 @@ class NetWorkAnalog(Canvas): if not validate_ip_address(packet_ip.get()): messagebox.showerror("注意", message="IP地址不规范!") return - if packet_mac.get() == "": - messagebox.showerror("注意", message="mac地址不能为空!") - return if packet_message.get() == "": messagebox.showerror("注意", message="消息不能为空!") return - self.chose_obj.create_packet(packet_ip.get(), - packet_mac.get(), - packet_message.get()) + ip = packet_ip.get() + message = packet_message.get() child2.destroy() + self.chose_obj.create_packet(ip, + None, + message) + tk.Button(child2, text='确定', font=('黑体', 16), height=1, command=send).grid(row=3, column=0, sticky='e', pady=10) tk.Button(child2, text='取消', font=('黑体', 16), height=1, command=child2.destroy).grid(row=3, column=1, sticky='e', pady=10) @@ -840,15 +678,11 @@ class NetWorkAnalog(Canvas): tk.Label(child2, text='目的IP:', font=('黑体', 16)).grid(row=0, column=0, columnspan=2, sticky='w', ipady=10) packet_ip = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) packet_ip.grid(row=0, column=1, pady=5) - tk.Label(child2, text='目的MAC:', font=('黑体', 16)).grid(row=1, column=0, sticky='w', - pady=5) # ,sticky='w'靠左显示 - packet_mac = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) - packet_mac.grid(row=1, column=1, pady=5) - tk.Label(child2, text='消息:', font=('黑体', 16)).grid(row=2, column=0, sticky='w', pady=5) # ,sticky='w'靠左显示 + tk.Label(child2, text='消息:', font=('黑体', 16)).grid(row=1, column=0, sticky='w', pady=5) # ,sticky='w'靠左显示 packet_message = tk.Entry(child2, font=('黑体', 16), textvariable=tk.StringVar()) - packet_message.grid(row=2, column=1, pady=5) + packet_message.grid(row=1, column=1, pady=5) host = StringVar() - tk.Label(child2, text='发送主机:', font=('黑体', 16)).grid(row=3, column=0, sticky='w', + tk.Label(child2, text='发送主机:', font=('黑体', 16)).grid(row=2, column=0, sticky='w', pady=5) # ,sticky='w'靠左显示 combobox = ttk.Combobox( master=child2, # 父容器 @@ -859,7 +693,7 @@ class NetWorkAnalog(Canvas): textvariable=host, # 通过StringVar设置可改变的值 values=list(hosts.keys()), # 设置下拉框的选项 ) - combobox.grid(row=3, column=1, pady=5) + combobox.grid(row=2, column=1, pady=5) packet_frame = tk.Frame(child2) packet_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) packet_treeview = ttk.Treeview(packet_frame, columns=("source_host", "ip", "mac", "message"), show="headings") @@ -879,15 +713,14 @@ class NetWorkAnalog(Canvas): def add(): ip = packet_ip.get() - mac = packet_mac.get() message = packet_message.get() chose_host = host.get() - if ip == "" or mac == "" or message == "" or chose_host == "": + if ip == "" or message == "" or chose_host == "": messagebox.showerror("注意", message="输入框不能为空") return packet = SimPacket(self.AllSimObjs[hosts[chose_host]].interface[0]["ip"], self.AllSimObjs[hosts[chose_host]].interface[0]["mac"], - ip, mac, message) + ip, None, message) packets.append((self.AllSimObjs[hosts[chose_host]], packet)) packet_treeview.delete(*packet_treeview.get_children()) for datas in packets: @@ -928,6 +761,99 @@ class NetWorkAnalog(Canvas): self.AllSimObjs.clear() self.conns.clear() + def export_data(self): + try: + export = ExportUtil(sys.path[0] + "/database.xlsx") + export.export() + self.message.show_message("文件导出成功!文件位置在{}".format(sys.path[0] + "/database.xlsx")) + except Exception as E: + self.message.show_message(str(E), color="red") + + def import_data(self): + truncate_db() + for tag_id, tag in self.AllSimObjs.items(): + self.delete(tag_id) + self.delete(tag_id + "text") + for conn in self.conns: + conn.delete_line() + self.delete("rectangle") + self.AllSimObjs.clear() + self.conns.clear() + file_path = filedialog.askopenfilename() + if file_path: + export = ExportUtil(file_path) + export.import_data() + self.message.show_message("文件导入成功!") + self.reload_data() + + def check_network(self): + """ + 校验 + :return: + """ + hosts = {} + for key, tag in self.AllSimObjs.items(): + if tag.ObjType == 1 and tag.ConfigCorrect == 1: + hosts[tag.ObjLabel] = tag.ObjID + child2 = tk.Toplevel() + child2.title("数据包配置") + child2.geometry('392x168+200+110') + child2.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + tk.Label(child2, text='发送主机:', font=('黑体', 16)).grid(row=0, column=0, columnspan=2, sticky='w', pady=10) + send_host = StringVar() + host_combobox = ttk.Combobox( + master=child2, # 父容器 + height=5, # 高度,下拉显示的条目数量 + width=18, # 宽度 + state='readonly', # readonly(只可选) + font=('', 16), # 字体 + textvariable=send_host, # 通过StringVar设置可改变的值 + values=list(hosts.keys()), # 设置下拉框的选项 + ) + + host_combobox.grid(row=0, column=1, pady=10) + tk.Label(child2, text='接收主机:', font=('黑体', 16)).grid(row=1, column=0, sticky='w', + pady=10) # ,sticky='w'靠左显示 + receive_host = StringVar() + ttk.Combobox( + master=child2, # 父容器 + height=5, # 高度,下拉显示的条目数量 + width=18, # 宽度 + state='readonly', # readonly(只可选) + font=('', 16), # 字体 + textvariable=receive_host, # 通过StringVar设置可改变的值 + values=list(hosts.keys()), # 设置下拉框的选项 + ).grid(row=1, column=1, pady=10) + + def check(): + """ + 发送数据包 + :return: + """ + if send_host.get() == "" or receive_host.get() == "": + messagebox.showerror("注意", message="信息不能为空") + return + if send_host.get() == receive_host.get(): + messagebox.showerror("注意", message="发送主机和接收主机不能相同!") + send_host_obj: SimHost = self.AllSimObjs[hosts[send_host.get()]] + receive_host_obj: SimHost = self.AllSimObjs[hosts[receive_host.get()]] + child2.destroy() + send_host_obj.create_packet(receive_host_obj.interface[0]["ip"], receive_host_obj.interface[0]["mac"], "ping", True) + TransferAnimate(self, self.receive == receive_host_obj) + if self.receive == receive_host_obj: + self.message.show_message("校验成功:{} 与 {} 能正常连通".format(send_host_obj.ObjLabel, receive_host_obj.ObjLabel)) + else: + self.message.show_message( + "校验成功:{} 与 {} 无法正常连通,请检查一下组件 {} 的配置".format(send_host_obj.ObjLabel, receive_host_obj.ObjLabel, self.check_error_obj.ObjLabel), "red") + self.check_error_obj = None + self.receive = None + + + tk.Button(child2, text='校验', font=('黑体', 16), height=1, command=check).grid(row=5, column=0, sticky='e', + pady=10) + tk.Button(child2, text='取消', font=('黑体', 16), height=1, command=child2.destroy).grid(row=5, column=1, + sticky='e', pady=10) + def create_config_button(self): # 创建一个菜单栏,这里我们可以把他理解成一个容器,在窗口的上方 menubar = tk.Menu(root) @@ -958,6 +884,9 @@ class NetWorkAnalog(Canvas): setMenu.add_command(label='批量发送数据包', command=self.send_packet_list) menubar.add_command(label='清除屏幕', command=self.clear_canvas) + menubar.add_command(label='导出数据', command=self.export_data) + menubar.add_command(label='导入数据', command=self.import_data) + menubar.add_command(label='校验', command=self.check_network) root.config(menu=menubar) def create_widget(self): @@ -966,21 +895,22 @@ class NetWorkAnalog(Canvas): 创建整体页面布局 :return: """ - self.create_rectangle(0, 0, self.width * 0.8, self.height * 0.85, outline="#7f6000", width=3) # 矩形框,左上角坐标,右下角坐标 - round_rectangle(self, self.width * 0.82, 30, self.width * 0.98, self.height * 0.85 / 2 - 30, outline="#ffff00", width=3, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_rectangle(self.canvas_size[0], self.canvas_size[1], self.canvas_size[2], self.canvas_size[3], outline="#7f6000", width=0) # 矩形框,左上角坐标,右下角坐标 + self.create_image(self.width * 0.82, 30, image=self.label_top_img, anchor=NW) # 矩形框,左上角坐标,右下角坐标 self.create_text(self.width * 0.82 + 120, 30 + 15, text="网络配置信息", anchor="n", font=('微软雅黑', 18, 'bold')) - round_rectangle(self, self.width * 0.82, self.height * 0.85 / 2, self.width * 0.98, self.height * 0.85 - 30, outline="#ffff00", width=2, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 - self.create_text(self.width * 0.82 + 120, self.height * 0.85 / 2 + 15, text="路由/交换表信息", anchor="n", font=('微软雅黑', 18, 'bold')) + self.create_image(self.width * 0.82, self.canvas_size[3] / 2, image=self.label_bottom_img, anchor=NW) # 矩形框,左上角坐标,右下角坐标 + # round_rectangle(self, self.width * 0.82, self.canvas_size[3] / 2, self.width * 0.98, self.canvas_size[3] - 30, outline="#ffff00", width=2, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_text(self.width * 0.82 + 120, self.canvas_size[3] / 2 + 15, text="路由/交换表信息", anchor="n", font=('微软雅黑', 18, 'bold')) # 显示左边的固定图片 - img_height, text_height, split_width = self.height - 120, self.height - 60, 120 - self.create_text(split_width, text_height, text="路由器", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 - router = self.create_image(split_width, img_height, image=self.router_img, anchor="nw") - self.create_text(split_width * 2, text_height, text="交换机", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 - switch = self.create_image(split_width * 2, img_height, image=self.switch_img, anchor="nw") - self.create_text(split_width * 3, text_height, text="集线器", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 - hub = self.create_image(split_width * 3, img_height, image=self.hub_img, anchor="nw") - self.create_text(split_width * 4, text_height, text="主机", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 - host = self.create_image(split_width * 4, img_height, image=self.host_img, anchor="nw") + img_height, text_height, split_width = 50, 40, 120 + self.create_text(text_height, split_width + 40, text="路由器", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 + router = self.create_image(img_height, split_width, image=self.router_img, anchor="nw") + self.create_text(text_height, split_width * 2 + 40, text="交换机", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 + switch = self.create_image(img_height, split_width * 2, image=self.switch_img, anchor="nw") + self.create_text(text_height, split_width * 3 + 40, text="集线器", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 + hub = self.create_image(img_height, split_width * 3, image=self.hub_img, anchor="nw") + self.create_text(text_height, split_width * 4 + 40, text="主机", anchor="nw", font=('微软雅黑', 18, 'bold')) # 显示文字 + host = self.create_image(img_height, split_width * 4, image=self.host_img, anchor="nw") self.bind_event(router, "路由器") self.bind_event(switch, "交换机") self.bind_event(hub, "集线器") @@ -990,7 +920,7 @@ class NetWorkAnalog(Canvas): if __name__ == '__main__': root = Window() root.title('网络拓扑图') - screen_width = root.winfo_screenwidth() # winfo方法来获取当前电脑屏幕大小 + screen_width = root.winfo_screenwidth() # winfo 方法来获取当前电脑屏幕大小 screen_height = root.winfo_screenheight() root_attr = { "width": screen_width * 0.83, diff --git a/NetworkAnalog/SimObjs.py b/NetworkAnalog/SimObjs.py index 64227d6..4eedab3 100644 --- a/NetworkAnalog/SimObjs.py +++ b/NetworkAnalog/SimObjs.py @@ -27,21 +27,21 @@ class SimBase(): self.ObjY = y self.canvas = canvas self.host_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/主机.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/主机@2x.png").resize((40, 40))) self.host_img_tm = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/主机_tm.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/主机--暗色@2x.png").resize((40, 40))) self.router_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/路由器.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/路由器@2x.png").resize((40, 40))) self.router_img_tm = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/路由器_tm.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/路由器--暗色@2x.png").resize((40, 40))) self.switch_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/交换机.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/交换机@2x.png").resize((40, 40))) self.switch_img_tm = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/交换机_tm.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/交换机--暗色@2x.png").resize((40, 40))) self.hub_img = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/集线器.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/集线器@2x.png").resize((40, 40))) self.hub_img_tm = ImageTk.PhotoImage( - Image.open(sys.path[0] + "/../datas/images/集线器_tm.png").resize((60, 60))) + Image.open(sys.path[0] + "/../datas/images/集线器--暗色@2x.png").resize((40, 40))) self.img = None self.img_tm = None self.interface = [{}, {}, {}, {}] @@ -90,10 +90,8 @@ class SimBase(): self.canvas.chose_obj = None dx = event.x - self.start_x dy = event.y - self.start_y - width = int(self.canvas.cget("width")) - height = int(self.canvas.cget("height")) # 移动范围限制, 超出移动范围则直接返回 - if not (0 + 20 <= self.ObjX + dx <= width * 0.8 - 20 and 20 <= self.ObjY + dy <= height * 0.85 - 20): + if not (self.canvas.canvas_size[0] + 20 <= self.ObjX + dx <= self.canvas.canvas_size[2] - 20 and 20 + self.canvas.canvas_size[1] <= self.ObjY + dy <= self.canvas.canvas_size[3] - 20): return self.ObjX += dx self.ObjY += dy @@ -127,8 +125,8 @@ class SimBase(): image=self.img if self.ConfigCorrect == 1 else self.img_tm, anchor="nw", tags=self.ObjID) self.canvas.dtag("L", "L") - self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 16, "bold"), - fill="#7030a0", tags=self.ObjID + "text", anchor="nw") + self.canvas.create_text(self.ObjX - 20, self.ObjY - 60, text=self.ObjLabel, font=("", 16, "bold"), + fill="#9b78eb", tags=self.ObjID + "text", anchor="nw") self.canvas.tag_raise(id) self.bind_event() @@ -177,7 +175,7 @@ class SimBase(): :return: """ self.canvas.delete(self.ObjID + "text") - self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 16, "bold"), + self.canvas.create_text(self.ObjX - 20, self.ObjY - 60, text=self.ObjLabel, font=("", 16, "bold"), fill="#7030a0", tags=self.ObjID + "text", anchor="nw") sql = f"update sim_objs set objLabel='{self.ObjLabel}', ObjX={self.ObjX}," \ f"ObjY={self.ObjY}, ConfigCorrect={self.ConfigCorrect} where ObjID='{self.ObjID}'" @@ -220,7 +218,7 @@ class SimPacket(): """ 数据包类 """ - def __init__(self, source_ip, source_mac, destination_ip, destination_mac, message): + def __init__(self, source_ip, source_mac, destination_ip, destination_mac, message, ping=False): """ :param source_mac: 源主机mac地址 :param source_ip: 源主机ip地址 @@ -234,6 +232,7 @@ class SimPacket(): self.destination_ip = destination_ip self.message = message self.up_jump = None # 上一跳连接对象 + self.ping = ping self.img = ImageTk.PhotoImage( Image.open(sys.path[0] + "/../datas/images/packet.png").resize((30, 30))) self.id = str(uuid4()) @@ -260,6 +259,8 @@ class SimPacket(): cv.delete(self.id) def transfer_packet(self, cv: Canvas, nodex_tag: SimBase, nodey_tag: SimBase): + if self.ping: + return cv.create_image(nodex_tag.ObjX - 15, nodex_tag.ObjY - 15, image=self.img, anchor="nw", tags=self.id) self.move(cv, nodey_tag.ObjX - 15, nodey_tag.ObjY - 15, 500) @@ -280,7 +281,7 @@ class AllSimConnect(): self.NobjE = nodey self.IfsS = nodex_ifs self.IfsE = nodey_ifs - self.width = 1 if self.ConfigCorrect == 0 else 4 # 线的粗细 + self.width = False if self.ConfigCorrect == 0 else True # 线的粗细 def is_connected_to(self, other): """ @@ -342,16 +343,21 @@ class AllSimConnect(): :return: """ if source_node == self.NobjS: - packet.transfer_packet(self.canvas, self.NobjS, self.NobjE) + if not packet.ping: + packet.transfer_packet(self.canvas, self.NobjS, self.NobjE) self.NobjE.receive(packet) else: packet.transfer_packet(self.canvas, self.NobjE, self.NobjS) self.NobjS.receive(packet) def draw_line(self): - line = self.canvas.create_line(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjE.ObjX, self.NobjE.ObjY, - width=self.width, fill="#5b9bd5", tags=self.NobjS.ObjID + self.NobjE.ObjID + "line") - self.canvas.tag_lower(line) + if self.width: + line = self.canvas.create_line(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjE.ObjX, self.NobjE.ObjY, + width=2, fill="#c372f0", tags=self.NobjS.ObjID + self.NobjE.ObjID + "line") + else: + line = self.canvas.create_line(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjE.ObjX, self.NobjE.ObjY, + width=2, dash=(250, 4), fill="#c372f0", tags=self.NobjS.ObjID + self.NobjE.ObjID + "line") + self.canvas.tag_lower(line, 2) self.analyseIFS(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjS.ObjLabel, self.IfsS, self.NobjE.ObjX, self.NobjE.ObjY, self.NobjE.ObjLabel, self.IfsE) @@ -384,6 +390,7 @@ class AllSimConnect(): :param SLabel: S对象的标签 :param IfsS: S对象要显示的接口号 ''' + offset = 15 if (EX - SX) == 0: # 即垂直的时候,NobjS在NobjE对象的正上方和正下方,x坐标无变化,只需要y变化 R = 18 x = 0 # x无偏移量 @@ -401,10 +408,10 @@ class AllSimConnect(): x_E = EX - x y_E = EY - y # NobjE的连接点坐标 # 显示S接口号 - self.canvas.create_text((x_S - 5, y_S - 5), text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text((x_S + offset, y_S - offset), text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 # 显示E接口号 - self.canvas.create_text(x_E - 10, y_E - 10, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_E - offset, y_E - offset, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 elif SX < EX and SY > EY: # NobjS在NobjE的左下角,NobjS的右上角连接NobjE的左下角 x_S = SX + x @@ -412,10 +419,10 @@ class AllSimConnect(): x_E = EX - x y_E = EY + y # NobjE的连接点坐标 # 显示S接口号 - self.canvas.create_text(x_S + 5, y_S - 10, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_S + offset, y_S - offset, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 # 显示E接口号 - self.canvas.create_text(x_E - 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_E - offset, y_E, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 elif SX > EX and SY < EY: # NobjS在NobjE的右上角,NobjS的左下角连接NobjE的右上角 x_S = SX - x @@ -423,10 +430,10 @@ class AllSimConnect(): x_E = EX + x y_E = EY - y # NobjE的连接点坐标 # 显示S接口号 - self.canvas.create_text(x_S - 5, y_S, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_S - offset, y_S, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 # 显示E接口号 - self.canvas.create_text(x_E + 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_E + offset, y_E, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 elif SX >= EX and SY >= EY: # NobjS在NobjE的右下角,NobjS的左上角连接NobjE的右下角 x_S = SX - x @@ -434,10 +441,10 @@ class AllSimConnect(): x_E = EX + x y_E = EY + y # NobjE的连接点坐标 # 显示S接口号 - self.canvas.create_text(x_S - 5, y_S - 15, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_S - offset, y_S - offset, text=IfsS, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 # 显示E接口号 - self.canvas.create_text(x_E - 5, y_E - 5, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="red", + self.canvas.create_text(x_E + offset, y_E - offset, text=IfsE, anchor="nw", font=("幼圆", 16, "bold"), fill="#5fa6d6", tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 def save(self): @@ -485,7 +492,7 @@ class SimHost(SimBase): self.set_default_config() - def create_packet(self, ip, mac, message): + def create_packet(self, ip, mac, message, ping=False): """ 创建数据包 :param ip: 目的主机ip @@ -493,7 +500,10 @@ class SimHost(SimBase): :param message: 消息 :return: """ - packet = SimPacket(self.interface[0]["ip"], self.interface[0]["mac"], ip, mac, message) + if ping: + packet = SimPacket(self.interface[0]["ip"], self.interface[0]["mac"], ip, mac, message, True) + else: + packet = SimPacket(self.interface[0]["ip"], self.interface[0]["mac"], ip, mac, message) print(f"创建数据包成功,数据包由{packet.source_ip} 发往 {packet.destination_ip}") self.send(packet) @@ -516,7 +526,10 @@ class SimHost(SimBase): """ print(f"主机{self.ObjLabel}接受到数据{packet.message}") if packet.destination_ip == self.interface[0]["ip"]: - self.transfer_animate(True, packet) + if not packet.ping: + self.transfer_animate(True, packet) + else: + self.canvas.receive = self else: self.transfer_animate(False, packet) @@ -606,6 +619,9 @@ class SimRouter(SimBase): conn.transfer(self, packet) break error_message = "路由寻址失败" + if packet.ping: + self.canvas.check_error_obj = self + return self.transfer_animate(False, packet, error_message) def receive(self, packet): diff --git a/NetworkAnalog/SimUtil.py b/NetworkAnalog/SimUtil.py new file mode 100644 index 0000000..ef40d6a --- /dev/null +++ b/NetworkAnalog/SimUtil.py @@ -0,0 +1,332 @@ +import ipaddress +import re +import sqlite3 +from time import sleep +from tkinter import messagebox + +import ttkbootstrap as tk +from ttkbootstrap import * +from ttkbootstrap import ttk +import pandas as pd +import sys +from PIL import Image, ImageTk + +from SimObjs import SimRouter + + +def round_rectangle(cv, x1, y1, x2, y2, radius=30, **kwargs): + """ + 绘制圆角矩形 + :param cv: canvas对象 + :param radius: 圆角值 + :return: + """ + points = [x1 + radius, y1, + x1 + radius, y1, + x2 - radius, y1, + x2 - radius, y1, + x2, y1, + x2, y1 + radius, + x2, y1 + radius, + x2, y2 - radius, + x2, y2 - radius, + x2, y2, + x2 - radius, y2, + x2 - radius, y2, + x1 + radius, y2, + x1 + radius, y2, + x1, y2, + x1, y2 - radius, + x1, y2 - radius, + x1, y1 + radius, + x1, y1 + radius, + x1, y1] + + return cv.create_polygon(points, **kwargs, smooth=True) + +def validate_ip_address(ip_address): + """ + 匹配ip地址格式是否规范 + :param ip_address: IP地址 + :return: Boolean + """ + # 定义IP地址的正则表达式模式 + pattern_with_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$' + pattern_without_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' + # 使用re模块进行匹配 + match_with_subnet = re.match(pattern_with_subnet, ip_address) + match_without_subnet = re.match(pattern_without_subnet, ip_address) + if match_with_subnet: + # 带有子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_with_subnet.groups()[:4]: + if not (0 <= int(group) <= 255): + return False + # 检查子网掩码的取值范围是否在 0-32 之间 + subnet_mask = int(match_with_subnet.groups()[4]) + if not (0 <= subnet_mask <= 32): + return False + return True + elif match_without_subnet: + # 不带子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_without_subnet.groups(): + if not (0 <= int(group) <= 255): + return False + return True + else: + # IP地址格式不正确 + return False + + +class ExportUtil(): + def __init__(self, path): + self.conn = sqlite3.connect('./network.db') + self.path = path + + def get_table_names(self): + cursor = self.conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") # 如果你使用SQLite数据库 + tables = cursor.fetchall() + cursor.close() + return [table[0] for table in tables] + + def export(self): + tables = self.get_table_names() + with pd.ExcelWriter(self.path, engine='openpyxl') as writer: + for table in tables: + table_name = table + # a. 从数据库中获取表的数据并存储在DataFrame中 + query = f"SELECT * FROM {table_name}" + df = pd.read_sql(query, self.conn) + # b. 使用Pandas将数据写入Excel文件的不同sheet中 + df.to_excel(writer, sheet_name=table_name, index=False) + + def import_data(self): + excel_file = pd.ExcelFile(self.path) + sheet_names = excel_file.sheet_names + print(sheet_names) + cursor = self.conn.cursor() + for sheet_name in sheet_names: + # 4. 使用 Pandas 读取工作表数据 + df = pd.read_excel(excel_file, sheet_name=sheet_name) + # 5. 获取工作表的列名 + columns = df.columns.tolist() + # 6. 构造插入语句 + columns_str = ', '.join(columns) + placeholders = ', '.join(['?' for _ in range(len(columns))]) + sql = f"INSERT INTO {sheet_name} ({columns_str}) VALUES ({placeholders})" + # 7. 将数据插入数据库 + for index, row in df.iterrows(): + # 8. 使用动态生成的 SQL 语句将数据插入数据库 + cursor.execute(sql, tuple(row)) + self.conn.commit() + + +class RouterConfigWindow(tk.Toplevel): + def __init__(self, parent, router_obj): + super().__init__(parent) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}路由表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_interface_inputs(self): + label_text = ["接口1", "接口2", "接口3", "接口4"] + for i in range(4): + label = tk.Label(self, text=label_text[i], font=("黑体", 16)) + label.grid(row=i, column=0, padx=10, pady=5, sticky="w") + entry = tk.Entry(self, width=20, font=("黑体", 16),) + entry.grid(row=i, column=1, padx=10, pady=5, sticky="w") + self.interface_entries.append(entry) + button = tk.Button(self, text="添加", font=("黑体", 16), command=lambda index=i: self.add_router_entry(index)) + button.grid(row=i, column=2, padx=10, pady=5) + lab = LabelFrame(self, text="示例") + lab.grid(row=4, column=0, columnspan=3, sticky=W, padx=20) + Label(lab, text="10.1.2.0/24 或者 10.1.2.12" if self.router_obj.ObjType == 2 else "MAC11", font=("黑体", 16)).pack() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + style = ttk.Style() + style.configure("Custom.Treeview.Heading", font=("宋体", 15)) + style.configure("Custom.Treeview", rowheight=30, font=("宋体", 15)) + self.router_treeview = ttk.Treeview(self.router_table_frame, style="Custom.Treeview", columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="网段") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.router_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(self.master, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + try: + ipaddress.ip_network(entry_text) + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.update_router_table() + self.master.message.show_message(f"{self.router_obj.ObjLabel}添加配置{entry_text}成功!") + except: + messagebox.showerror("注意", message="网段格式错误!网段示例如下:\n10.1.2.0/24\n10.1.2.12") + return + + def update_router_table(self): + self.router_treeview.delete(*self.router_treeview.get_children()) + for i, entrys in self.router_table.items(): + for entry in entrys: + self.router_treeview.insert("", "end", values=(f"接口{i}", entry)) + + +class SwitchConfigWindow(RouterConfigWindow): + def __init__(self, parent, router_obj): + super().__init__(parent, router_obj) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}交换表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="mac") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.mac_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(self.master, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.master.message.show_message(f"{self.router_obj.ObjLabel}添加配置{entry_text}成功!") + self.update_router_table() + + +class TransferAnimate: + """ + 校验动画 + """ + def __init__(self, master, flag=False): + self.master = master + self.flag = flag + self.width, self.height = 320, 140 + self.host_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/主机@2x.png").resize((40, 40))) + self.filename = ImageTk.PhotoImage(Image.open("../datas/images/背景@2x.png").resize((self.width, self.height))) + self.toplevel = Toplevel(alpha=0.9) + self.toplevel.geometry("{}x{}+200+150".format(self.width, self.height)) + self.toplevel.title("校验") + self.toplevel.resizable(width=False, height=False) + self.canvas = Canvas(self.toplevel, width=self.width, height=self.height, bg="red") + self.canvas.place(x=0, y=0, anchor=NW) + back = self.canvas.create_image(10, 20, image=master.check_filename) + self.send_x, self.receive_x, self.obj_y = 50, self.width - 50, self.height / 2 - 20 + self.canvas.create_image(self.send_x, self.obj_y, image=master.host_img) + self.canvas.create_image(self.receive_x, self.obj_y, image=master.host_img) + line = self.canvas.create_line(self.send_x, self.obj_y, self.receive_x, self.obj_y, width=2, fill="#c372f0") + self.canvas.tag_lower(line) + self.canvas.tag_lower(back) + self.animate() + + def animate(self): + for i in range(3): + self.canvas.create_image(self.send_x, self.obj_y, image=self.master.pack_img, tags="check_img") + self.move(self.canvas, self.receive_x, self.obj_y, 500) + self.canvas.delete("check_img") + self.canvas.update() + self.canvas.create_image(self.receive_x - self.send_x - 45, self.obj_y, image=self.master.true_img if self.flag else self.master.false_img) + self.canvas.update() + sleep(2) + if not self.flag: + messagebox.showerror("提示", "校验失败,请检查组件{}的相关配置!".format(self.master.check_error_obj.ObjLabel)) + self.toplevel.destroy() + def move(self, cv, target_x, target_y, duration): + start_x, start_y = cv.coords("check_img") + distance_x = target_x - start_x + distance_y = target_y - start_y + steps = duration // 10 # 以10毫秒为间隔进行移动 + step_x = distance_x / steps + step_y = distance_y / steps + self._move_step(cv, start_x, start_y, target_x, target_y, step_x, step_y, steps) + + def _move_step(self, cv, start_x, start_y, target_x, target_y, step_x, step_y, steps): + if steps > 0: + new_x = start_x + step_x + new_y = start_y + step_y + cv.coords("check_img", new_x, new_y) + cv.update() # 更新画布显示 + sleep(0.01) # 添加延迟以控制动画速度 + self._move_step(cv, new_x, new_y, target_x, target_y, step_x, step_y, steps - 1) + else: + cv.coords("check_img", target_x, target_y) + cv.delete("check_img") + diff --git a/NetworkAnalog/basic.png b/NetworkAnalog/basic.png index 5da2abb..7ad9081 100644 Binary files a/NetworkAnalog/basic.png and b/NetworkAnalog/basic.png differ diff --git a/NetworkAnalog/network.db b/NetworkAnalog/network.db index a1239a1..6f4a422 100644 Binary files a/NetworkAnalog/network.db and b/NetworkAnalog/network.db differ diff --git a/NetworkAnalog/tkTest.py b/NetworkAnalog/tkTest.py index f9e6f84..f8174ae 100644 --- a/NetworkAnalog/tkTest.py +++ b/NetworkAnalog/tkTest.py @@ -1,8 +1,34 @@ -import sys +import sqlite3 +import pandas as pd +import os +import xlwt from PIL import Image -path = sys.path[0] + "/../datas/images/packet.png" -Image.open(path).resize((60, 60)).save(path) +class ExportUtil(): + def __init__(self, path): + self.conn = sqlite3.connect('./network.db') + self.path = path + + def get_table_names(self): + cursor = self.conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") # 如果你使用SQLite数据库 + tables = cursor.fetchall() + cursor.close() + return [table[0] for table in tables] + + def export(self): + tables = self.get_table_names() + with pd.ExcelWriter(self.path, engine='openpyxl') as writer: + for table in tables: + table_name = table + # a. 从数据库中获取表的数据并存储在DataFrame中 + query = f"SELECT * FROM {table_name}" + df = pd.read_sql(query, self.conn) + # b. 使用Pandas将数据写入Excel文件的不同sheet中 + df.to_excel(writer, sheet_name=table_name, index=False) + +export = ExportUtil("./data.xlsx") +export.export() diff --git a/datas/images/主机--暗色.svg b/datas/images/主机--暗色.svg new file mode 100644 index 0000000..c5250f3 --- /dev/null +++ b/datas/images/主机--暗色.svg @@ -0,0 +1,9 @@ + + + 主机--暗色 + + + + + + \ No newline at end of file diff --git a/datas/images/主机--暗色@1x.png b/datas/images/主机--暗色@1x.png new file mode 100644 index 0000000..9e0b53a Binary files /dev/null and b/datas/images/主机--暗色@1x.png differ diff --git a/datas/images/主机--暗色@2x.png b/datas/images/主机--暗色@2x.png new file mode 100644 index 0000000..198bd06 Binary files /dev/null and b/datas/images/主机--暗色@2x.png differ diff --git a/datas/images/主机.svg b/datas/images/主机.svg new file mode 100644 index 0000000..dd17a26 --- /dev/null +++ b/datas/images/主机.svg @@ -0,0 +1,55 @@ + + + 主机 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/datas/images/主机@1x.png b/datas/images/主机@1x.png new file mode 100644 index 0000000..2f0d607 Binary files /dev/null and b/datas/images/主机@1x.png differ diff --git a/datas/images/主机@2x.png b/datas/images/主机@2x.png new file mode 100644 index 0000000..f7f2600 Binary files /dev/null and b/datas/images/主机@2x.png differ diff --git a/datas/images/交换机--暗色.svg b/datas/images/交换机--暗色.svg new file mode 100644 index 0000000..d0b01d1 --- /dev/null +++ b/datas/images/交换机--暗色.svg @@ -0,0 +1,9 @@ + + + 交换机--暗色 + + + + + + \ No newline at end of file diff --git a/datas/images/交换机--暗色@1x.png b/datas/images/交换机--暗色@1x.png new file mode 100644 index 0000000..664084a Binary files /dev/null and b/datas/images/交换机--暗色@1x.png differ diff --git a/datas/images/交换机--暗色@2x.png b/datas/images/交换机--暗色@2x.png new file mode 100644 index 0000000..b3c782d Binary files /dev/null and b/datas/images/交换机--暗色@2x.png differ diff --git a/datas/images/交换机.svg b/datas/images/交换机.svg new file mode 100644 index 0000000..8c4551e --- /dev/null +++ b/datas/images/交换机.svg @@ -0,0 +1,391 @@ + + + 交换机 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/datas/images/交换机@1x.png b/datas/images/交换机@1x.png new file mode 100644 index 0000000..32115d7 Binary files /dev/null and b/datas/images/交换机@1x.png differ diff --git a/datas/images/交换机@2x.png b/datas/images/交换机@2x.png new file mode 100644 index 0000000..22799bc Binary files /dev/null and b/datas/images/交换机@2x.png differ diff --git a/datas/images/右上框.png b/datas/images/右上框.png new file mode 100644 index 0000000..15c561e Binary files /dev/null and b/datas/images/右上框.png differ diff --git a/datas/images/右上框@2x.png b/datas/images/右上框@2x.png new file mode 100644 index 0000000..d9e9ede Binary files /dev/null and b/datas/images/右上框@2x.png differ diff --git a/datas/images/右下框.png b/datas/images/右下框.png new file mode 100644 index 0000000..f29a074 Binary files /dev/null and b/datas/images/右下框.png differ diff --git a/datas/images/右下框@2x.png b/datas/images/右下框@2x.png new file mode 100644 index 0000000..38fdbf6 Binary files /dev/null and b/datas/images/右下框@2x.png differ diff --git a/datas/images/日志消息展示框.png b/datas/images/日志消息展示框.png new file mode 100644 index 0000000..e280753 Binary files /dev/null and b/datas/images/日志消息展示框.png differ diff --git a/datas/images/日志消息展示框@2x.png b/datas/images/日志消息展示框@2x.png new file mode 100644 index 0000000..6999741 Binary files /dev/null and b/datas/images/日志消息展示框@2x.png differ diff --git a/datas/images/背景.png b/datas/images/背景.png new file mode 100644 index 0000000..827527b Binary files /dev/null and b/datas/images/背景.png differ diff --git a/datas/images/背景@2x.png b/datas/images/背景@2x.png new file mode 100644 index 0000000..615f60e Binary files /dev/null and b/datas/images/背景@2x.png differ diff --git a/datas/images/路由器--暗色.svg b/datas/images/路由器--暗色.svg new file mode 100644 index 0000000..d40a4e1 --- /dev/null +++ b/datas/images/路由器--暗色.svg @@ -0,0 +1,9 @@ + + + 路由器--暗色 + + + + + + \ No newline at end of file diff --git a/datas/images/路由器--暗色@1x.png b/datas/images/路由器--暗色@1x.png new file mode 100644 index 0000000..f836c2f Binary files /dev/null and b/datas/images/路由器--暗色@1x.png differ diff --git a/datas/images/路由器--暗色@2x.png b/datas/images/路由器--暗色@2x.png new file mode 100644 index 0000000..cbbb492 Binary files /dev/null and b/datas/images/路由器--暗色@2x.png differ diff --git a/datas/images/路由器.svg b/datas/images/路由器.svg new file mode 100644 index 0000000..ee28574 --- /dev/null +++ b/datas/images/路由器.svg @@ -0,0 +1,68 @@ + + + 路由器 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/datas/images/路由器@1x.png b/datas/images/路由器@1x.png new file mode 100644 index 0000000..d6959d7 Binary files /dev/null and b/datas/images/路由器@1x.png differ diff --git a/datas/images/路由器@2x.png b/datas/images/路由器@2x.png new file mode 100644 index 0000000..ffb4ee4 Binary files /dev/null and b/datas/images/路由器@2x.png differ diff --git a/datas/images/集线器--暗色.svg b/datas/images/集线器--暗色.svg new file mode 100644 index 0000000..fda9fec --- /dev/null +++ b/datas/images/集线器--暗色.svg @@ -0,0 +1,9 @@ + + + 集线器--暗色 + + + + + + \ No newline at end of file diff --git a/datas/images/集线器--暗色@1x.png b/datas/images/集线器--暗色@1x.png new file mode 100644 index 0000000..12356bf Binary files /dev/null and b/datas/images/集线器--暗色@1x.png differ diff --git a/datas/images/集线器--暗色@2x.png b/datas/images/集线器--暗色@2x.png new file mode 100644 index 0000000..418f181 Binary files /dev/null and b/datas/images/集线器--暗色@2x.png differ diff --git a/datas/images/集线器.svg b/datas/images/集线器.svg new file mode 100644 index 0000000..7ac89c9 --- /dev/null +++ b/datas/images/集线器.svg @@ -0,0 +1,1743 @@ + + + 集线器 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/datas/images/集线器@1x.png b/datas/images/集线器@1x.png new file mode 100644 index 0000000..3d204f3 Binary files /dev/null and b/datas/images/集线器@1x.png differ diff --git a/datas/images/集线器@2x.png b/datas/images/集线器@2x.png new file mode 100644 index 0000000..1e41c6c Binary files /dev/null and b/datas/images/集线器@2x.png differ diff --git a/datas/network.db b/datas/network.db index f4128ed..bde5b24 100644 Binary files a/datas/network.db and b/datas/network.db differ