diff --git a/USER_MANUAL.md b/USER_MANUAL.md new file mode 100644 index 0000000..96495b5 --- /dev/null +++ b/USER_MANUAL.md @@ -0,0 +1,390 @@ +# 电动扳手自动拧紧系统 - 使用说明 + +## 目录 + +1. [系统概述](#系统概述) +2. [系统要求](#系统要求) +3. [安装与启动](#安装与启动) +4. [功能说明](#功能说明) +5. [操作流程](#操作流程) +6. [常见问题](#常见问题) + +--- + +## 系统概述 + +电动扳手自动拧紧系统是一个用于管理和执行螺栓拧紧任务的自动化系统。系统支持: + +- 工单管理:创建、查询、认领、提交工单 +- 设备管理:管理扳手设备,监控设备状态 +- 自动拧紧:根据工单要求自动执行螺栓拧紧任务 +- 数据追溯:记录每次拧紧的详细数据,包括目标扭矩、实际扭矩、设备信息等 + +--- + +## 系统要求 + +### 软件要求 +- Python 3.7 或更高版本 +- 操作系统:Windows 10/11 或 Linux + +### 硬件要求 +- 支持TCP/IP通信的电动扳手设备 +- 网络连接(用于设备通信) + +### 依赖库 +系统需要以下Python库: +- `flask` - Web框架 +- `flask-cors` - 跨域支持 +- `tkinter` - GUI界面(通常随Python安装) +- `requests` - HTTP请求库 + +安装依赖: +```bash +pip install flask flask-cors requests +``` + +--- + +## 安装与启动 + +### 1. 启动后端服务器 + +1. 打开命令行,进入项目目录 +2. 进入 `backend` 目录 +3. 运行后端服务器: + ```bash + cd backend + python app.py + ``` +4. 看到以下信息表示启动成功: + ``` + ============================================================ + 电动扳手后端API服务器 + ============================================================ + 启动服务器... + API地址: http://localhost:5000 + 数据库: wrench.db + ============================================================ + ``` + +### 2. 启动前端GUI + +1. 打开新的命令行窗口 +2. 进入项目目录 +3. 进入 `frontend` 目录 +4. 运行前端程序: + ```bash + cd frontend + python wrench_gui.py + ``` +5. 系统界面将自动全屏显示 + +### 3. 启动扳手模拟器(可选,用于测试) + +如果需要测试系统功能,可以启动扳手模拟器: + +```bash +python wrench_simulator.py +``` + +模拟器默认监听 `127.0.0.1:7888` + +--- + +## 功能说明 + +### 主界面 + +主界面包含以下区域: + +1. **工单列表区域**:显示所有可用的工单 +2. **工单信息区域**:显示当前认领的工单详情 +3. **当前螺栓信息区域**:显示正在处理的螺栓信息 +4. **进度条区域**:显示整体拧紧进度 +5. **螺栓列表区域**:显示工单中的螺栓列表和执行状态 +6. **操作控制区域**:包含所有操作按钮,分为三组: + - **工单操作**:认领工单按钮 + - **设备选择**:选择扳手下拉框和设备管理按钮 + - **执行控制**:开始拧紧和停止按钮 +7. **日志区域**:显示系统运行日志 + +### 工单管理 + +#### 查看工单列表 +- 系统会自动刷新工单列表 +- 可以点击"刷新列表"按钮手动刷新 +- 可以勾选"自动刷新"来启用/禁用自动刷新 + +#### 认领工单 +1. 在工单列表中选择一个工单(点击列表项) +2. 点击"认领工单"按钮 +3. 系统会锁定该工单,其他操作员无法认领 + +#### 释放工单 +- 如果认领了工单但不想执行,可以点击"释放工单"按钮 +- 释放后,工单会重新变为可用状态 + +### 设备管理 + +#### 选择设备 +1. 在操作控制区域的"设备选择"组中,点击"选择扳手"下拉框 +2. 从下拉列表中选择要使用的设备 +3. 设备状态会显示在设备名称前: + - 🟢 表示设备在线 + - 🔴 表示设备离线 + +#### 设备管理窗口 +在操作控制区域的"设备选择"组中,点击"设备管理"按钮打开设备管理窗口,可以: + +- **添加设备**:添加新的扳手设备 + - 设备名称:设备的显示名称 + - IP地址:设备的IP地址(必填) + - 端口:设备端口,默认7888 + - 地址码:设备地址码,默认1 + - SN码:设备序列号(可选,可留空) + +- **编辑设备**:修改设备信息 + - 选择设备后点击"编辑设备"按钮 + - 修改需要更改的信息 + - 点击"保存" + +- **删除设备**:删除不需要的设备 + - 选择设备后点击"删除设备"按钮 + - 确认删除 + +- **测试连接**:测试设备是否在线 + - 选择设备后点击"测试连接"按钮 + - 系统会尝试连接设备并更新设备状态 + +### 执行拧紧任务 + +#### 开始拧紧 +1. **认领工单**:在操作控制区域的"工单操作"组中,点击"认领工单"按钮 +2. **选择设备**:在"设备选择"组的下拉框中选择要使用的扳手设备 +3. **检查设备状态**:确保设备在线(显示🟢) +4. **点击"开始拧紧"按钮**:在"执行控制"组中点击"开始拧紧"按钮 + - 系统会连接扳手设备 + - 启用远程控制 + - 按照工单中的螺栓顺序依次拧紧 + - 每个螺栓拧紧完成后会显示结果 + +#### 拧紧过程 +- 系统会自动执行以下步骤: + 1. 连接扳手设备 + 2. 启用远程控制 + 3. 对每个螺栓: + - 设置目标扭矩和参数 + - 启动扳手 + - 等待拧紧完成 + - 检查结果是否在容差范围内 + - 如果失败,自动重试 + 4. 所有螺栓完成后,自动提交结果 + +#### 停止拧紧 +- 在拧紧过程中,可以在操作控制区域的"执行控制"组中,随时点击"停止"按钮停止任务 +- 停止后,已完成的螺栓数据会被保存 + +### 数据查看 + +#### 查看执行结果 +系统会自动保存每次拧紧的执行结果,包括: +- 追溯号和工序号 +- 每个螺栓的目标扭矩和实际扭矩 +- 拧紧是否成功 +- 使用的设备信息(设备SN和名称) +- 执行时间 + +可以通过后端API查询执行结果(详见API文档)。 + +--- + +## 操作流程 + +### 标准操作流程 + +1. **启动系统** + - 启动后端服务器 + - 启动前端GUI + - (可选)启动扳手模拟器 + +2. **设备准备** + - 打开设备管理窗口 + - 添加或检查扳手设备 + - 测试设备连接,确保设备在线 + +3. **选择工单** + - 在工单列表中查看可用工单 + - 选择要执行的工单(点击列表项) + +4. **认领工单** + - 在操作控制区域的"工单操作"组中,点击"认领工单"按钮 + - 确认工单信息正确 + +5. **选择设备** + - 在操作控制区域的"设备选择"组中,从下拉框选择要使用的扳手设备 + - 确认设备状态为在线(🟢) + +6. **开始执行** + - 在操作控制区域的"执行控制"组中,点击"开始拧紧"按钮 + - 观察日志和螺栓列表,了解执行进度 + - 等待所有螺栓完成 + +7. **完成提交** + - 系统会自动提交执行结果 + - 查看日志确认提交成功 + +### 异常处理流程 + +#### 设备离线 +- 如果设备显示离线(🔴),系统会提示警告 +- 可以选择继续(如果确定设备可用) +- 建议先测试设备连接 + +#### 拧紧失败 +- 如果某个螺栓拧紧失败,系统会自动重试 +- 如果多次重试仍失败,会记录失败信息 +- 可以查看日志了解失败原因 + +#### 网络中断 +- 如果与设备的网络连接中断,系统会提示错误 +- 需要检查网络连接后重新开始 + +--- + +## 常见问题 + +### Q1: 设备一直显示离线怎么办? + +**A:** +1. 检查设备是否开机 +2. 检查网络连接是否正常 +3. 检查IP地址和端口配置是否正确 +4. 在设备管理窗口中点击"测试连接"按钮 +5. 如果使用模拟器,确保 `wrench_simulator.py` 正在运行 + +### Q2: 认领工单后无法开始拧紧? + +**A:** +1. 确保已选择扳手设备 +2. 确保设备状态为在线 +3. 检查工单中是否有螺栓数据 +4. 查看日志了解具体错误信息 + +### Q3: 螺栓拧紧一直失败? + +**A:** +1. 检查目标扭矩设置是否合理 +2. 检查扭矩容差设置 +3. 检查设备是否正常工作 +4. 查看日志了解失败原因(扭矩过大/过小等) + +### Q4: 如何查看历史执行记录? + +**A:** +可以通过后端API查询执行结果: +``` +GET http://localhost:5000/api/work-results?trace_id=追溯号&process_id=工序号 +``` +详见API文档。 + +### Q5: 如何添加新的工单? + +**A:** +可以通过后端API创建工单: +``` +POST http://localhost:5000/api/work-orders/create +``` +详见API文档。 + +### Q6: 系统界面显示不正常? + +**A:** +1. 检查Python版本是否符合要求 +2. 检查tkinter是否正常安装 +3. 尝试重启程序 +4. 检查屏幕分辨率设置 + +### Q7: 后端服务器无法启动? + +**A:** +1. 检查端口5000是否被占用 +2. 检查数据库文件权限 +3. 查看错误日志了解具体问题 +4. 确保所有依赖库已正确安装 + +### Q8: 如何修改API服务器地址? + +**A:** +编辑 `frontend/wrench_gui.py` 文件,修改以下行: +```python +self.api_base_url = "http://localhost:5000/api" +``` +改为你的服务器地址。 + +--- + +## 注意事项 + +1. **设备连接** + - 确保扳手设备与计算机在同一网络 + - 确保防火墙允许相关端口通信 + - 设备IP地址和端口配置必须正确 + +2. **工单管理** + - 一个工单只能被一个操作员认领 + - 认领后需要完成或释放才能被其他人使用 + - 工单提交后无法修改 + +3. **数据安全** + - 定期备份数据库文件(`backend/wrench.db`) + - 执行结果会自动保存,无需手动保存 + +4. **系统维护** + - 定期检查设备状态 + - 定期清理不需要的工单和设备 + - 保持系统更新 + +--- + +## 技术支持 + +如遇到问题,请: + +1. 查看系统日志了解错误信息 +2. 参考本文档的常见问题部分 +3. 查看API文档了解接口详情 +4. 联系技术支持团队 + +--- + +## 更新日志 + +### v1.0.0 (2026-01-19) +- 初始版本发布 +- 支持工单管理、设备管理、自动拧紧功能 +- 支持数据追溯和结果查询 + +--- + +## 附录 + +### 快捷键说明 + +- **F5**: 刷新工单列表(如果支持) +- **ESC**: 关闭当前窗口(部分窗口) + +### 配置文件说明 + +- `backend/wrench.db`: SQLite数据库文件,存储所有数据 +- `backend/work_orders.json`: 初始工单数据(可选) +- `config.json`: 扳手控制器配置文件(可选) + +### 日志文件 + +系统日志会显示在GUI的日志区域,后端服务器日志会显示在控制台。 + +--- + +**祝使用愉快!** + diff --git a/backend/wrench.db b/backend/wrench.db index c2e0d11..74a14cf 100644 Binary files a/backend/wrench.db and b/backend/wrench.db differ diff --git a/frontend/device_manager.py b/frontend/device_manager.py index 3902bfb..91d4d57 100644 --- a/frontend/device_manager.py +++ b/frontend/device_manager.py @@ -120,17 +120,6 @@ class DeviceManagerWindow: ) self.delete_button.pack(side=tk.LEFT, padx=5) - self.query_sn_button = tk.Button( - button_frame, - text="查询SN码", - font=("微软雅黑", 10), - bg="#f39c12", - fg="white", - command=self.query_device_sn, - state=tk.DISABLED - ) - self.query_sn_button.pack(side=tk.LEFT, padx=5) - self.test_connect_button = tk.Button( button_frame, text="测试连接", @@ -197,13 +186,11 @@ class DeviceManagerWindow: self.selected_device_id = values[0] self.edit_button.config(state=tk.NORMAL) self.delete_button.config(state=tk.NORMAL) - self.query_sn_button.config(state=tk.NORMAL) self.test_connect_button.config(state=tk.NORMAL) else: self.selected_device_id = None self.edit_button.config(state=tk.DISABLED) self.delete_button.config(state=tk.DISABLED) - self.query_sn_button.config(state=tk.DISABLED) self.test_connect_button.config(state=tk.DISABLED) def add_device(self): @@ -258,71 +245,6 @@ class DeviceManagerWindow: except Exception as e: messagebox.showerror("错误", f"删除失败: {e}") - def query_device_sn(self): - """查询设备SN码""" - if not self.selected_device_id: - return - - device = None - for d in self.devices: - if d.get('id') == self.selected_device_id: - device = d - break - - if not device: - return - - # 在新线程中查询 - def query_thread(): - try: - device_config = { - "ip_address": device.get('ip_address'), - "port": device.get('port', 7888), - "address_code": device.get('address_code', 1) - } - - wrench = WrenchController(device_config=device_config) - if wrench.connect(): - sn = wrench.query_device_sn() - wrench.disconnect() - - if sn: - # 更新设备SN码 - self.window.after(0, lambda: self.update_device_sn(device.get('id'), sn)) - else: - self.window.after(0, lambda: messagebox.showerror("错误", "查询SN码失败")) - else: - self.window.after(0, lambda: messagebox.showerror("错误", "无法连接到设备")) - except Exception as e: - self.window.after(0, lambda: messagebox.showerror("错误", f"查询失败: {e}")) - - threading.Thread(target=query_thread, daemon=True).start() - messagebox.showinfo("提示", "正在查询SN码,请稍候...") - - def update_device_sn(self, device_id, sn): - """更新设备SN码""" - try: - device = None - for d in self.devices: - if d.get('id') == device_id: - device = d - break - - if device: - device['device_sn'] = sn - url = f"{self.api_base_url}/wrench-devices/{device_id}" - response = requests.put(url, json=device, timeout=5) - - if response.status_code == 200: - messagebox.showinfo("成功", f"SN码查询成功: {sn}") - self.load_devices() - if self.refresh_callback: - self.refresh_callback() - else: - messagebox.showerror("错误", "更新SN码失败") - except Exception as e: - messagebox.showerror("错误", f"更新失败: {e}") - def test_connection(self): """测试设备连接""" if not self.selected_device_id: diff --git a/frontend/wrench_gui.py b/frontend/wrench_gui.py index 6ac785b..5e5f751 100644 --- a/frontend/wrench_gui.py +++ b/frontend/wrench_gui.py @@ -25,10 +25,29 @@ from device_manager import DeviceManagerWindow class WrenchGUI: """电动扳手图形界面(前端)""" + @staticmethod + def translate_status(status): + """将英文状态转换为中文""" + status_map = { + 'pending': '待处理', + 'claimed': '已认领', + 'completed': '已完成' + } + return status_map.get(status, status) + def __init__(self, root): self.root = root self.root.title("电动扳手自动拧紧系统") - self.root.geometry("1000x900") + # 默认全屏显示 + self.root.state('zoomed') # Windows全屏 + # 如果zoomed不支持,使用geometry设置全屏 + try: + self.root.state('zoomed') + except: + # 获取屏幕尺寸并设置窗口大小 + screen_width = self.root.winfo_screenwidth() + screen_height = self.root.winfo_screenheight() + self.root.geometry(f"{screen_width}x{screen_height}") # API配置 self.api_base_url = "http://localhost:5000/api" @@ -208,9 +227,9 @@ class WrenchGUI: list_frame = tk.LabelFrame(self.root, text="螺栓列表", font=("微软雅黑", 12), padx=10, pady=10) list_frame.pack(fill=tk.BOTH, expand=False, padx=10, pady=10) - # 创建表格 + # 创建表格(减少高度,确保按钮区域可见) columns = ("序号", "名称", "扭矩(Nm)", "状态", "实际扭矩", "时间") - self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=6) + self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=4) # 设置列 for col in columns: @@ -237,71 +256,93 @@ class WrenchGUI: self.tree.tag_configure("failed", foreground="#e74c3c") # 控制按钮区域(放在螺栓列表之后,确保可见) - button_frame = tk.LabelFrame(self.root, text="操作控制", font=("微软雅黑", 12), padx=10, pady=10) + button_frame = tk.LabelFrame(self.root, text="操作控制", font=("微软雅黑", 12), padx=15, pady=15) button_frame.pack(fill=tk.X, padx=10, pady=10) - button_inner_frame = tk.Frame(button_frame) - button_inner_frame.pack() + # 使用grid布局,将所有按钮放在同一行确保可见 + # 第一列:工单操作 + col1_frame = tk.Frame(button_frame) + col1_frame.grid(row=0, column=0, padx=(0, 20), sticky=tk.W) + tk.Label(col1_frame, text="工单操作", font=("微软雅黑", 10, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 5)) self.claim_button = tk.Button( - button_inner_frame, + col1_frame, text="认领工单", - font=("微软雅黑", 12, "bold"), + font=("微软雅黑", 11, "bold"), bg="#f39c12", fg="white", - width=15, + width=12, height=2, cursor="hand2", command=self.claim_work_order ) - self.claim_button.pack(side=tk.LEFT, padx=10, pady=5) + self.claim_button.pack() - # 扳手选择 - device_select_frame = tk.Frame(button_inner_frame) - device_select_frame.pack(side=tk.LEFT, padx=10, pady=5) + # 第二列:设备选择 + col2_frame = tk.Frame(button_frame) + col2_frame.grid(row=0, column=1, padx=(0, 20), sticky=tk.W) - tk.Label(device_select_frame, text="选择扳手:", font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5) - self.device_combo = ttk.Combobox(device_select_frame, width=20, state="readonly", font=("微软雅黑", 10)) - self.device_combo.pack(side=tk.LEFT, padx=5) + tk.Label(col2_frame, text="设备选择", font=("微软雅黑", 10, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 5)) + device_select_inner = tk.Frame(col2_frame) + device_select_inner.pack() + + tk.Label(device_select_inner, text="选择扳手:", font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=(0, 5)) + self.device_combo = ttk.Combobox(device_select_inner, width=20, state="readonly", font=("微软雅黑", 10)) + self.device_combo.pack(side=tk.LEFT, padx=(0, 5)) self.device_combo.bind("<>", self.on_device_selected) self.device_manage_button = tk.Button( - device_select_frame, + device_select_inner, text="设备管理", font=("微软雅黑", 9), bg="#3498db", fg="white", + width=10, + height=1, command=self.open_device_manager ) - self.device_manage_button.pack(side=tk.LEFT, padx=5) + self.device_manage_button.pack(side=tk.LEFT) + + # 第三列:执行控制(放在同一行,确保可见) + col3_frame = tk.Frame(button_frame) + col3_frame.grid(row=0, column=2, padx=(0, 0), sticky=tk.W) + + tk.Label(col3_frame, text="执行控制", font=("微软雅黑", 10, "bold"), fg="#2c3e50").pack(anchor=tk.W, pady=(0, 5)) + exec_control_inner = tk.Frame(col3_frame) + exec_control_inner.pack() self.start_button = tk.Button( - button_inner_frame, + exec_control_inner, text="开始拧紧", - font=("微软雅黑", 12, "bold"), + font=("微软雅黑", 11, "bold"), bg="#27ae60", fg="white", - width=15, + width=12, height=2, cursor="hand2", command=self.start_process, state=tk.DISABLED ) - self.start_button.pack(side=tk.LEFT, padx=10, pady=5) + self.start_button.pack(side=tk.LEFT, padx=(0, 10)) self.stop_button = tk.Button( - button_inner_frame, + exec_control_inner, text="停止", - font=("微软雅黑", 12, "bold"), + font=("微软雅黑", 11, "bold"), bg="#e74c3c", fg="white", - width=15, + width=12, height=2, cursor="hand2", state=tk.DISABLED, command=self.stop_process ) - self.stop_button.pack(side=tk.LEFT, padx=10, pady=5) + self.stop_button.pack(side=tk.LEFT) + + # 配置grid列权重 + button_frame.grid_columnconfigure(0, weight=0) + button_frame.grid_columnconfigure(1, weight=1) + button_frame.grid_columnconfigure(2, weight=0) # 日志区域 log_frame = tk.LabelFrame(self.root, text="操作日志", font=("微软雅黑", 10), padx=5, pady=5) @@ -398,13 +439,15 @@ class WrenchGUI: # 清空并重新填充列表 self.order_tree.delete(*self.order_tree.get_children()) for order in orders: + status = order.get('status', 'pending') + status_cn = self.translate_status(status) self.order_tree.insert("", tk.END, values=( order.get('trace_id', '--'), order.get('process_id', '--'), order.get('process_name', '--'), order.get('product_name', '--'), order.get('bolt_count', 0), - order.get('status', 'pending') + status_cn )) # 恢复选中状态 diff --git a/API_DOCUMENTATION.md b/电动扳手系统 API 接口文档.md similarity index 100% rename from API_DOCUMENTATION.md rename to 电动扳手系统 API 接口文档.md