Commit 260dd38b authored by wanghao's avatar wanghao

1 机械臂功能开发

parent cd00704b
...@@ -104,9 +104,10 @@ netty: ...@@ -104,9 +104,10 @@ netty:
max-frame-length: 65535 # 最大帧长度 max-frame-length: 65535 # 最大帧长度
heartbeat-timeout: 10 # 心跳超时时间(秒) heartbeat-timeout: 10 # 心跳超时时间(秒)
# 机械臂UDP配置 # 机械臂UDP配置
robot: robot:
arm: arm:
udp: udp:
ip: 192.168.2.16 ip: 192.168.2.14
port: 6000 port: 6000
...@@ -22,7 +22,11 @@ ...@@ -22,7 +22,11 @@
<groupId>com.zehong</groupId> <groupId>com.zehong</groupId>
<artifactId>zhmes-agecal-common</artifactId> <artifactId>zhmes-agecal-common</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
</project> </project>
\ No newline at end of file
package com.zehong.framework.config; package com.zehong.system.netty;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/** /**
* @author lenovo * @author lenovo
* @date 2025/7/31 * @date 2025/7/31
* @description Netty配置类 * @description Netty配置类
*/ */
@Component @Configuration
@ConfigurationProperties(prefix = "netty")
public class NettyConfig { public class NettyConfig {
/** /**
* 服务端口 * 服务端口
*/ */
private int port = 6001; @Value("${netty.port}")
private int port;
/** /**
* 主线程数 * 主线程数
*/ */
@Value("${netty.boss-group-thread-count}") // 注意配置文件中的横杠命名对应类中的驼峰命名
private int bossGroupThreadCount = 1; private int bossGroupThreadCount = 1;
/** /**
* 工作线程数 * 工作线程数(从配置文件读取,默认值为CPU核心数*2)
*/ */
private int workerGroupThreadCount = Runtime.getRuntime().availableProcessors() * 2; @Value("${netty.worker-group-thread-count}") // 新增配置映射
private int workerGroupThreadCount;
/** /**
* 最大帧长度 * 最大帧长度
*/ */
private int maxFrameLength = 65535; @Value("${netty.max-frame-length}") // 配置文件中的横杠对应类中的驼峰
private int maxFrameLength;
/** /**
* 心跳检测超时时间(秒) * 心跳检测超时时间(秒)
*/ */
private int heartbeatTimeout = 10; @Value("${netty.heartbeat-timeout}") // 配置文件中的横杠对应类中的驼峰
private int heartbeatTimeout;
// getter和setter方法 // getter和setter方法
public int getPort() { public int getPort() {
......
package com.zehong.framework.netty; package com.zehong.system.netty;
import com.zehong.framework.config.NettyConfig; import com.zehong.system.netty.handler.NettyUdpServerHandler;
import com.zehong.framework.netty.handler.NettyUdpServerHandler;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.Bootstrap;
...@@ -19,9 +18,9 @@ import io.netty.handler.timeout.IdleStateHandler; ...@@ -19,9 +18,9 @@ import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* @author lenovo * @author lenovo
...@@ -32,10 +31,10 @@ import java.util.concurrent.TimeUnit; ...@@ -32,10 +31,10 @@ import java.util.concurrent.TimeUnit;
public class NettyUdpServer { public class NettyUdpServer {
private static final Logger log = LoggerFactory.getLogger(NettyUdpServer.class); private static final Logger log = LoggerFactory.getLogger(NettyUdpServer.class);
@Autowired @Resource
private NettyConfig nettyConfig; private NettyConfig nettyConfig;
@Autowired @Resource
private NettyUdpServerHandler nettyUdpServerHandler; private NettyUdpServerHandler nettyUdpServerHandler;
private EventLoopGroup group; private EventLoopGroup group;
......
package com.zehong.framework.netty.handler; package com.zehong.system.netty.handler;
import com.zehong.system.service.IRobotArmCommandService; import com.zehong.system.service.IRobotArmCommandService;
import com.zehong.system.service.websocket.RobotArmWebSocketHandler; import com.zehong.system.service.websocket.RobotArmWebSocketHandler;
import com.zehong.system.udp.RobotArmMessageParser;
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;
...@@ -14,8 +15,12 @@ import org.slf4j.LoggerFactory; ...@@ -14,8 +15,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.io.*; import java.io.*;
...@@ -36,6 +41,17 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -36,6 +41,17 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
// 日期格式器,用于生成文件名和日志时间 // 日期格式器,用于生成文件名和日志时间
private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd"); private static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("yyyyMMdd");
private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final SimpleDateFormat TIME_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 添加状态跟踪变量
private volatile boolean isProcessingCommand = false;
private volatile long lastActivityTime = System.currentTimeMillis();
private volatile long lastIdleTime = System.currentTimeMillis();
/**
* 当前正在处理的指令信息
*/
private final Map<SocketAddress, CommandExecution> currentCommands = new ConcurrentHashMap<>();
// 线程安全锁,确保文件写入安全 // 线程安全锁,确保文件写入安全
private final ReentrantLock fileLock = new ReentrantLock(); private final ReentrantLock fileLock = new ReentrantLock();
...@@ -80,29 +96,31 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -80,29 +96,31 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
//saveMessageToFile(packet.sender().toString(), correctMessage); //saveMessageToFile(packet.sender().toString(), correctMessage);
// 处理消息逻辑 // 处理消息逻辑
String responseSave = "服务器已收到UDP消息:" + correctMessage; String response = "服务器已收到UDP消息:" + correctMessage;
// 处理机械臂完成消息 // 记录最后活动时间
if (correctMessage.startsWith("COMPLETE,")) { lastActivityTime = System.currentTimeMillis();
String[] parts = correctMessage.split(",");
if (parts.length >= 3) { // 解析消息
String trayCode = parts[1]; RobotArmMessageParser.RobotArmStatus status =
String storeyCode = parts[2]; RobotArmMessageParser.parseMessage(correctMessage);
// 更新指令状态为已完成 if (status != null) {
robotArmCommandService.completeCommand(trayCode, storeyCode); // 处理状态消息
processStatusMessage(status, packet.sender());
// 发送成功响应
String response = "CMD_COMPLETE_ACK"; // 检查是否为完全空闲状态
byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); if (status.isFullyIdle()) {
ctx.writeAndFlush(new DatagramPacket( handleFullyIdleState();
io.netty.buffer.Unpooled.copiedBuffer(responseBytes),
packet.sender()));
} }
} }
// 当收到消息时,更新状态为运行中 // 回复客户端
sendStatusToWebSocket("running"); byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
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);
// 出现异常时发送故障状态 // 出现异常时发送故障状态
...@@ -110,50 +128,49 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -110,50 +128,49 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
} }
} }
private void processStatusMessage(RobotArmMessageParser.RobotArmStatus status, SocketAddress sender) {
// 更新前端状态
if (status.isBusy()) {
sendStatusToWebSocket("running");
} else if (status.isOk()) {
sendStatusToWebSocket("idle");
} else {
sendStatusToWebSocket("error");
}
// 处理指令完成
if (status.isFullyIdle()) {
CommandExecution execution = currentCommands.get(sender);
if (execution != null) {
robotArmCommandService.completeCommand(execution.commandId);
currentCommands.remove(sender);
log.info("指令完成: {}", execution.commandId);
}
}
// 记录详细状态
log.debug("机械臂状态: code={}, text={}, position=({},{},{},{})",
status.getCode(), status.getText(),
status.getX(), status.getY(), status.getZ(), status.getR());
}
/** /**
* 将消息保存到本地文件 * 记录当前执行的指令
*
* @param clientAddress 客户端地址
* @param message 消息内容
*/ */
private void saveMessageToFile(String clientAddress, String message) { public void registerCommandExecution(SocketAddress address, Long commandId) {
// 创建日志目录(如果不存在) CommandExecution execution = new CommandExecution();
File dir = new File(LOG_DIR); execution.commandId = commandId;
if (!dir.exists()) { execution.startTime = System.currentTimeMillis();
dir.mkdirs(); currentCommands.put(address, execution);
} log.info("注册指令跟踪: {} -> {}", address, commandId);
}
// 按日期生成文件名,每天一个文件
String fileName = LOG_DIR + "udp_log_" + DATE_FORMATTER.format(new Date()) + ".log";
// 构建日志内容
String logContent = String.format("[%s] 客户端[%s]:%s%n",
TIME_FORMATTER.format(new Date()),
clientAddress,
message);
// 使用锁确保多线程写入安全 private void handleFullyIdleState() {
fileLock.lock(); // 重置空闲计时
BufferedWriter writer = null; lastIdleTime = System.currentTimeMillis();
try {
// 以追加模式写入文件 // 处理待执行指令
writer = new BufferedWriter(new OutputStreamWriter( robotArmCommandService.processPendingCommands();
new FileOutputStream(fileName, true),
StandardCharsets.UTF_8));
writer.write(logContent);
writer.flush();
} catch (IOException e) {
log.error("保存UDP消息到文件失败", e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
log.error("关闭文件写入流失败", e);
}
}
fileLock.unlock();
}
} }
/** /**
...@@ -190,17 +207,42 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -190,17 +207,42 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
IdleStateEvent event = (IdleStateEvent) evt; IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.ALL_IDLE) { if (event.state() == IdleState.ALL_IDLE) {
log.info("UDP服务器超过规定时间未收到数据"); log.info("UDP服务器超过规定时间未收到数据");
// UDP无连接,一般不关闭通道
// 通过WebSocket发送空闲状态给前端 long now = System.currentTimeMillis();
sendStatusToWebSocket("idle"); long idleDuration = now - lastActivityTime;
// 处理空闲状态
robotArmCommandService.processPendingCommands(); log.info("UDP通道空闲: {}ms", idleDuration);
// 如果长时间空闲(>2秒)
if (idleDuration > 20000) {
handleLongIdleState();
}
} }
} else { } else {
super.userEventTriggered(ctx, evt); super.userEventTriggered(ctx, evt);
} }
} }
private void handleLongIdleState() {
// 检查是否有超时的指令
long now = System.currentTimeMillis();
Iterator<Map.Entry<SocketAddress, CommandExecution>> it = currentCommands.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<SocketAddress, CommandExecution> entry = it.next();
CommandExecution exec = entry.getValue();
// 5分钟超时
if (now - exec.startTime > 30000) {
robotArmCommandService.markCommandTimeout(exec.commandId);
it.remove();
log.warn("指令超时: {}", exec.commandId);
}
}
// 如果当前没有执行中的指令,尝试处理待执行指令
if (currentCommands.isEmpty()) {
handleFullyIdleState();
}
}
/** /**
* 发送状态到WebSocket * 发送状态到WebSocket
*/ */
...@@ -212,4 +254,55 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP ...@@ -212,4 +254,55 @@ public class NettyUdpServerHandler extends SimpleChannelInboundHandler<DatagramP
} }
} }
// 添加指令执行状态
private static class CommandExecution {
Long commandId;
long startTime;
}
/**
* 将消息保存到本地文件
*
* @param clientAddress 客户端地址
* @param message 消息内容
*/
private void saveMessageToFile(String clientAddress, String message) {
// 创建日志目录(如果不存在)
File dir = new File(LOG_DIR);
if (!dir.exists()) {
dir.mkdirs();
}
// 按日期生成文件名,每天一个文件
String fileName = LOG_DIR + "udp_log_" + DATE_FORMATTER.format(new Date()) + ".log";
// 构建日志内容
String logContent = String.format("[%s] 客户端[%s]:%s%n",
TIME_FORMATTER.format(new Date()),
clientAddress,
message);
// 使用锁确保多线程写入安全
fileLock.lock();
BufferedWriter writer = null;
try {
// 以追加模式写入文件
writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(fileName, true),
StandardCharsets.UTF_8));
writer.write(logContent);
writer.flush();
} catch (IOException e) {
log.error("保存UDP消息到文件失败", e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
log.error("关闭文件写入流失败", e);
}
}
fileLock.unlock();
}
}
} }
package com.zehong.framework.netty.listener; package com.zehong.system.netty.listener;
import com.zehong.framework.netty.NettyUdpServer; import com.zehong.system.netty.NettyUdpServer;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
......
...@@ -64,6 +64,9 @@ public interface IRobotArmCommandService ...@@ -64,6 +64,9 @@ public interface IRobotArmCommandService
public void processIdleState(); public void processIdleState();
void completeCommand(Long commandId);
void markCommandTimeout(Long commandId);
public void processPendingCommands(); public void processPendingCommands();
public void completeCommand(String trayCode,String storeyCode); public void completeCommand(String trayCode,String storeyCode);
......
package com.zehong.system.service.impl; package com.zehong.system.service.impl;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import com.zehong.common.utils.DateUtils; import com.zehong.common.utils.DateUtils;
import com.zehong.system.domain.TStoreyInfo; import com.zehong.system.domain.TStoreyInfo;
import com.zehong.system.mapper.TStoreyInfoMapper; import com.zehong.system.mapper.TStoreyInfoMapper;
import com.zehong.system.netty.handler.NettyUdpServerHandler;
import com.zehong.system.service.websocket.RobotArmWebSocketHandler; import com.zehong.system.service.websocket.RobotArmWebSocketHandler;
import com.zehong.system.udp.UdpCommandSender; import com.zehong.system.udp.UdpCommandSender;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.zehong.system.mapper.RobotArmCommandMapper; import com.zehong.system.mapper.RobotArmCommandMapper;
import com.zehong.system.domain.RobotArmCommand; import com.zehong.system.domain.RobotArmCommand;
...@@ -28,6 +34,9 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService ...@@ -28,6 +34,9 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService
{ {
private static final Logger log = LoggerFactory.getLogger(RobotArmCommandServiceImpl.class); private static final Logger log = LoggerFactory.getLogger(RobotArmCommandServiceImpl.class);
@Resource
private NettyUdpServerHandler nettyUdpServerHandler;
@Resource @Resource
private RobotArmWebSocketHandler robotArmWebSocketHandler; private RobotArmWebSocketHandler robotArmWebSocketHandler;
...@@ -39,47 +48,93 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService ...@@ -39,47 +48,93 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService
@Resource @Resource
private UdpCommandSender udpCommandSender; private UdpCommandSender udpCommandSender;
@Value("${robot.arm.udp.ip}")
private String robotArmAddress;
@Value("${robot.arm.udp.port}")
private int robotArmPort;
private SocketAddress getRobotAddress() {
try {
return new InetSocketAddress(
InetAddress.getByName(robotArmAddress), robotArmPort);
} catch (UnknownHostException e) {
throw new RuntimeException("无效的机械臂地址: " + robotArmAddress);
}
}
@Override @Override
@Transactional @Transactional
public void processIdleState() { public void processPendingCommands() {
// 1. 更新执行中状态为完成状态 // 1. 处理待执行的上料指令
robotArmCommandMapper.updateExecutingToCompleted(); List<RobotArmCommand> loadingCommands =
robotArmCommandMapper.selectPendingLoadingCommands();
// 2. 优先处理上料指令
List<RobotArmCommand> loadingCommands = robotArmCommandMapper.selectPendingLoadingCommands();
if (!loadingCommands.isEmpty()) { if (!loadingCommands.isEmpty()) {
RobotArmCommand command = loadingCommands.get(0); sendCommand(loadingCommands.get(0), "LOAD");
sendLoadingCommand(command);
return; return;
} }
// 3. 处理下料指令 // 2. 处理待执行的下料指令
List<RobotArmCommand> unloadingCommands = robotArmCommandMapper.selectPendingUnloadingCommands(); List<RobotArmCommand> unloadingCommands =
robotArmCommandMapper.selectPendingUnloadingCommands();
if (!unloadingCommands.isEmpty()) { if (!unloadingCommands.isEmpty()) {
RobotArmCommand command = unloadingCommands.get(0); sendCommand(unloadingCommands.get(0), "UNLOAD");
sendUnloadingCommand(command);
} }
} }
private void sendCommand(RobotArmCommand command, String commandType) {
// 更新状态为执行中
command.setStatus("1");
command.setStartExecutionTime(new Date());
robotArmCommandMapper.updateRobotArmCommand(command);
// 通过WebSocket广播更新
robotArmWebSocketHandler.broadcastCommandUpdate();
// 发送UDP指令
SocketAddress address = getRobotAddress();
udpCommandSender.sendCommandWithId(
address,
command.getRobotArmCommandId(),
commandType,
command.getTrayCode(),
command.getStoreyCode()
);
// 注册指令跟踪
nettyUdpServerHandler.registerCommandExecution(address, command.getRobotArmCommandId());
}
@Override @Override
@Transactional @Transactional
public void processPendingCommands() { public void completeCommand(Long commandId) {
// 1. 只处理待执行指令(状态为0) RobotArmCommand command = robotArmCommandMapper.selectRobotArmCommandById(commandId);
List<RobotArmCommand> loadingCommands = robotArmCommandMapper.selectPendingLoadingCommands(); if (command != null && "1".equals(command.getStatus())) {
if (!loadingCommands.isEmpty()) { command.setStatus("3");
RobotArmCommand command = loadingCommands.get(0); command.setEndExecutionTime(new Date());
sendLoadingCommand(command); robotArmCommandMapper.updateRobotArmCommand(command);
return;
// 通过WebSocket广播更新
robotArmWebSocketHandler.broadcastCommandUpdate();
} }
}
// 2. 处理待执行的下料指令 @Override
List<RobotArmCommand> unloadingCommands = robotArmCommandMapper.selectPendingUnloadingCommands(); @Transactional
if (!unloadingCommands.isEmpty()) { public void markCommandTimeout(Long commandId) {
RobotArmCommand command = unloadingCommands.get(0); RobotArmCommand command = robotArmCommandMapper.selectRobotArmCommandById(commandId);
sendUnloadingCommand(command); if (command != null && "1".equals(command.getStatus())) {
command.setStatus("3"); // 标记为未上电
robotArmCommandMapper.updateRobotArmCommand(command);
// 通过WebSocket广播更新
robotArmWebSocketHandler.broadcastCommandUpdate();
robotArmWebSocketHandler.broadcastStatus("idle");
} }
} }
@Override @Override
@Transactional @Transactional
public void completeCommand(String trayCode, String storeyCode) { public void completeCommand(String trayCode, String storeyCode) {
...@@ -245,6 +300,28 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService ...@@ -245,6 +300,28 @@ public class RobotArmCommandServiceImpl implements IRobotArmCommandService
return i; return i;
} }
@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);
}
}
private void notifyCommandsUpdate() { private void notifyCommandsUpdate() {
robotArmWebSocketHandler.broadcastCommandUpdate(); robotArmWebSocketHandler.broadcastCommandUpdate();
} }
......
package com.zehong.system.udp;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author lenovo
* @date 2025/8/5
* @description TODO
*/
public class RobotArmMessageParser {
private static final Pattern STATUS_PATTERN = Pattern.compile(
"PPP (\\w+) (\\S+)(?: (\\d+),(\\d+),(\\d+),(\\d+),)?"
);
public static RobotArmStatus parseMessage(String message) {
Matcher matcher = STATUS_PATTERN.matcher(message);
if (matcher.find()) {
String statusCode = matcher.group(1);
String statusText = matcher.group(2);
String x = matcher.group(3);
String y = matcher.group(4);
String z = matcher.group(5);
String r = matcher.group(6);
return new RobotArmStatus(statusCode, statusText,
parseCoord(x), parseCoord(y),
parseCoord(z), parseCoord(r));
}
return null;
}
private static Integer parseCoord(String coord) {
if (coord == null || coord.isEmpty()) return null;
try {
return Integer.parseInt(coord);
} catch (NumberFormatException e) {
return null;
}
}
public static class RobotArmStatus {
private final String code;
private final String text;
private final Integer x;
private final Integer y;
private final Integer z;
private final Integer r;
public RobotArmStatus(String code, String text,
Integer x, Integer y, Integer z, Integer r) {
this.code = code;
this.text = text;
this.x = x;
this.y = y;
this.z = z;
this.r = r;
}
public boolean isBusy() {
return "BUSY".equals(code);
}
public boolean isOk() {
return "OK".equals(code);
}
public boolean isComplete() {
return isOk() && "完成".equals(text);
}
public boolean isFullyIdle() {
return isComplete() && x == 0 && y == 0 && z == 0 && r == 0;
}
// Getters
public String getCode() { return code; }
public String getText() { return text; }
public Integer getX() { return x; }
public Integer getY() { return y; }
public Integer getZ() { return z; }
public Integer getR() { return r; }
}
}
...@@ -11,7 +11,9 @@ import java.io.IOException; ...@@ -11,7 +11,9 @@ import java.io.IOException;
import java.net.DatagramPacket; import java.net.DatagramPacket;
import java.net.DatagramSocket; import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* @author lenovo * @author lenovo
...@@ -31,6 +33,7 @@ public class UdpCommandSender { ...@@ -31,6 +33,7 @@ public class UdpCommandSender {
private DatagramSocket socket; private DatagramSocket socket;
private InetAddress address; private InetAddress address;
private final ReentrantLock lock = new ReentrantLock();
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
...@@ -42,6 +45,38 @@ public class UdpCommandSender { ...@@ -42,6 +45,38 @@ public class UdpCommandSender {
} }
} }
/**
* 发送指令到指定地址
*/
public void sendCommand(SocketAddress address, String message) {
if (socket == null || address == null) {
log.error("UDP命令发送器未初始化或地址无效");
return;
}
lock.lock();
try {
byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address);
socket.send(packet);
log.info("已发送UDP指令到 {}: {}", address, message);
} catch (IOException e) {
log.error("发送UDP指令失败: {}", message, e);
} finally {
lock.unlock();
}
}
/**
* 发送带指令ID的命令
*/
public void sendCommandWithId(SocketAddress address, Long commandId,
String commandType, String trayCode, String storeyCode) {
String message = String.format("CMD_ID:%d#%s#%s#%s",
commandId, commandType, trayCode, storeyCode);
sendCommand(address, message);
}
public void sendCommand(String message) { public void sendCommand(String message) {
if (socket == null || address == null) { if (socket == null || address == null) {
log.error("UDP命令发送器未初始化,无法发送消息"); log.error("UDP命令发送器未初始化,无法发送消息");
......
...@@ -152,7 +152,7 @@ export default { ...@@ -152,7 +152,7 @@ export default {
name: 'RoboticArm', name: 'RoboticArm',
data() { data() {
return { return {
status: 'idle', // idle, running, error status: 'unknown', // idle, running, error
showAddDialog: false, showAddDialog: false,
trayCode: '', trayCode: '',
loadingCommands: [ loadingCommands: [
...@@ -195,14 +195,16 @@ export default { ...@@ -195,14 +195,16 @@ export default {
return { return {
'idle': this.status === 'idle', 'idle': this.status === 'idle',
'running': this.status === 'running', 'running': this.status === 'running',
'error': this.status === 'error' 'error': this.status === 'error',
'unknown': this.status === 'unknown'
}; };
}, },
statusText() { statusText() {
return { return {
'idle': '空闲中', 'idle': '空闲中',
'running': '运行中', 'running': '运行中',
'error': '故障' 'error': '故障',
'unknown': '未知'
}[this.status]; }[this.status];
} }
}, },
...@@ -233,13 +235,14 @@ export default { ...@@ -233,13 +235,14 @@ export default {
this.websocket.onopen = () => { this.websocket.onopen = () => {
console.log('机械臂指令WebSocket连接成功'); console.log('机械臂指令WebSocket连接成功');
this.status = 'running'; this.status = 'unknown';
this.sendWebSocketMessage({ type: 'request', commands: ['loading', 'unloading'] }); this.sendWebSocketMessage({ type: 'request', commands: ['loading', 'unloading'] });
}; };
this.websocket.onmessage = (event) => { this.websocket.onmessage = (event) => {
try { try {
const message = JSON.parse(event.data); const message = JSON.parse(event.data);
console.log('收到WebSocket消息:', message);
if (message.type === 'loading') { if (message.type === 'loading') {
this.loadingCommands = message.data.map(cmd => ({ this.loadingCommands = message.data.map(cmd => ({
robotArmCommandId: cmd.robotArmCommandId, robotArmCommandId: cmd.robotArmCommandId,
...@@ -488,6 +491,17 @@ export default { ...@@ -488,6 +491,17 @@ export default {
animation: blink 1s infinite; animation: blink 1s infinite;
} }
/* 添加未知状态样式 */
.status-light.unknown {
background-color: #a0a0a0;
color: #a0a0a0;
animation: blink-gray 1s infinite;
}
@keyframes blink-gray {
0%, 100% { opacity: 0.6; }
50% { opacity: 1; }
}
.status-text { .status-text {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
...@@ -992,7 +1006,6 @@ export default { ...@@ -992,7 +1006,6 @@ export default {
min-width: auto; min-width: auto;
} }
} }
/* 指令状态样式 */ /* 指令状态样式 */
.command-item.status-pending { .command-item.status-pending {
background: rgba(0, 40, 80, 0.3); background: rgba(0, 40, 80, 0.3);
......
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