首次提交

main
risingLee 2026-01-19 16:59:52 +08:00
commit 459ce57cd4
7 changed files with 1483 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
__pycache__/

29
config.json Normal file
View File

@ -0,0 +1,29 @@
{
"wrench": {
"host": "192.168.110.122",
"port": 7888,
"timeout": 30,
"address": 1,
"auto_reconnect": true,
"max_reconnect_attempts": 3,
"description": "扳手连接配置auto_reconnect=自动重连max_reconnect_attempts=最大重连次数"
},
"test_mode": {
"enabled": true,
"description": "测试模式:失败也算成功,用于无钉子测试"
},
"bolt_default_config": {
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"description": "螺栓默认配置mode=1(M1扭矩模式), torque_tolerance=10%, angle_min=1度, angle_max=360度"
},
"default_parameters": {
"product_id": "0000000000",
"station_name": "11111111111111111111",
"employee_id": "2222222222",
"tool_sn": "0000000000",
"controller_sn": "3333333333"
}
}

58
work_order.json Normal file
View File

@ -0,0 +1,58 @@
{
"order_id": "WO20260119001",
"product_name": "汽车底盘组件",
"station": "装配工位A1",
"operator": "张三",
"bolts": [
{
"bolt_id": 1,
"name": "前轮螺栓1",
"target_torque": 280,
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"status": "pending"
},
{
"bolt_id": 2,
"name": "前轮螺栓2",
"target_torque": 300,
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"status": "pending"
},
{
"bolt_id": 3,
"name": "后轮螺栓1",
"target_torque": 320,
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"status": "pending"
},
{
"bolt_id": 4,
"name": "后轮螺栓2",
"target_torque": 310,
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"status": "pending"
},
{
"bolt_id": 5,
"name": "后轮螺栓3",
"target_torque": 310,
"mode": 1,
"torque_tolerance": 0.10,
"angle_min": 1,
"angle_max": 360,
"status": "pending"
}
]
}

524
wrench_controller.py Normal file
View File

@ -0,0 +1,524 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电动扳手通讯控制模块
支持扭矩设定和结果解析
"""
import socket
import struct
import json
from datetime import datetime
from typing import Tuple, Optional
from pathlib import Path
class WrenchController:
"""电动扳手控制器"""
def __init__(self, config_file: str = "config.json"):
"""
初始化扳手控制器
:param config_file: 配置文件路径
"""
self.config = self._load_config(config_file)
self.host = self.config["wrench"]["host"]
self.port = self.config["wrench"]["port"]
self.timeout = self.config["wrench"].get("timeout", 30)
self.address = self.config["wrench"].get("address", 0x01)
self.test_mode = self.config.get("test_mode", {}).get("enabled", False)
self.auto_reconnect = self.config["wrench"].get("auto_reconnect", True)
self.max_reconnect_attempts = self.config["wrench"].get("max_reconnect_attempts", 3)
self.sock = None
self.is_connected = False
print(f"加载配置: {self.host}:{self.port}")
if self.test_mode:
print("⚠️ 测试模式已启用:失败也算成功")
if self.auto_reconnect:
print(f"✅ 自动重连已启用:最多尝试{self.max_reconnect_attempts}")
def _load_config(self, config_file: str) -> dict:
"""加载配置文件"""
try:
config_path = Path(config_file)
if not config_path.exists():
print(f"配置文件不存在: {config_file},使用默认配置")
return self._get_default_config()
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
print(f"成功加载配置文件: {config_file}")
return config
except Exception as e:
print(f"加载配置文件失败: {e},使用默认配置")
return self._get_default_config()
def _get_default_config(self) -> dict:
"""获取默认配置"""
return {
"wrench": {
"host": "192.168.110.122",
"port": 7888,
"timeout": 30,
"address": 1,
"auto_reconnect": True,
"max_reconnect_attempts": 3
},
"test_mode": {
"enabled": False,
"description": "测试模式:失败也算成功,用于无钉子测试"
},
"default_parameters": {
"product_id": "0000000000",
"station_name": "11111111111111111111",
"employee_id": "2222222222",
"tool_sn": "0000000000",
"controller_sn": "3333333333"
}
}
def connect(self, timeout: float = 5.0):
"""连接到扳手"""
try:
print(f"正在连接到 {self.host}:{self.port} ...")
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
self.sock.connect((self.host, self.port))
self.is_connected = True
print(f"✅ 已连接到扳手: {self.host}:{self.port}")
return True
except socket.timeout:
print(f"❌ 连接超时: {self.host}:{self.port}")
self.is_connected = False
return False
except ConnectionRefusedError:
print(f"❌ 连接被拒绝: {self.host}:{self.port} (扳手可能未开机或端口错误)")
self.is_connected = False
return False
except Exception as e:
print(f"❌ 连接失败: {e}")
self.is_connected = False
return False
def reconnect(self):
"""尝试重新连接"""
if not self.auto_reconnect:
return False
print("🔄 尝试重新连接...")
self.disconnect()
for attempt in range(1, self.max_reconnect_attempts + 1):
print(f"重连尝试 {attempt}/{self.max_reconnect_attempts}")
if self.connect():
print("✅ 重连成功!")
return True
if attempt < self.max_reconnect_attempts:
import time
time.sleep(2) # 等待2秒后重试
print("❌ 重连失败,已达到最大尝试次数")
return False
def disconnect(self):
"""断开连接"""
if self.sock:
try:
self.sock.close()
except:
pass
self.sock = None
self.is_connected = False
print("已断开连接")
def _calculate_checksum(self, data: bytes) -> int:
"""计算校验和(累加和)"""
return sum(data) & 0xFF
def _send_command(self, command: bytes) -> bool:
"""发送命令"""
try:
if not self.sock or not self.is_connected:
print("❌ Socket未连接")
if self.auto_reconnect:
if self.reconnect():
# 重连成功,重新发送
return self._send_command(command)
return False
self.sock.sendall(command)
print(f"✅ 已发送命令: {command.hex(' ').upper()}")
return True
except BrokenPipeError:
print("❌ 连接已断开")
self.is_connected = False
if self.auto_reconnect:
if self.reconnect():
return self._send_command(command)
return False
except Exception as e:
print(f"❌ 发送命令失败: {e}")
self.is_connected = False
if self.auto_reconnect:
if self.reconnect():
return self._send_command(command)
return False
def _receive_response(self, timeout: float = 2.0) -> Optional[bytes]:
"""接收响应"""
try:
if not self.sock or not self.is_connected:
print("❌ Socket未连接")
return None
self.sock.settimeout(timeout)
response = self.sock.recv(1024)
if response:
print(f"✅ 收到响应: {response.hex(' ').upper()}")
return response
else:
print("❌ 收到空响应(连接可能已关闭)")
self.is_connected = False
return None
except socket.timeout:
print("⏱️ 接收响应超时")
return None
except ConnectionResetError:
print("❌ 连接被重置")
self.is_connected = False
return None
except Exception as e:
print(f"❌ 接收响应失败: {e}")
self.is_connected = False
return None
def enable_remote_control(self, enable: bool = True) -> bool:
"""
启用/停止远程控制
:param enable: True启用False停止
:return: 是否成功
"""
data = bytearray([
0xC5, 0xC5, # 报文头
self.address, # 地址码
0x11, # 功能码
0xFF, 0xFF, # 保留
0x01, # 数据长度
0x01 if enable else 0x00 # 数据内容
])
data.append(self._calculate_checksum(data))
print(f"{'启用' if enable else '停止'}远程控制...")
return self._send_command(bytes(data))
def set_torque_parameters(self,
target_torque: int,
mode: int = 1,
torque_tolerance: float = 0.10,
target_angle: int = 0,
angle_min: int = 1,
angle_max: int = 360,
product_id: str = None,
station_name: str = None,
employee_id: str = None,
tool_sn: str = None,
controller_sn: str = None) -> bytes:
"""
设定扭矩参数功能码0x10
:param target_torque: 目标扭矩(Nm)
:param mode: 模式 1=M1模式(扭矩模式), 2=M2模式(角度模式)
:param torque_tolerance: 扭矩偏差百分比(仅M1模式)如0.10表示±10%
:param target_angle: 目标角度()M1模式填0
:param angle_min: 角度下限()
:param angle_max: 角度上限()
:param product_id: 产品ID(10字节)
:param station_name: 工位名称(20字节)
:param employee_id: 员工号(10字节)
:param tool_sn: 工具系列号(10字节)
:param controller_sn: 控制器系列号(10字节)
:return: 生成的报文
"""
# 从配置文件获取默认参数
defaults = self.config.get("default_parameters", {})
product_id = product_id or defaults.get("product_id", "0000000000")
station_name = station_name or defaults.get("station_name", "11111111111111111111")
employee_id = employee_id or defaults.get("employee_id", "2222222222")
tool_sn = tool_sn or defaults.get("tool_sn", "0000000000")
controller_sn = controller_sn or defaults.get("controller_sn", "3333333333")
# 获取当前时间
now = datetime.now()
# 计算扭矩上下限
if mode == 1: # M1模式
torque_min = int(target_torque * (1 - torque_tolerance))
torque_max = int(target_torque * (1 + torque_tolerance))
else: # M2模式
torque_min = int(target_torque * 0.8) # 默认下限
torque_max = int(target_torque * 1.2) # 默认上限
# 角度需要乘10
angle_value = target_angle * 10
angle_min_value = angle_min * 10
angle_max_value = angle_max * 10
# 构建报文
data = bytearray([
0xC5, 0xC5, # 报文头
self.address, # 地址码
0x10, # 功能码
0xFF, 0xFF, # 反退角(保留)
0x50, # 数据长度
])
# 产品ID (10字节)
data.extend(product_id.encode('ascii')[:10].ljust(10, b'\x00'))
# 工位名称 (20字节)
data.extend(station_name.encode('utf-8')[:20].ljust(20, b'\x00'))
# 员工号 (10字节)
data.extend(employee_id.encode('ascii')[:10].ljust(10, b'\x00'))
# 工具系列号 (10字节)
data.extend(tool_sn.encode('ascii')[:10].ljust(10, b'\x00'))
# 控制器系列号 (10字节)
data.extend(controller_sn.encode('ascii')[:10].ljust(10, b'\x00'))
# 时间
data.extend(struct.pack('>H', now.year)) # 年(2字节)
data.append(now.month) # 月
data.append(now.day) # 日
data.append(now.hour) # 时
data.append(now.minute) # 分
data.append(now.second) # 秒
# 参数模式
data.append(mode)
# 目标扭矩 (2字节)
data.extend(struct.pack('>H', target_torque))
# 扭矩下限 (2字节)
data.extend(struct.pack('>H', torque_min))
# 扭矩上限 (2字节)
data.extend(struct.pack('>H', torque_max))
# 目标角度 (2字节)
data.extend(struct.pack('>H', angle_value))
# 角度上限 (2字节)
data.extend(struct.pack('>H', angle_max_value))
# 角度下限 (2字节)
data.extend(struct.pack('>H', angle_min_value))
# 计算并添加校验码
checksum = self._calculate_checksum(data)
data.append(checksum)
# 打印报文信息
mode_str = "M1(扭矩模式)" if mode == 1 else "M2(角度模式)"
print(f"\n设定扭矩参数:")
print(f" 模式: {mode_str}")
print(f" 目标扭矩: {target_torque} Nm")
print(f" 扭矩范围: {torque_min}-{torque_max} Nm")
print(f" 目标角度: {target_angle}°")
print(f" 角度范围: {angle_min}-{angle_max}°")
print(f" 报文: {data.hex(' ').upper()}")
# 发送命令
self._send_command(bytes(data))
return bytes(data)
def start_wrench(self, direction: int = 1) -> bool:
"""
启动扳手
:param direction: 0=停止, 1=正转, 2=反转
:return: 是否成功
"""
data = bytearray([
0xC5, 0xC5, # 报文头
self.address, # 地址码
0x01, # 功能码
0xFF, 0xFF, # 保留
0x01, # 数据长度
direction # 数据内容
])
data.append(self._calculate_checksum(data))
action = {0: "停止", 1: "正转启动", 2: "反转启动"}
print(f"\n{action.get(direction, '未知操作')}扳手...")
return self._send_command(bytes(data))
def parse_result(self, response: bytes) -> dict:
"""
解析执行结果反馈功能码0x15
:param response: 接收到的响应报文
:return: 解析后的结果字典
"""
if not response or len(response) < 44:
return {"error": "响应数据不完整"}
# 验证报文头和功能码
if response[0:2] != b'\xC5\xC5':
return {"error": "报文头错误"}
if response[3] != 0x15:
return {"error": f"功能码错误期望0x15实际0x{response[3]:02X}"}
# 解析结果状态
status_code = response[4]
status_map = {
0x00: "成功-OK",
0x01: "成功-扭矩到达",
0x02: "成功-位置到达",
0x10: "失败-NG",
0x11: "失败-小于扭矩下限",
0x12: "失败-大于扭矩上限",
0x13: "失败-打滑",
0x14: "失败-小于角度下限",
0x15: "失败-大于角度上限",
0x16: "失败-2次拧紧"
}
status = status_map.get(status_code, f"未知状态(0x{status_code:02X})")
is_success = status_code in [0x00, 0x01, 0x02]
# 测试模式:失败也算成功
if self.test_mode and not is_success:
print(f"⚠️ 测试模式:将失败状态({status})转换为成功")
is_success = True
status = f"测试模式-{status}"
# 解析其他数据
employee_id = response[7:17].decode('ascii', errors='ignore').rstrip('\x00')
bolt_no = struct.unpack('>H', response[17:19])[0]
year = struct.unpack('>H', response[19:21])[0]
month = response[21]
day = response[22]
hour = response[23]
minute = response[24]
second = response[25]
timestamp = f"{year}-{month:02d}-{day:02d} {hour:02d}:{minute:02d}:{second:02d}"
mode = "M1(扭矩模式)" if response[26] == 0x01 else "M2(角度模式)"
target_torque = struct.unpack('>H', response[27:29])[0]
actual_torque = struct.unpack('>H', response[29:31])[0]
target_angle = struct.unpack('>H', response[31:33])[0] / 10
actual_angle = struct.unpack('>H', response[33:35])[0] / 10
torque_max = struct.unpack('>H', response[35:37])[0]
torque_min = struct.unpack('>H', response[37:39])[0]
angle_max = struct.unpack('>H', response[39:41])[0] / 10
angle_min = struct.unpack('>H', response[41:43])[0] / 10
result = {
"success": is_success,
"status": status,
"status_code": f"0x{status_code:02X}",
"employee_id": employee_id,
"bolt_no": bolt_no,
"timestamp": timestamp,
"mode": mode,
"target_torque": target_torque,
"actual_torque": actual_torque,
"target_angle": target_angle,
"actual_angle": actual_angle,
"torque_range": f"{torque_min}-{torque_max} Nm",
"angle_range": f"{angle_min}-{angle_max}°"
}
return result
def print_result(self, result: dict):
"""打印结果"""
if "error" in result:
print(f"\n❌ 错误: {result['error']}")
return
print("\n" + "="*50)
if result["success"]:
print("✅ 执行成功!")
else:
print("❌ 执行失败!")
print("="*50)
print(f"状态: {result['status']} ({result['status_code']})")
print(f"时间: {result['timestamp']}")
print(f"员工号: {result['employee_id']}")
print(f"螺栓号: {result['bolt_no']}")
print(f"模式: {result['mode']}")
print(f"目标扭矩: {result['target_torque']} Nm")
print(f"实际扭矩: {result['actual_torque']} Nm")
print(f"扭矩范围: {result['torque_range']}")
print(f"目标角度: {result['target_angle']}°")
print(f"实际角度: {result['actual_angle']}°")
print(f"角度范围: {result['angle_range']}")
print("="*50)
def wait_for_result(self, timeout: float = None) -> Optional[dict]:
"""
等待并解析执行结果
:param timeout: 超时时间()默认使用配置文件中的值
:return: 解析后的结果字典
"""
if timeout is None:
timeout = self.timeout
print(f"\n等待扳手执行结果(超时{timeout}秒)...")
response = self._receive_response(timeout)
if response:
print(f"收到响应: {response.hex(' ').upper()}")
result = self.parse_result(response)
self.print_result(result)
return result
else:
print("未收到响应")
return None
# 使用示例
if __name__ == "__main__":
# 创建控制器实例自动从config.json加载配置
wrench = WrenchController()
# 连接到扳手
if wrench.connect():
try:
# 1. 启用远程控制
wrench.enable_remote_control(True)
# 2. 设定扭矩参数 - M1模式目标扭矩300Nm
wrench.set_torque_parameters(
target_torque=300,
mode=1, # M1模式
torque_tolerance=0.10, # ±10%
angle_max=360,
angle_min=1
)
# 3. 启动扳手
wrench.start_wrench(direction=1)
# 4. 等待并获取执行结果(使用配置文件中的超时时间)
result = wrench.wait_for_result()
# 5. 根据结果进行后续处理
if result and result.get("success"):
print("\n✅ 扳手操作成功完成!")
else:
print("\n❌ 扳手操作失败!")
finally:
# 断开连接
wrench.disconnect()
else:
print("无法连接到扳手")

447
wrench_gui.py Normal file
View File

@ -0,0 +1,447 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
电动扳手自动拧紧界面程序
支持多螺栓自动拧紧流程
"""
import tkinter as tk
from tkinter import ttk, messagebox
import json
import threading
import time
from datetime import datetime
from wrench_controller import WrenchController
class WrenchGUI:
"""电动扳手图形界面"""
def __init__(self, root):
self.root = root
self.root.title("电动扳手自动拧紧系统")
self.root.geometry("900x750")
# 数据
self.work_order = None
self.current_bolt_index = 0
self.wrench = None
self.is_running = False
self.thread = None
self.test_mode = False
# 创建界面
self._create_widgets()
# 加载工单数据
self.load_work_order()
# 检查测试模式
self.check_test_mode()
def _create_widgets(self):
"""创建界面组件"""
# 标题
title_frame = tk.Frame(self.root, bg="#2c3e50", height=60)
title_frame.pack(fill=tk.X)
title_frame.pack_propagate(False)
title_label = tk.Label(
title_frame,
text="电动扳手自动拧紧系统",
font=("微软雅黑", 20, "bold"),
fg="white",
bg="#2c3e50"
)
title_label.pack(pady=15)
# 测试模式指示器
self.test_mode_label = tk.Label(
title_frame,
text="",
font=("微软雅黑", 10, "bold"),
fg="#f39c12",
bg="#2c3e50"
)
self.test_mode_label.place(relx=1.0, rely=0.5, anchor=tk.E, x=-20)
# 工单信息区域
info_frame = tk.LabelFrame(self.root, text="工单信息", font=("微软雅黑", 12), padx=10, pady=10)
info_frame.pack(fill=tk.X, padx=10, pady=10)
self.order_id_label = tk.Label(info_frame, text="工单号: --", font=("微软雅黑", 10))
self.order_id_label.grid(row=0, column=0, sticky=tk.W, padx=5, pady=2)
self.product_label = tk.Label(info_frame, text="产品: --", font=("微软雅黑", 10))
self.product_label.grid(row=0, column=1, sticky=tk.W, padx=5, pady=2)
self.station_label = tk.Label(info_frame, text="工位: --", font=("微软雅黑", 10))
self.station_label.grid(row=1, column=0, sticky=tk.W, padx=5, pady=2)
self.operator_label = tk.Label(info_frame, text="操作员: --", font=("微软雅黑", 10))
self.operator_label.grid(row=1, column=1, sticky=tk.W, padx=5, pady=2)
# 当前螺栓信息
current_frame = tk.LabelFrame(self.root, text="当前螺栓", font=("微软雅黑", 12), padx=10, pady=10)
current_frame.pack(fill=tk.X, padx=10, pady=10)
self.current_bolt_label = tk.Label(
current_frame,
text="等待开始...",
font=("微软雅黑", 16, "bold"),
fg="#2c3e50"
)
self.current_bolt_label.pack(pady=5)
self.current_torque_label = tk.Label(
current_frame,
text="目标扭矩: -- Nm",
font=("微软雅黑", 12)
)
self.current_torque_label.pack(pady=2)
self.current_status_label = tk.Label(
current_frame,
text="状态: 待机",
font=("微软雅黑", 12),
fg="#7f8c8d"
)
self.current_status_label.pack(pady=2)
# 进度条
progress_frame = tk.Frame(self.root, padx=10, pady=5)
progress_frame.pack(fill=tk.X, padx=10)
self.progress_label = tk.Label(progress_frame, text="总进度: 0/0", font=("微软雅黑", 10))
self.progress_label.pack(anchor=tk.W)
self.progress_bar = ttk.Progressbar(progress_frame, mode='determinate', length=400)
self.progress_bar.pack(fill=tk.X, pady=5)
# 螺栓列表
list_frame = tk.LabelFrame(self.root, text="螺栓列表", font=("微软雅黑", 12), padx=10, pady=10)
list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建表格
columns = ("序号", "名称", "扭矩(Nm)", "状态", "实际扭矩", "时间")
self.tree = ttk.Treeview(list_frame, columns=columns, show="headings", height=6)
# 设置列
self.tree.heading("序号", text="序号")
self.tree.heading("名称", text="名称")
self.tree.heading("扭矩(Nm)", text="扭矩(Nm)")
self.tree.heading("状态", text="状态")
self.tree.heading("实际扭矩", text="实际扭矩(Nm)")
self.tree.heading("时间", text="完成时间")
self.tree.column("序号", width=60, anchor=tk.CENTER)
self.tree.column("名称", width=150, anchor=tk.W)
self.tree.column("扭矩(Nm)", width=100, anchor=tk.CENTER)
self.tree.column("状态", width=100, anchor=tk.CENTER)
self.tree.column("实际扭矩", width=120, anchor=tk.CENTER)
self.tree.column("时间", width=150, anchor=tk.CENTER)
# 滚动条
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
self.tree.configure(yscrollcommand=scrollbar.set)
self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 配置标签颜色
self.tree.tag_configure("pending", foreground="#7f8c8d")
self.tree.tag_configure("running", foreground="#3498db", font=("微软雅黑", 10, "bold"))
self.tree.tag_configure("success", foreground="#27ae60", font=("微软雅黑", 10, "bold"))
self.tree.tag_configure("failed", foreground="#e74c3c")
# 控制按钮
button_frame = tk.Frame(self.root, padx=10, pady=10)
button_frame.pack(fill=tk.X)
self.start_button = tk.Button(
button_frame,
text="开始拧紧",
font=("微软雅黑", 12, "bold"),
bg="#27ae60",
fg="white",
width=15,
height=1,
cursor="hand2",
command=self.start_process
)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = tk.Button(
button_frame,
text="停止",
font=("微软雅黑", 12, "bold"),
bg="#e74c3c",
fg="white",
width=15,
height=1,
cursor="hand2",
state=tk.DISABLED,
command=self.stop_process
)
self.stop_button.pack(side=tk.LEFT, padx=5)
self.reload_button = tk.Button(
button_frame,
text="重新加载工单",
font=("微软雅黑", 12),
bg="#3498db",
fg="white",
width=15,
height=1,
cursor="hand2",
command=self.load_work_order
)
self.reload_button.pack(side=tk.LEFT, padx=5)
# 日志区域
log_frame = tk.LabelFrame(self.root, text="操作日志", font=("微软雅黑", 10), padx=5, pady=5)
log_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
self.log_text = tk.Text(log_frame, height=5, font=("Consolas", 9))
self.log_text.pack(fill=tk.BOTH, expand=True)
log_scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)
self.log_text.configure(yscrollcommand=log_scrollbar.set)
log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def check_test_mode(self):
"""检查测试模式"""
try:
with open("config.json", "r", encoding="utf-8") as f:
config = json.load(f)
self.test_mode = config.get("test_mode", {}).get("enabled", False)
if self.test_mode:
self.test_mode_label.config(text="⚠️ 测试模式")
self.log("⚠️ 测试模式已启用:失败也算成功", "WARN")
else:
self.test_mode_label.config(text="")
except Exception as e:
self.log(f"读取配置失败: {e}", "ERROR")
def log(self, message, level="INFO"):
"""添加日志"""
timestamp = datetime.now().strftime("%H:%M:%S")
log_message = f"[{timestamp}] [{level}] {message}\n"
self.log_text.insert(tk.END, log_message)
self.log_text.see(tk.END)
print(log_message.strip())
def load_work_order(self):
"""加载工单数据"""
try:
with open("work_order.json", "r", encoding="utf-8") as f:
self.work_order = json.load(f)
# 更新界面
self.order_id_label.config(text=f"工单号: {self.work_order['order_id']}")
self.product_label.config(text=f"产品: {self.work_order['product_name']}")
self.station_label.config(text=f"工位: {self.work_order['station']}")
self.operator_label.config(text=f"操作员: {self.work_order['operator']}")
# 更新螺栓列表
self.tree.delete(*self.tree.get_children())
for bolt in self.work_order['bolts']:
self.tree.insert("", tk.END, values=(
bolt['bolt_id'],
bolt['name'],
bolt['target_torque'],
"待拧紧",
"--",
"--"
), tags=("pending",))
# 更新进度
total = len(self.work_order['bolts'])
self.progress_label.config(text=f"总进度: 0/{total}")
self.progress_bar['maximum'] = total
self.progress_bar['value'] = 0
self.current_bolt_index = 0
self.log(f"成功加载工单: {self.work_order['order_id']}, 共{total}个螺栓")
except Exception as e:
messagebox.showerror("错误", f"加载工单失败: {e}")
self.log(f"加载工单失败: {e}", "ERROR")
def start_process(self):
"""开始拧紧流程"""
if not self.work_order:
messagebox.showwarning("警告", "请先加载工单数据")
return
if self.is_running:
return
self.is_running = True
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.reload_button.config(state=tk.DISABLED)
# 启动工作线程
self.thread = threading.Thread(target=self.work_thread, daemon=True)
self.thread.start()
self.log("开始自动拧紧流程")
def stop_process(self):
"""停止拧紧流程"""
self.is_running = False
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.reload_button.config(state=tk.NORMAL)
self.log("用户停止流程")
def work_thread(self):
"""工作线程"""
try:
# 连接扳手
self.log("正在连接扳手...")
self.wrench = WrenchController()
if not self.wrench.connect():
self.log("❌ 连接扳手失败请检查IP和端口配置", "ERROR")
self.root.after(0, lambda: messagebox.showerror(
"连接失败",
f"无法连接到扳手\nIP: {self.wrench.host}\n端口: {self.wrench.port}\n\n请检查:\n1. 扳手是否开机\n2. 网络连接是否正常\n3. IP和端口配置是否正确"
))
self.stop_process()
return
self.log(f"✅ 已连接到扳手: {self.wrench.host}:{self.wrench.port}", "SUCCESS")
# 启用远程控制
self.log("正在启用远程控制...")
if not self.wrench.enable_remote_control(True):
self.log("❌ 启用远程控制失败", "ERROR")
self.root.after(0, lambda: messagebox.showerror("错误", "启用远程控制失败"))
self.stop_process()
return
self.log("✅ 已启用远程控制", "SUCCESS")
# 遍历所有螺栓
bolts = self.work_order['bolts']
for index, bolt in enumerate(bolts):
if not self.is_running:
break
self.current_bolt_index = index
self.process_bolt(bolt, index)
# 完成
if self.is_running:
self.log("所有螺栓拧紧完成!", "SUCCESS")
self.root.after(0, lambda: messagebox.showinfo("完成", "所有螺栓已成功拧紧!"))
except Exception as e:
self.log(f"流程异常: {e}", "ERROR")
finally:
if self.wrench:
self.wrench.disconnect()
self.root.after(0, self.stop_process)
def process_bolt(self, bolt, index):
"""处理单个螺栓"""
bolt_id = bolt['bolt_id']
bolt_name = bolt['name']
target_torque = bolt['target_torque']
self.log(f"开始拧紧: [{bolt_id}] {bolt_name}, 目标扭矩: {target_torque} Nm")
# 更新界面
self.root.after(0, lambda: self.update_current_bolt(bolt, "拧紧中..."))
self.root.after(0, lambda: self.update_tree_item(index, "拧紧中", "running"))
# 设定参数
self.log(f"设定参数: 扭矩={target_torque}Nm, 模式=M{bolt.get('mode', 1)}")
self.wrench.set_torque_parameters(
target_torque=target_torque,
mode=bolt.get('mode', 1),
torque_tolerance=bolt.get('torque_tolerance', 0.10),
angle_min=bolt.get('angle_min', 1),
angle_max=bolt.get('angle_max', 360)
)
# 循环尝试,直到成功
attempt = 0
while self.is_running:
attempt += 1
self.log(f"{attempt} 次尝试拧紧螺栓 {bolt_id}")
# 启动扳手
self.log("发送启动命令...")
if not self.wrench.start_wrench(direction=1):
self.log("❌ 发送启动命令失败", "ERROR")
time.sleep(1)
continue
# 等待结果
self.log("等待扳手响应...")
result = self.wrench.wait_for_result()
if result and result.get("success"):
# 成功
actual_torque = result.get("actual_torque", 0)
timestamp = datetime.now().strftime("%H:%M:%S")
self.log(f"✅ 螺栓 {bolt_id} 拧紧成功! 实际扭矩: {actual_torque} Nm", "SUCCESS")
# 更新界面
self.root.after(0, lambda at=actual_torque, ts=timestamp: self.update_tree_item(
index, "成功", "success", at, ts
))
self.root.after(0, lambda: self.update_progress(index + 1))
break
else:
# 失败,继续尝试
if result:
self.log(f"❌ 螺栓 {bolt_id} 拧紧失败: {result.get('status', '未知错误')}, 继续重试...", "WARN")
else:
self.log(f"❌ 螺栓 {bolt_id} 无响应, 继续重试...", "WARN")
time.sleep(1) # 等待1秒后重试
# 完成后等待一下
time.sleep(0.5)
def update_current_bolt(self, bolt, status):
"""更新当前螺栓显示"""
self.current_bolt_label.config(text=f"[{bolt['bolt_id']}] {bolt['name']}")
self.current_torque_label.config(text=f"目标扭矩: {bolt['target_torque']} Nm")
self.current_status_label.config(text=f"状态: {status}")
def update_tree_item(self, index, status, tag, actual_torque="--", timestamp="--"):
"""更新表格项"""
items = self.tree.get_children()
if index < len(items):
item = items[index]
values = list(self.tree.item(item)['values'])
values[3] = status
if actual_torque != "--":
values[4] = actual_torque
if timestamp != "--":
values[5] = timestamp
self.tree.item(item, values=values, tags=(tag,))
def update_progress(self, completed):
"""更新进度"""
total = len(self.work_order['bolts'])
self.progress_label.config(text=f"总进度: {completed}/{total}")
self.progress_bar['value'] = completed
def main():
root = tk.Tk()
app = WrenchGUI(root)
root.mainloop()
if __name__ == "__main__":
main()

