Compare commits

..

5 Commits

28 changed files with 12504 additions and 226 deletions

View File

@ -1,3 +1,3 @@
# fluent_widgets_pyside6
机器臂控制系统的 UI界面
法奥机器臂控制系统的 UI界面 以及密胺制品的相关控制代码

View 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
View 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/工件ID0-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
View 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
View 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
View 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
View 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():
# 检查对应位是否为11表示开启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()

View 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():
# 检查对应位是否为11表示开启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)

View 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=2False表示停止/暂停/拖动/异常
"""
# 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
View 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
View 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

View 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()

View 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()

View File

@ -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,59 +98,61 @@ class TabInterface(QWidget):
# editable combo box
self.comboBox = EditableComboBox()
self.comboBox.addItems([
"10",
"20",
"30",
"40",
"50",
"60",
"70",
"80",
])
self.comboBox.addItems(
[
"10",
"20",
"30",
"40",
"50",
"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,33 +325,33 @@ 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())
# def connectSignalToSlot(self):
# self.movableCheckBox.stateChanged.connect(
# lambda: self.tabBar.setMovable(self.movableCheckBox.isChecked()))
# self.scrollableCheckBox.stateChanged.connect(
# lambda: self.tabBar.setScrollable(self.scrollableCheckBox.isChecked()))
# self.shadowEnabledCheckBox.stateChanged.connect(
# lambda: self.tabBar.setTabShadowEnabled(self.shadowEnabledCheckBox.isChecked()))
# self.movableCheckBox.stateChanged.connect(
# lambda: self.tabBar.setMovable(self.movableCheckBox.isChecked()))
# self.scrollableCheckBox.stateChanged.connect(
# lambda: self.tabBar.setScrollable(self.scrollableCheckBox.isChecked()))
# 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)
# self.tabBar.tabAddRequested.connect(self.addTab)
# self.tabBar.tabCloseRequested.connect(self.removeTab)
# self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
# self.stackedWidget.currentChanged.connect(self.onCurrentIndexChanged)
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,11 +438,10 @@ 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)
@ -421,12 +459,12 @@ class TabInterface(QWidget):
# self.tabBar.setCloseButtonDisplayMode(mode)
# def onCurrentIndexChanged(self, index):
# widget = self.stackedWidget.widget(index)
# if not widget:
# return
# widget = self.stackedWidget.widget(index)
# if not widget:
# return
# self.tabBar.setCurrentTab(widget.objectName())
# qrouter.push(self.stackedWidget, widget.objectName())
# self.tabBar.setCurrentTab(widget.objectName())
# qrouter.push(self.stackedWidget, widget.objectName())
def update_frame(self):
ret, frame = self.cap.read()
@ -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

File diff suppressed because it is too large Load Diff

6
fairino/setup.py Normal file
View 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
View 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()

View File

@ -4,4 +4,5 @@ darkdetect
colorthief
scipy
pillow
opencv-python
opencv-python
cython

17
tests/common.py Normal file
View 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
View 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())

View File

@ -1,3 +1,5 @@
from common import *
import sys
from PySide6.QtWidgets import (
QApplication,

View File

@ -1,3 +1,5 @@
from common import *
import sys
from PySide6.QtWidgets import (
QApplication,

View File

@ -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):
# ========== 布局设计 =========

View 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())

View File

@ -1,3 +1,5 @@
from common import *
from PySide6.QtWidgets import (
QApplication,
QWidget,

View File

@ -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
View 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())