Compare commits
5 Commits
7cb36c75ea
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| faea225e24 | |||
| 80272b92b4 | |||
| b90395ea24 | |||
| 3c9784b362 | |||
| 17abd13df7 |
@ -1,3 +1,3 @@
|
||||
# fluent_widgets_pyside6
|
||||
|
||||
机器臂控制系统的 UI界面
|
||||
法奥机器臂控制系统的 UI界面 以及密胺制品的相关控制代码
|
||||
116
app/controller/mi_an/main_controller.py
Normal file
116
app/controller/mi_an/main_controller.py
Normal file
@ -0,0 +1,116 @@
|
||||
from ...view.mi_an_main_window import Window as MainWindow
|
||||
from ...view.cood_forms_interface import CoordinateFormsWidget
|
||||
from ...view.cood_forms_interface import CoordinateTableWidget
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from ...service.robot_service import RobotService
|
||||
|
||||
# 法奥机器人
|
||||
from fairino import Robot
|
||||
|
||||
|
||||
# 子界面管理
|
||||
class SubViewsDict(TypedDict):
|
||||
position: CoordinateFormsWidget
|
||||
|
||||
|
||||
class MainController:
|
||||
def __init__(self):
|
||||
# 主界面
|
||||
self.main_window = MainWindow()
|
||||
|
||||
# 初始化子界面
|
||||
self._initSubViews()
|
||||
|
||||
# 初始化子控制器
|
||||
self._initSubControllers()
|
||||
|
||||
# 机器人实例
|
||||
self.robot_client = Robot.RPC("192.168.58.2")
|
||||
|
||||
self.robot_service = RobotService(self.robot_client)
|
||||
|
||||
self.__connectSignals()
|
||||
|
||||
def _initSubViews(self):
|
||||
self.sub_views: SubViewsDict = {
|
||||
"system": self.main_window.system, # 系统设置
|
||||
"product": self.main_window.product, # 生产界面
|
||||
"robot": self.main_window.robot, # 机器臂基础设置
|
||||
"io": self.main_window.io, # io面板
|
||||
"position": self.main_window.position.formsWidget, # 点位设置
|
||||
"basic": self.main_window.basic, # 基础设置
|
||||
"point": self.main_window.point, # 点位调试
|
||||
"other": self.main_window.other, # 其他设置
|
||||
"data": self.main_window.data, # 数据采集
|
||||
}
|
||||
|
||||
def _initSubControllers(self):
|
||||
# self.sub_controllers = {
|
||||
# "system": SystemController(self.sub_views["system"]),
|
||||
# "produce": ProduceController(self.sub_views["produce"]),
|
||||
# }
|
||||
pass
|
||||
|
||||
def showMainWindow(self):
|
||||
self.main_window.show()
|
||||
|
||||
def __connectSignals(self):
|
||||
self.sub_views["position"].form_move_signal.connect(self.handleMove)
|
||||
self.sub_views["position"].form_update_signal.connect(self.handleGetPosition)
|
||||
self.sub_views["position"].form_machine_stop_signal.connect(self.handleMachineStop)
|
||||
|
||||
# 机器臂停止移动
|
||||
def handleMachineStop(self):
|
||||
self.robot_service.stop_moves()
|
||||
|
||||
# 设置点位
|
||||
def handleGetPosition(self, form_obj: CoordinateTableWidget):
|
||||
result = self.robot_client.GetActualTCPPose() # 获取 工具坐标
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
# 成功:解包错误码和位姿
|
||||
error, tcp_pos = result
|
||||
# print(f"成功,位姿:{tcp_pos}")
|
||||
|
||||
# 保留三位小数
|
||||
tcp_pos_rounded = [round(pos, 3) for pos in tcp_pos]
|
||||
# print(f"保留三位小数后:{tcp_pos_rounded}")
|
||||
else:
|
||||
# 失败:result 即为错误码
|
||||
print(f"设置位置失败,错误码:{result}")
|
||||
return
|
||||
|
||||
form_obj.update_table_data(tcp_pos_rounded)
|
||||
|
||||
# 机器臂移动
|
||||
# pos_list 中一定是合法坐标
|
||||
def handleMove(self, pos_list: list, name_list: list):
|
||||
# print("handleMove 移动 pos_list: ", pos_list)
|
||||
# print("handleMove 移动 pos_list: ", name_list)
|
||||
# 后续需要根据状态来设置
|
||||
tool_id = 1 # 当前 1为吸盘
|
||||
vel = 20.0
|
||||
|
||||
# self.robot_service.start_moves(pos_list, name_list, tool_id=tool_id, vel=vel)
|
||||
|
||||
try:
|
||||
self.robot_service.start_moves(
|
||||
pos_list=pos_list,
|
||||
name_list=name_list,
|
||||
tool_id=tool_id,
|
||||
vel=vel
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
# 处理“坐标列表与名称列表长度不一致”的错误
|
||||
print(f"❌ 移动任务提交失败(参数错误):{str(e)}")
|
||||
|
||||
# for desc_pos in pos_list:
|
||||
# print("handleMove 移动 desc_pos: ", pos_list)
|
||||
# error_code = self.robot_client.MoveCart(desc_pos=desc_pos, tool=tool_id, user=0,vel=vel)
|
||||
# if error_code == 0:
|
||||
# print("运动成功")
|
||||
# else:
|
||||
# print(f"运动失败,错误码: {error_code}")
|
||||
|
||||
128
app/model/point_state.py
Normal file
128
app/model/point_state.py
Normal file
@ -0,0 +1,128 @@
|
||||
"""
|
||||
包括一个点位需要的状态:
|
||||
1、点位名称
|
||||
2、速度
|
||||
3、工具id
|
||||
4、工件id
|
||||
5、关节坐标 J1-J6
|
||||
6、运动类型
|
||||
7、平滑时间
|
||||
"""
|
||||
class PointState:
|
||||
VALID_SPEED_RANGE = (0, 100)
|
||||
VALID_TOOL_WORK_ID_RANGE = (0, 14)
|
||||
VALID_JOINT_COUNT = 6
|
||||
VALID_JOINT_RANGE = (-180, 180)
|
||||
VALID_MOTION_TYPES = ["直线", "曲线中间点", "曲线终点", "自由路径"]
|
||||
VALID_BLEND_TIME_RANGE = (0, 500)
|
||||
|
||||
def __init__(self, pos_name, speed, tool_id, work_id, joint_values, motion_type, blend_time):
|
||||
# 数据合法性判断
|
||||
self.pos_name = self._validate_pos_name(pos_name)
|
||||
self.speed = self._validate_speed(speed)
|
||||
self.tool_id = self._validate_tool_work_id(tool_id, "工具ID")
|
||||
self.work_id = self._validate_tool_work_id(work_id, "工件ID")
|
||||
self.joint_values = self._validate_joint_values(joint_values)
|
||||
self.motion_type = self._validate_motion_type(motion_type)
|
||||
self.blend_time = self._validate_blend_time(blend_time)
|
||||
|
||||
def _validate_pos_name(self, pos_name):
|
||||
"""校验点位名称(非空)"""
|
||||
if not isinstance(pos_name, str):
|
||||
raise TypeError("点位名称必须是字符串类型")
|
||||
stripped_name = pos_name.strip()
|
||||
if not stripped_name:
|
||||
raise ValueError("点位名称不能为空或仅包含空格")
|
||||
return stripped_name
|
||||
|
||||
def _validate_speed(self, speed):
|
||||
"""校验速度(0-100范围)"""
|
||||
if not isinstance(speed, (int, float)):
|
||||
raise TypeError(f"速度必须是数字类型,当前类型:{type(speed).__name__}")
|
||||
min_val, max_val = self.VALID_SPEED_RANGE
|
||||
if not (min_val <= speed <= max_val):
|
||||
raise ValueError(f"速度必须在 {min_val}-{max_val} 之间,当前值:{speed}")
|
||||
return speed
|
||||
|
||||
def _validate_tool_work_id(self, value, field_name):
|
||||
"""校验工具ID/工件ID(0-14范围,整数)"""
|
||||
if not isinstance(value, int):
|
||||
raise TypeError(f"{field_name}必须是整数类型,当前类型:{type(value).__name__}")
|
||||
min_val, max_val = self.VALID_TOOL_WORK_ID_RANGE
|
||||
if not (min_val <= value <= max_val):
|
||||
raise ValueError(f"{field_name}必须在 {min_val}-{max_val} 之间,当前值:{value}")
|
||||
return value
|
||||
|
||||
def _validate_joint_values(self, joint_values):
|
||||
"""校验关节值(6个元素,每个在-180~180范围)"""
|
||||
if not isinstance(joint_values, list):
|
||||
raise TypeError(f"关节值必须是列表类型,当前类型:{type(joint_values).__name__}")
|
||||
if len(joint_values) != self.VALID_JOINT_COUNT:
|
||||
raise ValueError(
|
||||
f"关节值必须包含 {self.VALID_JOINT_COUNT} 个元素(J1-J6),"
|
||||
f"当前数量:{len(joint_values)}"
|
||||
)
|
||||
|
||||
# 逐个校验关节值
|
||||
validated_joints = []
|
||||
for i, val in enumerate(joint_values, 1):
|
||||
if not isinstance(val, (int, float)):
|
||||
raise TypeError(f"J{i}关节值必须是数字类型,当前类型:{type(val).__name__}")
|
||||
|
||||
min_val, max_val = self.VALID_JOINT_RANGE
|
||||
if not (min_val <= val <= max_val):
|
||||
raise ValueError(
|
||||
f"J{i}关节值必须在 {min_val}~{max_val} 之间,当前值:{val}"
|
||||
)
|
||||
validated_joints.append(val)
|
||||
|
||||
return validated_joints
|
||||
|
||||
def _validate_motion_type(self, motion_type):
|
||||
"""校验运动类型(必须是预定义的选项)"""
|
||||
if not isinstance(motion_type, str):
|
||||
raise TypeError(f"运动类型必须是字符串类型,当前类型:{type(motion_type).__name__}")
|
||||
if motion_type not in self.VALID_MOTION_TYPES:
|
||||
raise ValueError(
|
||||
f"运动类型必须是以下之一:{self.VALID_MOTION_TYPES},"
|
||||
f"当前值:{motion_type}"
|
||||
)
|
||||
return motion_type
|
||||
|
||||
def _validate_blend_time(self, blend_time):
|
||||
"""校验平滑时间(-1表示停止,否则0-500范围)"""
|
||||
if not isinstance(blend_time, (int, float)):
|
||||
raise TypeError(f"平滑时间必须是数字类型,当前类型:{type(blend_time).__name__}")
|
||||
|
||||
# 停止模式(-1)或正常平滑时间(0-500)
|
||||
if blend_time == -1:
|
||||
return -1
|
||||
|
||||
min_val, max_val = self.VALID_BLEND_TIME_RANGE
|
||||
if not (min_val <= blend_time <= max_val):
|
||||
raise ValueError(
|
||||
f"平滑时间必须是-1(停止)或 {min_val}-{max_val} 之间(毫秒),"
|
||||
f"当前值:{blend_time}"
|
||||
)
|
||||
return blend_time
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典,方便序列化或存储"""
|
||||
return {
|
||||
"pos_name": self.pos_name,
|
||||
"speed": self.speed,
|
||||
"tool_id": self.tool_id,
|
||||
"work_id": self.work_id,
|
||||
"joint_values": self.joint_values,
|
||||
"motion_type": self.motion_type,
|
||||
"blend_time": self.blend_time
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""打印调试"""
|
||||
return (
|
||||
f"点位名:{self.pos_name},速度:{self.speed}%\n"
|
||||
f"工具ID: {self.tool_id},工件ID: {self.work_id}\n"
|
||||
f"关节值:{self.joint_values}\n"
|
||||
f"运动类型:{self.motion_type},平滑时间:{self.blend_time}"
|
||||
)
|
||||
87
app/service/EMV.py
Normal file
87
app/service/EMV.py
Normal file
@ -0,0 +1,87 @@
|
||||
import socket
|
||||
import binascii
|
||||
import time
|
||||
|
||||
# 网络继电器的 IP 和端口
|
||||
HOST = "192.168.58.18"
|
||||
PORT = 50000
|
||||
|
||||
# 电磁阀控制报文
|
||||
valve_commands = {
|
||||
1: {
|
||||
"open": "00000000000601050000FF00",
|
||||
"close": "000000000006010500000000",
|
||||
},
|
||||
2: {
|
||||
"open": "00000000000601050001FF00",
|
||||
"close": "000000000006010500010000",
|
||||
},
|
||||
3: {
|
||||
"open": "00000000000601050002FF00",
|
||||
"close": "000000000006010500020000",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# 将十六进制字符串转换为字节数据并发送
|
||||
def send_command(command):
|
||||
byte_data = binascii.unhexlify(command)
|
||||
|
||||
# 创建套接字并连接到继电器
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
sock.connect((HOST, PORT))
|
||||
sock.send(byte_data)
|
||||
# 接收响应
|
||||
response = sock.recv(1024)
|
||||
# print(f"收到响应: {binascii.hexlify(response)}")
|
||||
# 校验响应
|
||||
if response == byte_data:
|
||||
# print("命令成功下发,继电器已执行操作。")
|
||||
return True
|
||||
else:
|
||||
print("命令下发失败,响应与请求不符。")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"通信错误: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 控制电磁阀打开
|
||||
def open(grasp, shake, throw):
|
||||
if grasp:
|
||||
# print("打开电磁阀 1")
|
||||
if send_command(valve_commands[1]["open"]):
|
||||
time.sleep(1)
|
||||
if shake:
|
||||
# print("打开电磁阀 2")
|
||||
if send_command(valve_commands[2]["open"]):
|
||||
time.sleep(0.05)
|
||||
if throw:
|
||||
print("打开电磁阀 3")
|
||||
if send_command(valve_commands[3]["open"]):
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
# 控制电磁阀关闭
|
||||
def close(grasp, shake, throw):
|
||||
if grasp:
|
||||
# print("关闭电磁阀 1")
|
||||
if send_command(valve_commands[1]["close"]):
|
||||
time.sleep(1)
|
||||
if shake:
|
||||
# print("关闭电磁阀 2")
|
||||
if send_command(valve_commands[2]["close"]):
|
||||
time.sleep(0.05)
|
||||
if throw:
|
||||
# print("关闭电磁阀 3")
|
||||
if send_command(valve_commands[3]["close"]):
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
# 关闭电磁阀
|
||||
# open(False, False, True) # 参数传True和False
|
||||
# close(True,False,True)
|
||||
# for i in range(10):
|
||||
# open(False,True,True)
|
||||
# close(True,True,True)
|
||||
58
app/service/catch.py
Normal file
58
app/service/catch.py
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/python3
|
||||
import time
|
||||
from enum import Enum
|
||||
|
||||
# import Constant
|
||||
# from COM.COM_Robot import RobotClient
|
||||
# from Util.util_time import CClockPulse, CTon
|
||||
from .EMV import *
|
||||
|
||||
|
||||
class CatchStatus(Enum):
|
||||
CNone = 0
|
||||
CTake = 1 # 抓取
|
||||
CRelease = 2 # 放开
|
||||
CDrop = 3
|
||||
CShake = 4
|
||||
COk = 5
|
||||
|
||||
class CatchTool:
|
||||
def __init__(self):
|
||||
self.catch_status = CatchStatus.CNone
|
||||
|
||||
# =========================================
|
||||
# 密胺餐盘
|
||||
# 电磁阀1 需要在 吸盘吸的时候 关闭
|
||||
# 电磁阀1 需要在 夹爪工作的时候打开
|
||||
|
||||
# 电磁阀2 开 -> 吸盘吸
|
||||
# 电磁阀2 关 -> 吸盘放开
|
||||
|
||||
# 电磁阀3 开-> 打开夹爪
|
||||
# 电磁阀3 关 -> 闭合夹爪
|
||||
|
||||
# =======================================
|
||||
# 夹爪 抓取
|
||||
def gripper_grasp(self):
|
||||
close(0, 0, 1) # 电磁阀3关
|
||||
open(1, 0, 0) # 电磁阀1开
|
||||
time.sleep(1) # 间隔1秒
|
||||
|
||||
# 夹爪 放开
|
||||
def gripper_release(self):
|
||||
open(1, 0, 1) # 电磁阀1开,电磁阀3开
|
||||
time.sleep(1) # 间隔1秒
|
||||
|
||||
# 吸盘 吸取
|
||||
def suction_pick(self):
|
||||
close(1, 0, 0) # 电磁阀1关
|
||||
open(0, 1, 0) # 电磁阀2开
|
||||
time.sleep(1) # 间隔1秒
|
||||
|
||||
# 吸盘 释放(松开)
|
||||
def suction_release(self):
|
||||
close(0, 1, 0) # 电磁阀2关
|
||||
time.sleep(1) # 间隔1秒
|
||||
|
||||
# catch_tool = CatchTool()
|
||||
# catch_tool.gripper_grasp()
|
||||
146
app/service/drop.py
Normal file
146
app/service/drop.py
Normal file
@ -0,0 +1,146 @@
|
||||
import time
|
||||
|
||||
# 倒料函数
|
||||
# 工具坐标tool 修改为 夹具
|
||||
# 倒料时的旋转角度rot_angle 默认是 180度,可以是负的, 注意限位问题
|
||||
def start_drop(robot_client, rot_angle=180, tool=1, user=0, drop_vel=100):
|
||||
# 1、让机器臂第六轴旋转180° (倒料)
|
||||
# 获取倒料点 关节坐标,准备倒料
|
||||
result = robot_client.GetActualJointPosDegree()
|
||||
# result = robot.GetActualJointPosRadian()
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
# 成功:错误码和关节坐标(度数)
|
||||
_, joint_pos = result
|
||||
print(f"倒料点关节坐标:{joint_pos}")
|
||||
else:
|
||||
# 失败:result 为错误码
|
||||
print(f"start_drop错误: 获取倒料点关节坐标失败,错误码:{result}")
|
||||
return False
|
||||
|
||||
# 修改第六轴关节坐标角度,准备旋转
|
||||
new_joint_pos = joint_pos.copy()
|
||||
# 修改第六轴角度, 加上旋转角度
|
||||
new_joint_pos[-1] = new_joint_pos[-1] + rot_angle
|
||||
# print("旋转前关节坐标:", joint_pos)
|
||||
# print("确认旋转后关节坐标:", new_joint_pos)
|
||||
|
||||
# 旋转第六轴,倒料
|
||||
errno = robot_client.MoveJ(
|
||||
new_joint_pos, tool=tool, user=user, vel=drop_vel
|
||||
)
|
||||
if errno == 0:
|
||||
print(f"执行坐标: {new_joint_pos} 成功")
|
||||
print("倒料成功!!!!!")
|
||||
else:
|
||||
print(f"start_drop错误: 旋转第六轴失败,错误码: {errno}")
|
||||
return False
|
||||
|
||||
# 确认倒料完成
|
||||
time.sleep(2)
|
||||
# 这里需要抖动?
|
||||
|
||||
# 第六轴复位
|
||||
errno = robot_client.MoveJ(joint_pos, tool=tool, user=user, vel=drop_vel)
|
||||
if errno == 0:
|
||||
print(f"执行坐标: {joint_pos} 成功")
|
||||
print("第六轴复位成功!!!!!")
|
||||
else:
|
||||
print(f"start_drop错误: 第六轴复位失败,错误码: {errno}")
|
||||
return False
|
||||
|
||||
return True # 倒料成功,包括 旋转 和 复原
|
||||
|
||||
# 倒料旋转函数(只旋转不复原)
|
||||
# 工具坐标tool 修改为 夹具
|
||||
# 倒料时的旋转角度rot_angle 默认是 180度,可以是负的, 注意限位问题
|
||||
def start_drop_rotate(robot_client, rot_angle=180, tool=1, user=0, drop_vel=100, global_vel = 40):
|
||||
# 0. 设置全局速度为 100%, 达到最快的倒料旋转速度
|
||||
robot_client.SetSpeed(100)
|
||||
|
||||
# 1、让机器臂第六轴旋转180° (倒料)
|
||||
# 获取倒料点 关节坐标,准备倒料
|
||||
result = robot_client.GetActualJointPosDegree()
|
||||
# result = robot.GetActualJointPosRadian()
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
# 成功:错误码和关节坐标(度数)
|
||||
_, joint_pos = result
|
||||
print(f"倒料旋转点关节坐标:{joint_pos}")
|
||||
else:
|
||||
# 失败:result 为错误码
|
||||
print(f"start_drop_rotate错误: 获取倒料点关节坐标失败,错误码:{result}")
|
||||
return False
|
||||
|
||||
# 修改第六轴关节坐标角度,准备旋转
|
||||
new_joint_pos = joint_pos.copy()
|
||||
# 修改第六轴角度, 加上旋转角度
|
||||
new_joint_pos[-1] = new_joint_pos[-1] + rot_angle
|
||||
# print("旋转前关节坐标:", joint_pos)
|
||||
# print("确认旋转后关节坐标:", new_joint_pos)
|
||||
|
||||
# 旋转第六轴,倒料
|
||||
errno = robot_client.MoveJ(
|
||||
new_joint_pos, tool=tool, user=user, vel=drop_vel
|
||||
)
|
||||
|
||||
# 2. 设置回原来的 全局速度
|
||||
robot_client.SetSpeed(global_vel)
|
||||
|
||||
# 旋转倒料状态
|
||||
if errno == 0:
|
||||
print(f"执行坐标: {new_joint_pos} 成功")
|
||||
print("倒料旋转成功!!!!!")
|
||||
else:
|
||||
print(f"start_drop_rotate错误: 旋转第六轴失败,错误码: {errno}")
|
||||
return False
|
||||
|
||||
# 确认倒料完成
|
||||
time.sleep(1)
|
||||
# 这里需要抖动?
|
||||
|
||||
return True # 倒料旋转成功
|
||||
|
||||
# 倒料复原函数(复原, 第六轴旋转回去)
|
||||
# 工具坐标tool 修改为 夹具
|
||||
# 复原的旋转角度rot_angle 默认是 180度,可以是负的, 注意限位问题
|
||||
def start_drop_reset(robot_client, rot_angle=180, tool=1, user=0, drop_vel=100, global_vel = 40):
|
||||
# 0. 设置全局速度为 100%, 达到最快的倒料旋转速度
|
||||
robot_client.SetSpeed(100)
|
||||
|
||||
# 1、让机器臂第六轴旋转180° (复原)
|
||||
# 获取复原点 关节坐标,准备复原
|
||||
result = robot_client.GetActualJointPosDegree()
|
||||
# result = robot.GetActualJointPosRadian()
|
||||
if isinstance(result, tuple) and len(result) == 2:
|
||||
# 成功:错误码和关节坐标(度数)
|
||||
_, joint_pos = result
|
||||
print(f"倒料复原点关节坐标:{joint_pos}")
|
||||
else:
|
||||
# 失败:result 为错误码
|
||||
print(f"start_drop_reset错误: 获取倒料点关节坐标失败,错误码:{result}")
|
||||
return False
|
||||
|
||||
# 修改第六轴关节坐标角度,准备旋转
|
||||
new_joint_pos = joint_pos.copy()
|
||||
# 修改第六轴角度, 减去旋转角度复原
|
||||
new_joint_pos[-1] = new_joint_pos[-1] - rot_angle
|
||||
# print("旋转前关节坐标:", joint_pos)
|
||||
# print("确认旋转后关节坐标:", new_joint_pos)
|
||||
|
||||
# 旋转第六轴,复原
|
||||
errno = robot_client.MoveJ(
|
||||
new_joint_pos, tool=tool, user=user, vel=drop_vel
|
||||
)
|
||||
|
||||
# 2. 设置回原来的 全局速度
|
||||
robot_client.SetSpeed(global_vel)
|
||||
|
||||
if errno == 0:
|
||||
print(f"执行坐标: {new_joint_pos} 成功")
|
||||
print("倒料复原成功!!!!!")
|
||||
else:
|
||||
print(f"start_drop_reset错误: 复原第六轴失败,错误码: {errno}")
|
||||
return False
|
||||
|
||||
return True # 倒料复原成功
|
||||
|
||||
|
||||
198
app/service/high_machine.py
Normal file
198
app/service/high_machine.py
Normal file
@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
# @Time : 2025/6/19 11:20
|
||||
# @Author : reenrr
|
||||
# @File : EMV-test1.py
|
||||
# 功能描述 :通过网络控制继电器模块,实现对高周波开启和停止按钮的操作
|
||||
'''
|
||||
import socket
|
||||
import binascii
|
||||
import time
|
||||
|
||||
# 网络继电器的 IP 和端口
|
||||
# 高周波 19
|
||||
HOST = '192.168.58.19'
|
||||
PORT = 50000
|
||||
|
||||
# 开启按钮
|
||||
START_BUTTON = 'start_button'
|
||||
# 紧急停止按钮
|
||||
STOP_BUTTON = 'stop_button'
|
||||
|
||||
'''
|
||||
控件控制报文,存储各按钮的开关命令
|
||||
'''
|
||||
valve_commands = {
|
||||
START_BUTTON: {
|
||||
'open': '00000000000601050000FF00',
|
||||
'close': '000000000006010500000000',
|
||||
},
|
||||
STOP_BUTTON: {
|
||||
'open': '00000000000601050001FF00',
|
||||
'close': '000000000006010500010000',
|
||||
},
|
||||
}
|
||||
|
||||
'''
|
||||
读取状态命令,获取设备当前状态的指令
|
||||
'''
|
||||
read_status_command = {
|
||||
'button':'000000000006010100000008',
|
||||
}
|
||||
|
||||
# 控件对应 DO 位(从低到高)
|
||||
button_bit_map = {
|
||||
START_BUTTON: 0,
|
||||
STOP_BUTTON: 1,
|
||||
}
|
||||
|
||||
# 控件名称映射表
|
||||
button_name_map = {
|
||||
START_BUTTON: "开启按钮",
|
||||
STOP_BUTTON: "紧急停止按钮",
|
||||
}
|
||||
|
||||
def send_command(command):
|
||||
'''
|
||||
发送控制命令到网络继电器,
|
||||
|
||||
参数:
|
||||
command: 十六进制字符串,控制指令
|
||||
返回:
|
||||
设备响应数据(字节类型),失败返回 False
|
||||
'''
|
||||
|
||||
# 将十六进制字符串转换为字节数据并发送
|
||||
byte_data = binascii.unhexlify(command)
|
||||
|
||||
# 创建套接字并连接到继电器
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
sock.connect((HOST, PORT))
|
||||
sock.send(byte_data)
|
||||
# 接收响应
|
||||
response = sock.recv(1024)
|
||||
print(f"收到响应: {binascii.hexlify(response)}")
|
||||
# 校验响应
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"通信错误: {e}")
|
||||
return False
|
||||
|
||||
def get_all_button_status(command_type='button'):
|
||||
'''
|
||||
获取所有按钮的当前状态
|
||||
|
||||
参数:
|
||||
command_type: 读取类型,默认为 'button'
|
||||
返回:
|
||||
字典,键为按钮名称,值为 True/False,失败返回空字典
|
||||
'''
|
||||
|
||||
# 获取对应的读取命令
|
||||
command = read_status_command.get(command_type)
|
||||
if not command:
|
||||
print(f"未知的读取类型: {command_type}")
|
||||
return {}
|
||||
|
||||
response = send_command(command)
|
||||
status_dict = {}
|
||||
|
||||
# 校验响应是否有效(至少10字节)
|
||||
if response and len(response) >= 10:
|
||||
# 状态信息存储在响应的第10个字节(索引9)
|
||||
status_byte = response[9] # 状态在第10字节
|
||||
# 将字节转换为8位二进制字符串,并反转使低位在前
|
||||
status_bin = f"{status_byte:08b}"[::-1]
|
||||
|
||||
if command_type == 'button':
|
||||
bit_map = button_bit_map
|
||||
name_map = button_name_map
|
||||
else:
|
||||
print("不支持的映射类型")
|
||||
return{}
|
||||
|
||||
# 解析每个按钮的状态
|
||||
for key, bit_index in bit_map.items():
|
||||
# 检查对应位是否为1(1表示开启,0表示关闭)
|
||||
state = status_bin[bit_index] == '1'
|
||||
status_dict[key] = state
|
||||
else:
|
||||
print("读取状态失败或响应无效")
|
||||
|
||||
return status_dict
|
||||
|
||||
def get_button_status(button_name, command_type='button'):
|
||||
'''
|
||||
获取单个控件的当前状态
|
||||
|
||||
参数:
|
||||
device_name: 控件名称,如 RESIN_MOLDING_BUTTON、FINSHING_AGENT_BUTTON、STOP_BUTTON
|
||||
command_type: 读取类型,默认为 'button'
|
||||
返回:
|
||||
True: 开启, False: 关闭, None: 无法读取
|
||||
'''
|
||||
status = get_all_button_status(command_type)
|
||||
return status.get(button_name, None)
|
||||
|
||||
def open(start_button=False, stop_button=False):
|
||||
'''
|
||||
打开指定的按钮(仅在按钮当前处于关闭状态时才执行)
|
||||
|
||||
参数:
|
||||
resin_molding_button: 素面操作按钮,默认为 False
|
||||
finishing_agent_button: 烫金操作按钮,默认为 False
|
||||
stop_button: 紧急停止按钮,默认为 False
|
||||
'''
|
||||
|
||||
status = get_all_button_status()
|
||||
|
||||
if start_button and not status.get(START_BUTTON, False):
|
||||
print("打开开启按钮")
|
||||
send_command(valve_commands[START_BUTTON]['open'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if stop_button and not status.get(STOP_BUTTON, False):
|
||||
print("打开紧急停止按钮")
|
||||
send_command(valve_commands[STOP_BUTTON]['open'])
|
||||
time.sleep(0.05)
|
||||
|
||||
def close(start_button=False, stop_button=False):
|
||||
'''
|
||||
关闭指定的按钮(仅在按钮当前处于开启状态时才执行)
|
||||
|
||||
参数:
|
||||
resin_molding_button: 素面操作按钮,默认为 False
|
||||
finishing_agent_button: 烫金操作按钮,默认为 False
|
||||
stop_button: 紧急停止按钮,默认为 False
|
||||
'''
|
||||
status = get_all_button_status()
|
||||
|
||||
if start_button and status.get(START_BUTTON, True):
|
||||
print("关闭开启按钮")
|
||||
send_command(valve_commands[START_BUTTON]['close'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if stop_button and status.get(STOP_BUTTON, True):
|
||||
print("关闭紧急停止按钮")
|
||||
send_command(valve_commands[STOP_BUTTON]['close'])
|
||||
time.sleep(0.05)
|
||||
|
||||
def start_high(h_time = 80):
|
||||
# 操作按钮需要先打开后立即关闭
|
||||
open(start_button=True)
|
||||
time.sleep(0.5)
|
||||
close(start_button=True)
|
||||
print(f"等待高周波完成,等待时间为{h_time}秒......")
|
||||
time.sleep(h_time)
|
||||
print("高周波已经完成...")
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 操作按钮需要先打开后立即关闭
|
||||
# open(start_button=True)
|
||||
# time.sleep(0.5)
|
||||
# close(start_button=True)
|
||||
start_high()
|
||||
|
||||
|
||||
213
app/service/press_machine.py
Normal file
213
app/service/press_machine.py
Normal file
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
# @Time : 2025/6/19 11:20
|
||||
# @Author : reenrr
|
||||
# @File : EMV-test1.py
|
||||
# 功能描述 :通过网络控制继电器模块,实现对冲压机素面操作按钮、烫金操作按钮和紧急停止按钮的远程控制
|
||||
'''
|
||||
import socket
|
||||
import binascii
|
||||
import time
|
||||
|
||||
# 网络继电器的 IP 和端口
|
||||
# 冲压机20
|
||||
HOST = '192.168.58.20'
|
||||
PORT = 50000
|
||||
|
||||
# 素面操作按钮(冲压机)或高压启动按钮(高周波)
|
||||
RESIN_MOLDING_BUTTON ='resin_molding_button'
|
||||
# 烫金操作按钮
|
||||
FINSHING_AGENT_BUTTON = 'finishing_agent_button'
|
||||
# 紧急停止按钮
|
||||
STOP_BUTTON = 'stop_button'
|
||||
|
||||
'''
|
||||
控件控制报文,存储各按钮的开关命令
|
||||
'''
|
||||
valve_commands = {
|
||||
RESIN_MOLDING_BUTTON: {
|
||||
'open': '00000000000601050000FF00',
|
||||
'close': '000000000006010500000000',
|
||||
},
|
||||
FINSHING_AGENT_BUTTON: {
|
||||
'open': '00000000000601050001FF00',
|
||||
'close': '000000000006010500010000',
|
||||
},
|
||||
STOP_BUTTON: {
|
||||
'open': '00000000000601050002FF00',
|
||||
'close': '000000000006010500020000',
|
||||
}
|
||||
}
|
||||
|
||||
'''
|
||||
读取状态命令,获取设备当前状态的指令
|
||||
'''
|
||||
read_status_command = {
|
||||
'button':'000000000006010100000008',
|
||||
}
|
||||
|
||||
# 控件对应 DO 位(从低到高)
|
||||
button_bit_map = {
|
||||
RESIN_MOLDING_BUTTON: 0,
|
||||
FINSHING_AGENT_BUTTON: 1,
|
||||
STOP_BUTTON: 2,
|
||||
}
|
||||
|
||||
# 控件名称映射表
|
||||
button_name_map = {
|
||||
RESIN_MOLDING_BUTTON: "素面操作按钮",
|
||||
FINSHING_AGENT_BUTTON: "烫金操作按钮",
|
||||
STOP_BUTTON: "紧急停止按钮",
|
||||
}
|
||||
|
||||
def send_command(command):
|
||||
'''
|
||||
发送控制命令到网络继电器,
|
||||
|
||||
参数:
|
||||
command: 十六进制字符串,控制指令
|
||||
返回:
|
||||
设备响应数据(字节类型),失败返回 False
|
||||
'''
|
||||
|
||||
# 将十六进制字符串转换为字节数据并发送
|
||||
byte_data = binascii.unhexlify(command)
|
||||
|
||||
# 创建套接字并连接到继电器
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
sock.connect((HOST, PORT))
|
||||
sock.send(byte_data)
|
||||
# 接收响应
|
||||
response = sock.recv(1024)
|
||||
print(f"收到响应: {binascii.hexlify(response)}")
|
||||
# 校验响应
|
||||
return response
|
||||
except Exception as e:
|
||||
print(f"通信错误: {e}")
|
||||
return False
|
||||
|
||||
def get_all_button_status(command_type='button'):
|
||||
'''
|
||||
获取所有按钮的当前状态
|
||||
|
||||
参数:
|
||||
command_type: 读取类型,默认为 'button'
|
||||
返回:
|
||||
字典,键为按钮名称,值为 True/False,失败返回空字典
|
||||
'''
|
||||
|
||||
# 获取对应的读取命令
|
||||
command = read_status_command.get(command_type)
|
||||
if not command:
|
||||
print(f"未知的读取类型: {command_type}")
|
||||
return {}
|
||||
|
||||
response = send_command(command)
|
||||
status_dict = {}
|
||||
|
||||
# 校验响应是否有效(至少10字节)
|
||||
if response and len(response) >= 10:
|
||||
# 状态信息存储在响应的第10个字节(索引9)
|
||||
status_byte = response[9] # 状态在第10字节
|
||||
# 将字节转换为8位二进制字符串,并反转使低位在前
|
||||
status_bin = f"{status_byte:08b}"[::-1]
|
||||
|
||||
if command_type == 'button':
|
||||
bit_map = button_bit_map
|
||||
name_map = button_name_map
|
||||
else:
|
||||
print("不支持的映射类型")
|
||||
return{}
|
||||
|
||||
# 解析每个按钮的状态
|
||||
for key, bit_index in bit_map.items():
|
||||
# 检查对应位是否为1(1表示开启,0表示关闭)
|
||||
state = status_bin[bit_index] == '1'
|
||||
status_dict[key] = state
|
||||
else:
|
||||
print("读取状态失败或响应无效")
|
||||
|
||||
return status_dict
|
||||
|
||||
def get_button_status(button_name, command_type='button'):
|
||||
'''
|
||||
获取单个控件的当前状态
|
||||
|
||||
参数:
|
||||
device_name: 控件名称,如 RESIN_MOLDING_BUTTON、FINSHING_AGENT_BUTTON、STOP_BUTTON
|
||||
command_type: 读取类型,默认为 'button'
|
||||
返回:
|
||||
True: 开启, False: 关闭, None: 无法读取
|
||||
'''
|
||||
status = get_all_button_status(command_type)
|
||||
return status.get(button_name, None)
|
||||
|
||||
def open(resin_molding_button=False, finishing_agent_button=False, stop_button=False):
|
||||
'''
|
||||
打开指定的按钮(仅在按钮当前处于关闭状态时才执行)
|
||||
|
||||
参数:
|
||||
resin_molding_button: 素面操作按钮,默认为 False
|
||||
finishing_agent_button: 烫金操作按钮,默认为 False
|
||||
stop_button: 紧急停止按钮,默认为 False
|
||||
'''
|
||||
|
||||
status = get_all_button_status()
|
||||
|
||||
if resin_molding_button and not status.get(RESIN_MOLDING_BUTTON, False):
|
||||
print("打开素面操作按钮")
|
||||
send_command(valve_commands[RESIN_MOLDING_BUTTON]['open'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if finishing_agent_button and not status.get(FINSHING_AGENT_BUTTON, False):
|
||||
print("打开烫金操作按钮")
|
||||
send_command(valve_commands[FINSHING_AGENT_BUTTON]['open'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if stop_button and not status.get(STOP_BUTTON, False):
|
||||
print("打开紧急操作按钮")
|
||||
send_command(valve_commands[STOP_BUTTON]['open'])
|
||||
time.sleep(0.05)
|
||||
|
||||
def close(resin_molding_button=False, finishing_agent_button=False, stop_button=False):
|
||||
'''
|
||||
关闭指定的按钮(仅在按钮当前处于开启状态时才执行)
|
||||
|
||||
参数:
|
||||
resin_molding_button: 素面操作按钮,默认为 False
|
||||
finishing_agent_button: 烫金操作按钮,默认为 False
|
||||
stop_button: 紧急停止按钮,默认为 False
|
||||
'''
|
||||
status = get_all_button_status()
|
||||
|
||||
if resin_molding_button and status.get(RESIN_MOLDING_BUTTON, True):
|
||||
print("关闭素面操作按钮")
|
||||
send_command(valve_commands[RESIN_MOLDING_BUTTON]['close'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if finishing_agent_button and status.get(FINSHING_AGENT_BUTTON, True):
|
||||
print("关闭烫金操作按钮")
|
||||
send_command(valve_commands[FINSHING_AGENT_BUTTON]['close'])
|
||||
time.sleep(0.05)
|
||||
|
||||
if stop_button and status.get(STOP_BUTTON, True):
|
||||
print("关闭紧急操作按钮")
|
||||
send_command(valve_commands[STOP_BUTTON]['close'])
|
||||
time.sleep(0.05)
|
||||
|
||||
def start_press():
|
||||
# 操作按钮需要先打开后立即关闭
|
||||
open(resin_molding_button=True)
|
||||
time.sleep(0.5)
|
||||
close(resin_molding_button=True)
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# # 操作按钮需要先打开后立即关闭
|
||||
# open(resin_molding_button=True)
|
||||
# time.sleep(0.5)
|
||||
# close(resin_molding_button=True)
|
||||
|
||||
|
||||
|
||||
394
app/service/robot_service.py
Normal file
394
app/service/robot_service.py
Normal file
@ -0,0 +1,394 @@
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
# # 法奥机器人
|
||||
from fairino import Robot
|
||||
|
||||
# 夹具和吸盘控制
|
||||
from .catch import CatchTool
|
||||
|
||||
# 震动函数
|
||||
from .vibrate import start_vibrate
|
||||
|
||||
# 称重函数
|
||||
from .weight import start_weight
|
||||
|
||||
# 倒料函数
|
||||
from .drop import start_drop, start_drop_rotate, start_drop_reset
|
||||
|
||||
# 高周波
|
||||
from .high_machine import start_high
|
||||
|
||||
# 冲压机
|
||||
from .press_machine import start_press
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class RobotService:
|
||||
def __init__(self, robot_client: Robot.RPC):
|
||||
self.robot_client = robot_client # 机器臂实例
|
||||
|
||||
self.move_thread = None # 移动任务线程对象
|
||||
self.moving_running = False # 移动线程运行状态标记
|
||||
|
||||
# 跟踪线程相关
|
||||
# 跟踪线程的机器臂调用
|
||||
self.robot_tracking_client = Robot.RPC(robot_client.ip_address)
|
||||
self.point_sequence = [] # 点位序列元数据 (保存点位状态status和索引index)
|
||||
self.current_target_index = -1 # 当前正在朝向(移动)的点位索引(初始为-1)
|
||||
self.completed_indices = set() # 已运动过的点位索引集合
|
||||
self.tracking_lock = threading.Lock()
|
||||
|
||||
# 末端工具对象
|
||||
self.catch_tool = CatchTool()
|
||||
|
||||
def start_moves(
|
||||
self,
|
||||
pos_list: List[list],
|
||||
name_list: List[str],
|
||||
tool_id: int = 1,
|
||||
vel: float = 5.0,
|
||||
):
|
||||
"""启动移动任务(对外接口)"""
|
||||
# 校验参数:坐标列表和名称列表长度必须一致
|
||||
if len(pos_list) != len(name_list):
|
||||
raise ValueError("pos_list与name_list长度不一致")
|
||||
|
||||
# 如果已有移动任务线程在运行,先停止
|
||||
if self.move_thread and self.move_thread.is_alive():
|
||||
self.stop_moves()
|
||||
|
||||
# 保存任务参数
|
||||
self.pos_list = pos_list
|
||||
self.name_list = name_list
|
||||
self.tool_id = tool_id
|
||||
self.vel = vel
|
||||
self.moving_running = True
|
||||
|
||||
# 初始化点位序列
|
||||
self.init_point_sequence()
|
||||
|
||||
# 启动移动点位跟踪
|
||||
self.start_tracking()
|
||||
|
||||
# 启动移动任务线程, 执行移动任务
|
||||
self.move_thread = threading.Thread(target=self._process_moves)
|
||||
self.move_thread.daemon = True
|
||||
self.move_thread.start()
|
||||
|
||||
def stop_moves(self):
|
||||
"""停止移动任务"""
|
||||
self.moving_running = False
|
||||
|
||||
if self.stop_motion() == 0:
|
||||
print("stop_moves: 机器臂停止移动成功...")
|
||||
|
||||
# 打印 停止时,正在朝哪个点移动
|
||||
point_dict = self.get_current_target_point()
|
||||
if point_dict:
|
||||
print("停止前, 正在朝向移动的目标点位为:")
|
||||
print(
|
||||
f"所选中的所有点中的第{point_dict['index']+1} 个点",
|
||||
"pos:",
|
||||
point_dict["pos"],
|
||||
)
|
||||
else:
|
||||
print("所有的点位已经移动完成, 无朝向移动的目标点!!!!")
|
||||
|
||||
# 停止跟踪
|
||||
self.stop_tracking()
|
||||
|
||||
def stop_motion(self):
|
||||
error = self.robot_client.send_message("/f/bIII4III102III4IIISTOPIII/b/f")
|
||||
return error
|
||||
|
||||
def _process_moves(self):
|
||||
"""线程内执行的移动任务逻辑"""
|
||||
for i in range(len(self.pos_list)):
|
||||
if not self.moving_running:
|
||||
break # 若已触发停止,则退出循环
|
||||
|
||||
current_pos = self.pos_list[i]
|
||||
current_name = self.name_list[i]
|
||||
print(f"坐标名: {current_name},坐标:{current_pos}")
|
||||
|
||||
if current_name == "normal":
|
||||
# 中间的普通点加入平滑时间
|
||||
error_code = self.robot_client.MoveCart(
|
||||
desc_pos=current_pos,
|
||||
tool=self.tool_id,
|
||||
user=0,
|
||||
vel=self.vel,
|
||||
blendT=200, # 平滑, 200毫秒
|
||||
)
|
||||
# 加了平滑会出现一个问题,那就是该函数不会阻塞,就直接返回了
|
||||
else:
|
||||
# 执行机器臂移动
|
||||
error_code = self.robot_client.MoveCart(
|
||||
desc_pos=current_pos, tool=self.tool_id, user=0, vel=self.vel
|
||||
)
|
||||
|
||||
# 根据移动结果处理
|
||||
# 必须要根据函数返回来决定
|
||||
if self.moving_running and error_code == 0:
|
||||
print(f" {current_name} 移动成功 或者 移动指令发送成功!!! ")
|
||||
# 根据点位名称调用对应函数(核心判断逻辑)
|
||||
# 功能点会调用相应的函数来进行相应的操作
|
||||
# 注意: 功能点不能设置为 平滑点!!!!! 否则不能根据函数返回判断是否已经运动到该点位
|
||||
self._call_action_by_name(current_name) # 可以优化
|
||||
else:
|
||||
# 1、该点位移动失败....
|
||||
# 2、机器臂停止运动,还会进入到这里....
|
||||
print(
|
||||
f"移动到 {current_name} 失败,错误码:{error_code}, 运行状态: {self.moving_running}"
|
||||
)
|
||||
# 可选:是否继续执行下一个点?
|
||||
self.moving_running = False
|
||||
|
||||
# 所有移动任务完成
|
||||
self.moving_running = False
|
||||
print("================所有点位任务已完成===============")
|
||||
|
||||
def _call_action_by_name(self, point_name: str):
|
||||
"""根据点位名称调用对应动作(核心判断逻辑)"""
|
||||
# 1. 夹取相关点位
|
||||
if point_name in ["抓取点", "夹取点"]:
|
||||
self.catch_tool.gripper_grasp()
|
||||
|
||||
# 2. 放开相关点位
|
||||
elif point_name in ["放开点"]:
|
||||
self.catch_tool.gripper_release()
|
||||
|
||||
# 3. 称重相关点位
|
||||
elif point_name in ["称重点"]:
|
||||
start_weight()
|
||||
|
||||
# 4. 震频点相关点位
|
||||
elif point_name in ["震频点", "震动点"]:
|
||||
start_vibrate()
|
||||
|
||||
# 5. 倒料相关点位
|
||||
elif point_name in ["倒料点"]:
|
||||
start_drop(self.robot_client)
|
||||
|
||||
# start_high
|
||||
elif point_name in ["高周波点"]:
|
||||
start_high()
|
||||
|
||||
# start_press
|
||||
elif point_name in ["冲压点", "冲压机点"]:
|
||||
start_press()
|
||||
|
||||
elif point_name in ["冲压机等待点"]:
|
||||
# 冲压机花费60秒,这里等待一段时间
|
||||
print("等待冲压机冲压完成, 等待时间为40秒......")
|
||||
# 因为高周波点等待了80秒,所以这里只需要等待40秒,一共120秒
|
||||
# 这里会一起计算高周波等待点的80秒
|
||||
time.sleep(40)
|
||||
print("冲压已经完成......")
|
||||
|
||||
# start_drop_rotate
|
||||
# 旋转180°倒料
|
||||
elif point_name in ["倒料旋转点", "旋转点"]:
|
||||
start_drop_rotate(self.robot_client)
|
||||
|
||||
# 旋转复原点
|
||||
elif point_name in ["倒料复原点", "旋转复原点"]:
|
||||
start_drop_reset(self.robot_client)
|
||||
|
||||
# 吸取点
|
||||
# 吸盘吸取
|
||||
elif point_name in ["吸取点"]:
|
||||
self.catch_tool.suction_pick()
|
||||
|
||||
# 释放点
|
||||
# 吸盘释放
|
||||
elif point_name in ["释放点"]:
|
||||
self.catch_tool.suction_release()
|
||||
|
||||
elif point_name in ["冲压机等待点2"]:
|
||||
# 冲压机花费90秒,这里等待一段时间
|
||||
# 这里是专门等待冲压机的....,不会一起计算高周波等待点的时间...
|
||||
print("等待冲压机冲压完成, 等待时间为90秒......")
|
||||
time.sleep(90)
|
||||
print("冲压已经完成......")
|
||||
|
||||
# 4. 可扩展其他功能点位类型
|
||||
# elif point_name in ["XXX点"]:
|
||||
# self._xxx_action()
|
||||
|
||||
# else:
|
||||
# print(f"点位 {point_name} 无对应动作,不执行额外操作")
|
||||
|
||||
def init_point_sequence(self):
|
||||
"""初始化点位序列,添加元数据"""
|
||||
self.point_sequence.clear() # 清空
|
||||
for idx, point in enumerate(self.pos_list):
|
||||
self.point_sequence.append(
|
||||
{
|
||||
"index": idx,
|
||||
"pos": point, # 坐标 [x,y,z,rx,ry,rz]
|
||||
"status": "pending", # 状态:pending(未发送)/sent(已发送)/completed(已经过)/current(当前目标)
|
||||
}
|
||||
)
|
||||
self.current_target_index = (
|
||||
0 if self.point_sequence else -1
|
||||
) # 初始目标为第一个点
|
||||
|
||||
def get_robot_status(self):
|
||||
"""获取机器人实时状态:是否在运动 + 当前位置"""
|
||||
try:
|
||||
with self.tracking_lock:
|
||||
# 1. 判断是否正在运动
|
||||
is_moving = self.is_moving()
|
||||
|
||||
# 2. 获取当前TCP位置
|
||||
# [x,y,z,rx,ry,rz]
|
||||
result = self.robot_tracking_client.GetActualTCPPose()
|
||||
|
||||
# 解析返回值
|
||||
if isinstance(result, tuple) and len(result) >= 2 and result[0] == 0:
|
||||
# 成功:提取坐标列表(第二个元素)
|
||||
current_pos = result[1]
|
||||
# 校验坐标格式(6个数字)
|
||||
if not (
|
||||
isinstance(current_pos, list)
|
||||
and len(current_pos) == 6
|
||||
and all(isinstance(p, (int, float)) for p in current_pos)
|
||||
):
|
||||
print(f"坐标格式错误:{current_pos}, 需为6个数字的列表")
|
||||
return None
|
||||
else:
|
||||
# 失败:打印错误码
|
||||
error_code = result[0] if isinstance(result, tuple) else result
|
||||
print(f"get_robot_status: 获取TCP位置失败, 错误码:{error_code}")
|
||||
return None
|
||||
|
||||
return {
|
||||
"is_moving": is_moving,
|
||||
"current_pos": current_pos,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"get_robot_status: 获取状态失败:{e}")
|
||||
return None
|
||||
|
||||
def is_moving(self) -> bool:
|
||||
"""
|
||||
判断机器臂是否正在运动
|
||||
|
||||
返回:
|
||||
bool: True表示正在运动(robot_state=2),False表示停止/暂停/拖动/异常
|
||||
"""
|
||||
# 1. 检查状态包是否已初始化
|
||||
if self.robot_client.robot_state_pkg is None:
|
||||
print("is_moving: 警告:机器人状态包未初始化,无法判断运动状态")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 2. 从状态包中获取robot_state(取值1-4)
|
||||
time.sleep(0.05) # 等待状态刷新
|
||||
robot_state = self.robot_client.robot_state_pkg.robot_state
|
||||
|
||||
# 3. 校验robot_state是否在合法范围内
|
||||
if robot_state not in (1, 2, 3, 4):
|
||||
print(
|
||||
f"is_moving: 警告: 无效的robot_state值 {robot_state},按'停止'处理"
|
||||
)
|
||||
return False
|
||||
|
||||
# 4. 仅robot_state=2表示正在运动
|
||||
return robot_state == 2
|
||||
|
||||
except Exception as e:
|
||||
print(f"is_moving: 获取运动状态失败:{str(e)}")
|
||||
return False
|
||||
|
||||
def update_point_status(self):
|
||||
"""更新点位状态:哪些已完成,当前移动的目标点位是哪个"""
|
||||
status = self.get_robot_status()
|
||||
if not status:
|
||||
return
|
||||
|
||||
current_pos = status["current_pos"]
|
||||
is_moving = status["is_moving"]
|
||||
threshold = 6.0 # 判定“经过该点”的距离阈值(mm),根据精度调整
|
||||
|
||||
# 遍历所有点位,判断是否已经过
|
||||
for point in self.point_sequence:
|
||||
idx = point["index"]
|
||||
# 跳过已标记为“已完成”的点
|
||||
if point["status"] == "completed":
|
||||
continue
|
||||
|
||||
# 计算当前位置与点位的三维距离
|
||||
dx = current_pos[0] - point["pos"][0]
|
||||
dy = current_pos[1] - point["pos"][1]
|
||||
dz = current_pos[2] - point["pos"][2]
|
||||
distance = (dx**2 + dy**2 + dz**2) ** 0.5
|
||||
|
||||
# 如果距离小于阈值,标记为“已完成”
|
||||
if distance < threshold:
|
||||
self.completed_indices.add(idx)
|
||||
point["status"] = "completed"
|
||||
max_completed = (
|
||||
max(self.completed_indices) if self.completed_indices else -1
|
||||
)
|
||||
self.current_target_index = max_completed + 1
|
||||
if 0 <= self.current_target_index < len(self.point_sequence):
|
||||
self.point_sequence[self.current_target_index]["status"] = "current"
|
||||
|
||||
# 停止移动时判断当前目标点
|
||||
if not is_moving and 0 <= self.current_target_index < len(self.point_sequence):
|
||||
target_point = self.point_sequence[self.current_target_index]
|
||||
if target_point["status"] != "completed":
|
||||
dx = current_pos[0] - target_point["pos"][0]
|
||||
dy = current_pos[1] - target_point["pos"][1]
|
||||
dz = current_pos[2] - target_point["pos"][2]
|
||||
distance = (dx**2 + dy**2 + dz**2) ** 0.5
|
||||
if distance < threshold:
|
||||
self.completed_indices.add(self.current_target_index)
|
||||
target_point["status"] = "completed"
|
||||
if self.current_target_index + 1 < len(self.point_sequence):
|
||||
# 将下一个点设置为目前正在朝向移动的点
|
||||
self.current_target_index += 1
|
||||
self.point_sequence[self.current_target_index][
|
||||
"status"
|
||||
] = "current"
|
||||
|
||||
def start_tracking(self):
|
||||
"""清空已经移动的点位的集合, 开启下一轮跟踪"""
|
||||
self.completed_indices.clear()
|
||||
|
||||
"""启动实时跟踪线程"""
|
||||
self.tracking_running = True
|
||||
self.tracking_thread = threading.Thread(target=self._tracking_loop, daemon=True)
|
||||
self.tracking_thread.start()
|
||||
|
||||
def _tracking_loop(self):
|
||||
"""跟踪循环:持续更新点位状态"""
|
||||
while self.tracking_running:
|
||||
self.update_point_status()
|
||||
time.sleep(0.2) # 5Hz更新频率
|
||||
|
||||
def stop_tracking(self):
|
||||
"""停止跟踪线程"""
|
||||
self.tracking_running = False
|
||||
if self.tracking_thread.is_alive():
|
||||
self.tracking_thread.join()
|
||||
|
||||
def get_completed_points(self):
|
||||
"""返回已运动过的点(索引和坐标)"""
|
||||
return [
|
||||
{"index": point["index"], "pos": point["pos"]}
|
||||
for point in self.point_sequence
|
||||
if point["status"] == "completed"
|
||||
]
|
||||
|
||||
def get_current_target_point(self):
|
||||
"""返回当前正在朝向移动的点(索引和坐标)"""
|
||||
if 0 <= self.current_target_index < len(self.point_sequence):
|
||||
point = self.point_sequence[self.current_target_index]
|
||||
return {"index": point["index"], "pos": point["pos"]}
|
||||
return None # 无当前朝向移动的目标点位(已完成所有点位的移动)
|
||||
52
app/service/vibrate.py
Normal file
52
app/service/vibrate.py
Normal file
@ -0,0 +1,52 @@
|
||||
import socket
|
||||
import json
|
||||
import time
|
||||
|
||||
set_vibrate_time = 5 # 震动时间 单位/秒
|
||||
|
||||
cmd_set_vibrate = { # 振动控制
|
||||
"command": "set_vibrate",
|
||||
"payload": {"time": set_vibrate_time}, # 单位S
|
||||
}
|
||||
|
||||
|
||||
# 使用 with 语句确保 socket 在使用完毕后正确关闭
|
||||
def start_vibrate(host="192.168.58.25", port=5000):
|
||||
try:
|
||||
with socket.socket() as s:
|
||||
s.connect((host, port))
|
||||
s.sendall(json.dumps(cmd_set_vibrate).encode())
|
||||
# print("已发送振动控制指令")
|
||||
|
||||
# 记录震动开始时间
|
||||
recv_start_time = time.time()
|
||||
|
||||
# 接收响应, 检查 震动是否完成
|
||||
data = s.recv(1024)
|
||||
if data:
|
||||
# 计算震动耗时(秒)
|
||||
recv_elapsed_time = time.time() - recv_start_time
|
||||
print("震动秒数:", recv_elapsed_time)
|
||||
# print("收到响应:", data.decode())
|
||||
response = json.loads(data.decode())
|
||||
if response.get("vibrate_isok", False):
|
||||
print("振动执行完成")
|
||||
return True # 震动完成
|
||||
else:
|
||||
print("start_vibrate错误: 振动执行失败")
|
||||
return False
|
||||
|
||||
else:
|
||||
print("start_vibrate错误: 服务器无响应,连接已关闭")
|
||||
return False
|
||||
|
||||
except (socket.error, json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
print(f"start_vibrate错误: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
if start_vibrate():
|
||||
print("震频完毕")
|
||||
else:
|
||||
print("震频发生错误")
|
||||
exit(-1)
|
||||
109
app/service/weight.py
Normal file
109
app/service/weight.py
Normal file
@ -0,0 +1,109 @@
|
||||
import socket
|
||||
import json
|
||||
import time
|
||||
|
||||
target_weight = 660 # 目标重量
|
||||
|
||||
# 称重指令
|
||||
cmd_set_target = {
|
||||
# 称量
|
||||
"command": "set_target",
|
||||
"payload": {
|
||||
"target_weight": target_weight,
|
||||
"algorithm": "pid",
|
||||
"direction_control": False,
|
||||
},
|
||||
}
|
||||
|
||||
# 清零
|
||||
cmd_set_zero = {
|
||||
# 去皮
|
||||
"command": "set_zero"
|
||||
}
|
||||
|
||||
cmd_get_weight = {
|
||||
# 获取重量
|
||||
"command": "get_weight"
|
||||
}
|
||||
|
||||
|
||||
def start_weight(host="192.168.58.25", port=5000):
|
||||
"""
|
||||
连接称重设备,发送目标重量并等待达到指定值
|
||||
|
||||
Args:
|
||||
target_weight: 目标重量值
|
||||
cmd_set_zero: 去皮
|
||||
cmd_set_target: 称重
|
||||
cmd_get_weight: 获取重量
|
||||
host: 服务器IP地址
|
||||
port: 服务器端口
|
||||
|
||||
Returns:
|
||||
成功时返回达到的目标重量, 失败时返回None
|
||||
"""
|
||||
try:
|
||||
with socket.socket() as s:
|
||||
s.connect((host, port))
|
||||
|
||||
# 去皮
|
||||
s.sendall(json.dumps(cmd_set_zero).encode())
|
||||
time.sleep(1) # 等待去皮完成
|
||||
|
||||
# 检查去皮是否完成
|
||||
s.sendall(json.dumps(cmd_get_weight).encode())
|
||||
weight_data = s.recv(1024)
|
||||
response = json.loads(weight_data.decode())
|
||||
zero_weight = response.get("current_weight")
|
||||
print("清零后的重量:", zero_weight)
|
||||
except (socket.error, json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||
print(f"start_weight错误: {e}")
|
||||
return None
|
||||
|
||||
try:
|
||||
with socket.socket() as s:
|
||||
if isinstance(zero_weight, (int, float)) and abs(zero_weight) < 0.01:
|
||||
s.connect((host, port))
|
||||
# 称重
|
||||
s.sendall(json.dumps(cmd_set_target).encode())
|
||||
|
||||
while True:
|
||||
data = s.recv(1024)
|
||||
if not data:
|
||||
print("start_weight错误: 服务器连接已关闭")
|
||||
return None
|
||||
|
||||
try:
|
||||
decoded = data.decode()
|
||||
response = json.loads(decoded)
|
||||
current_weight = response.get("current_weight")
|
||||
if current_weight is not None:
|
||||
print(f"当前重量:{current_weight}")
|
||||
if current_weight >= target_weight:
|
||||
return current_weight # 达到目标重量后返回
|
||||
else:
|
||||
print(f"获取的重量为None: {current_weight}")
|
||||
return None
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"start_weight错误: 无效的JSON格式: {decoded}")
|
||||
return None
|
||||
except UnicodeDecodeError:
|
||||
print(
|
||||
f"start_weight错误: 字节解码错误, 无法解析字节流data为字符串"
|
||||
)
|
||||
return None
|
||||
else:
|
||||
print("start_weight错误: 去皮失败, 中止称量流程, 请检查设备")
|
||||
return None
|
||||
except socket.error as e:
|
||||
print(f"start_weight错误: 通信错误: {e}")
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if start_weight():
|
||||
print("称重完毕")
|
||||
else:
|
||||
print("称重发生错误")
|
||||
exit(-1)
|
||||
File diff suppressed because it is too large
Load Diff
382
app/view/mi_an/status_edit_dialog.py
Normal file
382
app/view/mi_an/status_edit_dialog.py
Normal file
@ -0,0 +1,382 @@
|
||||
from PySide6.QtCore import Qt, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QFormLayout,
|
||||
QGroupBox,
|
||||
QDialog,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
PushButton,
|
||||
ComboBox,
|
||||
DoubleSpinBox,
|
||||
SpinBox,
|
||||
setTheme,
|
||||
Theme,
|
||||
StrongBodyLabel,
|
||||
EditableComboBox,
|
||||
RadioButton,
|
||||
isDarkTheme,
|
||||
MessageBox,
|
||||
)
|
||||
from ...model.point_state import PointState
|
||||
|
||||
|
||||
class StatusEditDialog(QDialog):
|
||||
point_state_applied = Signal(int, PointState)
|
||||
|
||||
def __init__(self, pos_name, selected_row_idx, parent=None):
|
||||
super().__init__(parent)
|
||||
# 窗口基本设置
|
||||
self.setWindowTitle("状态编辑") # 设置窗口标题
|
||||
self.resize(600, 660) # 窗口大小
|
||||
self.setMinimumSize(550, 560) # 最小尺寸限制
|
||||
|
||||
# 点位名称
|
||||
self.pos_name = pos_name
|
||||
|
||||
# 选中行的行索引
|
||||
self.selected_row_idx = selected_row_idx
|
||||
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
# 创建控件
|
||||
self.__createWidget()
|
||||
|
||||
# 设置样式
|
||||
self.__initStyles()
|
||||
|
||||
# 设置布局
|
||||
self.__initLayout()
|
||||
|
||||
# 绑定
|
||||
self.__bind()
|
||||
|
||||
# 创建相关控件
|
||||
def __createWidget(self):
|
||||
# 1. 点位名称输入
|
||||
self.name_combo = EditableComboBox()
|
||||
|
||||
self.name_combo.addItems(
|
||||
["抓取点", "破袋点", "震动点", "扔袋点", "相机/待抓点"]
|
||||
)
|
||||
|
||||
# 检查点位名称在下拉框是否已经存在
|
||||
target_pos_name = self.pos_name
|
||||
pos_name_index = self.name_combo.findText(target_pos_name)
|
||||
# 若未找到(索引=-1),则添加 表单中 的点位名字
|
||||
if pos_name_index == -1:
|
||||
self.name_combo.addItem(self.pos_name)
|
||||
# 选中新添加的这项
|
||||
new_index = self.name_combo.count() - 1
|
||||
self.name_combo.setCurrentIndex(new_index)
|
||||
else:
|
||||
# 已经存在的话就直接选中
|
||||
self.name_combo.setCurrentIndex(pos_name_index)
|
||||
|
||||
self.name_combo.setPlaceholderText("请设置点位名称")
|
||||
|
||||
# 2. 工具坐标系id
|
||||
self.tool_coord_spin = SpinBox()
|
||||
self.tool_coord_spin.setRange(0, 99) # 0-99范围
|
||||
self.tool_coord_spin.setValue(0) # 默认值
|
||||
self.tool_coord_btn = PushButton("获取当前工具坐标id")
|
||||
|
||||
# 3. 工件坐标系id
|
||||
self.work_coord_spin = SpinBox()
|
||||
self.work_coord_spin.setRange(0, 99) # 0-99范围
|
||||
self.work_coord_spin.setValue(0) # 默认值
|
||||
self.work_coord_btn = PushButton("获取当前工件坐标id")
|
||||
|
||||
# 4-9. 关节坐标 J1 到 J6
|
||||
self.j_spins = []
|
||||
for _ in range(6):
|
||||
spin = DoubleSpinBox()
|
||||
spin.setRange(-180, 180) # 角度范围 (-180度到180度)
|
||||
spin.setDecimals(3) # 保留3位小数
|
||||
spin.setSingleStep(0.001) # 默认步长
|
||||
self.j_spins.append(spin)
|
||||
|
||||
# 关节坐标默认值 (默认为无效值)
|
||||
self.j_spins[0].setValue(-9999)
|
||||
self.j_spins[1].setValue(-9999)
|
||||
self.j_spins[2].setValue(-9999)
|
||||
self.j_spins[3].setValue(-9999)
|
||||
self.j_spins[4].setValue(-9999)
|
||||
self.j_spins[5].setValue(-9999)
|
||||
|
||||
# 关节坐标设置 右侧的步长设置 和 获取关节坐标按钮
|
||||
self.step_group = QGroupBox("单击步长设置")
|
||||
self.step_input = DoubleSpinBox()
|
||||
self.step_input.setRange(0.001, 180.0) # 步长范围
|
||||
self.step_input.setDecimals(3) # 保留3位小数
|
||||
self.step_input.setValue(0.001) # 默认步长
|
||||
self.get_values_btn = PushButton("获取当前J1-J6值")
|
||||
|
||||
# 10. 速度 (移动速度)
|
||||
self.approach_speed_spin = DoubleSpinBox()
|
||||
self.approach_speed_spin.setRange(0, 100)
|
||||
self.approach_speed_spin.setDecimals(0) # 小数点
|
||||
self.approach_speed_spin.setValue(20)
|
||||
self.approach_speed_spin.setSingleStep(10)
|
||||
|
||||
# 11. 运动类型(下拉选择)
|
||||
self.motion_type_combo = ComboBox()
|
||||
self.motion_type_combo.addItems(["直线", "曲线中间点", "曲线终点", "自由路径"])
|
||||
|
||||
# 12. 平滑选择
|
||||
self.stop_radio = RadioButton("停止")
|
||||
self.smooth_radio = RadioButton("平滑过渡")
|
||||
self.smooth_ms_spin = DoubleSpinBox() # 平滑过渡的时间(毫秒)
|
||||
self.smooth_ms_spin.setRange(0, 500) # 范围:0 - 500 ms
|
||||
self.smooth_ms_spin.setDecimals(0) # 整数毫秒
|
||||
self.smooth_ms_spin.setValue(0) # 默认值0
|
||||
self.smooth_ms_spin.setSingleStep(10) # 步长:10毫秒
|
||||
self.smooth_ms_spin.setEnabled(False) # 初始禁用(仅“平滑过渡”选中时启用)
|
||||
self.stop_radio.setChecked(True) # 默认选“停止”
|
||||
|
||||
# 13. 应用按钮
|
||||
self.apply_btn = PushButton("应用")
|
||||
self.apply_btn.setMinimumWidth(160) # 按钮最小宽度
|
||||
|
||||
def __initStyles(self):
|
||||
# 根据主题设置样式表
|
||||
if isDarkTheme(): # 深色主题
|
||||
self.step_group.setStyleSheet(
|
||||
"""
|
||||
QGroupBox {
|
||||
color: white; /* 标题文字颜色 */
|
||||
border: 1px solid white; /* 边框线条颜色和宽度 */
|
||||
border-radius: 6px; /* 边框圆角 */
|
||||
margin-top: 10px; /* 标题与边框的距离 */
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left; /* 标题位置 */
|
||||
left: 10px; /* 标题左边距 */
|
||||
padding: 0 3px 0 3px; /* 标题内边距 */
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.setStyleSheet("background-color: rgb(32, 32, 32);")
|
||||
else: # 浅色主题
|
||||
self.step_group.setStyleSheet(
|
||||
"""
|
||||
QGroupBox {
|
||||
color: black; /* 标题文字颜色 */
|
||||
border: 1px solid black; /* 边框线条颜色和宽度 */
|
||||
border-radius: 6px; /* 边框圆角 */
|
||||
margin-top: 10px; /* 标题与边框的距离 */
|
||||
}
|
||||
QGroupBox::title {
|
||||
subcontrol-origin: margin;
|
||||
subcontrol-position: top left; /* 标题位置 */
|
||||
left: 10px; /* 标题左边距 */
|
||||
padding: 0 3px 0 3px; /* 标题内边距 */
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.setStyleSheet("background-color: rgb(243, 243, 243);")
|
||||
|
||||
def __initLayout(self):
|
||||
# 主布局(直接应用于当前Widget)
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(24, 24, 24, 24) # 边距
|
||||
main_layout.setSpacing(16) # 控件间距
|
||||
|
||||
# 表单布局(管理标签和输入框)
|
||||
form_layout = QFormLayout()
|
||||
form_layout.setRowWrapPolicy(QFormLayout.DontWrapRows) # 不自动换行
|
||||
# 标签右对齐+垂直居中
|
||||
form_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
form_layout.setSpacing(12) # 表单行间距
|
||||
|
||||
# 1. 添加点位名称布局
|
||||
form_layout.addRow(StrongBodyLabel("点位名称"), self.name_combo)
|
||||
|
||||
# 2. 添加工具坐标布局
|
||||
tool_coord_layout = QHBoxLayout()
|
||||
tool_coord_layout.addWidget(self.tool_coord_spin)
|
||||
tool_coord_layout.addWidget(self.tool_coord_btn)
|
||||
form_layout.addRow(StrongBodyLabel("工具坐标id"), tool_coord_layout)
|
||||
|
||||
# 3. 添加工件坐标布局
|
||||
work_coord_layout = QHBoxLayout() # 工件坐标水平布局
|
||||
work_coord_layout.addWidget(self.work_coord_spin)
|
||||
work_coord_layout.addWidget(self.work_coord_btn)
|
||||
form_layout.addRow(StrongBodyLabel("工件坐标id"), work_coord_layout)
|
||||
|
||||
# 4-9 关节坐标布局
|
||||
joint_control_layout = QHBoxLayout()
|
||||
|
||||
# 左侧:关节角输入(J1-J6)
|
||||
joint_input_layout = QFormLayout()
|
||||
joint_input_layout.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
||||
joint_input_layout.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
joint_input_layout.setSpacing(12)
|
||||
|
||||
for index in range(6):
|
||||
joint_input_layout.addRow(
|
||||
StrongBodyLabel(f"J{index + 1} (°)"), self.j_spins[index]
|
||||
)
|
||||
|
||||
# 将关节坐标输入布局 添加到 关节坐标布局
|
||||
joint_control_layout.addLayout(joint_input_layout)
|
||||
|
||||
# 右侧:步长设置和获取按钮
|
||||
control_panel_layout = QVBoxLayout()
|
||||
control_panel_layout.setSpacing(16)
|
||||
step_layout = QVBoxLayout(self.step_group)
|
||||
step_layout.setContentsMargins(10, 15, 10, 15)
|
||||
step_layout.setSpacing(10)
|
||||
step_layout.addWidget(self.step_input)
|
||||
|
||||
# step_layout添加到控制面板布局
|
||||
control_panel_layout.addWidget(self.step_group)
|
||||
control_panel_layout.addWidget(self.get_values_btn)
|
||||
control_panel_layout.addStretch() # 拉伸项,使内容靠上
|
||||
|
||||
# 将 控制面板布局(右侧) 添加到 关节控制布局
|
||||
joint_control_layout.addLayout(control_panel_layout)
|
||||
|
||||
# 将关节控制水平布局添加到表单布局
|
||||
form_layout.addRow(StrongBodyLabel("关节坐标"), joint_control_layout)
|
||||
|
||||
# 10. 速度布局
|
||||
form_layout.addRow(StrongBodyLabel("速度 (%)"), self.approach_speed_spin)
|
||||
|
||||
# 11. 运动类型(下拉选择)布局
|
||||
form_layout.addRow(StrongBodyLabel("运动类型"), self.motion_type_combo)
|
||||
|
||||
# 12. "在此点" 平滑选择布局
|
||||
stop_layout = QHBoxLayout()
|
||||
stop_layout.addWidget(self.stop_radio)
|
||||
stop_layout.addWidget(self.smooth_radio)
|
||||
stop_layout.addWidget(self.smooth_ms_spin)
|
||||
stop_layout.addWidget(StrongBodyLabel("ms"))
|
||||
stop_layout.setAlignment(Qt.AlignLeft) # 与标签左对齐
|
||||
form_layout.addRow(StrongBodyLabel("在此点"), stop_layout)
|
||||
|
||||
# 将表单布局添加到主布局
|
||||
main_layout.addLayout(form_layout)
|
||||
|
||||
# 13. 底部按钮布局(居中显示)
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setAlignment(Qt.AlignHCenter) # 水平居中
|
||||
btn_layout.addWidget(self.apply_btn)
|
||||
|
||||
# 将底部按钮布局添加到主布局
|
||||
main_layout.addLayout(btn_layout)
|
||||
|
||||
# 让表单控件顶部对齐
|
||||
main_layout.addStretch(1)
|
||||
|
||||
def __bind(self):
|
||||
# 更新 J1 到 J6 的步长
|
||||
self.step_input.valueChanged.connect(self.onUpdateStepSize)
|
||||
|
||||
# 获取 J1 到 J6 的值(外部相关)
|
||||
self.get_values_btn.clicked.connect(self.onGetJointValues)
|
||||
|
||||
# 调整平滑时间设置控件可不可用
|
||||
self.stop_radio.toggled.connect(
|
||||
lambda checked: self.smooth_ms_spin.setEnabled(not checked)
|
||||
)
|
||||
self.smooth_radio.toggled.connect(
|
||||
lambda checked: self.smooth_ms_spin.setEnabled(checked)
|
||||
)
|
||||
|
||||
# 应用按钮点击 (外部相关)
|
||||
self.apply_btn.clicked.connect(self.onApplyBtnClicked)
|
||||
|
||||
# 设置状态编辑框中的 点位的状态
|
||||
def setPointStateValue(self, pos_state_dict: dict):
|
||||
# 设置除了点位名字之外的所有 点位状态的值
|
||||
self.approach_speed_spin.setValue(pos_state_dict["speed"])
|
||||
self.tool_coord_spin.setValue(pos_state_dict["tool_id"])
|
||||
self.work_coord_spin.setValue(pos_state_dict["work_id"])
|
||||
for index in range(6):
|
||||
self.j_spins[index].setValue(pos_state_dict["joint_values"][index])
|
||||
|
||||
# 运动状态设置
|
||||
# 查找目标文本对应的索引
|
||||
target_motion_type = pos_state_dict["motion_type"]
|
||||
# 1. 查找目标文本对应的索引
|
||||
motion_index = self.motion_type_combo.findText(target_motion_type)
|
||||
# 2. 若未找到(索引=-1),默认选中第0项;否则选中对应索引
|
||||
if motion_index == -1:
|
||||
self.motion_type_combo.setCurrentIndex(0)
|
||||
else:
|
||||
self.motion_type_combo.setCurrentIndex(motion_index)
|
||||
|
||||
if pos_state_dict["blend_time"] == -1: # 此时为 停止
|
||||
self.stop_radio.setChecked(True)
|
||||
else:
|
||||
self.smooth_radio.setChecked(True)
|
||||
self.smooth_ms_spin.setValue(pos_state_dict["blend_time"])
|
||||
|
||||
def onUpdateStepSize(self, value):
|
||||
"""更新所有关节角输入框的步长"""
|
||||
for spin in self.j_spins:
|
||||
spin.setSingleStep(value)
|
||||
|
||||
def onGetJointValues(self):
|
||||
"""获取J1-J6的值(这里用示例值模拟)"""
|
||||
# 实际应用中,这里应该从设备或其他数据源获取值
|
||||
# 这里用随机值模拟
|
||||
import random
|
||||
|
||||
for i in range(6):
|
||||
# 生成一个-180到180之间的随机数,保留3位小数
|
||||
value = round(random.uniform(-180, 180), 3)
|
||||
self.j_spins[i].setValue(value)
|
||||
print("已获取并更新J1-J6的值")
|
||||
|
||||
def onApplyBtnClicked(self):
|
||||
"""应用按钮点击事件处理"""
|
||||
# 1、获取点名称
|
||||
pos_name = self.name_combo.text()
|
||||
# 2、速度
|
||||
speed = self.approach_speed_spin.value()
|
||||
# 3、tool_id
|
||||
tool_id = self.tool_coord_spin.value()
|
||||
# 4、work_id
|
||||
work_id = self.work_coord_spin.value()
|
||||
# 5-10、所有关节坐标 J1 到 J6
|
||||
joint_values = [spin.value() for spin in self.j_spins]
|
||||
# 11、运动类型 (直线、 曲线中间点、 曲线终点、 自由路径)
|
||||
motion_type = self.motion_type_combo.currentText()
|
||||
# 12、平滑时间(停止=-1,否则取输入值)
|
||||
blend_time = -1 if self.stop_radio.isChecked() else self.smooth_ms_spin.value()
|
||||
|
||||
try:
|
||||
point_state = PointState(
|
||||
pos_name=pos_name,
|
||||
speed=speed,
|
||||
tool_id=tool_id,
|
||||
work_id=work_id,
|
||||
joint_values=joint_values,
|
||||
motion_type=motion_type,
|
||||
blend_time=blend_time,
|
||||
)
|
||||
# print("状态编辑结果:", point_state.__str__())
|
||||
|
||||
# 发送信号给 表单窗口 CoordinateTableWidget对象
|
||||
self.point_state_applied.emit(self.selected_row_idx, point_state)
|
||||
|
||||
# 关闭状态编辑窗口
|
||||
self.close()
|
||||
except ValueError as e:
|
||||
# 捕获校验错误,弹窗提示用户
|
||||
MessageBox("状态错误", str(e), self).exec()
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app = QApplication([])
|
||||
# setTheme(Theme.DARK) # 设置浅色主题(可选:Theme.DARK)
|
||||
# widget = StatusEditWidget()
|
||||
# widget.show() # 显示窗口
|
||||
# app.exec()
|
||||
233
app/view/mi_an_main_window.py
Normal file
233
app/view/mi_an_main_window.py
Normal file
@ -0,0 +1,233 @@
|
||||
# coding:utf-8
|
||||
import sys
|
||||
import os
|
||||
from PySide6.QtCore import Qt, QRect, QUrl, Signal
|
||||
from PySide6.QtGui import (
|
||||
QIcon,
|
||||
QPainter,
|
||||
QImage,
|
||||
QBrush,
|
||||
QColor,
|
||||
QFont,
|
||||
QDesktopServices,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QFrame,
|
||||
QStackedWidget,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from qfluentwidgets import (
|
||||
NavigationInterface,
|
||||
NavigationItemPosition,
|
||||
NavigationWidget,
|
||||
MessageBox,
|
||||
isDarkTheme,
|
||||
setTheme,
|
||||
Theme,
|
||||
setThemeColor,
|
||||
qrouter,
|
||||
FluentWindow,
|
||||
NavigationAvatarWidget,
|
||||
)
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
from qframelesswindow import FramelessWindow, StandardTitleBar
|
||||
|
||||
from app.view.system_interface import SystemInterface
|
||||
from app.view.produce_interface import ProduceInterface
|
||||
from app.view.text_interface import TextInterface
|
||||
from app.view.data_interface import DataInterface
|
||||
|
||||
from app.view.cood_forms_interface import CoodFormsInterface
|
||||
|
||||
|
||||
class Widget(QFrame):
|
||||
|
||||
def __init__(self, text: str, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.label = QLabel(text, self)
|
||||
self.label.setAlignment(Qt.AlignCenter)
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.hBoxLayout.addWidget(self.label, 1, Qt.AlignCenter)
|
||||
self.setObjectName(text.replace(" ", "-"))
|
||||
|
||||
|
||||
class Window(FramelessWindow):
|
||||
## 定义信号:调整高度
|
||||
heightChanged = Signal(int)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setTitleBar(StandardTitleBar(self))
|
||||
|
||||
# use dark theme mode
|
||||
setTheme(Theme.DARK)
|
||||
|
||||
# change the theme color
|
||||
# setThemeColor('#0078d4')
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.navigationInterface = NavigationInterface(self, showMenuButton=True)
|
||||
self.stackWidget = QStackedWidget(self)
|
||||
|
||||
# create sub interface
|
||||
self.system = SystemInterface(self) # 系统设置
|
||||
self.product = ProduceInterface(self)
|
||||
self.robot = Widget("机械臂基础设置", self) # 暂时不用
|
||||
self.io = Widget("IO面板", self) # 需要完成
|
||||
self.position = CoodFormsInterface(self) # 位置设定
|
||||
self.basic = Widget("基础设置", self) # 需要完成
|
||||
self.point = Widget("点位调试", self)
|
||||
self.other = Widget("其他设置", self)
|
||||
self.data = DataInterface(self) # 数据采集
|
||||
|
||||
# initialize layout
|
||||
self.initLayout()
|
||||
|
||||
# add items to navigation interface
|
||||
self.initNavigation()
|
||||
|
||||
self.initWindow()
|
||||
|
||||
def initLayout(self):
|
||||
self.hBoxLayout.setSpacing(0)
|
||||
self.hBoxLayout.setContentsMargins(0, self.titleBar.height(), 0, 0)
|
||||
self.hBoxLayout.addWidget(self.navigationInterface)
|
||||
self.hBoxLayout.addWidget(self.stackWidget)
|
||||
self.hBoxLayout.setStretchFactor(self.stackWidget, 1)
|
||||
|
||||
def initNavigation(self):
|
||||
# enable acrylic effect
|
||||
# self.navigationInterface.setAcrylicEnabled(True)
|
||||
|
||||
# self.addSubInterface 加入导航栏页面
|
||||
self.addSubInterface(
|
||||
self.system, FIF.SETTING, "系统设置", NavigationItemPosition.SCROLL
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.product, FIF.COMPLETED, "生产界面", parent=self.system
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.robot, FIF.ROBOT, "机械臂基础设置", parent=self.system
|
||||
)
|
||||
self.addSubInterface(self.io, FIF.GAME, "IO面板", parent=self.system)
|
||||
self.addSubInterface(self.position, FIF.IOT, "位置设定", parent=self.system)
|
||||
self.addSubInterface(
|
||||
self.basic, FIF.DEVELOPER_TOOLS, "基础设置", parent=self.system
|
||||
)
|
||||
self.addSubInterface(self.point, FIF.MOVE, "点位调试", parent=self.system)
|
||||
|
||||
# self.navigationInterface.addSeparator()
|
||||
self.addSubInterface(
|
||||
self.other, FIF.APPLICATION, "其他设置", NavigationItemPosition.SCROLL
|
||||
)
|
||||
|
||||
self.addSubInterface(
|
||||
self.data, FIF.PHOTO, "数据采集", NavigationItemPosition.SCROLL
|
||||
)
|
||||
# add navigation items to scroll area
|
||||
# for i in range(1, 21):
|
||||
# self.navigationInterface.addItem(
|
||||
# f'folder{i}',
|
||||
# FIF.FOLDER,
|
||||
# f'Folder {i}',
|
||||
# lambda: print('Folder clicked'),
|
||||
# position=NavigationItemPosition.SCROLL
|
||||
# )
|
||||
|
||||
# add custom widget to bottom
|
||||
self.navigationInterface.addWidget(
|
||||
routeKey="avatar",
|
||||
widget=NavigationAvatarWidget("用户设置", "resource/shoko.png"),
|
||||
onClick=self.showMessageBox,
|
||||
position=NavigationItemPosition.BOTTOM,
|
||||
)
|
||||
|
||||
#!IMPORTANT: don't forget to set the default route key if you enable the return button
|
||||
# qrouter.setDefaultRouteKey(self.stackWidget, self.musicInterface.objectName())
|
||||
|
||||
# set the maximum width
|
||||
self.navigationInterface.setExpandWidth(220)
|
||||
|
||||
self.stackWidget.currentChanged.connect(self.onCurrentInterfaceChanged)
|
||||
self.stackWidget.setCurrentIndex(1)
|
||||
|
||||
# always expand
|
||||
self.navigationInterface.setCollapsible(False)
|
||||
|
||||
def initWindow(self):
|
||||
self.resize(900, 700)
|
||||
self.setWindowIcon(QIcon("resource/logo.png"))
|
||||
self.setWindowTitle("密胺投料控制系统")
|
||||
self.titleBar.setAttribute(Qt.WA_StyledBackground)
|
||||
|
||||
desktop = QApplication.screens()[0].availableGeometry()
|
||||
w, h = desktop.width(), desktop.height()
|
||||
self.move(w // 2 - self.width() // 2, h // 2 - self.height() // 2)
|
||||
|
||||
# NOTE: set the minimum window width that allows the navigation panel to be expanded
|
||||
self.navigationInterface.setMinimumExpandWidth(900)
|
||||
self.navigationInterface.expand(useAni=True)
|
||||
|
||||
self.setQss()
|
||||
|
||||
def addSubInterface(
|
||||
self,
|
||||
interface,
|
||||
icon,
|
||||
text: str,
|
||||
position=NavigationItemPosition.TOP,
|
||||
parent=None,
|
||||
):
|
||||
"""add sub interface"""
|
||||
self.stackWidget.addWidget(interface)
|
||||
self.navigationInterface.addItem(
|
||||
routeKey=interface.objectName(),
|
||||
icon=icon,
|
||||
text=text,
|
||||
onClick=lambda: self.switchTo(interface),
|
||||
position=position,
|
||||
tooltip=text,
|
||||
parentRouteKey=parent.objectName() if parent else None,
|
||||
)
|
||||
|
||||
def setQss(self):
|
||||
color = "dark" if isDarkTheme() else "light"
|
||||
with open(f"resource/{color}/demo.qss", encoding="utf-8") as f:
|
||||
self.setStyleSheet(f.read())
|
||||
|
||||
def switchTo(self, widget):
|
||||
self.stackWidget.setCurrentWidget(widget)
|
||||
|
||||
def onCurrentInterfaceChanged(self, index):
|
||||
widget = self.stackWidget.widget(index)
|
||||
self.navigationInterface.setCurrentItem(widget.objectName())
|
||||
|
||||
#!IMPORTANT: This line of code needs to be uncommented if the return button is enabled
|
||||
# qrouter.push(self.stackWidget, widget.objectName())
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self.heightChanged.emit(self.height())
|
||||
|
||||
def showMessageBox(self):
|
||||
w = MessageBox(
|
||||
"支持作者🥰",
|
||||
"个人开发不易,如果这个项目帮助到了您,可以考虑请作者喝一瓶快乐水🥤。您的支持就是作者开发和维护项目的动力🚀",
|
||||
self,
|
||||
)
|
||||
w.yesButton.setText("来啦老弟")
|
||||
w.cancelButton.setText("下次一定")
|
||||
|
||||
if w.exec():
|
||||
QDesktopServices.openUrl(QUrl("https://afdian.net/a/zhiyiYo"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
w = Window()
|
||||
w.show()
|
||||
app.exec()
|
||||
@ -1,48 +1,77 @@
|
||||
# coding:utf-8
|
||||
from PySide6.QtCore import Qt, QEasingCurve, QTimer
|
||||
from PySide6.QtGui import QColor, QImage, QPixmap
|
||||
from PySide6.QtWidgets import QWidget, QStackedWidget, QVBoxLayout, QLabel, QHBoxLayout, QFrame, QSizePolicy
|
||||
from qfluentwidgets import (Pivot, qrouter, SegmentedWidget, TabBar, CheckBox, ComboBox,
|
||||
TabCloseButtonDisplayMode, BodyLabel, SpinBox, BreadcrumbBar,
|
||||
SegmentedToggleToolWidget, FluentIcon, TransparentPushButton, EditableComboBox, PrimaryPushButton, Slider, DisplayLabel, TextBrowser, SwitchButton, PillPushButton, ToggleButton)
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QStackedWidget,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QFrame,
|
||||
QSizePolicy,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
Pivot,
|
||||
qrouter,
|
||||
SegmentedWidget,
|
||||
TabBar,
|
||||
CheckBox,
|
||||
ComboBox,
|
||||
TabCloseButtonDisplayMode,
|
||||
BodyLabel,
|
||||
SpinBox,
|
||||
BreadcrumbBar,
|
||||
SegmentedToggleToolWidget,
|
||||
FluentIcon,
|
||||
TransparentPushButton,
|
||||
EditableComboBox,
|
||||
PrimaryPushButton,
|
||||
Slider,
|
||||
DisplayLabel,
|
||||
TextBrowser,
|
||||
SwitchButton,
|
||||
PillPushButton,
|
||||
ToggleButton,
|
||||
)
|
||||
|
||||
from .gallery_interface import GalleryInterface
|
||||
from ..common.translator import Translator
|
||||
from ..common.style_sheet import StyleSheet
|
||||
import cv2
|
||||
|
||||
|
||||
class SystemInterface(GalleryInterface):
|
||||
""" Navigation view interface """
|
||||
"""Navigation view interface"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
t = Translator()
|
||||
super().__init__(
|
||||
title=t.navigation,
|
||||
subtitle="qfluentwidgets.components.navigation",
|
||||
parent=parent
|
||||
parent=parent,
|
||||
)
|
||||
self.setObjectName('systemInterface')
|
||||
self.setObjectName("systemInterface")
|
||||
|
||||
# breadcrumb bar
|
||||
|
||||
card = self.addExampleCard(
|
||||
title=self.tr(''),
|
||||
title=self.tr(""),
|
||||
widget=TabInterface(self),
|
||||
sourcePath='https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/PySide6/examples/navigation/tab_view/demo.py',
|
||||
stretch=1
|
||||
sourcePath="https://github.com/zhiyiYo/PyQt-Fluent-Widgets/blob/PySide6/examples/navigation/tab_view/demo.py",
|
||||
stretch=1,
|
||||
)
|
||||
card.topLayout.setContentsMargins(12, 0, 0, 0)
|
||||
|
||||
def createToggleToolWidget(self):
|
||||
w = SegmentedToggleToolWidget(self)
|
||||
w.addItem('k1', FluentIcon.TRANSPARENT)
|
||||
w.addItem('k2', FluentIcon.CHECKBOX)
|
||||
w.addItem('k3', FluentIcon.CONSTRACT)
|
||||
w.setCurrentItem('k1')
|
||||
w.addItem("k1", FluentIcon.TRANSPARENT)
|
||||
w.addItem("k2", FluentIcon.CHECKBOX)
|
||||
w.addItem("k3", FluentIcon.CONSTRACT)
|
||||
w.setCurrentItem("k1")
|
||||
return w
|
||||
|
||||
|
||||
class TabInterface(QWidget):
|
||||
""" Tab interface """
|
||||
"""Tab interface"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
@ -53,12 +82,14 @@ class TabInterface(QWidget):
|
||||
self.tabView = QWidget(self)
|
||||
self.controlPanel = QFrame(self)
|
||||
|
||||
#clock
|
||||
self.clock=TransparentPushButton("2025-08-04 16:55", self, FluentIcon.DATE_TIME)
|
||||
# clock
|
||||
self.clock = TransparentPushButton(
|
||||
"2025-08-04 16:55", self, FluentIcon.DATE_TIME
|
||||
)
|
||||
|
||||
# combo box
|
||||
self.comboBox1 = ComboBox()
|
||||
self.comboBox1.addItems(['反应釜1', '反应釜2'])
|
||||
self.comboBox1.addItems(["反应釜1", "反应釜2"])
|
||||
self.comboBox1.setCurrentIndex(0)
|
||||
self.comboBox1.setMinimumWidth(180)
|
||||
|
||||
@ -67,7 +98,8 @@ class TabInterface(QWidget):
|
||||
|
||||
# editable combo box
|
||||
self.comboBox = EditableComboBox()
|
||||
self.comboBox.addItems([
|
||||
self.comboBox.addItems(
|
||||
[
|
||||
"10",
|
||||
"20",
|
||||
"30",
|
||||
@ -76,50 +108,51 @@ class TabInterface(QWidget):
|
||||
"60",
|
||||
"70",
|
||||
"80",
|
||||
])
|
||||
]
|
||||
)
|
||||
self.comboBox.setPlaceholderText("自定义数量")
|
||||
self.comboBox.setMinimumWidth(90)
|
||||
|
||||
#hBoxLayout
|
||||
# hBoxLayout
|
||||
self.container1 = QWidget()
|
||||
self.hBoxLayout1 = QHBoxLayout(self.container1)
|
||||
self.hBoxLayout1.addWidget(self.label)
|
||||
self.hBoxLayout1.addWidget(self.comboBox)
|
||||
|
||||
#button
|
||||
self.primaryButton1 = PrimaryPushButton(FluentIcon.PLAY, '启动', self)
|
||||
self.primaryButton2 = PrimaryPushButton(FluentIcon.PAUSE, '暂停', self)
|
||||
self.primaryButton3 = PrimaryPushButton(FluentIcon.POWER_BUTTON, '停止', self)
|
||||
self.primaryButton4 = PrimaryPushButton(FluentIcon.ROTATE, '复位', self)
|
||||
self.primaryButton5 = PrimaryPushButton(FluentIcon.CLOSE, '急停', self)
|
||||
self.primaryButton6 = PrimaryPushButton(FluentIcon.SYNC, '清除', self)
|
||||
# button
|
||||
self.primaryButton1 = PrimaryPushButton(FluentIcon.PLAY, "启动", self)
|
||||
self.primaryButton2 = PrimaryPushButton(FluentIcon.PAUSE, "暂停", self)
|
||||
self.primaryButton3 = PrimaryPushButton(FluentIcon.POWER_BUTTON, "停止", self)
|
||||
self.primaryButton4 = PrimaryPushButton(FluentIcon.ROTATE, "复位", self)
|
||||
self.primaryButton5 = PrimaryPushButton(FluentIcon.CLOSE, "急停", self)
|
||||
self.primaryButton6 = PrimaryPushButton(FluentIcon.SYNC, "清除", self)
|
||||
|
||||
self.primaryButton1.setObjectName('primaryButton1')
|
||||
self.primaryButton2.setObjectName('primaryButton2')
|
||||
self.primaryButton3.setObjectName('primaryButton3')
|
||||
self.primaryButton4.setObjectName('primaryButton4')
|
||||
self.primaryButton5.setObjectName('primaryButton5')
|
||||
self.primaryButton6.setObjectName('primaryButton6')
|
||||
self.primaryButton1.setObjectName("primaryButton1")
|
||||
self.primaryButton2.setObjectName("primaryButton2")
|
||||
self.primaryButton3.setObjectName("primaryButton3")
|
||||
self.primaryButton4.setObjectName("primaryButton4")
|
||||
self.primaryButton5.setObjectName("primaryButton5")
|
||||
self.primaryButton6.setObjectName("primaryButton6")
|
||||
|
||||
#hBoxLayout2
|
||||
# hBoxLayout2
|
||||
self.container2 = QWidget()
|
||||
self.hBoxLayout2 = QHBoxLayout(self.container2)
|
||||
self.hBoxLayout2.addWidget(self.primaryButton1)
|
||||
self.hBoxLayout2.addWidget(self.primaryButton2)
|
||||
|
||||
#hBoxLayout3
|
||||
# hBoxLayout3
|
||||
self.container3 = QWidget()
|
||||
self.hBoxLayout3 = QHBoxLayout(self.container3)
|
||||
self.hBoxLayout3.addWidget(self.primaryButton3)
|
||||
self.hBoxLayout3.addWidget(self.primaryButton4)
|
||||
|
||||
#hBoxLayout4
|
||||
# hBoxLayout4
|
||||
self.container4 = QWidget()
|
||||
self.hBoxLayout4 = QHBoxLayout(self.container4)
|
||||
self.hBoxLayout4.addWidget(self.primaryButton5)
|
||||
self.hBoxLayout4.addWidget(self.primaryButton6)
|
||||
|
||||
#滑动条
|
||||
# 滑动条
|
||||
self.slider = Slider(Qt.Horizontal)
|
||||
self.slider.setFixedWidth(200)
|
||||
|
||||
@ -130,62 +163,66 @@ class TabInterface(QWidget):
|
||||
# Displaylabel
|
||||
self.label2 = DisplayLabel("目标袋数")
|
||||
self.label3 = DisplayLabel("0")
|
||||
self.label3.setObjectName('label3')
|
||||
self.label3.setObjectName("label3")
|
||||
self.label3.setStyleSheet("color: red;")
|
||||
self.label4 = DisplayLabel("剩余袋数")
|
||||
self.label5 = DisplayLabel("0")
|
||||
self.label5.setObjectName('label5')
|
||||
self.label5.setObjectName("label5")
|
||||
self.label5.setStyleSheet("color: green;")
|
||||
|
||||
#hBoxLayout
|
||||
# hBoxLayout
|
||||
self.container5 = QWidget()
|
||||
self.hBoxLayout5 = QHBoxLayout(self.container5)
|
||||
self.hBoxLayout5.addWidget(self.label2)
|
||||
self.hBoxLayout5.addWidget(self.label3)
|
||||
|
||||
#hBoxLayout
|
||||
# hBoxLayout
|
||||
self.container6 = QWidget()
|
||||
self.hBoxLayout6 = QHBoxLayout(self.container6)
|
||||
self.hBoxLayout6.addWidget(self.label4)
|
||||
self.hBoxLayout6.addWidget(self.label5)
|
||||
|
||||
#self.movableCheckBox = CheckBox(self.tr('IsTabMovable'), self)
|
||||
#self.scrollableCheckBox = CheckBox(self.tr('IsTabScrollable'), self)
|
||||
#self.shadowEnabledCheckBox = CheckBox(self.tr('IsTabShadowEnabled'), self)
|
||||
#self.tabMaxWidthLabel = BodyLabel(self.tr('TabMaximumWidth'), self)
|
||||
# self.movableCheckBox = CheckBox(self.tr('IsTabMovable'), self)
|
||||
# self.scrollableCheckBox = CheckBox(self.tr('IsTabScrollable'), self)
|
||||
# self.shadowEnabledCheckBox = CheckBox(self.tr('IsTabShadowEnabled'), self)
|
||||
# self.tabMaxWidthLabel = BodyLabel(self.tr('TabMaximumWidth'), self)
|
||||
# self.tabMaxWidthSpinBox = SpinBox(self)
|
||||
#self.closeDisplayModeLabel = BodyLabel(self.tr('TabCloseButtonDisplayMode'), self)
|
||||
#self.closeDisplayModeComboBox = ComboBox(self)
|
||||
# self.closeDisplayModeLabel = BodyLabel(self.tr('TabCloseButtonDisplayMode'), self)
|
||||
# self.closeDisplayModeComboBox = ComboBox(self)
|
||||
|
||||
self.hBoxLayout = QHBoxLayout(self)
|
||||
self.vBoxLayout = QVBoxLayout(self.tabView)
|
||||
self.panelLayout = QVBoxLayout(self.controlPanel)
|
||||
|
||||
#富文本编辑栏用来显示日志
|
||||
# 富文本编辑栏用来显示日志
|
||||
self.textBrowser = TextBrowser()
|
||||
#self.textBrowser.setMarkdown("## Steel Ball Run \n * Johnny Joestar 🦄 \n * Gyro Zeppeli 🐴 aaa\n * aaa\n * aaa\n * aaa\n * aaa\n *")
|
||||
self.textBrowser.setMarkdown("## 日志\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n *")
|
||||
# self.textBrowser.setMarkdown("## Steel Ball Run \n * Johnny Joestar 🦄 \n * Gyro Zeppeli 🐴 aaa\n * aaa\n * aaa\n * aaa\n * aaa\n *")
|
||||
self.textBrowser.setMarkdown(
|
||||
"## 日志\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n * 2025-08-06 09:54:24 - 🦄进入系统\n * 2025-08-06 09:54:24 - 🐴无回复\n *"
|
||||
)
|
||||
|
||||
#日志切换按钮
|
||||
# 日志切换按钮
|
||||
self.button = SwitchButton()
|
||||
self.button.checkedChanged.connect(lambda checked: print("是否选中按钮:", checked))
|
||||
# self.button.checkedChanged.connect(
|
||||
# lambda checked: print("是否选中按钮:", checked)
|
||||
# )
|
||||
# 更改按钮状态
|
||||
self.button.setChecked(True)
|
||||
# 获取按钮是否选中
|
||||
self.button.setOffText("报警")
|
||||
self.button.setOnText("日志")
|
||||
|
||||
#状态按钮
|
||||
#PillPushButton(self.tr('Tag'), self, FluentIcon.TAG),
|
||||
# 状态按钮
|
||||
# PillPushButton(self.tr('Tag'), self, FluentIcon.TAG),
|
||||
self.state_button1 = PillPushButton()
|
||||
self.state_button1.setFixedHeight(15)
|
||||
self.state_button1.setFixedWidth(100)
|
||||
|
||||
self.state_button2 = PillPushButton("1",self,"")
|
||||
self.state_button2 = PillPushButton("1", self, "")
|
||||
self.state_button2.setFixedHeight(15)
|
||||
self.state_button2.setFixedWidth(100)
|
||||
|
||||
self.state_button3 = PillPushButton("0",self,"")
|
||||
self.state_button3 = PillPushButton("0", self, "")
|
||||
self.state_button3.setFixedHeight(15)
|
||||
self.state_button3.setFixedWidth(100)
|
||||
|
||||
@ -193,11 +230,11 @@ class TabInterface(QWidget):
|
||||
self.state_button4.setFixedHeight(15)
|
||||
self.state_button4.setFixedWidth(100)
|
||||
|
||||
self.state_button5 = PillPushButton("0",self,"")
|
||||
self.state_button5 = PillPushButton("0", self, "")
|
||||
self.state_button5.setFixedHeight(15)
|
||||
self.state_button5.setFixedWidth(100)
|
||||
|
||||
self.state_button6 = PillPushButton("0",self,"")
|
||||
self.state_button6 = PillPushButton("0", self, "")
|
||||
self.state_button6.setFixedHeight(15)
|
||||
self.state_button6.setFixedWidth(100)
|
||||
|
||||
@ -208,7 +245,7 @@ class TabInterface(QWidget):
|
||||
self.state5 = DisplayLabel("当前工具号:")
|
||||
self.state6 = DisplayLabel("报警代码:")
|
||||
|
||||
#状态hBoxLayout
|
||||
# 状态hBoxLayout
|
||||
self.state_container1 = QWidget()
|
||||
self.state_hBoxLayout1 = QHBoxLayout(self.state_container1)
|
||||
self.state_hBoxLayout1.addWidget(self.state1)
|
||||
@ -239,13 +276,13 @@ class TabInterface(QWidget):
|
||||
self.state_hBoxLayout6.addWidget(self.state6)
|
||||
self.state_hBoxLayout6.addWidget(self.state_button6)
|
||||
|
||||
#日志vboxlayout
|
||||
# 日志vboxlayout
|
||||
self.container7 = QWidget()
|
||||
self.vBoxLayout7 = QVBoxLayout(self.container7)
|
||||
self.vBoxLayout7.addWidget(self.button)
|
||||
self.vBoxLayout7.addWidget(self.textBrowser)
|
||||
|
||||
#状态vboxlayout
|
||||
# 状态vboxlayout
|
||||
self.container9 = QWidget()
|
||||
self.vBoxLayout9 = QVBoxLayout(self.container9)
|
||||
self.vBoxLayout9.addWidget(self.state_container1)
|
||||
@ -255,13 +292,13 @@ class TabInterface(QWidget):
|
||||
self.vBoxLayout9.addWidget(self.state_container5)
|
||||
self.vBoxLayout9.addWidget(self.state_container6)
|
||||
|
||||
#日志+状态vboxlayout
|
||||
# 日志+状态vboxlayout
|
||||
self.container8 = QWidget()
|
||||
self.hBoxLayout8 = QHBoxLayout(self.container8)
|
||||
self.hBoxLayout8.addWidget(self.container7)
|
||||
self.hBoxLayout8.addWidget(self.container9)
|
||||
|
||||
#self.songInterface = QLabel('Song Interface', self)
|
||||
# self.songInterface = QLabel('Song Interface', self)
|
||||
# self.albumInterface = QLabel('Album Interface', self)
|
||||
# self.artistInterface = QLabel('Artist Interface', self)
|
||||
|
||||
@ -274,7 +311,7 @@ class TabInterface(QWidget):
|
||||
# self.shadowEnabledCheckBox.setChecked(True)
|
||||
|
||||
# self.tabMaxWidthSpinBox.setRange(60, 400)
|
||||
#self.tabMaxWidthSpinBox.setValue(self.tabBar.tabMaximumWidth())
|
||||
# self.tabMaxWidthSpinBox.setValue(self.tabBar.tabMaximumWidth())
|
||||
|
||||
# self.closeDisplayModeComboBox.addItem(self.tr('Always'), userData=TabCloseButtonDisplayMode.ALWAYS)
|
||||
# self.closeDisplayModeComboBox.addItem(self.tr('OnHover'), userData=TabCloseButtonDisplayMode.ON_HOVER)
|
||||
@ -288,10 +325,10 @@ class TabInterface(QWidget):
|
||||
# self.addSubInterface(self.artistInterface,
|
||||
# 'tabArtistInterface', self.tr('Artist'), ':/gallery/images/Singer.png')
|
||||
|
||||
self.controlPanel.setObjectName('controlPanel')
|
||||
self.controlPanel.setObjectName("controlPanel")
|
||||
StyleSheet.SYSTEM_INTERFACE.apply(self)
|
||||
|
||||
#self.connectSignalToSlot()
|
||||
# self.connectSignalToSlot()
|
||||
|
||||
# qrouter.setDefaultRouteKey(
|
||||
# self.stackedWidget, self.songInterface.objectName())
|
||||
@ -304,7 +341,7 @@ class TabInterface(QWidget):
|
||||
# self.shadowEnabledCheckBox.stateChanged.connect(
|
||||
# lambda: self.tabBar.setTabShadowEnabled(self.shadowEnabledCheckBox.isChecked()))
|
||||
|
||||
#self.tabMaxWidthSpinBox.valueChanged.connect(self.tabBar.setTabMaximumWidth)
|
||||
# self.tabMaxWidthSpinBox.valueChanged.connect(self.tabBar.setTabMaximumWidth)
|
||||
|
||||
# self.tabBar.tabAddRequested.connect(self.addTab)
|
||||
# self.tabBar.tabCloseRequested.connect(self.removeTab)
|
||||
@ -314,7 +351,7 @@ class TabInterface(QWidget):
|
||||
def initLayout(self):
|
||||
# self.tabBar.setTabMaximumWidth(200)
|
||||
|
||||
#self.setFixedHeight(450)
|
||||
# self.setFixedHeight(450)
|
||||
self.setMaximumSize(787, 800)
|
||||
self.setMinimumSize(450, 450)
|
||||
self.controlPanel.setFixedWidth(220)
|
||||
@ -322,7 +359,7 @@ class TabInterface(QWidget):
|
||||
self.hBoxLayout.addWidget(self.controlPanel, 0, Qt.AlignRight)
|
||||
self.hBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
#self.vBoxLayout.addWidget(self.tabBar)
|
||||
# self.vBoxLayout.addWidget(self.tabBar)
|
||||
# self.vBoxLayout.addWidget(self.stackedWidget)
|
||||
self.vBoxLayout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
@ -361,13 +398,13 @@ class TabInterface(QWidget):
|
||||
# self.panelLayout.addWidget(self.closeDisplayModeLabel)
|
||||
# self.panelLayout.addWidget(self.closeDisplayModeComboBox)
|
||||
|
||||
#左边窗口
|
||||
# 左边窗口
|
||||
# 创建加载框
|
||||
#self.loading_button1=ToggleButton(self.tr('Start practicing'), self, FluentIcon.BASKETBALL)
|
||||
self.loading_button1=ToggleButton()
|
||||
self.loading_button2=ToggleButton()
|
||||
self.loading_button3=ToggleButton()
|
||||
self.loading_button4=ToggleButton()
|
||||
# self.loading_button1=ToggleButton(self.tr('Start practicing'), self, FluentIcon.BASKETBALL)
|
||||
self.loading_button1 = ToggleButton()
|
||||
self.loading_button2 = ToggleButton()
|
||||
self.loading_button3 = ToggleButton()
|
||||
self.loading_button4 = ToggleButton()
|
||||
self.loading1 = DisplayLabel("取料中...")
|
||||
self.loading2 = DisplayLabel("拍照中...")
|
||||
self.loading3 = DisplayLabel("抓料中...")
|
||||
@ -390,7 +427,9 @@ class TabInterface(QWidget):
|
||||
self.vBoxLayout.addWidget(self.video_label)
|
||||
|
||||
# 使用 OpenCV 读取视频
|
||||
self.cap = cv2.VideoCapture("./app/resource/video/test.mp4") # 替换为你的视频路径
|
||||
self.cap = cv2.VideoCapture(
|
||||
"./app/resource/video/test.mp4"
|
||||
) # 替换为你的视频路径
|
||||
if not self.cap.isOpened():
|
||||
print("无法打开视频文件!")
|
||||
return
|
||||
@ -399,12 +438,11 @@ class TabInterface(QWidget):
|
||||
self.timer = QTimer()
|
||||
self.timer.timeout.connect(self.update_frame)
|
||||
self.timer.start(30) # 30ms 更新一帧(约 33 FPS)
|
||||
#self.vBoxLayout.addWidget()
|
||||
# self.vBoxLayout.addWidget()
|
||||
|
||||
#左边窗口下面的日志栏位和状态栏
|
||||
# 左边窗口下面的日志栏位和状态栏
|
||||
self.vBoxLayout.addWidget(self.container8)
|
||||
|
||||
|
||||
def addSubInterface(self, widget: QLabel, objectName, text, icon):
|
||||
widget.setObjectName(objectName)
|
||||
widget.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
@ -441,4 +479,6 @@ class TabInterface(QWidget):
|
||||
bytes_per_line = ch * w
|
||||
q_img = QImage(frame.data, w, h, bytes_per_line, QImage.Format_RGB888)
|
||||
pixmap = QPixmap.fromImage(q_img)
|
||||
self.video_label.setPixmap(pixmap.scaled(self.video_label.size())) # 自适应窗口大小
|
||||
self.video_label.setPixmap(
|
||||
pixmap.scaled(self.video_label.size())
|
||||
) # 自适应窗口大小
|
||||
|
||||
8851
fairino/Robot.py
Normal file
8851
fairino/Robot.py
Normal file
File diff suppressed because it is too large
Load Diff
6
fairino/setup.py
Normal file
6
fairino/setup.py
Normal file
@ -0,0 +1,6 @@
|
||||
# setup.py
|
||||
# python3 setup.py build_ext --inplace
|
||||
# python setup.py build_ext --inplace
|
||||
from distutils.core import setup
|
||||
from Cython.Build import cythonize
|
||||
setup(name='Robot', ext_modules=cythonize('Robot.py'))
|
||||
8
mi_an_main.py
Normal file
8
mi_an_main.py
Normal file
@ -0,0 +1,8 @@
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from app.controller.mi_an.main_controller import MainController
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
controller = MainController()
|
||||
controller.showMainWindow()
|
||||
app.exec()
|
||||
@ -5,3 +5,4 @@ colorthief
|
||||
scipy
|
||||
pillow
|
||||
opencv-python
|
||||
cython
|
||||
17
tests/common.py
Normal file
17
tests/common.py
Normal file
@ -0,0 +1,17 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 注意: 只在 tests 目录下的 测试文件中使用
|
||||
# 设置项目目录 为搜索目录
|
||||
|
||||
|
||||
# 通用逻辑:添加项目根目录到搜索路径
|
||||
def add_project_root_to_path():
|
||||
# 获取项目根目录(tests的上级目录)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
if project_root not in sys.path:
|
||||
sys.path.append(project_root)
|
||||
|
||||
|
||||
# 自动执行(导入时就添加路径)
|
||||
add_project_root_to_path()
|
||||
234
tests/test_emv.py
Normal file
234
tests/test_emv.py
Normal file
@ -0,0 +1,234 @@
|
||||
from common import *
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QSpacerItem,
|
||||
QSizePolicy,
|
||||
QLabel,
|
||||
)
|
||||
from PySide6.QtCore import Qt, QTimer, Signal
|
||||
from qfluentwidgets import (
|
||||
PushButton,
|
||||
LineEdit,
|
||||
BodyLabel,
|
||||
MessageBox,
|
||||
setTheme,
|
||||
Theme,
|
||||
)
|
||||
import sys
|
||||
|
||||
|
||||
class DOWidget(QWidget):
|
||||
# open开关按下
|
||||
open_btn_clicked_sig = Signal()
|
||||
|
||||
# close开关按下
|
||||
close_btn_clicked_sig = Signal()
|
||||
|
||||
def __init__(self, title: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.title = title # DO名称(如"DO1")
|
||||
self.is_open = False # 初始状态:关闭
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
|
||||
# 创建控件
|
||||
self.__createWidget()
|
||||
|
||||
# 设置样式
|
||||
self.__initStyles()
|
||||
|
||||
# 设置布局
|
||||
self.__initLayout()
|
||||
|
||||
# 绑定
|
||||
self.__bind()
|
||||
|
||||
def __createWidget(self):
|
||||
# Do按钮名称的 label
|
||||
self.title_label = BodyLabel(self.title)
|
||||
self.title_label.setAlignment(Qt.AlignCenter)
|
||||
|
||||
# 状态指示灯
|
||||
self.status_light = QLabel()
|
||||
self.status_light.setFixedSize(40, 40)
|
||||
|
||||
# 打开和关闭按钮
|
||||
self.open_btn = PushButton("打开")
|
||||
self.close_btn = PushButton("关闭")
|
||||
|
||||
def __initStyles(self):
|
||||
# 状态指示灯样式(初始颜色:灰色(关闭))
|
||||
self.status_light.setStyleSheet(
|
||||
"""
|
||||
border-radius: 20px;
|
||||
background-color: gray;
|
||||
"""
|
||||
)
|
||||
|
||||
def __initLayout(self):
|
||||
# 垂直布局:标题 → 状态灯 → 按钮
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(15, 15, 15, 15) # 内边距
|
||||
main_layout.setSpacing(12) # 控件间距
|
||||
|
||||
# 按钮布局
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setSpacing(8)
|
||||
btn_layout.addWidget(self.open_btn)
|
||||
btn_layout.addWidget(self.close_btn)
|
||||
|
||||
main_layout.addWidget(self.title_label) # 添加按钮名称
|
||||
main_layout.addWidget(self.status_light, alignment=Qt.AlignCenter) # 添加指示灯
|
||||
main_layout.addLayout(btn_layout)
|
||||
|
||||
def __bind(self):
|
||||
self.open_btn.clicked.connect(self.open_btn_clicked_sig)
|
||||
self.close_btn.clicked.connect(self.close_btn_clicked_sig)
|
||||
|
||||
# def on_open(self):
|
||||
# """打开按钮点击事件:更新状态灯为绿色"""
|
||||
# self.is_open = True
|
||||
# self.status_light.setStyleSheet(
|
||||
# """
|
||||
# border-radius: 20px;
|
||||
# background-color: green; /* 打开 → 绿色 */
|
||||
# """
|
||||
# )
|
||||
|
||||
# def on_close(self):
|
||||
# """关闭按钮点击事件:更新状态灯为灰色"""
|
||||
# self.is_open = False
|
||||
# self.status_light.setStyleSheet(
|
||||
# """
|
||||
# border-radius: 20px;
|
||||
# background-color: gray; /* 关闭 → 灰色 */
|
||||
# """
|
||||
# )
|
||||
|
||||
|
||||
class EmvTestUi(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("继电器调试")
|
||||
self.resize(1000, 600)
|
||||
self.setStyleSheet("background-color: white;") # 背景颜色,根据主题来变化
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
|
||||
# 创建控件
|
||||
self.__createWidget()
|
||||
|
||||
# 设置样式
|
||||
self.__initStyles()
|
||||
|
||||
# 设置布局
|
||||
self.__initLayout()
|
||||
|
||||
# 绑定
|
||||
self.__bind()
|
||||
|
||||
def __createWidget(self):
|
||||
# Tcp连接控件
|
||||
# IP控件
|
||||
self.ip_label = BodyLabel("设备IP: ")
|
||||
self.ip_edit = LineEdit()
|
||||
self.ip_edit.setText("192.168.0.18") # 默认IP
|
||||
|
||||
# 端口控件
|
||||
self.port_label = BodyLabel("设备端口号: ")
|
||||
self.port_edit = LineEdit()
|
||||
self.port_edit.setText("50000") # 默认端口
|
||||
|
||||
# 连接按钮
|
||||
self.connect_btn = PushButton("连接")
|
||||
# 功能测试按钮
|
||||
self.test_btn = PushButton("功能测试")
|
||||
|
||||
# 三个Do模块
|
||||
self.do1_model = DOWidget("DO1")
|
||||
self.do2_model = DOWidget("DO2")
|
||||
self.do3_model = DOWidget("DO3")
|
||||
|
||||
def __initStyles(self):
|
||||
# 设置样式 (to do)
|
||||
pass
|
||||
|
||||
def __initLayout(self):
|
||||
# 顶部的连接布局
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.addWidget(self.ip_label)
|
||||
top_layout.addWidget(self.ip_edit)
|
||||
top_layout.addWidget(self.port_label)
|
||||
top_layout.addWidget(self.port_edit)
|
||||
top_layout.addWidget(self.connect_btn)
|
||||
top_layout.addWidget(self.test_btn)
|
||||
|
||||
# 三个Do开关区域布局
|
||||
do_layout = QHBoxLayout()
|
||||
do_layout.setSpacing(60) # DO模块之间的间距
|
||||
do_layout.setAlignment(Qt.AlignCenter) # 水平居中
|
||||
|
||||
do_layout.addWidget(self.do1_model)
|
||||
do_layout.addWidget(self.do2_model)
|
||||
do_layout.addWidget(self.do3_model)
|
||||
|
||||
# 主布局
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(60, 60, 60, 60)
|
||||
main_layout.setSpacing(30)
|
||||
|
||||
main_layout.addLayout(top_layout)
|
||||
main_layout.addLayout(do_layout)
|
||||
|
||||
main_layout.addItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||
)
|
||||
|
||||
def __bind(self):
|
||||
pass
|
||||
self.connect_btn.clicked.connect(self.on_connect)
|
||||
|
||||
def on_connect(self):
|
||||
"""连接按钮点击事件:切换状态为“连接中...”,模拟连接过程"""
|
||||
# 禁用按钮 + 修改文字
|
||||
self.connect_btn.setEnabled(False)
|
||||
self.connect_btn.setText("连接中...")
|
||||
|
||||
# 模拟连接过程(实际应替换为真实的网络请求,如socket连接)
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setSingleShot(True)
|
||||
self.timer.timeout.connect(self.simulate_connection_result)
|
||||
self.timer.start(2000) # 模拟2秒连接耗时
|
||||
|
||||
def simulate_connection_result(self):
|
||||
"""模拟连接结果(这里固定返回失败,实际需根据真实逻辑修改)"""
|
||||
# 模拟连接失败(实际中通过try-except或网络返回判断)
|
||||
connection_success = False # TODO: 替换为真实连接结果
|
||||
|
||||
if not connection_success:
|
||||
# 弹出连接失败对话框
|
||||
# 正确创建警告对话框:通过 type 参数指定为警告类型
|
||||
msg_box = MessageBox(
|
||||
"连接失败", # 标题
|
||||
"无法连接到设备, 请检查IP和端口是否正确!!", # 内容
|
||||
self, # 父窗口
|
||||
)
|
||||
msg_box.exec() # 显示对话框
|
||||
|
||||
# 恢复按钮状态
|
||||
self.connect_btn.setEnabled(True)
|
||||
self.connect_btn.setText("连接")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
setTheme(Theme.LIGHT)
|
||||
window = EmvTestUi()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@ -1,3 +1,5 @@
|
||||
from common import *
|
||||
|
||||
import sys
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@ -1,3 +1,5 @@
|
||||
from common import *
|
||||
|
||||
import sys
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@ -1,3 +1,5 @@
|
||||
from common import *
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
@ -13,7 +15,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QFont
|
||||
from qfluentwidgets import PushButton, ComboBox, LineEdit
|
||||
from qfluentwidgets import PushButton, ComboBox, LineEdit, PrimaryPushButton
|
||||
|
||||
|
||||
# 模具管理界面
|
||||
@ -50,33 +52,25 @@ class MoldManagementUI(QWidget):
|
||||
self.serial_edit.setFont(self.left_font) # 输入框字体
|
||||
|
||||
self.type_combo = ComboBox()
|
||||
self.type_combo.addItems(["一板一个", "一板多个", "其他类型"])
|
||||
self.type_combo.addItems(["一板一个", "一板两个", "一板三个", "一板四个"])
|
||||
self.type_combo.setFont(self.left_font) # 下拉框字体
|
||||
|
||||
# 左侧按钮(上传点位等)
|
||||
self.btn_upload_model = PushButton("从文件上传")
|
||||
self.btn_suction_point = PushButton("上传点位")
|
||||
self.btn_place_point = PushButton("上传点位")
|
||||
self.btn_pick_point = PushButton("上传点位")
|
||||
self.btn_high_freq = PushButton("上传点位")
|
||||
self.btn_upload_model = PrimaryPushButton("从文件上传")
|
||||
self.btn_suction_point = PrimaryPushButton("上传点位")
|
||||
self.btn_place_point = PrimaryPushButton("上传点位")
|
||||
self.btn_pick_point = PrimaryPushButton("上传点位")
|
||||
self.btn_high_freq = PrimaryPushButton("上传点位")
|
||||
|
||||
# 右侧操作按钮
|
||||
self.btn_upload_mold = PushButton("上传模具")
|
||||
self.btn_modify_mold = PushButton("修改模具")
|
||||
self.btn_delete_mold = PushButton("删除模具")
|
||||
self.btn_batch_upload = PushButton("通过文件上传") # 需要文件对话框
|
||||
self.btn_upload_mold = PrimaryPushButton("上传模具")
|
||||
self.btn_modify_mold = PrimaryPushButton("修改模具")
|
||||
self.btn_delete_mold = PrimaryPushButton("删除模具")
|
||||
self.btn_batch_upload = PrimaryPushButton("通过文件上传") # 需要文件对话框
|
||||
|
||||
def __initStyles(self):
|
||||
# ========== 设置样式 ==========
|
||||
# 1. 左侧按钮样式(适中大小)
|
||||
left_btn_style = """
|
||||
QPushButton {
|
||||
min-height: 40px;
|
||||
min-width: 150px;
|
||||
font-size: 14px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
"""
|
||||
for btn in [
|
||||
self.btn_upload_model,
|
||||
self.btn_suction_point,
|
||||
@ -84,24 +78,16 @@ class MoldManagementUI(QWidget):
|
||||
self.btn_pick_point,
|
||||
self.btn_high_freq,
|
||||
]:
|
||||
btn.setStyleSheet(left_btn_style)
|
||||
btn.setFixedHeight(46)
|
||||
|
||||
# 2. 右侧按钮样式(较大尺寸)
|
||||
right_btn_style = """
|
||||
QPushButton {
|
||||
min-height: 36px;
|
||||
min-width: 166px;
|
||||
font-size: 16px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
"""
|
||||
for btn in [
|
||||
self.btn_upload_mold,
|
||||
self.btn_modify_mold,
|
||||
self.btn_delete_mold,
|
||||
self.btn_batch_upload,
|
||||
]:
|
||||
btn.setStyleSheet(right_btn_style)
|
||||
btn.setFixedHeight(99)
|
||||
|
||||
def __initLayout(self):
|
||||
# ========== 布局设计 =========
|
||||
259
tests/test_moju_point_set.py
Normal file
259
tests/test_moju_point_set.py
Normal file
@ -0,0 +1,259 @@
|
||||
from common import *
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QLabel,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QDoubleSpinBox,
|
||||
QLayout,
|
||||
QApplication,
|
||||
QDialog,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
PrimaryPushButton,
|
||||
PushButton,
|
||||
StrongBodyLabel,
|
||||
SubtitleLabel,
|
||||
DoubleSpinBox,
|
||||
MessageBox,
|
||||
isDarkTheme,
|
||||
setTheme,
|
||||
Theme,
|
||||
)
|
||||
import random
|
||||
|
||||
|
||||
class MoJuPointSetDialog(QDialog):
|
||||
def __init__(self, point_name, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setWindowTitle("模具点位设置")
|
||||
self.resize(850, 320)
|
||||
|
||||
# 0. 传入的点位名称
|
||||
self.point_name = point_name
|
||||
# 1. 点位名称编号
|
||||
self.current_point_num = 1 # 当前点位编号(初始为1)
|
||||
# 2. 存储各轴输入框:key=轴名(X/Y/Z/Rx/Ry/Rz), value=DoubleSpinBox对象
|
||||
self.axis_spins = {}
|
||||
# 3. 存储已经设置好的点位 {点位名称1: [x, y, z, rx, ry, rz], ......}
|
||||
self.point_dict = {}
|
||||
|
||||
self.init_ui()
|
||||
|
||||
self._initThemeStyle()
|
||||
|
||||
def init_ui(self):
|
||||
# 主布局:垂直排列顶部区域和“添加下一个点”按钮
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setSpacing(30) # 组件间间距
|
||||
main_layout.setContentsMargins(20, 20, 20, 20) # 窗口边距
|
||||
|
||||
# ---------- 顶部区域:点标签 + 坐标输入 + 右侧按钮 ----------
|
||||
top_layout = QHBoxLayout()
|
||||
top_layout.setSpacing(40) # 左右区域间距
|
||||
|
||||
# ---- 左侧:点标签 + 坐标输入 ----
|
||||
left_layout = QVBoxLayout()
|
||||
left_layout.setSpacing(20)
|
||||
|
||||
# “点1:” 标签行
|
||||
point_label_layout = QHBoxLayout()
|
||||
self.point_label = SubtitleLabel(f"{self.point_name}{self.current_point_num}")
|
||||
point_label_layout.addWidget(self.point_label)
|
||||
point_label_layout.addStretch() # 让标签靠左
|
||||
left_layout.addLayout(point_label_layout)
|
||||
|
||||
# X/Y/Z 输入行
|
||||
xyz_layout = QHBoxLayout()
|
||||
self._add_axis_input(xyz_layout, "X")
|
||||
xyz_layout.addSpacing(30) # 轴之间的间距
|
||||
self._add_axis_input(xyz_layout, "Y")
|
||||
xyz_layout.addSpacing(30)
|
||||
self._add_axis_input(xyz_layout, "Z")
|
||||
left_layout.addLayout(xyz_layout)
|
||||
|
||||
# Rx/Ry/Rz 输入行
|
||||
rxyz_layout = QHBoxLayout()
|
||||
self._add_axis_input(rxyz_layout, "Rx")
|
||||
rxyz_layout.addSpacing(30)
|
||||
self._add_axis_input(rxyz_layout, "Ry")
|
||||
rxyz_layout.addSpacing(30)
|
||||
self._add_axis_input(rxyz_layout, "Rz")
|
||||
left_layout.addLayout(rxyz_layout)
|
||||
|
||||
top_layout.addLayout(left_layout)
|
||||
|
||||
# ---- 右侧:三个功能按钮 ----
|
||||
right_layout = QVBoxLayout()
|
||||
right_layout.setSpacing(20) # 按钮间间距
|
||||
|
||||
# “获取机械臂点位” 按钮
|
||||
get_robot_btn = PrimaryPushButton("获取机械臂点位")
|
||||
get_robot_btn.clicked.connect(self.onGetRobotPos)
|
||||
|
||||
# “确认上传” 按钮
|
||||
confirm_upload_btn = PrimaryPushButton("确认上传")
|
||||
confirm_upload_btn.clicked.connect(self.onConfirmUpload)
|
||||
|
||||
# “返回上页” 按钮
|
||||
return_btn = PrimaryPushButton("返回上页")
|
||||
return_btn.clicked.connect(self.onReturnBeforePage)
|
||||
|
||||
right_layout.addWidget(get_robot_btn)
|
||||
right_layout.addWidget(confirm_upload_btn)
|
||||
right_layout.addWidget(return_btn)
|
||||
|
||||
top_layout.addLayout(right_layout)
|
||||
main_layout.addLayout(top_layout)
|
||||
|
||||
# ---------- 底部:“添加下一个点” 按钮 ----------
|
||||
add_next_layout = QHBoxLayout()
|
||||
|
||||
add_next_layout.addStretch()
|
||||
add_next_btn = PrimaryPushButton("添加下一个点")
|
||||
add_next_btn.setMinimumSize(180, 40)
|
||||
add_next_btn.setMaximumSize(220, 45)
|
||||
add_next_btn.clicked.connect(self.onAddNextPoint)
|
||||
|
||||
add_next_layout.addWidget(add_next_btn)
|
||||
# 添加右侧伸缩项,将按钮固定在中间
|
||||
add_next_layout.addStretch()
|
||||
|
||||
main_layout.addLayout(add_next_layout)
|
||||
|
||||
def _add_axis_input(self, layout: QHBoxLayout, axis_name: str):
|
||||
"""添加“标签 + 双精度输入框”到布局"""
|
||||
label = StrongBodyLabel(f"{axis_name}:")
|
||||
spin = DoubleSpinBox()
|
||||
spin.setRange(-180, 180) # 数值范围
|
||||
spin.setSingleStep(0.1) # 步长(滚轮或上下键的变化量)
|
||||
spin.setDecimals(3) # 小数位数
|
||||
self.axis_spins[axis_name] = spin # 放双精度输入框的字典
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(spin)
|
||||
|
||||
# ---------- 按钮点击槽函数 ----------
|
||||
def onGetRobotPos(self):
|
||||
for spin in self.axis_spins.values():
|
||||
random_num = round(random.uniform(-180, 180), 3)
|
||||
spin.setValue(random_num)
|
||||
|
||||
def onConfirmUpload(self):
|
||||
# 确认上传
|
||||
|
||||
# 1、先保存当前界面的数据,再上传
|
||||
self.__saveCurentPoint()
|
||||
|
||||
# 2、点位写入数据库?
|
||||
print(self.point_dict)
|
||||
|
||||
# 3 、提示
|
||||
if self.point_dict:
|
||||
# 提取所有点位名(字典的key),并格式化为列表
|
||||
point_names = list(self.point_dict.keys())
|
||||
# 构造提示文本(用换行和符号美化显示)
|
||||
message = "以下点位上传成功:\n"
|
||||
for name in point_names:
|
||||
message += f"• {name}\n" # 用•符号列出每个点位名
|
||||
# 弹出成功提示框
|
||||
MessageBox(
|
||||
"上传成功", # 标题
|
||||
message, # 内容
|
||||
self, # 父窗口
|
||||
).exec()
|
||||
|
||||
# 4、确认上传之后,关闭点位上传对话框
|
||||
self.close()
|
||||
|
||||
# 返回上一页
|
||||
def onReturnBeforePage(self):
|
||||
if self.current_point_num <= 1:
|
||||
MessageBox(
|
||||
"错误",
|
||||
f"已处于第一个点位 ({self.point_name}1) ,无法继续返回上一页",
|
||||
self,
|
||||
).exec()
|
||||
return # 直接返回,不执行后续操作
|
||||
|
||||
# 0. 先保存当前界面的数据,再返回上一页
|
||||
self.__saveCurentPoint()
|
||||
|
||||
# 1、当前编号减一和更新标签
|
||||
self.current_point_num -= 1
|
||||
self.point_label.setText(f"{self.point_name}{self.current_point_num}")
|
||||
|
||||
# 2、显示上一页点位名的点位数据
|
||||
before_point_name = self.point_label.text()
|
||||
if before_point_name in self.point_dict:
|
||||
self.__showPointValue(before_point_name)
|
||||
else:
|
||||
# 上一页点位名不存在
|
||||
for spin in self.axis_spins.values():
|
||||
spin.setValue(0.0)
|
||||
# 上一页的点位数据缺失,请重新获取并设置点位
|
||||
# 这里是发生了异常,一般情况,不会没有保存上一页点位数据
|
||||
MessageBox(
|
||||
"异常",
|
||||
f"上一页的点位数据缺失,请重新获取并设置点位",
|
||||
self,
|
||||
).exec()
|
||||
|
||||
def onAddNextPoint(self):
|
||||
# 0. 先保存当前界面的数据,再跳转到下一页
|
||||
self.__saveCurentPoint()
|
||||
|
||||
# 1. 跳转下一页,更新点位编号和标签
|
||||
self.current_point_num += 1
|
||||
self.point_label.setText(f"{self.point_name}{self.current_point_num}")
|
||||
|
||||
# 2. 如果下一页的点位数据已经保存,则获取并显示
|
||||
# 否则清空所有输入框的值(设置为0.0), 表示仍然需要设置
|
||||
next_point_name = self.point_label.text()
|
||||
if next_point_name in self.point_dict:
|
||||
self.__showPointValue(next_point_name)
|
||||
else:
|
||||
for spin in self.axis_spins.values():
|
||||
spin.setValue(0.0)
|
||||
|
||||
# 保存当前界面的点位
|
||||
def __saveCurentPoint(self):
|
||||
current_point_value = [
|
||||
self.axis_spins["X"].value(),
|
||||
self.axis_spins["Y"].value(),
|
||||
self.axis_spins["Z"].value(),
|
||||
self.axis_spins["Rx"].value(),
|
||||
self.axis_spins["Ry"].value(),
|
||||
self.axis_spins["Rz"].value(),
|
||||
]
|
||||
self.point_dict[f"{self.point_name}{self.current_point_num}"] = (
|
||||
current_point_value
|
||||
)
|
||||
|
||||
# 根据点位名称,显示点位值 (字典中保存的)
|
||||
def __showPointValue(self, point_name: str):
|
||||
if point_name not in self.point_dict:
|
||||
return
|
||||
|
||||
coords = self.point_dict[point_name]
|
||||
self.axis_spins["X"].setValue(coords[0])
|
||||
self.axis_spins["Y"].setValue(coords[1])
|
||||
self.axis_spins["Z"].setValue(coords[2])
|
||||
self.axis_spins["Rx"].setValue(coords[3])
|
||||
self.axis_spins["Ry"].setValue(coords[4])
|
||||
self.axis_spins["Rz"].setValue(coords[5])
|
||||
|
||||
# 根据主题初始化样式表
|
||||
def _initThemeStyle(self):
|
||||
if isDarkTheme(): # 深色主题
|
||||
self.setStyleSheet("background-color: rgb(32, 32, 32);")
|
||||
else: # 浅色主题
|
||||
self.setStyleSheet("background-color: rgb(243, 243, 243);")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
setTheme(Theme.DARK)
|
||||
window = MoJuPointSetDialog("吸取点")
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@ -1,3 +1,5 @@
|
||||
from common import *
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
@ -1,3 +1,5 @@
|
||||
from common import *
|
||||
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
146
tests/test_weight.py
Normal file
146
tests/test_weight.py
Normal file
@ -0,0 +1,146 @@
|
||||
from common import *
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QWidget,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QVBoxLayout,
|
||||
QLabel,
|
||||
QSpacerItem,
|
||||
QSizePolicy,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from qfluentwidgets import LineEdit, PushButton, StrongBodyLabel
|
||||
|
||||
|
||||
class WeightControlUI(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# ========== 1. 窗口基础设置 ==========
|
||||
self.setWindowTitle("计量一体机调试")
|
||||
self.resize(800, 600) # 窗口大小
|
||||
self.setStyleSheet("background-color: white;") # 背景颜色,根据主题来变化
|
||||
|
||||
self.__initWidget()
|
||||
|
||||
def __initWidget(self):
|
||||
|
||||
# 创建控件
|
||||
self.__createWidget()
|
||||
|
||||
# 设置样式
|
||||
self.__initStyles()
|
||||
|
||||
# 设置布局
|
||||
self.__initLayout()
|
||||
|
||||
def __createWidget(self):
|
||||
# ========== 创建表单控件 ==========
|
||||
# 右侧编辑栏
|
||||
self.target_weight_edit = LineEdit()
|
||||
self.target_weight_edit.setPlaceholderText("目标重量")
|
||||
|
||||
self.current_weight_edit = LineEdit()
|
||||
self.current_weight_edit.setPlaceholderText("当前重量")
|
||||
self.current_weight_edit.setReadOnly(True) # 只读
|
||||
|
||||
self.error_edit = LineEdit()
|
||||
self.error_edit.setPlaceholderText("误差")
|
||||
self.error_edit.setReadOnly(True) # 只读
|
||||
|
||||
# ========== 按钮 ==========
|
||||
self.calibrate_btn = PushButton("重量标定")
|
||||
self.confirm_btn = PushButton("确认")
|
||||
self.compensate_btn = PushButton("补称")
|
||||
self.motor_forward_btn = PushButton("电机正转")
|
||||
self.motor_reverse_btn = PushButton("电机反转")
|
||||
|
||||
def __initStyles(self):
|
||||
# 设置按钮大小
|
||||
# 固定大小为 160x46
|
||||
for btn in [
|
||||
self.calibrate_btn,
|
||||
self.confirm_btn,
|
||||
self.compensate_btn,
|
||||
self.motor_forward_btn,
|
||||
self.motor_reverse_btn,
|
||||
]:
|
||||
btn.setFixedSize(160, 46) # 固定按钮尺寸
|
||||
|
||||
def __initLayout(self):
|
||||
# ========== 布局设计 ==========
|
||||
# 表单布局(标签+输入框,右对齐)
|
||||
form_layout = QFormLayout()
|
||||
form_layout.addRow(StrongBodyLabel("目标重量:"), self.target_weight_edit)
|
||||
form_layout.addRow(StrongBodyLabel("当前重量:"), self.current_weight_edit)
|
||||
form_layout.addRow(StrongBodyLabel("误差:"), self.error_edit)
|
||||
form_layout.setRowWrapPolicy(QFormLayout.DontWrapRows) # 禁止换行
|
||||
form_layout.setVerticalSpacing(20) # 行间距
|
||||
form_layout.setLabelAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 标签左对齐
|
||||
|
||||
# 用QHBoxLayout 包裹 form_layout,两侧加弹簧
|
||||
form_horizontal_layout = QHBoxLayout()
|
||||
# 左侧弹簧:占据左边多余空间
|
||||
form_horizontal_layout.addItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
)
|
||||
# 添加表单布局
|
||||
form_horizontal_layout.addLayout(form_layout)
|
||||
# 右侧弹簧:占据右边多余空间
|
||||
form_horizontal_layout.addItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
)
|
||||
|
||||
# 按钮布局(第一行:重量标定 + 确认;第二行:补称 + 电机正转 + 电机反转)
|
||||
# 第一行按钮布局(中间留空,让按钮居中)
|
||||
btn_row1_layout = QHBoxLayout()
|
||||
btn_row1_layout.addSpacerItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
)
|
||||
btn_row1_layout.addWidget(self.calibrate_btn)
|
||||
btn_row1_layout.addSpacerItem(
|
||||
QSpacerItem(160, 0, QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
) # 两个按钮之间的间距为160
|
||||
btn_row1_layout.addWidget(self.confirm_btn)
|
||||
btn_row1_layout.addSpacerItem(
|
||||
QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||
)
|
||||
|
||||
# 第二行按钮布局(均匀分布)
|
||||
btn_row2_layout = QHBoxLayout()
|
||||
btn_row2_layout.addWidget(self.compensate_btn)
|
||||
btn_row2_layout.addSpacerItem(
|
||||
QSpacerItem(60, 0, QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
)
|
||||
btn_row2_layout.addWidget(self.motor_forward_btn)
|
||||
btn_row2_layout.addSpacerItem(
|
||||
QSpacerItem(60, 0, QSizePolicy.Fixed, QSizePolicy.Minimum)
|
||||
)
|
||||
btn_row2_layout.addWidget(self.motor_reverse_btn)
|
||||
btn_row2_layout.setContentsMargins(0, 0, 0, 0) # 清除边距
|
||||
|
||||
# 主布局(垂直排列:表单 + 按钮行1 + 按钮行2)
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.addLayout(form_horizontal_layout)
|
||||
main_layout.addSpacing(40) # 表单与按钮的间距
|
||||
main_layout.addLayout(btn_row1_layout)
|
||||
main_layout.addSpacing(30) # 两行按钮的间距
|
||||
main_layout.addLayout(btn_row2_layout)
|
||||
main_layout.setContentsMargins(80, 60, 80, 60) # 窗口内边距
|
||||
# main_layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter) # 整体上对齐,水平居中
|
||||
main_layout.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter) # 垂直、水平居中
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
# 设置qfluentwidgets全局主题(如浅色主题)
|
||||
from qfluentwidgets import setTheme, Theme
|
||||
|
||||
setTheme(Theme.LIGHT)
|
||||
window = WeightControlUI()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
Reference in New Issue
Block a user