feat(mill): 添加UDP调试工具功能
- 在路由配置中新增tool模块和udp-debug页面 - 添加UDP通信相关依赖到ruoyi-mill模块 - 实现UdpProperties配置类并添加超时和重试参数 - 重构UdpSender实现重试机制和超时控制 - 创建application-mill.properties配置文件 - 定义IUdpService接口提供UDP通信服务 - 添加系统菜单初始化SQL脚本 - 实现前端API接口用于UDP配置和报文发送 - 开发UDP调试工具Vue组件界面 - 编写UDP调试工具快速启动指南文档
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
package com.ruoyi.mill.controller;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.mill.udp.UdpProperties;
|
||||
import com.ruoyi.mill.udp.UdpServer;
|
||||
import com.ruoyi.mill.udp.UdpSender;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* UDP 通信控制器
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Api(tags = "冷轧 - UDP通信")
|
||||
@RestController
|
||||
@RequestMapping("/mill/udp")
|
||||
public class UdpController extends BaseController {
|
||||
private static final Logger log = LoggerFactory.getLogger(UdpController.class);
|
||||
|
||||
@Autowired
|
||||
private UdpProperties udpProperties;
|
||||
|
||||
@Autowired
|
||||
private UdpServer udpServer;
|
||||
|
||||
@Autowired
|
||||
private UdpSender udpSender;
|
||||
|
||||
/**
|
||||
* 获取UDP配置
|
||||
*/
|
||||
@ApiOperation("获取UDP配置")
|
||||
@GetMapping("/config")
|
||||
public AjaxResult config() {
|
||||
return success(udpProperties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新UDP配置
|
||||
*/
|
||||
@ApiOperation("更新UDP配置")
|
||||
@PutMapping("/config")
|
||||
public AjaxResult updateConfig(@RequestBody UdpProperties properties) {
|
||||
// 验证端口范围
|
||||
if (properties.getLocalPort() < 1024 || properties.getLocalPort() > 65535) {
|
||||
return error("本地端口必须在1024-65535之间");
|
||||
}
|
||||
if (properties.getRemotePort() < 1024 || properties.getRemotePort() > 65535) {
|
||||
return error("目标端口必须在1024-65535之间");
|
||||
}
|
||||
if (StringUtils.isBlank(properties.getRemoteHost())) {
|
||||
return error("目标IP不能为空");
|
||||
}
|
||||
|
||||
// 这里应该调用服务层更新配置
|
||||
// udpPropertiesService.updateConfig(properties);
|
||||
|
||||
return success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送UDP报文
|
||||
*/
|
||||
@ApiOperation("发送UDP报文")
|
||||
@PostMapping("/send")
|
||||
public AjaxResult sendTelegram(@RequestBody Map<String, Object> requestData) {
|
||||
|
||||
try {
|
||||
String tcNo = (String) requestData.get("tcNo");
|
||||
Object payloadObj = requestData.get("payload");
|
||||
|
||||
byte[] payload;
|
||||
if (payloadObj instanceof java.util.List) {
|
||||
java.util.List<?> list = (java.util.List<?>) payloadObj;
|
||||
payload = new byte[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
payload[i] = ((Number) list.get(i)).byteValue();
|
||||
}
|
||||
} else if (payloadObj instanceof int[]) {
|
||||
int[] intArray = (int[]) payloadObj;
|
||||
payload = new byte[intArray.length];
|
||||
for (int i = 0; i < intArray.length; i++) {
|
||||
payload[i] = (byte) intArray[i];
|
||||
}
|
||||
} else if (payloadObj instanceof byte[]) {
|
||||
payload = (byte[]) payloadObj;
|
||||
} else if (payloadObj instanceof String) {
|
||||
payload = ((String) payloadObj).getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
return error("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
|
||||
}
|
||||
|
||||
boolean result = udpSender.send(tcNo, payload);
|
||||
|
||||
if (result) {
|
||||
log.info("[UDP-SEND] 成功发送电文 {},长度: {} bytes", tcNo, payload.length);
|
||||
return success();
|
||||
} else {
|
||||
log.warn("[UDP-SEND] 发送电文失败 {},长度: {} bytes", tcNo, payload.length);
|
||||
return error("发送失败,请检查网络连接");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[UDP-SEND] 发送电文异常: {}", e.getMessage());
|
||||
return error("发送异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析电文数据
|
||||
*/
|
||||
@ApiOperation("解析电文数据")
|
||||
@PostMapping("/parse")
|
||||
public AjaxResult parseTelegram(@RequestBody Map<String, Object> requestData) {
|
||||
|
||||
try {
|
||||
String tcNo = (String) requestData.get("tcNo");
|
||||
Object payloadObj = requestData.get("payload");
|
||||
|
||||
byte[] payload;
|
||||
if (payloadObj instanceof java.util.List) {
|
||||
java.util.List<?> list = (java.util.List<?>) payloadObj;
|
||||
payload = new byte[list.size()];
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
payload[i] = ((Number) list.get(i)).byteValue();
|
||||
}
|
||||
} else if (payloadObj instanceof int[]) {
|
||||
int[] intArray = (int[]) payloadObj;
|
||||
payload = new byte[intArray.length];
|
||||
for (int i = 0; i < intArray.length; i++) {
|
||||
payload[i] = (byte) intArray[i];
|
||||
}
|
||||
} else if (payloadObj instanceof byte[]) {
|
||||
payload = (byte[]) payloadObj;
|
||||
} else if (payloadObj instanceof String) {
|
||||
payload = ((String) payloadObj).getBytes(StandardCharsets.UTF_8);
|
||||
} else {
|
||||
return error("无效的payload类型: " + (payloadObj == null ? "null" : payloadObj.getClass().getName()));
|
||||
}
|
||||
|
||||
// 根据电文号进行解析
|
||||
String parsedResult = parseTelegramData(tcNo, payload);
|
||||
|
||||
return success(parsedResult);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[TELEGRAM-PARSE] 解析异常: {}", e.getMessage());
|
||||
return error("解析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文历史记录
|
||||
*/
|
||||
@ApiOperation("获取电文历史记录")
|
||||
@GetMapping("/history")
|
||||
public AjaxResult telegramHistory(
|
||||
@RequestParam(defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(defaultValue = "50") Integer pageSize) {
|
||||
|
||||
// TODO: 从数据库或缓存中获取历史记录
|
||||
// 这里返回模拟数据
|
||||
return success(getMockHistory(pageNum, pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文统计信息
|
||||
*/
|
||||
@ApiOperation("获取电文统计信息")
|
||||
@GetMapping("/stats")
|
||||
public AjaxResult telegramStats() {
|
||||
// TODO: 从数据库或缓存中获取统计数据
|
||||
// 这里返回模拟数据
|
||||
return success(getMockStats());
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析电文数据(内部方法)
|
||||
*/
|
||||
private String parseTelegramData(String tcNo, byte[] payload) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
switch (tcNo) {
|
||||
case "2FK101":
|
||||
result.append("电文类型: 作业命令信息\n");
|
||||
result.append("电文号: ").append(tcNo).append("\n");
|
||||
result.append("长度: ").append(payload.length).append(" bytes\n");
|
||||
result.append("时间: ").append(java.time.LocalDateTime.now()).append("\n");
|
||||
break;
|
||||
|
||||
case "K12F03":
|
||||
result.append("电文类型: 生产信息电文\n");
|
||||
result.append("电文号: ").append(tcNo).append("\n");
|
||||
result.append("长度: ").append(payload.length).append(" bytes\n");
|
||||
result.append("时间: ").append(java.time.LocalDateTime.now()).append("\n");
|
||||
break;
|
||||
|
||||
default:
|
||||
result.append("未知电文号: ").append(tcNo).append("\n");
|
||||
result.append("长度: ").append(payload.length).append(" bytes\n");
|
||||
result.append("原始数据: ");
|
||||
for (int i = 0; i < Math.min(payload.length, 32); i++) {
|
||||
result.append(String.format("%02X ", payload[i]));
|
||||
}
|
||||
if (payload.length > 32) {
|
||||
result.append("...");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟历史记录数据
|
||||
*/
|
||||
private Object getMockHistory(int pageNum, int pageSize) {
|
||||
// 这里应该从数据库查询真实数据
|
||||
// 返回模拟数据用于演示
|
||||
|
||||
java.util.List<Object> mockData = java.util.Arrays.asList(
|
||||
createMockRecord(1L, "2FK101", "IN", "2024-04-30 10:30:15", 128),
|
||||
createMockRecord(2L, "K12F03", "OUT", "2024-04-30 10:30:20", 96),
|
||||
createMockRecord(3L, "2FK102", "IN", "2024-04-30 10:30:25", 64),
|
||||
createMockRecord(4L, "K12F01", "OUT", "2024-04-30 10:30:30", 80)
|
||||
);
|
||||
|
||||
int total = mockData.size();
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, total);
|
||||
|
||||
if (fromIndex >= total) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("rows", mockData.subList(fromIndex, toIndex));
|
||||
result.put("total", total);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟记录
|
||||
*/
|
||||
private java.util.Map<String, Object> createMockRecord(
|
||||
Long id, String tcNo, String direction,
|
||||
String timestamp, Integer payloadLength) {
|
||||
|
||||
java.util.Map<String, Object> record = new java.util.HashMap<>();
|
||||
record.put("id", id);
|
||||
record.put("tcNo", tcNo);
|
||||
record.put("direction", direction);
|
||||
record.put("timestamp", timestamp);
|
||||
record.put("payloadLength", payloadLength);
|
||||
record.put("status", "成功");
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟统计数据
|
||||
*/
|
||||
private java.util.Map<String, Object> getMockStats() {
|
||||
java.util.Map<String, Object> stats = new java.util.HashMap<>();
|
||||
stats.put("todayReceived", 25);
|
||||
stats.put("totalReceived", 1247);
|
||||
stats.put("successRate", 98);
|
||||
stats.put("avgDelay", 150);
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.mill.service;
|
||||
|
||||
import com.ruoyi.mill.udp.UdpProperties;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* UDP 通信服务接口
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
public interface IUdpService {
|
||||
|
||||
/**
|
||||
* 获取UDP配置
|
||||
*/
|
||||
UdpProperties getUdpConfig();
|
||||
|
||||
/**
|
||||
* 更新UDP配置
|
||||
*/
|
||||
AjaxResult updateUdpConfig(UdpProperties properties);
|
||||
|
||||
/**
|
||||
* 发送UDP报文
|
||||
*/
|
||||
boolean sendTelegram(String tcNo, byte[] payload);
|
||||
|
||||
/**
|
||||
* 获取电文历史记录
|
||||
*/
|
||||
List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize);
|
||||
|
||||
/**
|
||||
* 获取电文统计数据
|
||||
*/
|
||||
Map<String, Object> getTelegramStats();
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.ruoyi.mill.service.impl;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.mill.udp.UdpProperties;
|
||||
import com.ruoyi.mill.udp.UdpServer;
|
||||
import com.ruoyi.mill.udp.UdpSender;
|
||||
import com.ruoyi.mill.service.IUdpService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* UDP 通信服务实现
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Service
|
||||
public class UdpServiceImpl implements IUdpService {
|
||||
private static final Logger log = LoggerFactory.getLogger(UdpServiceImpl.class);
|
||||
|
||||
@Autowired
|
||||
private UdpProperties udpProperties;
|
||||
|
||||
@Autowired
|
||||
private UdpServer udpServer;
|
||||
|
||||
@Autowired
|
||||
private UdpSender udpSender;
|
||||
|
||||
/**
|
||||
* 获取UDP配置
|
||||
*/
|
||||
@Override
|
||||
public UdpProperties getUdpConfig() {
|
||||
return udpProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新UDP配置
|
||||
*/
|
||||
@Override
|
||||
public AjaxResult updateUdpConfig(UdpProperties properties) {
|
||||
try {
|
||||
// 这里应该验证配置并保存到数据库
|
||||
// 暂时只是返回成功
|
||||
|
||||
log.info("[UDP-CONFIG] 更新UDP配置: 本地端口={}, 目标IP={}:{}",
|
||||
properties.getLocalPort(),
|
||||
properties.getRemoteHost(),
|
||||
properties.getRemotePort());
|
||||
|
||||
return AjaxResult.success();
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[UDP-CONFIG] 更新配置失败", e);
|
||||
return AjaxResult.error("更新配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送UDP报文
|
||||
*/
|
||||
@Override
|
||||
public boolean sendTelegram(String tcNo, byte[] payload) {
|
||||
try {
|
||||
boolean result = udpSender.send(tcNo, payload);
|
||||
if (result) {
|
||||
log.info("[UDP-SEND] 成功发送电文 {},长度: {} bytes", tcNo, payload.length);
|
||||
} else {
|
||||
log.warn("[UDP-SEND] 发送电文失败 {},长度: {} bytes", tcNo, payload.length);
|
||||
}
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[UDP-SEND] 发送电文异常 {}: {}", tcNo, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文历史记录
|
||||
*/
|
||||
@Override
|
||||
public List<Map<String, Object>> getTelegramHistory(int pageNum, int pageSize) {
|
||||
// TODO: 从数据库查询真实的历史记录
|
||||
// 这里返回模拟数据用于演示
|
||||
|
||||
List<Map<String, Object>> mockData = new ArrayList<>();
|
||||
|
||||
// 生成一些模拟的历史记录
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
Map<String, Object> record = new HashMap<>();
|
||||
record.put("id", i);
|
||||
record.put("tcNo", getRandomTcNo());
|
||||
record.put("direction", Math.random() > 0.5 ? "IN" : "OUT");
|
||||
record.put("timestamp", generateMockTimestamp(i));
|
||||
record.put("payloadLength", new Random().nextInt(200) + 32);
|
||||
record.put("status", "成功");
|
||||
|
||||
mockData.add(record);
|
||||
}
|
||||
|
||||
// 简单的分页逻辑
|
||||
int fromIndex = (pageNum - 1) * pageSize;
|
||||
int toIndex = Math.min(fromIndex + pageSize, mockData.size());
|
||||
|
||||
if (fromIndex >= mockData.size()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return mockData.subList(fromIndex, toIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取电文统计数据
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> getTelegramStats() {
|
||||
// TODO: 从数据库查询真实的统计数据
|
||||
// 这里返回模拟数据用于演示
|
||||
|
||||
Map<String, Object> stats = new HashMap<>();
|
||||
stats.put("todayReceived", 25 + new Random().nextInt(10));
|
||||
stats.put("totalReceived", 1247 + new Random().nextInt(100));
|
||||
stats.put("successRate", 95 + new Random().nextInt(5));
|
||||
stats.put("avgDelay", 120 + new Random().nextInt(60));
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机电文号(用于模拟)
|
||||
*/
|
||||
private String getRandomTcNo() {
|
||||
String[] tcNos = {"2FK101", "2FK102", "2FK103", "2FK104", "K12F01", "K12F02", "K12F03"};
|
||||
return tcNos[new Random().nextInt(tcNos.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成模拟时间戳
|
||||
*/
|
||||
private String generateMockTimestamp(int index) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(Calendar.MINUTE, -index * 5); // 每5分钟一条记录
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.format(calendar.getTime());
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,12 @@ public class UdpProperties {
|
||||
/** 接收缓冲区大小(字节) */
|
||||
private int bufferSize = 4096;
|
||||
|
||||
/** 超时时间(毫秒) */
|
||||
private int timeout = 5000;
|
||||
|
||||
/** 重试次数 */
|
||||
private int retryCount = 3;
|
||||
|
||||
public int getLocalPort() { return localPort; }
|
||||
public void setLocalPort(int p) { this.localPort = p; }
|
||||
public String getRemoteHost() { return remoteHost; }
|
||||
@@ -30,4 +36,8 @@ public class UdpProperties {
|
||||
public void setRemotePort(int p) { this.remotePort = p; }
|
||||
public int getBufferSize() { return bufferSize; }
|
||||
public void setBufferSize(int s) { this.bufferSize = s; }
|
||||
public int getTimeout() { return timeout; }
|
||||
public void setTimeout(int t) { this.timeout = t; }
|
||||
public int getRetryCount() { return retryCount; }
|
||||
public void setRetryCount(int r) { this.retryCount = r; }
|
||||
}
|
||||
|
||||
@@ -41,22 +41,50 @@ public class UdpSender {
|
||||
send(TelegramSchema.ID_K12F03, TelegramCodec.encode(TelegramSchema.SCHEMA_K12F03, data));
|
||||
}
|
||||
|
||||
private void send(String tcNo, byte[] payload) {
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
// 帧 = 6字节电文号 + payload
|
||||
byte[] tcNoBytes = Arrays.copyOf(
|
||||
tcNo.getBytes(StandardCharsets.US_ASCII), 6);
|
||||
byte[] frame = new byte[6 + payload.length];
|
||||
System.arraycopy(tcNoBytes, 0, frame, 0, 6);
|
||||
System.arraycopy(payload, 0, frame, 6, payload.length);
|
||||
/**
|
||||
* 发送UDP报文(供控制器调用)
|
||||
*/
|
||||
public boolean send(String tcNo, byte[] payload) {
|
||||
try {
|
||||
int retryCount = props.getRetryCount();
|
||||
int timeout = props.getTimeout();
|
||||
|
||||
for (int i = 0; i < retryCount; i++) {
|
||||
try {
|
||||
DatagramSocket socket = new DatagramSocket();
|
||||
socket.setSoTimeout(timeout);
|
||||
|
||||
// 帧 = 6字节电文号 + payload
|
||||
byte[] tcNoBytes = Arrays.copyOf(
|
||||
tcNo.getBytes(StandardCharsets.US_ASCII), 6);
|
||||
byte[] frame = new byte[6 + payload.length];
|
||||
System.arraycopy(tcNoBytes, 0, frame, 0, 6);
|
||||
System.arraycopy(payload, 0, frame, 6, payload.length);
|
||||
|
||||
InetAddress addr = InetAddress.getByName(props.getRemoteHost());
|
||||
DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, props.getRemotePort());
|
||||
socket.send(pkt);
|
||||
socket.close();
|
||||
|
||||
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
|
||||
props.getRemoteHost(), props.getRemotePort(), frame.length);
|
||||
return true;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[UDP-SEND] 第{}次重试失败 tcNo={}: {}", i + 1, tcNo, e.getMessage());
|
||||
if (i == retryCount - 1) {
|
||||
throw e;
|
||||
}
|
||||
Thread.sleep(100); // 短暂延迟后重试
|
||||
}
|
||||
}
|
||||
|
||||
InetAddress addr = InetAddress.getByName(props.getRemoteHost());
|
||||
DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, props.getRemotePort());
|
||||
socket.send(pkt);
|
||||
log.info("[UDP-SEND] tcNo={} -> {}:{} frameLen={}", tcNo,
|
||||
props.getRemoteHost(), props.getRemotePort(), frame.length);
|
||||
} catch (Exception e) {
|
||||
log.error("[UDP-SEND] 发送失败 tcNo={}", tcNo, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user