424
文档.txt Normal file
View File

@ -0,0 +1,424 @@
App3定扭矩扳手通讯协议(C协议)
一、通讯参数说明
1、装置端与上位端以TCP/IP连接传输报文装置端可以是Server端也可以是Client端
二、报文格式说明
报文格式
报文头
2字节 地址码
1字节 功能码
1字节 保留
2字节 数据长度
1字节 数据内容
校验码(累加和)
1字节
0xC5 0xC5 0x01 0x01 0xFF 0xFF XX XX… XX XX
报文头: 0xC5 0xC5
地址码: 表示每台装置的地址
功能码:表示该帧报文的功能类型定义
保留保留2字节今后扩展使用
数据长度:表示后面数据内容的长度,不包含报文头、地址码、功能码、保留字节和校验码
数据内容:根据功能码有不同数据定义
校验码:从报文头开始到数据内容结束的单字节累加和
三、功能码说明
1、功能吗0x11启停远程控制启用远程控制后锂电扳手才会接受远程启停控制(App->扳手)
2、功能码0x01扳手启停控制 (App->扳手) 此指令在启动远程控制0x11帧后生效
3、功能码0x06作为接收方应答发送方使用
4、功能码0x10 参数设置 (App -> 扳手)
5、功能码0x12: 每秒钟扭矩角度数据(扳手->App)
6、功能吗0x15: 运行结果反馈,在锂电扳手每次运行结束后发出该报文(扳手->App)
7、功能码0x21: 扳手请求对时(扳手->App)
8、功能码0x22: App发送对时报文(App -> 扳手)
9、功能码 0x33: 设备延时自动关机心跳报文(App -> 扳手), 每分钟向设备发送,保证设备不自动断电
10、功能码0x44设备GPS定位周期发送上电后一直发送包含信息是否有效、经度、纬度、高度
11、功能码 0x04电池电量百分比数据 (此帧可作为心跳包定时发送)
12、功能码0x50: 脉冲、离合、冲击扳手参数设置
13、功能码0x55: 脉冲、离合、冲击扳手执行结果反馈
脉冲、离合、冲击扳手运行过程扭矩数据同0x12帧
14、功能码0x23扳手发送复位螺母个数选择项
15、功能码0x25查询扳手SN码
16、功能码0x26 应答扳手SN码
17、功能码0x17: 发送结果反馈帧0x15确认(App -> 扳手)
18、功能码0x05: 发状态到APP
四、报文内容
1、功能码0x11:启动/停止远程控制
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x11
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0xXX 0x00停止远程控制
0x01启用远程控制
9 校验码(累加和) 0xXX
报文示例:
C5 C5 01 11 FF FF 01 01 9C
2、功能码0x01:启停控制
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x01
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0xXX 0x00停止STOP
0x01正转启动RUN
0x02反转启动RUN
9 校验码(累加和) 0xXX
报文示例:
C5 C5 01 01 FF FF 01 02 8D
3、功能码0x06: 应答控制
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x06
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0xXX 0x00应答ACK
0x01应答NAK
9 校验码(累加和) 1字节
报文示例:
C5 C5 01 06 FF FF 01 00 90
4、 功能码0x10: 参数设置(App -> 扳手)
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x10
5-6 反退角 0xFF 0xFF
7 数据长度 0x50
8-17 产品ID 10字节 ASCII码,不足补0
18-37 工位名称 20字节 中英字符,不足补0
38-47 员工号 10字节 ASCII码,不足补0
48-57 工具系列号 10字节 ASCII码, 不足补0
58-67 控制器系列号 10字节 ASCII码, 不足补0
68-69 时间-年 2字节
70 时间-月 1字节
71 时间-日 1字节
72 时间-小时 1字节
73 时间-分钟 1字节
74 时间-秒 1字节
75 参数模式 1字节 M1模式0x01 M2模式0x02
76-77 目标扭矩高位
目标扭矩低位 2字节 此参数为目标扭矩 此参数为预设扭矩
78-79 扭矩下限高位
扭矩下限低位 2字节 目标扭矩下限值
次值为=目标扭矩*1-A
A为扭矩偏差百分比 扭矩下限值
目标角度执行过程中超出扭矩上下限值即停机如填0不受上下限扭矩控制
80-81 扭矩上限高位
扭矩上限低位 2字节 目标扭矩上限值
次值为=目标扭矩*1+A
A为扭矩偏差百分比 扭矩上限值
目标角度执行过程中超出扭矩上下限值即停机如填0不受上下限扭矩控制
82-83 目标角度高位
目标角度低位 2字节
0x00
0x00 目标角度值
84-85 角度上限高位
角度上限低位 2字节
角度上限值
实际转角超出角度上、下限值即停机如填0不受角度上下限控制 目标角度上限值
86-87 角度下限高位
角度下限低位 2字节
角度下限值
实际转角超出角度上、下限值即停机如填0不受角度上下限控制 目标角度下限值
88 校验码(累加和) 1字节
此帧中所有角度设定值需要乘10后填入协议 例如需要设置目标角度20° 协议中应填写目标角度 00 C8
报文示例1
时间2024/9/30 10:01:08 M1模式 目标扭矩500扭矩偏差A=10%(450Nm-550Nm)角度上限360角度下限1
C5 C5 01 10 FF FF 50 30 30 30 30 30 30 30 30 30 30 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 32 32 32 32 32 32 32 32 32 32 30 30 30 30 30 30 30 30 30 30 33 33 33 33 33 33 33 33 33 33 07 E8 09 1E 0A 01 08 01 01 F4 01 C2 02 26 00 00 0E 10 00 0A A1
报文示例2
时间2024/9/30 10:01:08 M2模式 目标扭矩180扭矩下限200Nm,扭矩上限400NM目标角度值90目标角度上限360目标角度下限1
C5 C5 01 10 FF FF 50 30 30 30 30 30 30 30 30 30 30 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 31 32 32 32 32 32 32 32 32 32 32 30 30 30 30 30 30 30 30 30 30 33 33 33 33 33 33 33 33 33 33 07 E8 09 1E 0A 01 08 02 00 B4 00 C8 01 F4 03 84 0E 10 00 0A BA
5、功能码0x12: 每秒钟扭矩角度数据
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x12
5-6 保留 0xFF 0xFF
7 数据长度 12+n*4
8-17 员工号 10字节 ASCII码
18-19 螺栓号 2字节
20-21 第1个点扭矩值高位
第1个点扭矩值低位
22-23 第1个点角度值高位
第1个点角度值低位
24-25 第2个点扭矩值高位
第2个点扭矩值低位
26-27 第2个点角度值高位
第2个点角度值低位
……
第n个点扭矩值高位
第n个点扭矩值低位
第n个点角度值高位
第n个点角度值低位
校验码(累加和) 1字节
报文示例:
螺栓号0001第1点扭矩值0角度值0第2点扭矩值16角度值2第3点扭矩值49角度值6第4点扭矩值66角度值10第5点扭矩值85角度值17
C5 C5 01 12 FF FF 20 30 30 30 30 30 30 30 30 30 30 00 01 00 00 00 00 00 10 00 02 00 31 00 06 00 42 00 0A 00 55 00 11 97
6、功能码0x15: 执行结果反馈
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x15
5 结果状态 0xFF 0x00 : 成功OK
0x01: OK-扭矩到达
0x02: OK-位置到达
0x10: 失败NG
0x11: NG-小于扭矩下限
0x12: NG-大于扭矩上限
0x13: NG-打滑
0x14: NG-小于角度下限
0x15: NG-大于角度上限
0x16: NG-2次拧紧
6 保留 0xFF
7 数据长度 0x24
8-17 员工号 10字节 ASCII码
18-19 螺栓号 2字节
20-21 时间-年 2字节
22 时间-月 1字节
23 时间-日 1字节
24 时间-小时 1字节
25 时间-分钟 1字节
26 时间-秒 1字节
27 参数模式 1字节 M1模式0x01 M2模式0x02
28-29 目标扭矩高位
目标扭矩低位 2字节 目标扭矩值 预设扭矩实际值
30-31 实际扭矩高位
实际扭矩低位 2字节 目标扭矩实际值 最终扭矩实际值
32-33 目标角度高位
目标角度低位 2字节
0x00
0x00 目标角度设置值
34-35 实际角度高位
实际角度低位 2字节 角度实际值 目标角度实际值
36-37 扭矩上限高位
扭矩上限低位 2字节
目标扭矩上限值
最终扭矩上限值
38-39 扭矩下限高位
扭矩下限低位 2字节
目标扭矩下限值
最终扭矩下限值
40-41 角度上限高位
角度上限低位 2字节 角度上限值 目标角度上限值
42-43 角度下限高位
角度下限低位 2字节 角度下限值 目标角度下限值
44 校验码(累加和) 1字节
报文示例:
时间2024/9/30 10:01:08 M2模式 预设扭矩实际值181最终扭矩实际值485目标角度设置值45目标角度实际值44最终扭矩上限值500最终扭矩下限值400, 目标角度上限 100目标角度下限1.
C5 C5 01 15 FF FF 24 30 30 30 30 30 30 30 30 30 30 00 01 07 E8 09 1E 0A 01 08 02 00 B5 01 E5 00 2D 00 2C 01 F4 01 90 00 64 00 01 AD
7、功能码0x21: 扳手请求对时(扳手->App)
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x21
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0x01 0x01请求对时
9 校验码(累加和) 1字节
报文示例:
C5 C5 01 21 FF FF 01 01 AC
8、 功能码0x22: App发送对时报文(App -> 扳手)
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x22
5-6 保留 0xFF 0xFF
7 数据长度 0x07
8-9 时间-年 2字节
10 时间-月 1字节
11 时间-日 1字节
12 时间-小时 1字节
13 时间-分钟 1字节
14 时间-秒 1字节
15 校验码(累加和) 1字节
报文示例:
C5 C5 01 22 FF FF 07 07 E8 09 1E 0A 01 08 DB
9、功能码0x33: 设备延时自动关机心跳报文
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x33
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0x00
9 校验码(累加和) 1字节
报文示例:
C5 C5 01 33 FF FF 01 00 BD
10、功能码0x44: GPS信息报文
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x44
5-6 保留 0xFF 0xFF
7 数据长度
(包含”是否有效”字节+”数据内容”)
8 是否有效 0x01有效
0x00无效
数据内容(纬度,N/S,精度,E/W,高度,)
3225.62461,N,11923.81795,E,14, ASCII格式,‘分隔
N 校验码(累加和) 1字节
11、功能码 0x04电池电量百分比数据
报文头 地址码 功能码 保留 数据长度 数据内容 校验码(累加和)
0xC5 0xC5 0x01 0x04 0xFF 0xFF 0x01 1字节 XX
电池电量范围 0-100 表示电量0 -100%
12、 功能码0x50: 脉冲、离合、冲击扳手参数设置(App -> 扳手)
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01 该字节用于区分同一组中不同的扳手
4 功能码 0x50
5-6 保留 0xFF 0xFF
7 数据长度 0x50
8-17 产品ID 10字节 ASCII码, 不足补0
18-37 工位名称 20字节 中英字符,不足补0
38-47 员工号 10字节 ASCII码, 不足补0
48-57 工具系列号 10字节 ASCII码, 不足补0
58-67 螺栓型号 10字节 ASCII码, 不足补0
68-69 时间-年 2字节
70 时间-月 1字节
71 时间-日 1字节
72 时间-小时 1字节
73 时间-分钟 1字节
74 时间-秒 1字节
75 参数模式 1字节 扭矩模式0x00
定点硬连接(H)0x01
定点软连接(S)0x02
76-77 目标扭矩高位
目标扭矩低位 2字节 0-65535表示0-6553.5NM
78-79 K1值高位
K1值低位 2字节 数值范围
50-350 K1K2K3值只在定点硬连接和软连接模式下有效扭矩模式下填0x00
80-81 K2值高位
K2值低位 2字节 数值范围
1-80
82-83 K3值高位
K3值低位 2字节 数值范围
1-3000
84 螺栓状态 1字节 0%-100%
85 误差精度 1字节
86-87 螺栓个数高位
螺栓个数低位 2字节
88 校验码(累加和) 1字节
13、功能码0x55 : 脉冲、离合、冲击扳手执行结果反馈
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x55
5 结果状态 0xXX 0x00 : 成功OK
0x10: 失败NG
6 保留 0xFF
7 数据长度 0x1C
8-17 员工号 10字节 ASCII码
18-19 螺栓号 2字节
20-21 时间-年 2字节
22 时间-月 1字节
23 时间-日 1字节
24 时间-小时 1字节
25 时间-分钟 1字节
26 时间-秒 1字节
27 参数模式 1字节 扭矩模式0x00
定点硬连接(H)0x01
定点软连接(S)0x02
28-29 目标扭矩高位
目标扭矩低位 2字节 0-65535表示0-6553.5NM
30-31 实际扭矩高位
实际扭矩低位 2字节 0-65535表示0-6553.5NM
32-33 实际角度高位
实际角度低位 2字节
34-35 保留 2字节
36 校验码(累加和) 1字节
14、功能码0x23: 扳手发送复位螺母个数选择项报文
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x23
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 1字节 0x00: 退出螺母计数模式
0x01: 按原螺母数重新开始计数运行
9 校验码(累加和) 1字节
15、功能码0x25: 查询设备SN码
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x25
5-6 保留 0xFF 0xFF
7 数据长度 0x05
8-12 数据内容 00 00 00 00 00
13 校验码(累加和) 0xXX
报文举例查询SN码C5 C5 01 25 FF FF 05 00 00 00 00 00 B3
16、功能码0x26: 应答设备SN码
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x26
5-6 保留 0xFF 0xFF
7 数据长度 0x05
8-12 数据内容 设备SN码10位0-9数值 以压缩BCD码表示
13 校验码(累加和) 0xXX
报文举例应答SN码C5 C5 01 26 FF FF 05 12 34 56 78 90 58
设备SN码: 1234567890
17、功能码0x17: 发送结果反馈帧0x15报文的确认帧扳手在发送0x15结果帧后未在0.5S内收到该0x17帧结果帧将保存到扳手反之扳手收到0x17帧则扳手不保存结果帧。
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x17
5-6 保留 0xFF 0xFF
7 数据长度 0x01
8 数据内容 0x00
9 校验码(累加和) 1字节
8、功能码0x05 : (发状态:控制器→显示屏)
字节序号
1-2 报文头 0xC5 0xC5
3 地址码 0x01
4 功能码 0x05 发状态参数:控制器→显示屏
5-6 保留 0xFF 0xFF
7 数据长度 0x04
8-9 错误码 2字节 0 表示无错误,可以不显示;其余数值为错误码,
10-11 控制位 2字节 Bit1: 正向0
反向1 Bit2: 预留
Bit3: 预留 Bit4: 预留
Bit5: 预留 Bit6: 预留
Bit7: 预留 Bit8: 预留
Bit9---Bit16预留
12 校验码(累加和) 1字节

0
配置详细说明 Normal file
View File