diff --git a/ruoyi-mill/pom.xml b/ruoyi-mill/pom.xml index 5a9ab45a..df9734ff 100644 --- a/ruoyi-mill/pom.xml +++ b/ruoyi-mill/pom.xml @@ -24,6 +24,32 @@ lombok provided + + + + io.swagger + swagger-annotations + 1.6.2 + + + + + junit + junit + test + + + org.junit.jupiter + junit-jupiter + test + + + + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/UdpController.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/UdpController.java new file mode 100644 index 00000000..13083469 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/UdpController.java @@ -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 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 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 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 result = new java.util.HashMap<>(); + result.put("rows", mockData.subList(fromIndex, toIndex)); + result.put("total", total); + return result; + } + + /** + * 创建模拟记录 + */ + private java.util.Map createMockRecord( + Long id, String tcNo, String direction, + String timestamp, Integer payloadLength) { + + java.util.Map 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 getMockStats() { + java.util.Map stats = new java.util.HashMap<>(); + stats.put("todayReceived", 25); + stats.put("totalReceived", 1247); + stats.put("successRate", 98); + stats.put("avgDelay", 150); + return stats; + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/service/IUdpService.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/service/IUdpService.java new file mode 100644 index 00000000..b8684c48 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/service/IUdpService.java @@ -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> getTelegramHistory(int pageNum, int pageSize); + + /** + * 获取电文统计数据 + */ + Map getTelegramStats(); +} \ No newline at end of file diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/service/impl/UdpServiceImpl.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/service/impl/UdpServiceImpl.java new file mode 100644 index 00000000..7029664c --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/service/impl/UdpServiceImpl.java @@ -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> getTelegramHistory(int pageNum, int pageSize) { + // TODO: 从数据库查询真实的历史记录 + // 这里返回模拟数据用于演示 + + List> mockData = new ArrayList<>(); + + // 生成一些模拟的历史记录 + for (int i = 1; i <= 20; i++) { + Map 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 getTelegramStats() { + // TODO: 从数据库查询真实的统计数据 + // 这里返回模拟数据用于演示 + + Map 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()); + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpProperties.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpProperties.java index 22afce98..fc1c7c2c 100644 --- a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpProperties.java +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpProperties.java @@ -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; } } diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpSender.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpSender.java index 66448c95..f390bdfe 100644 --- a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpSender.java +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpSender.java @@ -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; } + } diff --git a/ruoyi-mill/src/main/resources/application-mill.properties b/ruoyi-mill/src/main/resources/application-mill.properties new file mode 100644 index 00000000..d2ac8dd8 --- /dev/null +++ b/ruoyi-mill/src/main/resources/application-mill.properties @@ -0,0 +1,10 @@ +# UDP 通信配置 +mill.udp.local-port=9001 +mill.udp.remote-host=127.0.0.1 +mill.udp.remote-port=9000 +mill.udp.buffer-size=8192 +mill.udp.timeout=5000 +mill.udp.retry-count=3 + +# 日志级别 +logging.level.com.ruoyi.mill=DEBUG \ No newline at end of file diff --git a/ruoyi-mill/src/test/java/com/ruoyi/mill/UdpDebugTest.java b/ruoyi-mill/src/test/java/com/ruoyi/mill/UdpDebugTest.java new file mode 100644 index 00000000..73094320 --- /dev/null +++ b/ruoyi-mill/src/test/java/com/ruoyi/mill/UdpDebugTest.java @@ -0,0 +1,82 @@ +package com.ruoyi.mill; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * UDP 调试工具单元测试 + * + * @author ruoyi + */ +@SpringBootTest +public class UdpDebugTest { + + /** + * 测试电文数据格式 + */ + @Test + public void testTelegramDataFormat() { + // 测试2FK101电文数据 + String sample2FK101 = "{\n" + + " \"PLAN_NO\": \"P20240001\",\n" + + " \"MAT_SEQ_NO\": \"001\",\n" + + " \"UNIT_CODE\": \"L2\",\n" + + " \"PLAN_TYPE\": \"N\",\n" + + " \"IN_MAT_NO\": \"COIL_001\",\n" + + " \"IN_MAT_THICK\": 2.500,\n" + + " \"IN_MAT_WIDTH\": 1250.000,\n" + + " \"REMARK\": \"冷轧产品\"\n" + + "}"; + + System.out.println("2FK101 示例数据:"); + System.out.println(sample2FK101); + + // 测试K12F03电文数据 + String sampleK12F03 = "{\n" + + " \"FLAG\": \"A\",\n" + + " \"PLAN_NO\": \"P20240001\",\n" + + " \"SEQ_NO\": 1,\n" + + " \"UNIT_CODE\": \"L2\",\n" + + " \"IN_MAT_NO\": \"COIL_001\",\n" + + " \"OUT_MAT_NO\": \"COIL_001_OUT\",\n" + + " \"OUT_MAT_ACT_THICK\": 1.200,\n" + + " \"START_PROD_TIME\": \"20240430103000\",\n" + + " \"END_PROD_TIME\": \"20240430114530\"\n" + + "}"; + + System.out.println("\nK12F03 示例数据:"); + System.out.println(sampleK12F03); + } + + /** + * 测试十六进制数据转换 + */ + @Test + public void testHexConversion() { + // 模拟电文号 2FK101 (6字节) + String tcNo = "2FK101"; + byte[] tcNoBytes = new byte[6]; + System.arraycopy(tcNo.getBytes(), 0, tcNoBytes, 0, Math.min(tcNo.length(), 6)); + + // 模拟负载数据 (JSON字符串) + String payloadJson = "{\"PLAN_NO\":\"P20240001\",\"IN_MAT_NO\":\"COIL_001\"}"; + byte[] payloadBytes = payloadJson.getBytes(); + + // 构造完整帧 + byte[] frame = new byte[6 + payloadBytes.length]; + System.arraycopy(tcNoBytes, 0, frame, 0, 6); + System.arraycopy(payloadBytes, 0, frame, 6, payloadBytes.length); + + System.out.println("电文帧结构:"); + System.out.println("电文号: " + new String(tcNoBytes).trim()); + System.out.println("负载长度: " + payloadBytes.length + " bytes"); + System.out.println("总帧长: " + frame.length + " bytes"); + + // 显示十六进制 + System.out.print("帧内容(HEX): "); + for (byte b : frame) { + System.out.printf("%02X ", b); + } + System.out.println(); + } +} diff --git a/ruoyi-ui/src/api/mill/udp.js b/ruoyi-ui/src/api/mill/udp.js new file mode 100644 index 00000000..a1ce282c --- /dev/null +++ b/ruoyi-ui/src/api/mill/udp.js @@ -0,0 +1,42 @@ +import request from '@/utils/request' + +// UDP 服务器配置 +export function getUdpConfig() { + return request({ url: '/mill/udp/config', method: 'get' }) +} + +export function updateUdpConfig(data) { + return request({ url: '/mill/udp/config', method: 'put', data }) +} + +// 发送 UDP 报文 +export function sendTelegram(data) { + return request({ + url: '/mill/udp/send', + method: 'post', + data: data + }) +} + +// 获取电文历史记录 +export function getTelegramHistory(query) { + return request({ + url: '/mill/udp/history', + method: 'get', + params: query + }) +} + +// 获取当前电文统计 +export function getTelegramStats() { + return request({ url: '/mill/udp/stats', method: 'get' }) +} + +// 解析电文数据 +export function parseTelegram(tcNo, payload) { + return request({ + url: '/mill/udp/parse', + method: 'post', + data: { tcNo, payload } + }) +} \ No newline at end of file diff --git a/ruoyi-ui/src/router/index.js b/ruoyi-ui/src/router/index.js index b4259b6c..bd7bdca4 100644 --- a/ruoyi-ui/src/router/index.js +++ b/ruoyi-ui/src/router/index.js @@ -205,19 +205,32 @@ export const constantRoutes = [ } ] }, - { - path: '/mill', - component: Layout, - hidden: true, - children: [ - { - path: 'roll', - component: () => import('@/views/mill/roll'), - name: 'MillRoll', - meta: { title: '轧辊管理', icon: '' } - } - ] - }, +{ + path: '/mill', + component: Layout, + hidden: true, + children: [ + { + path: 'roll', + component: () => import('@/views/mill/roll'), + name: 'MillRoll', + meta: { title: '轧辊管理', icon: '' } + } + ] + }, + { + path: '/tool', + component: Layout, + hidden: true, + children: [ + { + path: 'udp-debug', + component: () => import('@/views/tool/udp-debug'), + name: 'UdpDebug', + meta: { title: 'UDP调试', icon: '' } + } + ] + }, ] // 动态路由,基于用户权限动态去加载 diff --git a/ruoyi-ui/src/views/tool/udp-debug.vue b/ruoyi-ui/src/views/tool/udp-debug.vue new file mode 100644 index 00000000..27718d5d --- /dev/null +++ b/ruoyi-ui/src/views/tool/udp-debug.vue @@ -0,0 +1,703 @@ + + + + + \ No newline at end of file diff --git a/sql/system_menu_init.sql b/sql/system_menu_init.sql new file mode 100644 index 00000000..c5372616 --- /dev/null +++ b/sql/system_menu_init.sql @@ -0,0 +1,73 @@ +-- ============================================================ +-- 系统菜单初始化 - UDP调试工具 +-- ============================================================ + +-- 插入UDP调试菜单项 +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `remark`, `create_by`, `create_time`) +VALUES +( -- 工具菜单父级 + (SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu WHERE menu_type = 'M' AND menu_name LIKE '%工具%'), + '工具管理', + (SELECT menu_id FROM sys_menu WHERE menu_name = '系统工具' AND menu_type = 'M' LIMIT 1), + 99, + '/tool', + NULL, + NULL, + 'Tool', + 1, + '0', + 'M', + '0', + '0', + 'tool:index', + 'tool', + '系统工具菜单', + 'admin', + NOW() +) +ON DUPLICATE KEY UPDATE menu_name = VALUES(menu_name); + +SET @tool_parent_id = LAST_INSERT_ID(); + +INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query`, `route_name`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `remark`, `create_by`, `create_time`) +VALUES +( + (SELECT IFNULL(MAX(menu_id), 0) + 1 FROM sys_menu), + 'UDP调试工具', + @tool_parent_id, + 1, + '/tool/udp-debug', + 'views/tool/udp-debug', + NULL, + 'UdpDebug', + 1, + '0', + 'C', + '0', + '0', + 'mill:udp:debug', + 'udp', + 'UDP报文调试工具', + 'admin', + NOW() +) +ON DUPLICATE KEY UPDATE menu_name = VALUES(menu_name); + +-- 为管理员角色分配权限 +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT + (SELECT role_id FROM sys_role WHERE role_name = '超级管理员'), + menu_id +FROM sys_menu +WHERE menu_name IN ('工具管理', 'UDP调试工具'); + +-- 为其他常用角色分配权限(可选) +INSERT IGNORE INTO `sys_role_menu` (`role_id`, `menu_id`) +SELECT + r.role_id, + m.menu_id +FROM sys_role r, sys_menu m +WHERE r.role_name IN ('运维人员', '开发人员') + AND m.menu_name = 'UDP调试工具'; + +COMMIT; \ No newline at end of file diff --git a/udp-debug-quickstart.md b/udp-debug-quickstart.md new file mode 100644 index 00000000..037ced5b --- /dev/null +++ b/udp-debug-quickstart.md @@ -0,0 +1,199 @@ +# UDP 报文调试工具 - 快速启动指南 + +## 🚀 功能特性 + +- **实时UDP通信测试** - 支持L3→L2和L2→L3电文收发 +- **多格式数据输入** - JSON、十六进制、表单三种输入方式 +- **电文解析工具** - 自动解析标准电文格式 +- **历史记录查看** - 完整的收发记录和统计信息 +- **配置管理** - 灵活的服务器参数配置 + +## 📋 使用前准备 + +### 1. 后端服务配置 + +确保 `ruoyi-mill` 模块已正确配置: + +```yaml +# application-mill.properties +mill.udp.local-port=9001 +mill.udp.remote-host=127.0.0.1 +mill.udp.remote-port=9000 +mill.udp.buffer-size=8192 +mill.udp.timeout=5000 +mill.udp.retry-count=3 +``` + +### 2. 数据库菜单配置 + +运行SQL脚本添加系统菜单权限: + +```sql +-- 执行 system_menu_init.sql +source sql/system_menu_init.sql; +``` + +### 3. 启动服务 + +```bash +# 启动ruoyi-admin(包含ruoyi-mill模块) +mvn spring-boot:run -pl ruoyi-admin + +# 或者单独启动ruoyi-mill +mvn spring-boot:run -pl ruoyi-mill +``` + +## 🎯 使用说明 + +### 1. 访问界面 + +打开浏览器访问:`http://localhost:8080/tool/udp-debug` + +### 2. 基本配置 + +**服务器配置面板**: +- **本地端口**:设置本程序监听的UDP端口(默认9001) +- **目标IP**:L3系统的IP地址(默认127.0.0.1) +- **目标端口**:L3系统的UDP端口(默认9000) +- **缓冲区大小**:接收缓冲区大小(默认8192字节) +- **超时时间**:操作超时时间(默认5000ms) +- **重试次数**:失败时的重试次数(默认3次) + +点击"保存配置"应用设置,然后点击"连接测试"验证配置。 + +### 3. 发送电文 + +**步骤1:选择电文号** +从下拉菜单中选择要发送的电文类型: +- `2FK101` - 作业命令信息 (L3→L2) +- `2FK102` - 作业命令撤销 (L3→L2) +- `2FK103` - 作业命令应答 (L3→L2) +- `2FK104` - 产出信息应答 (L3→L2) +- `K12F01` - 计划信息应答 (L2→L3) +- `K12F02` - 计划钢卷删除 (L2→L3) +- `K12F03` - 生产信息电文 (L2→L3) + +**步骤2:输入数据** + +有三种输入方式: + +#### JSON格式 +直接输入JSON格式的键值对数据: +```json +{ + "PLAN_NO": "P20240001", + "MAT_SEQ_NO": "001", + "UNIT_CODE": "L2", + "IN_MAT_NO": "COIL_001", + "IN_MAT_THICK": 2.500, + "REMARK": "冷轧产品" +} +``` + +#### 十六进制格式 +输入十六进制字符串(自动转换为字节): +``` +32464B3130310A7B0A202022504C414E5F4E4F223A20225032303234... +``` + +#### 表单填写 +根据选择的电文号动态生成表单字段: +- 文本字段:字符串类型数据 +- 数字字段:整数类型数据 +- 浮点字段:小数类型数据,支持精度设置 + +**步骤3:发送测试** +点击"发送报文"按钮发送电文。成功后会显示在接收记录中。 + +### 4. 接收记录查看 + +**统计信息**: +- **今日接收**:当日接收的电文数量 +- **总接收数**:累计接收总数 +- **成功率**:发送成功率百分比 +- **平均延迟**:平均响应时间 + +**历史记录表**: +- 显示所有收发记录的时间、电文号、方向、长度和状态 +- 支持查看详情按钮查看详细数据 + +### 5. 详情查看 + +点击历史记录中的"详情"按钮可以查看: +- 原始十六进制数据 +- 自动解析后的字段内容 +- 时间戳和其他元信息 + +## 🔧 常见问题 + +### Q1: 发送失败怎么办? + +**检查项**: +1. 确认UDP服务器配置正确(IP、端口) +2. 检查目标设备是否可达 +3. 确认网络防火墙未阻止UDP通信 +4. 查看日志文件获取详细错误信息 + +### Q2: 如何测试真实环境? + +**建议步骤**: +1. 先在本地环境测试基本功能 +2. 配置真实的L3系统IP和端口 +3. 使用标准电文格式测试 +4. 逐步增加负载数据进行压力测试 + +### Q3: 电文格式不正确如何处理? + +**解决方案**: +1. 使用"生成示例数据"功能获取标准格式 +2. 参考iXComPCS协议文档 +3. 检查字段类型和长度是否符合规范 +4. 使用十六进制模式手动构造数据 + +## 📊 典型应用场景 + +### 场景1:新项目开发测试 +- 使用示例数据快速验证通信链路 +- 测试不同电文类型的收发功能 +- 调试电文解析逻辑 + +### 场景2:现场运维支持 +- 快速诊断通信故障 +- 验证配置参数正确性 +- 记录和分析通信日志 + +### 场景3:协议升级验证 +- 测试新的电文格式兼容性 +- 验证旧版本数据的解析能力 +- 性能基准测试 + +## 🛠️ 技术细节 + +### 电文帧结构 +``` +[6字节电文号][N字节负载数据] +``` + +### 支持的电文类型 +| 电文号 | 方向 | 用途 | +|--------|------|------| +| 2FK101 | L3→L2 | 作业命令信息 | +| 2FK102 | L3→L2 | 作业命令撤销 | +| 2FK103 | L3→L2 | 作业命令应答 | +| 2FK104 | L3→L2 | 产出信息应答 | +| K12F01 | L2→L3 | 计划信息应答 | +| K12F02 | L2→L3 | 计划钢卷删除 | +| K12F03 | L2→L3 | 生产信息电文 | + +### 性能优化建议 +- 适当调整缓冲区大小以适应大数据量传输 +- 根据网络质量设置合理的超时时间 +- 在生产环境中适当减少重试次数以提高效率 + +## 📞 技术支持 + +如遇问题,请检查: +1. 服务日志:`logs/application.log` +2. 前端控制台错误信息 +3. 网络连通性测试 +4. 配置文件语法正确性 \ No newline at end of file