Commit cd00704b authored by wanghao's avatar wanghao

1 机械臂功能开发

parent fe607a03
package com.zehong.web.controller.equipment;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zehong.common.core.controller.BaseController;
import com.zehong.common.core.domain.AjaxResult;
import com.zehong.system.domain.RobotArmCommand;
import com.zehong.system.service.IRobotArmCommandService;
import com.zehong.common.utils.poi.ExcelUtil;
import com.zehong.common.core.page.TableDataInfo;
/**
* 机械臂指令Controller
*
* @author zehong
* @date 2025-08-04
*/
@RestController
@RequestMapping("/robotArm/command")
public class RobotArmCommandController extends BaseController
{
@Autowired
private IRobotArmCommandService robotArmCommandService;
/**
* 查询机械臂指令列表
*/
@GetMapping("/list")
public TableDataInfo list(RobotArmCommand robotArmCommand)
{
startPage();
List<RobotArmCommand> list = robotArmCommandService.selectRobotArmCommandList(robotArmCommand);
return getDataTable(list);
}
/**
* 导出机械臂指令列表
*/
@GetMapping("/export")
public AjaxResult export(RobotArmCommand robotArmCommand)
{
List<RobotArmCommand> list = robotArmCommandService.selectRobotArmCommandList(robotArmCommand);
ExcelUtil<RobotArmCommand> util = new ExcelUtil<RobotArmCommand>(RobotArmCommand.class);
return util.exportExcel(list, "机械臂指令数据");
}
/**
* 获取机械臂指令详细信息
*/
@GetMapping(value = "/{robotArmCommandId}")
public AjaxResult getInfo(@PathVariable("robotArmCommandId") Long robotArmCommandId)
{
return AjaxResult.success(robotArmCommandService.selectRobotArmCommandById(robotArmCommandId));
}
/**
* 新增机械臂指令
*/
@PostMapping
public AjaxResult add(@RequestBody RobotArmCommand robotArmCommand)
{
return toAjax(robotArmCommandService.insertRobotArmCommand(robotArmCommand));
}
@PostMapping("/powerOn")
public AjaxResult powerOn(@RequestBody Map<String, Object> params) {
Long commandId = Long.parseLong(params.get("commandId").toString());
robotArmCommandService.powerOnCommand(commandId);
return AjaxResult.success("上电操作成功");
}
/**
* 修改机械臂指令
*/
@PutMapping
public AjaxResult edit(@RequestBody RobotArmCommand robotArmCommand)
{
return toAjax(robotArmCommandService.updateRobotArmCommand(robotArmCommand));
}
/**
* 删除机械臂指令
*/
@DeleteMapping("/{robotArmCommandIds}")
public AjaxResult remove(@PathVariable Long[] robotArmCommandIds)
{
return toAjax(robotArmCommandService.deleteRobotArmCommandByIds(robotArmCommandIds));
}
}
# 数据源配置 # 数据源配置
spring: spring:
datasource: datasource:
type: com.alibaba.druid.pool.DruidDataSource type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver driverClassName: com.mysql.cj.jdbc.Driver
druid: druid:
...@@ -56,7 +56,7 @@ spring: ...@@ -56,7 +56,7 @@ spring:
config: config:
multi-statement-allow: true multi-statement-allow: true
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: localhost host: localhost
# 端口,默认为6379 # 端口,默认为6379
...@@ -102,4 +102,11 @@ netty: ...@@ -102,4 +102,11 @@ netty:
boss-group-thread-count: 1 # 主线程数 boss-group-thread-count: 1 # 主线程数
worker-group-thread-count: 8 # 工作线程数 worker-group-thread-count: 8 # 工作线程数
max-frame-length: 65535 # 最大帧长度 max-frame-length: 65535 # 最大帧长度
heartbeat-timeout: 60 # 心跳超时时间(秒) heartbeat-timeout: 10 # 心跳超时时间(秒)
\ No newline at end of file
# 机械臂UDP配置
robot:
arm:
udp:
ip: 192.168.2.16
port: 6000
...@@ -102,4 +102,12 @@ netty: ...@@ -102,4 +102,12 @@ netty:
boss-group-thread-count: 1 # 主线程数 boss-group-thread-count: 1 # 主线程数
worker-group-thread-count: 8 # 工作线程数 worker-group-thread-count: 8 # 工作线程数
max-frame-length: 65535 # 最大帧长度 max-frame-length: 65535 # 最大帧长度
heartbeat-timeout: 60 # 心跳超时时间(秒) heartbeat-timeout: 10 # 心跳超时时间(秒)
\ No newline at end of file
# 机械臂UDP配置
robot:
arm:
udp:
ip: 192.168.2.16
port: 6000
\ No newline at end of file
...@@ -99,4 +99,12 @@ netty: ...@@ -99,4 +99,12 @@ netty:
boss-group-thread-count: 1 # 主线程数 boss-group-thread-count: 1 # 主线程数
worker-group-thread-count: 8 # 工作线程数 worker-group-thread-count: 8 # 工作线程数
max-frame-length: 65535 # 最大帧长度 max-frame-length: 65535 # 最大帧长度
heartbeat-timeout: 60 # 心跳超时时间(秒) heartbeat-timeout: 10 # 心跳超时时间(秒)
\ No newline at end of file
# 机械臂UDP配置
robot:
arm:
udp:
ip: 192.168.2.16
port: 6000
\ No newline at end of file
...@@ -130,6 +130,10 @@ ...@@ -130,6 +130,10 @@
<artifactId>netty-all</artifactId> <artifactId>netty-all</artifactId>
<version>4.1.86.Final</version> <version>4.1.86.Final</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
\ No newline at end of file
...@@ -35,7 +35,7 @@ public class NettyConfig { ...@@ -35,7 +35,7 @@ public class NettyConfig {
/** /**
* 心跳检测超时时间(秒) * 心跳检测超时时间(秒)
*/ */
private int heartbeatTimeout = 60; private int heartbeatTimeout = 10;
// getter和setter方法 // getter和setter方法
public int getPort() { public int getPort() {
......
...@@ -105,6 +105,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter ...@@ -105,6 +105,10 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
"/**/*.css", "/**/*.css",
"/**/*.js" "/**/*.js"
).permitAll() ).permitAll()
// 对于 websocket 匿名访问
.antMatchers("/ws-robot-arm").permitAll()
.antMatchers("/profile/**").anonymous() .antMatchers("/profile/**").anonymous()
.antMatchers("/common/download**").anonymous() .antMatchers("/common/download**").anonymous()
.antMatchers("/common/download/resource**").anonymous() .antMatchers("/common/download/resource**").anonymous()
......
package com.zehong.framework.netty.handler; package com.zehong.framework.netty.handler;
import com.zehong.system.service.IRobotArmCommandService;
import com.zehong.system.service.websocket.RobotArmWebSocketHandler;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
...@@ -11,6 +13,7 @@ import org.slf4j.Logger; ...@@ -11,6 +13,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
...@@ -36,6 +39,12 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -36,6 +39,12 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
// 线程安全锁,确保文件写入安全 // 线程安全锁,确保文件写入安全
private final ReentrantLock fileLock = new ReentrantLock(); private final ReentrantLock fileLock = new ReentrantLock();
@Resource
private RobotArmWebSocketHandler robotArmWebSocketHandler; // 注入WebSocket处理器
@Resource
private IRobotArmCommandService robotArmCommandService;
/** /**
* 接收UDP消息 * 接收UDP消息
*/ */
...@@ -68,18 +77,36 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -68,18 +77,36 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
} }
// 保存消息到文件 // 保存消息到文件
saveMessageToFile(packet.sender().toString(), correctMessage); //saveMessageToFile(packet.sender().toString(), correctMessage);
// 处理消息逻辑 // 处理消息逻辑
String response = "服务器已收到UDP消息:" + correctMessage; String responseSave = "服务器已收到UDP消息:" + correctMessage;
// 处理机械臂完成消息
if (correctMessage.startsWith("COMPLETE,")) {
String[] parts = correctMessage.split(",");
if (parts.length >= 3) {
String trayCode = parts[1];
String storeyCode = parts[2];
// 更新指令状态为已完成
robotArmCommandService.completeCommand(trayCode, storeyCode);
// 发送成功响应
String response = "CMD_COMPLETE_ACK";
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
ctx.writeAndFlush(new DatagramPacket(
io.netty.buffer.Unpooled.copiedBuffer(responseBytes),
packet.sender()));
}
}
// 回复客户端,明确使用UTF-8编码 // 当收到消息时,更新状态为运行中
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); sendStatusToWebSocket("running");
ctx.writeAndFlush(new DatagramPacket(
io.netty.buffer.Unpooled.copiedBuffer(responseBytes),
packet.sender()));
} catch (Exception e) { } catch (Exception e) {
log.error("处理UDP消息异常", e); log.error("处理UDP消息异常", e);
// 出现异常时发送故障状态
sendStatusToWebSocket("error");
} }
} }
...@@ -164,9 +191,25 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -164,9 +191,25 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
if (event.state() == IdleState.ALL_IDLE) { if (event.state() == IdleState.ALL_IDLE) {
log.info("UDP服务器超过规定时间未收到数据"); log.info("UDP服务器超过规定时间未收到数据");
// UDP无连接,一般不关闭通道 // UDP无连接,一般不关闭通道
// 通过WebSocket发送空闲状态给前端
sendStatusToWebSocket("idle");
// 处理空闲状态
robotArmCommandService.processPendingCommands();
} }
} else { } else {
super.userEventTriggered(ctx, evt); super.userEventTriggered(ctx, evt);
} }
} }
/**
* 发送状态到WebSocket
*/
private void sendStatusToWebSocket(String status) {
if (robotArmWebSocketHandler != null) {
robotArmWebSocketHandler.broadcastStatus(status);
} else {
log.warn("WebSocket处理器未初始化,无法发送状态");
}
}
} }
package com.zehong.system.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.zehong.common.annotation.Excel;
import com.zehong.common.core.domain.BaseEntity;
/**
* 机械臂指令对象 t_robot_arm_command
*
* @author zehong
* @date 2025-08-04
*/
public class RobotArmCommand extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** id */
private Long robotArmCommandId;
/** 托盘编号 */
@Excel(name = "托盘编号")
private String trayCode;
/** 绑定层编号 */
@Excel(name = "绑定层编号")
private String storeyCode;
/** 类型:0-待上料;1-待下料 */
@Excel(name = "类型:0-待上料;1-待下料")
private String type;
/** 状态:0-待执行;1-执行中;2-执行结束(上料就是绑定托盘,下料就是解绑托盘) */
@Excel(name = "状态:0-待执行;1-执行中;2-未上电,3-执行结束(上料就是绑定托盘,下料就是解绑托盘)")
private String status;
/** 指令开始执行时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "指令开始执行时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date startExecutionTime;
/** 指令结束执行时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "指令结束执行时间", width = 30, dateFormat = "yyyy-MM-dd")
private Date endExecutionTime;
/** 指令 */
private String command;
public void setRobotArmCommandId(Long robotArmCommandId)
{
this.robotArmCommandId = robotArmCommandId;
}
public Long getRobotArmCommandId()
{
return robotArmCommandId;
}
public void setTrayCode(String trayCode)
{
this.trayCode = trayCode;
}
public String getTrayCode()
{
return trayCode;
}
public void setStoreyCode(String storeyCode)
{
this.storeyCode = storeyCode;
}
public String getStoreyCode()
{
return storeyCode;
}
public void setType(String type)
{
this.type = type;
}
public String getType()
{
return type;
}
public void setStatus(String status)
{
this.status = status;
}
public String getStatus()
{
return status;
}
public void setStartExecutionTime(Date startExecutionTime)
{
this.startExecutionTime = startExecutionTime;
}
public Date getStartExecutionTime()
{
return startExecutionTime;
}
public void setEndExecutionTime(Date endExecutionTime)
{
this.endExecutionTime = endExecutionTime;
}
public Date getEndExecutionTime()
{
return endExecutionTime;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("robotArmCommandId", getRobotArmCommandId())
.append("trayCode", getTrayCode())
.append("storeyCode", getStoreyCode())
.append("type", getType())
.append("status", getStatus())
.append("startExecutionTime", getStartExecutionTime())
.append("endExecutionTime", getEndExecutionTime())
.append("createTime", getCreateTime())
.toString();
}
}
package com.zehong.system.mapper;
import java.util.List;
import com.zehong.system.domain.RobotArmCommand;
/**
* 机械臂指令Mapper接口
*
* @author zehong
* @date 2025-08-04
*/
public interface RobotArmCommandMapper
{
/**
* 查询机械臂指令
*
* @param robotArmCommandId 机械臂指令ID
* @return 机械臂指令
*/
public RobotArmCommand selectRobotArmCommandById(Long robotArmCommandId);
public RobotArmCommand findExecutingCommand(String trayCode, String storeyCode);
/**
* 查询机械臂指令列表
*
* @param robotArmCommand 机械臂指令
* @return 机械臂指令集合
*/
public List<RobotArmCommand> selectRobotArmCommandList(RobotArmCommand robotArmCommand);
public List<RobotArmCommand> findByType(String type);
/**
* 新增机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
public int insertRobotArmCommand(RobotArmCommand robotArmCommand);
/**
* 修改机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
public int updateRobotArmCommand(RobotArmCommand robotArmCommand);
/**
* 更新执行中状态为完成状态
*/
int updateExecutingToCompleted();
/**
* 获取待执行的上料指令
*/
List<RobotArmCommand> selectPendingLoadingCommands();
/**
* 获取待执行的下料指令
*/
List<RobotArmCommand> selectPendingUnloadingCommands();
/**
* 删除机械臂指令
*
* @param robotArmCommandId 机械臂指令ID
* @return 结果
*/
public int deleteRobotArmCommandById(Long robotArmCommandId);
/**
* 批量删除机械臂指令
*
* @param robotArmCommandIds 需要删除的数据ID
* @return 结果
*/
public int deleteRobotArmCommandByIds(Long[] robotArmCommandIds);
}
...@@ -27,6 +27,8 @@ public interface TStoreyInfoMapper ...@@ -27,6 +27,8 @@ public interface TStoreyInfoMapper
*/ */
public TStoreyInfo selectTStoreyInfoByCode(String fStoreyCode); public TStoreyInfo selectTStoreyInfoByCode(String fStoreyCode);
// 新增方法:查询离机械臂最近的空闲层
public TStoreyInfo selectNearestFreeStorey();
/** /**
* 查询老化层信息列表 * 查询老化层信息列表
* *
......
package com.zehong.system.service;
import java.util.List;
import com.zehong.system.domain.RobotArmCommand;
/**
* 机械臂指令Service接口
*
* @author zehong
* @date 2025-08-04
*/
public interface IRobotArmCommandService
{
/**
* 查询机械臂指令
*
* @param robotArmCommandId 机械臂指令ID
* @return 机械臂指令
*/
public RobotArmCommand selectRobotArmCommandById(Long robotArmCommandId);
/**
* 查询机械臂指令列表
*
* @param robotArmCommand 机械臂指令
* @return 机械臂指令集合
*/
public List<RobotArmCommand> selectRobotArmCommandList(RobotArmCommand robotArmCommand);
public List<RobotArmCommand> findByType(String type);
/**
* 新增机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
public int insertRobotArmCommand(RobotArmCommand robotArmCommand);
public void powerOnCommand(Long commandId);
/**
* 修改机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
public int updateRobotArmCommand(RobotArmCommand robotArmCommand);
/**
* 批量删除机械臂指令
*
* @param robotArmCommandIds 需要删除的机械臂指令ID
* @return 结果
*/
public int deleteRobotArmCommandByIds(Long[] robotArmCommandIds);
/**
* 删除机械臂指令信息
*
* @param robotArmCommandId 机械臂指令ID
* @return 结果
*/
public int deleteRobotArmCommandById(Long robotArmCommandId);
public void processIdleState();
public void processPendingCommands();
public void completeCommand(String trayCode,String storeyCode);
}
package com.zehong.system.service.impl;
import java.util.Date;
import java.util.List;
import com.zehong.common.utils.DateUtils;
import com.zehong.system.domain.TStoreyInfo;
import com.zehong.system.mapper.TStoreyInfoMapper;
import com.zehong.system.service.websocket.RobotArmWebSocketHandler;
import com.zehong.system.udp.UdpCommandSender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.zehong.system.mapper.RobotArmCommandMapper;
import com.zehong.system.domain.RobotArmCommand;
import com.zehong.system.service.IRobotArmCommandService;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
/**
* 机械臂指令Service业务层处理
*
* @author zehong
* @date 2025-08-04
*/
@Service
public class RobotArmCommandServiceImpl implements IRobotArmCommandService
{
private static final Logger log = LoggerFactory.getLogger(RobotArmCommandServiceImpl.class);
@Resource
private RobotArmWebSocketHandler robotArmWebSocketHandler;
@Resource
private RobotArmCommandMapper robotArmCommandMapper;
@Resource
private TStoreyInfoMapper storeyInfoMapper;
@Resource
private UdpCommandSender udpCommandSender;
@Override
@Transactional
public void processIdleState() {
// 1. 更新执行中状态为完成状态
robotArmCommandMapper.updateExecutingToCompleted();
// 2. 优先处理上料指令
List<RobotArmCommand> loadingCommands = robotArmCommandMapper.selectPendingLoadingCommands();
if (!loadingCommands.isEmpty()) {
RobotArmCommand command = loadingCommands.get(0);
sendLoadingCommand(command);
return;
}
// 3. 处理下料指令
List<RobotArmCommand> unloadingCommands = robotArmCommandMapper.selectPendingUnloadingCommands();
if (!unloadingCommands.isEmpty()) {
RobotArmCommand command = unloadingCommands.get(0);
sendUnloadingCommand(command);
}
}
@Override
@Transactional
public void processPendingCommands() {
// 1. 只处理待执行指令(状态为0)
List<RobotArmCommand> loadingCommands = robotArmCommandMapper.selectPendingLoadingCommands();
if (!loadingCommands.isEmpty()) {
RobotArmCommand command = loadingCommands.get(0);
sendLoadingCommand(command);
return;
}
// 2. 处理待执行的下料指令
List<RobotArmCommand> unloadingCommands = robotArmCommandMapper.selectPendingUnloadingCommands();
if (!unloadingCommands.isEmpty()) {
RobotArmCommand command = unloadingCommands.get(0);
sendUnloadingCommand(command);
}
}
@Override
@Transactional
public void completeCommand(String trayCode, String storeyCode) {
// 1. 查找对应的执行中指令
RobotArmCommand command = robotArmCommandMapper.findExecutingCommand(trayCode, storeyCode);
if (command == null) {
log.warn("未找到对应的执行中指令: {} @ {}", trayCode, storeyCode);
return;
}
// 2. 更新状态为已完成
command.setStatus("3");
command.setEndExecutionTime(new Date());
robotArmCommandMapper.updateRobotArmCommand(command);
log.info("指令完成: {} @ {}", trayCode, storeyCode);
// 3. 广播指令更新
robotArmWebSocketHandler.broadcastCommandUpdate();
}
private void sendLoadingCommand(RobotArmCommand command) {
// 更新状态为执行中
command.setStatus("1");
command.setStartExecutionTime(new Date());
robotArmCommandMapper.updateRobotArmCommand(command);
notifyCommandsUpdate();
// 发送UDP指令
String udpMessage = String.format("LOAD,%s,%s", command.getTrayCode(), command.getStoreyCode());
udpCommandSender.sendCommand(udpMessage);
}
private void sendUnloadingCommand(RobotArmCommand command) {
// 更新状态为执行中
command.setStatus("1");
command.setStartExecutionTime(new Date());
robotArmCommandMapper.updateRobotArmCommand(command);
notifyCommandsUpdate();
// 发送UDP指令
String udpMessage = String.format("UNLOAD,%s,%s", command.getTrayCode(), command.getStoreyCode());
udpCommandSender.sendCommand(udpMessage);
}
/**
* 查询机械臂指令
*
* @param robotArmCommandId 机械臂指令ID
* @return 机械臂指令
*/
@Override
public RobotArmCommand selectRobotArmCommandById(Long robotArmCommandId)
{
return robotArmCommandMapper.selectRobotArmCommandById(robotArmCommandId);
}
/**
* 查询机械臂指令列表
*
* @param robotArmCommand 机械臂指令
* @return 机械臂指令
*/
@Override
public List<RobotArmCommand> selectRobotArmCommandList(RobotArmCommand robotArmCommand)
{
return robotArmCommandMapper.selectRobotArmCommandList(robotArmCommand);
}
@Override
public List<RobotArmCommand> findByType(String type) {
return robotArmCommandMapper.findByType( type);
}
/**
* 新增机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
@Override
public int insertRobotArmCommand(RobotArmCommand robotArmCommand)
{
robotArmCommand.setCreateTime(DateUtils.getNowDate());
TStoreyInfo tStoreyInfo = storeyInfoMapper.selectNearestFreeStorey();
if(tStoreyInfo != null) {
robotArmCommand.setStoreyCode(tStoreyInfo.getfStoreyCode());
} else {
robotArmCommand.setStoreyCode("无空闲老化层");
}
int i = robotArmCommandMapper.insertRobotArmCommand(robotArmCommand);
notifyCommandsUpdate();
return i;
}
@Override
@Transactional
public void powerOnCommand(Long commandId) {
RobotArmCommand command = robotArmCommandMapper.selectRobotArmCommandById(commandId);
if (command == null) {
throw new RuntimeException("指令不存在");
}
if (!"2".equals(command.getStatus())) {
throw new RuntimeException("只有未上电状态的指令才能执行上电操作");
}
// 更新状态为执行中
command.setStatus("1");
robotArmCommandMapper.updateRobotArmCommand(command);
// 发送上电指令给机械臂
String udpMessage = String.format("POWER_ON,%s,%s", command.getTrayCode(), command.getStoreyCode());
udpCommandSender.sendCommand(udpMessage);
// 记录操作日志
log.info("执行上电操作: 指令ID={}, 托盘={}, 位置={}", commandId, command.getTrayCode(), command.getStoreyCode());
}
/**
* 修改机械臂指令
*
* @param robotArmCommand 机械臂指令
* @return 结果
*/
@Override
public int updateRobotArmCommand(RobotArmCommand robotArmCommand)
{
int i = robotArmCommandMapper.updateRobotArmCommand(robotArmCommand);
notifyCommandsUpdate();
return i;
}
/**
* 批量删除机械臂指令
*
* @param robotArmCommandIds 需要删除的机械臂指令ID
* @return 结果
*/
@Override
public int deleteRobotArmCommandByIds(Long[] robotArmCommandIds)
{
int i = robotArmCommandMapper.deleteRobotArmCommandByIds(robotArmCommandIds);
notifyCommandsUpdate();
return i;
}
/**
* 删除机械臂指令信息
*
* @param robotArmCommandId 机械臂指令ID
* @return 结果
*/
@Override
public int deleteRobotArmCommandById(Long robotArmCommandId)
{
int i = robotArmCommandMapper.deleteRobotArmCommandById(robotArmCommandId);
notifyCommandsUpdate();
return i;
}
private void notifyCommandsUpdate() {
robotArmWebSocketHandler.broadcastCommandUpdate();
}
}
package com.zehong.system.service.websocket;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zehong.system.domain.RobotArmCommand;
import com.zehong.system.service.IRobotArmCommandService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author lenovo
* @date 2025/8/4
* @description websocketHandler
*/
@Component
public class RobotArmWebSocketHandler extends TextWebSocketHandler {
private static final List<WebSocketSession> sessions = new CopyOnWriteArrayList<>();
private static final Logger log = LoggerFactory.getLogger(RobotArmWebSocketHandler.class);
@Resource
private IRobotArmCommandService robotArmCommandService;
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
sessions.add(session);
sendInitialData(session);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
// 处理客户端请求
if ("{\"type\":\"request\",\"commands\":[\"loading\",\"unloading\"]}".equals(payload)) {
sendInitialData(session);
}
}
private void sendInitialData(WebSocketSession session) throws IOException {
// 发送待上料指令
List<RobotArmCommand> loadingCommands = robotArmCommandService.findByType("0");
session.sendMessage(new TextMessage(createMessage("loading", loadingCommands)));
// 发送待下料指令
List<RobotArmCommand> unloadingCommands = robotArmCommandService.findByType("1");
session.sendMessage(new TextMessage(createMessage("unloading", unloadingCommands)));
}
private String createMessage(String type, List<RobotArmCommand> commands) {
ObjectMapper mapper = new ObjectMapper();
try {
// 创建包含更多信息的DTO列表
List<Map<String, Object>> commandData = new ArrayList<>();
for (RobotArmCommand cmd : commands) {
Map<String, Object> cmdMap = new HashMap<>();
cmdMap.put("robotArmCommandId", cmd.getRobotArmCommandId());
cmdMap.put("trayCode", cmd.getTrayCode());
cmdMap.put("storeyCode", cmd.getStoreyCode());
cmdMap.put("status", cmd.getStatus());
commandData.add(cmdMap);
}
Map<String, Object> message = new HashMap<>();
message.put("type", type);
message.put("data", commandData);
return mapper.writeValueAsString(message);
} catch (JsonProcessingException e) {
return "{\"error\":\"Failed to serialize data\"}";
}
}
public void broadcastCommandUpdate() {
List<RobotArmCommand> loadingCommands = robotArmCommandService.findByType("0");
List<RobotArmCommand> unloadingCommands = robotArmCommandService.findByType("1");
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(createMessage("loading", loadingCommands)));
session.sendMessage(new TextMessage(createMessage("unloading", unloadingCommands)));
} catch (IOException e) {
// 处理异常
}
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
}
/**
* 广播状态消息给所有客户端
*/
public void broadcastStatus(String status) {
ObjectMapper mapper = new ObjectMapper();
try {
Map<String, Object> message = new HashMap<>();
message.put("type", "status");
message.put("data", status);
String jsonMessage = mapper.writeValueAsString(message);
for (WebSocketSession session : sessions) {
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage(jsonMessage));
} catch (IOException e) {
log.error("发送状态消息到WebSocket失败", e);
}
}
}
} catch (JsonProcessingException e) {
log.error("序列化状态消息失败", e);
}
}
}
package com.zehong.system.service.websocket;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
/**
* @author lenovo
* @date 2025/8/4
* @description TODO
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private RobotArmWebSocketHandler robotArmWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(robotArmWebSocketHandler, "/ws-robot-arm")
.setAllowedOrigins("*");
}
}
package com.zehong.system.udp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
/**
* @author lenovo
* @date 2025/8/4
* @description TODO
*/
@Component
public class UdpCommandSender {
private static final Logger log = LoggerFactory.getLogger(UdpCommandSender.class);
@Value("${robot.arm.udp.ip}")
private String robotArmIp;
@Value("${robot.arm.udp.port}")
private int robotArmPort;
private DatagramSocket socket;
private InetAddress address;
@PostConstruct
public void init() {
try {
socket = new DatagramSocket();
address = InetAddress.getByName(robotArmIp);
log.info("UDP命令发送器初始化成功,目标地址: {}:{}", robotArmIp, robotArmPort);
} catch (Exception e) {
log.error("UDP命令发送器初始化失败", e);
}
}
public void sendCommand(String message) {
if (socket == null || address == null) {
log.error("UDP命令发送器未初始化,无法发送消息");
return;
}
try {
byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, robotArmPort);
socket.send(packet);
log.info("已发送UDP指令: {}", message);
} catch (IOException e) {
log.error("发送UDP指令失败: {}", message, e);
}
}
@PreDestroy
public void cleanup() {
if (socket != null && !socket.isClosed()) {
socket.close();
log.info("UDP命令发送器已关闭");
}
}
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zehong.system.mapper.RobotArmCommandMapper">
<resultMap type="RobotArmCommand" id="RobotArmCommandResult">
<result property="robotArmCommandId" column="f_robot_arm_command_id" />
<result property="trayCode" column="f_tray_code" />
<result property="storeyCode" column="f_storey_code" />
<result property="type" column="f_type" />
<result property="status" column="f_status" />
<result property="startExecutionTime" column="f_start_execution_time" />
<result property="endExecutionTime" column="f_end_execution_time" />
<result property="createTime" column="f_create_time" />
</resultMap>
<sql id="selectRobotArmCommandVo">
select f_robot_arm_command_id, f_tray_code, f_storey_code, f_type, f_status, f_start_execution_time, f_end_execution_time, f_create_time, f_command from t_robot_arm_command
</sql>
<select id="findByType" parameterType="string" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
where f_type = #{type}
</select>
<select id="selectRobotArmCommandList" parameterType="RobotArmCommand" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
<where>
<if test="trayCode != null and trayCode != ''"> and f_tray_code = #{trayCode}</if>
<if test="storeyCode != null and storeyCode != ''"> and f_storey_code = #{storeyCode}</if>
<if test="type != null and type != ''"> and f_type = #{type}</if>
<if test="status != null and status != ''"> and f_status = #{status}</if>
<if test="startExecutionTime != null "> and f_start_execution_time = #{startExecutionTime}</if>
<if test="endExecutionTime != null "> and f_end_execution_time = #{endExecutionTime}</if>
<if test="createTime != null "> and f_create_time = #{createTime}</if>
</where>
</select>
<select id="selectRobotArmCommandById" parameterType="Long" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
where f_robot_arm_command_id = #{robotArmCommandId}
</select>
<!-- 查找执行中的指令 -->
<select id="findExecutingCommand" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
WHERE f_tray_code = #{trayCode}
AND f_storey_code = #{storeyCode}
AND f_status = '1' <!-- 状态为执行中 -->
LIMIT 1
</select>
<insert id="insertRobotArmCommand" parameterType="RobotArmCommand" useGeneratedKeys="true" keyProperty="robotArmCommandId">
insert into t_robot_arm_command
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="trayCode != null">f_tray_code,</if>
<if test="storeyCode != null">f_storey_code,</if>
<if test="type != null">f_type,</if>
<if test="status != null">f_status,</if>
<if test="startExecutionTime != null">f_start_execution_time,</if>
<if test="endExecutionTime != null">f_end_execution_time,</if>
<if test="createTime != null">f_create_time,</if>
<if test="command != null">f_command,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="trayCode != null">#{trayCode},</if>
<if test="storeyCode != null">#{storeyCode},</if>
<if test="type != null">#{type},</if>
<if test="status != null">#{status},</if>
<if test="startExecutionTime != null">#{startExecutionTime},</if>
<if test="endExecutionTime != null">#{endExecutionTime},</if>
<if test="createTime != null">#{createTime},</if>
<if test="command != null">#{command},</if>
</trim>
</insert>
<update id="updateRobotArmCommand" parameterType="RobotArmCommand">
update t_robot_arm_command
<trim prefix="SET" suffixOverrides=",">
<if test="trayCode != null">f_tray_code = #{trayCode},</if>
<if test="storeyCode != null">f_storey_code = #{storeyCode},</if>
<if test="type != null">f_type = #{type},</if>
<if test="status != null">f_status = #{status},</if>
<if test="startExecutionTime != null">f_start_execution_time = #{startExecutionTime},</if>
<if test="endExecutionTime != null">f_end_execution_time = #{endExecutionTime},</if>
<if test="createTime != null">f_create_time = #{createTime},</if>
<if test="command != null">f_command = #{command},</if>
</trim>
where f_robot_arm_command_id = #{robotArmCommandId}
</update>
<!-- 在 XML 中添加 -->
<!-- 更新执行中状态为完成状态 -->
<update id="updateExecutingToCompleted">
UPDATE t_robot_arm_command
SET f_status = '2'
WHERE f_status = '1' and f_end_execution_time IS NOT NULL
</update>
<!-- 获取待执行的上料指令 -->
<select id="selectPendingLoadingCommands" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
WHERE f_type = '0' AND f_status = '0'
ORDER BY f_create_time ASC
LIMIT 1
</select>
<!-- 获取待执行的下料指令 -->
<select id="selectPendingUnloadingCommands" resultMap="RobotArmCommandResult">
<include refid="selectRobotArmCommandVo"/>
WHERE f_type = '1' AND f_status = '0'
ORDER BY f_create_time ASC
LIMIT 1
</select>
<delete id="deleteRobotArmCommandById" parameterType="Long">
delete from t_robot_arm_command where f_robot_arm_command_id = #{robotArmCommandId}
</delete>
<delete id="deleteRobotArmCommandByIds" parameterType="String">
delete from t_robot_arm_command where f_robot_arm_command_id in
<foreach item="robotArmCommandId" collection="array" open="(" separator="," close=")">
#{robotArmCommandId}
</foreach>
</delete>
</mapper>
\ No newline at end of file
...@@ -48,7 +48,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" ...@@ -48,7 +48,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectTStoreyInfoVo"/> <include refid="selectTStoreyInfoVo"/>
where f_storey_code = #{fStoreyCode} where f_storey_code = #{fStoreyCode}
</select> </select>
<select id="selectNearestFreeStorey" resultMap="TStoreyInfoResult">
SELECT
f_storey_id, f_equipment_id, f_storey_code, f_ip, f_status, f_port,
f_aging_start_time, f_update_time, f_create_time, f_alarm_time
FROM (
SELECT
*,
CASE
WHEN cabinet_num BETWEEN 1 AND 18 THEN (cabinet_num - 1) * 10 + layer_num
WHEN cabinet_num BETWEEN 19 AND 36 THEN (cabinet_num - 19) * 10 + layer_num
END AS distance
FROM (
SELECT
*,
CAST(SUBSTRING_INDEX(f_storey_code, '-', 1) AS UNSIGNED) AS cabinet_num,
CAST(SUBSTRING_INDEX(f_storey_code, '-', -1) AS UNSIGNED) AS layer_num
FROM t_storey_info
WHERE f_status = '0' -- 空闲状态
) AS parsed
) AS calculated
ORDER BY distance ASC
LIMIT 1
</select>
<insert id="insertTStoreyInfo" parameterType="TStoreyInfo"> <insert id="insertTStoreyInfo" parameterType="TStoreyInfo">
insert into t_storey_info insert into t_storey_info
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
......
import request from '@/utils/request'
// 查询机械臂指令列表
export function listCommand(query) {
return request({
url: '/robotArm/command/list',
method: 'get',
params: query
})
}
// 查询机械臂指令详细
export function getCommand(robotArmCommandId) {
return request({
url: '/robotArm/command/' + robotArmCommandId,
method: 'get'
})
}
// 新增机械臂指令
export function addCommand(data) {
return request({
url: '/robotArm/command',
method: 'post',
data: data
})
}
// 执行上电操作
export function powerOnCommand(commandId) {
return request({
url: '/robotArm/command/powerOn',
method: 'post',
data: { commandId }
})
}
// 修改机械臂指令
export function updateCommand(data) {
return request({
url: '/robotArm/command',
method: 'put',
data: data
})
}
// 删除机械臂指令
export function delCommand(robotArmCommandId) {
return request({
url: '/robotArm/command/' + robotArmCommandId,
method: 'delete'
})
}
// 导出机械臂指令
export function exportCommand(query) {
return request({
url: '/robotArm/command/export',
method: 'get',
params: query
})
}
<template> <template>
<div class="robotic-arm-panel"> <div class="robotic-arm-panel">
<!-- 左上角标题 -->
<!-- 标题区域 --> <!-- 标题区域 -->
<div class="panel-title"> <div class="panel-title">
<div class="title-text">机械臂</div> <!-- 左侧标题+状态指示灯组合 -->
<div class="title-line"></div> <div class="title-with-status">
<div class="board-header"></div> <div class="title-left">
<div class="title-text">机械臂</div>
<div class="title-line"></div>
</div>
<!-- 状态指示灯:紧挨着标题文字右侧 -->
<div class="status-indicator">
<div class="status-light" :class="statusClass"></div>
<div class="status-text">{{ statusText }}</div>
</div>
</div>
<!-- 上料按钮 -->
<div class="title-right">
<button class="add-button" @click="showAddDialog = true">
<i class="el-icon-plus"></i> 上料
</button>
</div>
</div> </div>
<!-- 状态指示灯 --> <!-- 扫码对话框 -->
<div class="status-indicator"> <div class="dialog-mask" v-if="showAddDialog" @click.self="closeDialog">
<div class="status-light" :class="statusClass"></div> <div class="dialog-container">
<div class="status-text">{{ statusText }}</div> <div class="dialog-header">上料操作</div>
<div class="dialog-body">
<div class="dialog-content">
<div class="scan-prompt">请扫描托盘二维码</div>
<div class="scan-input">
<input
type="text"
v-model="trayCode"
placeholder="手动输入或扫码"
ref="trayInput"
@keyup.enter="confirmAdd"
>
</div>
</div>
</div>
<div class="dialog-footer">
<button class="cancel-button" @click="closeDialog">取消</button>
<button class="confirm-button" @click="confirmAdd">确定</button>
</div>
</div>
</div> </div>
<!-- 主内容区:指令区+机械臂 --> <!-- 主内容区:指令区+机械臂 -->
...@@ -21,10 +55,17 @@ ...@@ -21,10 +55,17 @@
<div class="loading-command"> <div class="loading-command">
<div class="command-title">待上料指令</div> <div class="command-title">待上料指令</div>
<div class="command-list"> <div class="command-list">
<div v-for="(cmd, index) in loadingCommands" :key="index" class="command-item"> <div
v-for="(cmd, index) in loadingCommands"
:key="index"
class="command-item"
:class="getCommandStatusClass(cmd.status)"
@click="handleCommandClick(cmd)"
>
<div class="cmd-info"> <div class="cmd-info">
<div class="cmd-tray">托盘: {{ cmd.tray }}</div> <div class="cmd-tray">托盘: {{ cmd.trayCode }}</div>
<div class="cmd-position">位置: {{ cmd.position }}</div> <div class="cmd-position">位置: {{ cmd.position }}</div>
<div class="cmd-status">状态: {{ getStatusText(cmd.status) }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -34,33 +75,23 @@ ...@@ -34,33 +75,23 @@
<div class="arm-center-wrapper"> <div class="arm-center-wrapper">
<div class="robotic-arm-container"> <div class="robotic-arm-container">
<div class="robotic-arm"> <div class="robotic-arm">
<!-- 机械臂底座 --> <!-- 机械臂各部件 -->
<div class="arm-base"> <div class="arm-base">
<div class="base-top"></div> <div class="base-top"></div>
<div class="base-bottom"></div> <div class="base-bottom"></div>
</div> </div>
<!-- 机械臂关节1 -->
<div class="arm-joint joint-1"> <div class="arm-joint joint-1">
<div class="joint-body"></div> <div class="joint-body"></div>
</div> </div>
<!-- 机械臂臂1 -->
<div class="arm-segment segment-1"> <div class="arm-segment segment-1">
<div class="segment-body"></div> <div class="segment-body"></div>
</div> </div>
<!-- 机械臂关节2 -->
<div class="arm-joint joint-2"> <div class="arm-joint joint-2">
<div class="joint-body"></div> <div class="joint-body"></div>
</div> </div>
<!-- 机械臂臂2 -->
<div class="arm-segment segment-2"> <div class="arm-segment segment-2">
<div class="segment-body"></div> <div class="segment-body"></div>
</div> </div>
<!-- 机械臂夹具 -->
<div class="arm-gripper"> <div class="arm-gripper">
<div class="gripper-left"></div> <div class="gripper-left"></div>
<div class="gripper-right"></div> <div class="gripper-right"></div>
...@@ -73,53 +104,90 @@ ...@@ -73,53 +104,90 @@
<div class="unloading-command"> <div class="unloading-command">
<div class="command-title">待下料指令</div> <div class="command-title">待下料指令</div>
<div class="command-list"> <div class="command-list">
<div v-for="(cmd, index) in unloadingCommands" :key="index" class="command-item"> <div
v-for="(cmd, index) in unloadingCommands"
:key="index"
class="command-item"
:class="getCommandStatusClass(cmd.status)"
@click="handleCommandClick(cmd)"
>
<div class="cmd-info"> <div class="cmd-info">
<div class="cmd-tray">托盘: {{ cmd.tray }}</div> <div class="cmd-tray">托盘: {{ cmd.trayCode }}</div>
<div class="cmd-position">位置: {{ cmd.position }}</div> <div class="cmd-position">位置: {{ cmd.position }}</div>
<div class="cmd-status">状态: {{ getStatusText(cmd.status) }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- 上电对话框 -->
<div class="dialog-mask" v-if="showPowerOnDialog" @click.self="closePowerOnDialog">
<div class="dialog-container">
<div class="dialog-header">上电操作</div>
<div class="dialog-body">
<div class="dialog-content">
<div class="scan-prompt">确认对以下托盘执行上电操作?</div>
<div class="power-on-info">
<div><span class="label">托盘编号:</span> {{ selectedCommand.trayCode }}</div>
<div><span class="label">位置:</span> {{ selectedCommand.position }}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="dialog-footer">
<button class="cancel-button" @click="closePowerOnDialog">取消</button>
<button class="confirm-button" @click="confirmPowerOn">确认上电</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import {addCommand,powerOnCommand} from "@/api/robotArm/robotArmCommand"
export default { export default {
name: 'RoboticArm', name: 'RoboticArm',
data() { data() {
return { return {
status: 'idle', // idle, running, error status: 'idle', // idle, running, error
showAddDialog: false,
trayCode: '',
loadingCommands: [ loadingCommands: [
{ tray: 'TP-10023', position: 'A区-3号架' }, { trayCode: 'TP-10023', position: 'A区-3号架' },
{ tray: 'TP-10045', position: 'B区-1号架' }, { trayCode: 'TP-10045', position: 'B区-1号架' },
{ tray: 'TP-10067', position: 'C区-5号架' }, { trayCode: 'TP-10067', position: 'C区-5号架' },
{ tray: 'TP-10023', position: 'A区-3号架' }, { trayCode: 'TP-10023', position: 'A区-3号架' },
{ tray: 'TP-10045', position: 'B区-1号架' }, { trayCode: 'TP-10045', position: 'B区-1号架' },
{ tray: 'TP-10067', position: 'C区-5号架' }, { trayCode: 'TP-10067', position: 'C区-5号架' },
{ tray: 'TP-10023', position: 'A区-3号架' }, { trayCode: 'TP-10023', position: 'A区-3号架' },
{ tray: 'TP-10045', position: 'B区-1号架' }, { trayCode: 'TP-10045', position: 'B区-1号架' },
{ tray: 'TP-10067', position: 'C区-5号架' }, { trayCode: 'TP-10067', position: 'C区-5号架' },
{ tray: 'TP-10023', position: 'A区-3号架' }, { trayCode: 'TP-10023', position: 'A区-3号架' },
{ tray: 'TP-10045', position: 'B区-1号架' }, { trayCode: 'TP-10045', position: 'B区-1号架' },
{ tray: 'TP-10067', position: 'C区-5号架' } { trayCode: 'TP-10067', position: 'C区-5号架' }
], ],
unloadingCommands: [ unloadingCommands: [
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' }, { trayCode: 'TP-10101', position: '老化区-3号柜' },
{ tray: 'TP-10089', position: '老化区-1号柜' }, { trayCode: 'TP-10089', position: '老化区-1号柜' },
{ tray: 'TP-10101', position: '老化区-3号柜' } { trayCode: 'TP-10101', position: '老化区-3号柜' }
] ],
websocket: null,
reconnectInterval: null,
showPowerOnDialog: false,
selectedCommand: null
}; };
}, },
computed: { computed: {
...@@ -139,35 +207,233 @@ export default { ...@@ -139,35 +207,233 @@ export default {
} }
}, },
mounted() { mounted() {
this.simulateStatus(); this.initWebSocket();
},
beforeDestroy() {
this.disconnectWebSocket();
},
watch: {
showAddDialog(val) {
if (val) {
this.$nextTick(() => {
this.$refs.trayInput.focus();
});
} else {
this.trayCode = '';
}
}
}, },
methods: { methods: {
simulateStatus() { initWebSocket() {
setInterval(() => { const backendUrl = process.env.VUE_APP_API_BASE_URL || 'http://localhost:8080';
const statuses = ['idle', 'running', 'error']; const wsUrl = backendUrl.replace('http', 'ws') + '/ws-robot-arm';
const randomIndex = Math.floor(Math.random() * statuses.length);
this.status = statuses[randomIndex]; try {
}, 10000); this.websocket = new WebSocket(wsUrl);
this.websocket.onopen = () => {
console.log('机械臂指令WebSocket连接成功');
this.status = 'running';
this.sendWebSocketMessage({ type: 'request', commands: ['loading', 'unloading'] });
};
this.websocket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
if (message.type === 'loading') {
this.loadingCommands = message.data.map(cmd => ({
robotArmCommandId: cmd.robotArmCommandId,
trayCode: cmd.trayCode,
position: cmd.storeyCode,
status: cmd.status || '0' // 默认待执行状态
}));
}
else if (message.type === 'unloading') {
this.unloadingCommands = message.data.map(cmd => ({
robotArmCommandId: cmd.robotArmCommandId,
trayCode: cmd.trayCode,
position: cmd.storeyCode,
status: cmd.status || '0' // 默认待执行状态
}));
}
// 新增:处理状态更新消息
else if (message.type === 'status') {
this.status = message.data; // 更新机械臂状态
}
} catch (e) {
console.error('解析WebSocket消息失败:', e);
}
};
this.websocket.onerror = (error) => {
console.error('WebSocket错误:', error);
this.status = 'error';
this.scheduleReconnect();
};
this.websocket.onclose = () => {
console.log('WebSocket连接关闭');
this.scheduleReconnect();
};
} catch (e) {
console.error('创建WebSocket失败:', e);
this.scheduleReconnect();
}
},
sendWebSocketMessage(message) {
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify(message));
}
},
scheduleReconnect() {
if (this.reconnectInterval) clearInterval(this.reconnectInterval);
this.reconnectInterval = setInterval(() => {
if (!this.websocket || this.websocket.readyState !== WebSocket.OPEN) {
console.log('尝试重新连接WebSocket...');
this.disconnectWebSocket();
this.initWebSocket();
}
}, 5000);
},
disconnectWebSocket() {
if (this.websocket) {
try {
this.websocket.close();
} catch (e) {
console.error('关闭WebSocket时出错:', e);
}
}
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
}
this.websocket = null;
},
closeDialog() {
this.showAddDialog = false;
},
confirmAdd() {
if (!this.trayCode.trim()) {
alert('请输入托盘编号');
return;
}
const robotArmCommand = {
trayCode: this.trayCode,
storeyCode: '待分配位置',
type: '0'
};
addCommand(robotArmCommand).then(res => {
if(res.code === 200) {
this.msgSuccess("添加成功");
this.showAddDialog = false;
} else {
this.msgSuccess("添加失败");
}
})
},
getCommandStatusClass(status) {
return {
'status-pending': status == '0', // 待执行
'status-executing': status == '1', // 执行中
'status-poweroff': status == '2', // 未上电
'status-completed': status == '3' // 执行结束
};
},
getStatusText(status) {
const statusMap = {
'0': '待执行',
'1': '执行中',
'2': '未上电',
'3': '执行结束'
};
return statusMap[status] || '未知状态';
},
handleCommandClick(cmd) {
// 只有未上电状态的指令才可点击
if (cmd.status === '2') {
this.selectedCommand = cmd;
this.showPowerOnDialog = true;
}
},
closePowerOnDialog() {
this.showPowerOnDialog = false;
this.selectedCommand = null;
},
confirmPowerOn() {
if (!this.selectedCommand) return;
powerOnCommand(this.selectedCommand.robotArmCommandId).then(res => {
if(res.code === 200) {
this.msgSuccess("上电操作成功");
// 更新本地指令状态
this.updateCommandStatus(this.selectedCommand.robotArmCommandId, '1');
this.closePowerOnDialog();
} else {
this.msgError("上电操作失败");
}
}).catch(err => {
this.msgError("上电操作失败");
});
},
updateCommandStatus(commandId, newStatus) {
// 更新上料指令
const loadingIndex = this.loadingCommands.findIndex(c => c.robotArmCommandId === commandId);
if (loadingIndex !== -1) {
this.loadingCommands[loadingIndex].status = newStatus;
return;
}
// 更新下料指令
const unloadingIndex = this.unloadingCommands.findIndex(c => c.robotArmCommandId === commandId);
if (unloadingIndex !== -1) {
this.unloadingCommands[unloadingIndex].status = newStatus;
}
} }
} }
}; };
</script> </script>
<style scoped> <style scoped>
/* 左上角标题样式 */ /* 标题区域样式 */
.panel-title { .panel-title {
position: absolute; position: absolute;
top: 20px; top: 20px;
left: 20px; left: 20px;
right: 20px; right: 20px;
z-index: 20; /* 确保在其他元素上方 */ z-index: 20;
display: flex;
align-items: center;
justify-content: space-between; /* 标题组合居左,按钮居右 */
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(64, 158, 255, 0.3);
} }
/* 标题区域横线样式 - 放在title-line下方 */ /* 标题+状态指示灯组合容器:水平排列 */
.board-header { .title-with-status {
border-bottom: 1px solid rgba(64, 158, 255, 0.3); display: flex;
padding-right: 20px; align-items: center; /* 垂直居中对齐 */
width: 100%; /* 横线全屏宽度 */ gap: 15px; /* 标题与状态指示灯之间的间距 */
}
.title-left {
display: flex;
flex-direction: column;
} }
.title-text { .title-text {
...@@ -186,41 +452,23 @@ export default { ...@@ -186,41 +452,23 @@ export default {
border-radius: 2px; border-radius: 2px;
} }
/* 外层容器:强制填满父元素高度 */ /* 状态指示灯:紧挨着标题文字右侧 */
.robotic-arm-panel {
width: 100%;
height: 100%;
min-height: 100%;
background: rgba(10, 20, 40, 0.85);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
border: 1px solid rgba(64, 158, 255, 0.3);
box-shadow: 0 0 20px rgba(0, 50, 120, 0.3);
box-sizing: border-box;
overflow: hidden;
position: relative; /* 为标题绝对定位提供参考 */
}
/* 状态指示灯区域 - 调整位置避免与标题重叠 */
.status-indicator { .status-indicator {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 20px; padding: 6px 12px;
padding: 10px;
background: rgba(0, 30, 60, 0.4); background: rgba(0, 30, 60, 0.4);
border-radius: 8px; border-radius: 20px;
width: fit-content; border: 1px solid rgba(64, 158, 255, 0.3);
margin-top: 60px; /* 增加顶部间距避开标题 */ margin: 0; /* 清除原有margin */
} }
.status-light { .status-light {
width: 16px; width: 12px;
height: 16px; height: 12px;
border-radius: 50%; border-radius: 50%;
margin-right: 12px; margin-right: 8px;
box-shadow: 0 0 10px currentColor; box-shadow: 0 0 6px currentColor;
} }
.status-light.idle { .status-light.idle {
...@@ -240,33 +488,174 @@ export default { ...@@ -240,33 +488,174 @@ export default {
animation: blink 1s infinite; animation: blink 1s infinite;
} }
@keyframes pulse { .status-text {
0% { opacity: 0.6; } font-size: 14px;
50% { opacity: 1; } font-weight: bold;
100% { opacity: 0.6; } color: #64c8ff;
} }
@keyframes blink { .title-right {
0%, 100% { opacity: 1; } display: flex;
50% { opacity: 0.3; } align-items: center;
} }
.status-text { .add-button {
background: linear-gradient(to right, #409EFF, #64c8ff);
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
display: flex;
align-items: center;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
transition: all 0.3s;
}
.add-button:hover {
background: linear-gradient(to right, #64c8ff, #409EFF);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.4);
}
.add-button i {
margin-right: 5px;
}
/* 对话框样式(不变) */
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.dialog-container {
width: 400px;
background: linear-gradient(145deg, #1a2a3a, #0f1c2a);
border-radius: 10px;
border: 1px solid rgba(64, 158, 255, 0.4);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.7);
overflow: hidden;
}
.dialog-header {
padding: 16px 20px;
background: rgba(0, 30, 60, 0.8);
border-bottom: 1px solid rgba(64, 158, 255, 0.3);
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
color: #64c8ff; color: #64c8ff;
text-align: center;
}
.dialog-body {
padding: 30px 20px;
}
.dialog-content {
text-align: center;
}
.scan-prompt {
font-size: 18px;
color: #a0d2ff;
margin-bottom: 20px;
}
.scan-input input {
width: 100%;
padding: 12px 15px;
background: rgba(0, 20, 40, 0.6);
border: 1px solid rgba(64, 158, 255, 0.5);
border-radius: 6px;
color: white;
font-size: 16px;
outline: none;
transition: all 0.3s;
}
.scan-input input:focus {
border-color: #409EFF;
box-shadow: 0 0 10px rgba(64, 158, 255, 0.5);
}
.dialog-footer {
padding: 15px 20px;
display: flex;
justify-content: flex-end;
background: rgba(0, 20, 40, 0.5);
border-top: 1px solid rgba(64, 158, 255, 0.3);
}
.cancel-button, .confirm-button {
padding: 8px 20px;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
margin-left: 10px;
}
.cancel-button {
background: rgba(100, 100, 100, 0.4);
color: #a0d2ff;
border: 1px solid rgba(100, 150, 255, 0.3);
}
.cancel-button:hover {
background: rgba(120, 120, 120, 0.5);
border-color: rgba(100, 150, 255, 0.5);
}
.confirm-button {
background: linear-gradient(to right, #409EFF, #64c8ff);
color: white;
border: none;
}
.confirm-button:hover {
background: linear-gradient(to right, #64c8ff, #409EFF);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
/* 外层容器(不变) */
.robotic-arm-panel {
width: 100%;
height: 100%;
min-height: 100%;
background: rgba(10, 20, 40, 0.85);
border-radius: 12px;
padding: 20px;
display: flex;
flex-direction: column;
border: 1px solid rgba(64, 158, 255, 0.3);
box-shadow: 0 0 20px rgba(0, 50, 120, 0.3);
box-sizing: border-box;
overflow: hidden;
position: relative;
} }
/* 主内容区:填充剩余高度 */ /* 主内容区(不变) */
.main-content { .main-content {
display: flex; display: flex;
flex: 1; flex: 1;
gap: 15px; gap: 15px;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
margin-top: 60px; /* 为标题区域留出空间 */
} }
/* 机械臂中心容器:保持固定比例和位置 */
.arm-center-wrapper { .arm-center-wrapper {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
...@@ -292,7 +681,7 @@ export default { ...@@ -292,7 +681,7 @@ export default {
transform: scale(0.9); transform: scale(0.9);
} }
/* 指令区域:自适应高度,超出滚动 */ /* 指令区域(不变) */
.loading-command, .unloading-command { .loading-command, .unloading-command {
width: 160px; width: 160px;
min-width: 160px; min-width: 160px;
...@@ -315,7 +704,6 @@ export default { ...@@ -315,7 +704,6 @@ export default {
border-bottom: 1px solid rgba(100, 180, 255, 0.2); border-bottom: 1px solid rgba(100, 180, 255, 0.2);
} }
/* 指令列表:自动扩展,超出滚动 */
.command-list { .command-list {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
...@@ -358,7 +746,7 @@ export default { ...@@ -358,7 +746,7 @@ export default {
justify-content: space-between; justify-content: space-between;
} }
/* 机械臂各部件样式 */ /* 机械臂各部件样式(不变) */
.arm-base { .arm-base {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
...@@ -476,7 +864,6 @@ export default { ...@@ -476,7 +864,6 @@ export default {
border-radius: 5px; border-radius: 5px;
} }
.gripper-left { .gripper-left {
transform: rotate(-15deg); transform: rotate(-15deg);
} }
...@@ -485,7 +872,7 @@ export default { ...@@ -485,7 +872,7 @@ export default {
transform: rotate(15deg); transform: rotate(15deg);
} }
/* 机械臂动画 */ /* 动画效果(不变) */
@keyframes rotateJoint1 { @keyframes rotateJoint1 {
0%, 100% { transform: translateX(-50%) rotate(0deg); } 0%, 100% { transform: translateX(-50%) rotate(0deg); }
25% { transform: translateX(-50%) rotate(45deg); } 25% { transform: translateX(-50%) rotate(45deg); }
...@@ -516,6 +903,17 @@ export default { ...@@ -516,6 +903,17 @@ export default {
75% { transform: translateX(-40%); } 75% { transform: translateX(-40%); }
} }
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* 响应式调整 */ /* 响应式调整 */
@media (max-width: 800px) { @media (max-width: 800px) {
.main-content { .main-content {
...@@ -537,13 +935,24 @@ export default { ...@@ -537,13 +935,24 @@ export default {
min-height: 200px; min-height: 200px;
} }
/* 响应式标题调整 */ /* 标题区域响应式调整 */
.title-with-status {
flex-direction: column; /* 小屏幕下标题和状态指示灯垂直排列 */
align-items: flex-start;
gap: 8px;
}
.title-text { .title-text {
font-size: 20px; font-size: 20px;
} }
.status-indicator { .panel-title {
margin-top: 50px; flex-wrap: wrap;
}
.title-right {
margin-top: 10px;
order: 3;
} }
} }
...@@ -567,6 +976,10 @@ export default { ...@@ -567,6 +976,10 @@ export default {
.title-line { .title-line {
width: 80px; width: 80px;
} }
.dialog-container {
width: 90%;
}
} }
@media (max-width: 300px) { @media (max-width: 300px) {
...@@ -579,4 +992,86 @@ export default { ...@@ -579,4 +992,86 @@ export default {
min-width: auto; min-width: auto;
} }
} }
/* 指令状态样式 */
.command-item.status-pending {
background: rgba(0, 40, 80, 0.3);
border: 1px solid rgba(100, 180, 255, 0.3);
}
.command-item.status-executing {
background: rgba(0, 80, 40, 0.3);
border: 1px solid rgba(100, 255, 180, 0.5);
animation: pulse-green 1.5s infinite;
}
.command-item.status-poweroff {
background: rgba(80, 0, 0, 0.3);
border: 1px solid rgba(255, 100, 100, 0.5);
cursor: pointer;
animation: blink-red 1s infinite;
}
.command-item.status-completed {
background: rgba(60, 60, 60, 0.3);
border: 1px solid rgba(180, 180, 180, 0.5);
}
/* 状态文本样式 */
.cmd-status {
font-size: 12px;
margin-top: 4px;
font-weight: bold;
}
.status-executing .cmd-status {
color: #64ffaa;
}
.status-poweroff .cmd-status {
color: #ff6464;
}
.status-completed .cmd-status {
color: #a0a0a0;
}
/* 上电对话框样式 */
.power-on-info {
background: rgba(0, 20, 40, 0.4);
padding: 15px;
border-radius: 6px;
margin-top: 15px;
text-align: left;
}
.power-on-info div {
margin-bottom: 10px;
}
.power-on-info .label {
display: inline-block;
width: 80px;
color: #64c8ff;
font-weight: bold;
}
/* 新增动画 */
@keyframes pulse-green {
0% { opacity: 0.8; box-shadow: 0 0 5px rgba(100, 255, 180, 0.5); }
50% { opacity: 1; box-shadow: 0 0 15px rgba(100, 255, 180, 0.8); }
100% { opacity: 0.8; box-shadow: 0 0 5px rgba(100, 255, 180, 0.5); }
}
@keyframes blink-red {
0%, 100% { opacity: 0.8; }
50% { opacity: 0.5; }
}
/* 响应式调整 */
@media (max-width: 500px) {
.cmd-status {
font-size: 11px;
}
}
</style> </style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment