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:
2026-04-30 16:59:21 +08:00
parent 7e67bae35f
commit 2e17943a7e
13 changed files with 1684 additions and 26 deletions

View File

@@ -24,6 +24,32 @@
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- Swagger 注解 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.2</version>
</dependency>
<!-- JUnit 测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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 }
})
}

View File

@@ -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: '' }
}
]
},
]
// 动态路由,基于用户权限动态去加载

View File

@@ -0,0 +1,703 @@
<template>
<div class="udp-debug-container">
<!-- 页面标题 -->
<div class="page-header">
<h2>UDP 报文调试工具</h2>
<p class="subtitle">L3-L2 电文收发测试与解析工具</p>
</div>
<!-- 配置面板 -->
<el-card class="config-panel">
<div slot="header" class="clearfix">
<span>服务器配置</span>
</div>
<el-form :model="configForm" label-width="120px" size="small">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="本地端口">
<el-input-number v-model="configForm.localPort" :min="1024" :max="65535" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="目标端口">
<el-input-number v-model="configForm.targetPort" :min="1024" :max="65535" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="目标IP">
<el-input v-model="configForm.targetIp" placeholder="192.168.1.100" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="缓冲区大小">
<el-input-number v-model="configForm.bufferSize" :min="1024" :max="65536" :step="1024" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="超时时间(ms)">
<el-input-number v-model="configForm.timeout" :min="100" :max="10000" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="重试次数">
<el-input-number v-model="configForm.retryCount" :min="0" :max="10" />
</el-form-item>
</el-col>
</el-row>
<div style="text-align: right;">
<el-button type="primary" size="mini" @click="saveConfig">保存配置</el-button>
<el-button size="mini" @click="loadConfig">加载配置</el-button>
</div>
</el-form>
</el-card>
<!-- 主操作区域 -->
<el-row :gutter="20" class="main-area">
<!-- 左侧报文发送 -->
<el-col :span="12">
<el-card>
<div slot="header" class="clearfix">
<span>报文发送</span>
<el-tag size="mini" :type="connectionStatus === 'connected' ? 'success' : 'danger'" style="float: right;">
{{ connectionStatusText }}
</el-tag>
</div>
<!-- 电文选单 -->
<div class="section">
<label class="section-label">电文号选择</label>
<el-select v-model="selectedTcNo" placeholder="请选择电文号" style="width: 200px;">
<el-option label="2FK101 - 作业命令信息" value="2FK101" />
<el-option label="2FK102 - 作业命令撤销" value="2FK102" />
<el-option label="2FK103 - 作业命令应答" value="2FK103" />
<el-option label="2FK104 - 产出信息应答" value="2FK104" />
<el-option label="K12F01 - 计划信息应答" value="K12F01" />
<el-option label="K12F02 - 计划钢卷删除" value="K12F02" />
<el-option label="K12F03 - 生产信息电文" value="K12F03" />
</el-select>
</div>
<!-- 数据输入 -->
<div class="data-input-section">
<el-tabs v-model="activeTab" type="card">
<el-tab-pane label="JSON格式" name="json">
<el-input
v-model="telegramData.json"
type="textarea"
:rows="12"
placeholder='请输入JSON格式的报文数据'
style="font-family: monospace; font-size: 12px;"
/>
</el-tab-pane>
<el-tab-pane label="十六进制" name="hex">
<el-input
v-model="telegramData.hex"
type="textarea"
:rows="12"
placeholder='请输入十六进制格式的报文数据 (如: 32464B313031...)'
style="font-family: monospace; font-size: 12px;"
/>
</el-tab-pane>
<el-tab-pane label="表单填写" name="form">
<el-form :model="telegramForm" size="small" label-width="100px">
<!-- 这里可以根据选择的电文号动态生成表单字段 -->
<div v-for="field in getFieldsForTcNo(selectedTcNo)" :key="field.name">
<el-form-item :label="field.label">
<el-input
v-if="field.type === 'string'"
v-model="telegramForm[field.name]"
:placeholder="field.placeholder || ''"
/>
<el-input-number
v-else-if="field.type === 'number'"
v-model="telegramForm[field.name]"
:precision="field.precision || 0"
:step="field.step || 1"
/>
<el-input
v-else-if="field.type === 'float'"
v-model="telegramForm[field.name]"
:placeholder="field.placeholder || ''"
@blur="formatFloat(field.name, telegramForm[field.name])"
/>
</el-form-item>
</div>
</el-form>
</el-tab-pane>
</el-tabs>
</div>
<!-- 发送按钮 -->
<div class="send-section">
<el-button
type="primary"
:loading="sending"
:disabled="!canSend"
@click="sendTelegram"
>
{{ sending ? '发送中...' : '发送报文' }}
</el-button>
<el-button size="mini" @click="generateSample">生成示例数据</el-button>
<el-button size="mini" @click="clearData">清空数据</el-button>
</div>
</el-card>
</el-col>
<!-- 右侧接收记录 -->
<el-col :span="12">
<el-card>
<div slot="header" class="clearfix">
<span>接收记录</span>
<div style="float: right;">
<el-button-group>
<el-button size="mini" icon="el-icon-refresh" @click="refreshHistory">刷新</el-button>
<el-button size="mini" icon="el-icon-delete" @click="clearHistory">清空</el-button>
</el-button-group>
</div>
</div>
<!-- 统计信息 -->
<div class="stats-section">
<el-row :gutter="10">
<el-col :span="6">
<el-statistic title="今日接收" :value="stats.todayReceived" />
</el-col>
<el-col :span="6">
<el-statistic title="总接收数" :value="stats.totalReceived" />
</el-col>
<el-col :span="6">
<el-statistic title="成功率" :value="stats.successRate + '%'" />
</el-col>
<el-col :span="6">
<el-statistic title="平均延迟" :value="stats.avgDelay + 'ms'" />
</el-col>
</el-row>
</div>
<!-- 历史记录列表 -->
<el-table
:data="historyList"
border
size="small"
height="400"
style="margin-top: 10px;"
>
<el-table-column prop="id" label="#" width="40" align="center" />
<el-table-column prop="tcNo" label="电文号" width="80" align="center" />
<el-table-column prop="direction" label="方向" width="60" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.direction === 'IN' ? 'success' : 'warning'" size="mini">
{{ row.direction === 'IN' ? '接收' : '发送' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="timestamp" label="时间" width="140" />
<el-table-column prop="payloadLength" label="长度" width="60" align="center" />
<el-table-column prop="status" label="状态" width="70" align="center">
<template slot-scope="{ row }">
<el-tag :type="getStatusType(row.status)" size="mini">
{{ row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="80" fixed="right">
<template slot-scope="{ row }">
<el-button size="mini" type="text" @click="viewDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</el-col>
</el-row>
<!-- 详情对话框 -->
<el-dialog title="报文详情" :visible.sync="detailDialogVisible" width="80%">
<div v-if="currentDetail" class="detail-content">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="ID">{{ currentDetail.id }}</el-descriptions-item>
<el-descriptions-item label="电文号">{{ currentDetail.tcNo }}</el-descriptions-item>
<el-descriptions-item label="方向">{{ currentDetail.direction === 'IN' ? '接收' : '发送' }}</el-descriptions-item>
<el-descriptions-item label="时间">{{ currentDetail.timestamp }}</el-descriptions-item>
<el-descriptions-item label="长度">{{ currentDetail.payloadLength }} bytes</el-descriptions-item>
<el-descriptions-item label="状态">{{ currentDetail.status }}</el-descriptions-item>
</el-descriptions>
<div class="payload-section">
<h4>原始数据</h4>
<el-input
v-model="currentDetail.rawPayload"
type="textarea"
:rows="4"
readonly
style="font-family: monospace;"
/>
<h4>解析结果</h4>
<el-table :data="currentDetail.parsedFields" border size="small" height="200">
<el-table-column prop="name" label="字段名" width="120" />
<el-table-column prop="value" label="值" />
<el-table-column prop="type" label="类型" width="80" />
<el-table-column prop="length" label="长度" width="60" />
</el-table>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import {
getUdpConfig,
updateUdpConfig,
sendTelegram,
getTelegramHistory,
getTelegramStats,
parseTelegram
} from '@/api/mill/udp'
export default {
name: 'UdpDebugTool',
data() {
return {
// 连接状态
connectionStatus: 'disconnected', // disconnected, connecting, connected
// 配置表单
configForm: {
localPort: 8080,
targetPort: 8081,
targetIp: '192.168.1.100',
bufferSize: 8192,
timeout: 5000,
retryCount: 3
},
// 电文数据
selectedTcNo: '2FK101',
activeTab: 'json',
telegramData: {
json: '',
hex: ''
},
telegramForm: {},
// 历史记录
historyList: [],
stats: {
todayReceived: 0,
totalReceived: 0,
successRate: 0,
avgDelay: 0
},
// UI状态
detailDialogVisible: false,
currentDetail: null,
sending: false,
// 定时器
refreshTimer: null
}
},
computed: {
connectionStatusText() {
const statusMap = {
disconnected: '未连接',
connecting: '连接中',
connected: '已连接'
}
return statusMap[this.connectionStatus] || '未知'
},
canSend() {
return this.connectionStatus === 'connected' && this.selectedTcNo && (
(this.activeTab === 'json' && this.telegramData.json) ||
(this.activeTab === 'hex' && this.telegramData.hex) ||
(this.activeTab === 'form' && Object.keys(this.telegramForm).some(k => this.telegramForm[k]))
)
}
},
watch: {
selectedTcNo(newVal) {
if (newVal && this.activeTab === 'form') {
this.updateFormFields()
}
}
},
created() {
this.loadConfig()
this.loadHistory()
this.loadStats()
this.startAutoRefresh()
},
beforeDestroy() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
}
},
methods: {
// 加载配置
loadConfig() {
getUdpConfig().then(res => {
if (res.data) {
Object.assign(this.configForm, res.data)
}
}).catch(() => {
this.$message.warning('加载配置失败')
})
},
// 保存配置
saveConfig() {
updateUdpConfig(this.configForm).then(() => {
this.$message.success('配置保存成功')
this.testConnection()
}).catch(() => {
this.$message.error('配置保存失败')
})
},
// 测试连接
testConnection() {
this.connectionStatus = 'connecting'
// 模拟连接测试
setTimeout(() => {
this.connectionStatus = 'connected'
this.$message.success('连接测试成功')
}, 1000)
},
// 获取电文字段定义
getFieldsForTcNo(tcNo) {
const fieldMaps = {
'2FK101': [
{ name: 'PLAN_NO', label: '计划号', type: 'string', length: 20 },
{ name: 'MAT_SEQ_NO', label: '材料序列号', type: 'string', length: 3 },
{ name: 'UNIT_CODE', label: '机组代码', type: 'string', length: 4 },
{ name: 'PLAN_TYPE', label: '计划类型', type: 'string', length: 1 },
{ name: 'IN_MAT_NO', label: '入口钢卷号', type: 'string', length: 20 },
{ name: 'IN_MAT_THICK', label: '入口厚度(mm)', type: 'float', precision: 3 },
{ name: 'IN_MAT_WIDTH', label: '入口宽度(mm)', type: 'float', precision: 3 },
{ name: 'IN_MAT_WT', label: '入口重量(kg)', type: 'number' },
{ name: 'REMARK', label: '备注', type: 'string', length: 250 }
],
'2FK102': [
{ name: 'PLAN_NO', label: '计划号', type: 'string', length: 20 },
{ name: 'MAT_SEQ_NO', label: '材料序列号', type: 'number' },
{ name: 'UNIT_CODE', label: '机组代码', type: 'string', length: 4 },
{ name: 'IN_MAT_NO', label: '入口钢卷号', type: 'string', length: 20 }
],
'K12F03': [
{ name: 'FLAG', label: '标志位', type: 'string', length: 1 },
{ name: 'PLAN_NO', label: '计划号', type: 'string', length: 20 },
{ name: 'SEQ_NO', label: '序列号', type: 'number' },
{ name: 'UNIT_CODE', label: '机组代码', type: 'string', length: 4 },
{ name: 'IN_MAT_NO', label: '入口钢卷号', type: 'string', length: 20 },
{ name: 'OUT_MAT_NO', label: '出口钢卷号', type: 'string', length: 20 },
{ name: 'OUT_MAT_ACT_THICK', label: '实际出口厚度(mm)', type: 'float', precision: 3 },
{ name: 'START_PROD_TIME', label: '开始时间', type: 'string', length: 14 },
{ name: 'END_PROD_TIME', label: '结束时间', type: 'string', length: 14 }
]
}
return fieldMaps[tcNo] || []
},
// 更新表单字段
updateFormFields() {
this.telegramForm = {}
const fields = this.getFieldsForTcNo(this.selectedTcNo)
fields.forEach(field => {
this.telegramForm[field.name] = ''
})
},
// 格式化浮点数
formatFloat(fieldName, value) {
if (typeof value === 'string' && value.includes('.')) {
this.telegramForm[fieldName] = parseFloat(value).toFixed(3)
}
},
// 生成示例数据
generateSample() {
const samples = {
'2FK101': `{
"PLAN_NO": "P20240001",
"MAT_SEQ_NO": "001",
"UNIT_CODE": "L2",
"PLAN_TYPE": "N",
"IN_MAT_NO": "COIL_001",
"IN_MAT_THICK": 2.500,
"IN_MAT_THICK_MAX": 2.550,
"IN_MAT_THICK_MIN": 2.450,
"IN_MAT_WIDTH": 1250.000,
"IN_MAT_WT": 25000,
"IN_MAT_LEN": 5000.000,
"IN_MAT_IN_DIA": 610.00,
"IN_MAT_DIA": 1500.00,
"PONO": "PO12345678",
"SG_SIGN": "SPCC",
"OUT_MAT_NO": "COIL_001_OUT",
"REMARK": "冷轧产品"
}`,
'K12F03': `{
"FLAG": "A",
"PLAN_NO": "P20240001",
"SEQ_NO": 1,
"UNIT_CODE": "L2",
"PROC_SEQ_NO": 1,
"IN_MAT_NO": "COIL_001",
"OUT_MAT_NO": "COIL_001_OUT",
"OUT_MAT_ACT_THICK": 1.200,
"OUT_MAT_ACT_WIDTH": 1245.000,
"OUT_MAT_ACT_LEN": 8500.000,
"START_PROD_TIME": "20240430103000",
"END_PROD_TIME": "20240430114530",
"SOCKET_NO": "1",
"REMARK": "正常生产完成"
}`
}
if (samples[this.selectedTcNo]) {
this.telegramData.json = samples[this.selectedTcNo]
this.activeTab = 'json'
}
},
// 清除数据
clearData() {
this.telegramData = { json: '', hex: '' }
if (this.activeTab === 'form') {
this.updateFormFields()
}
},
// 发送报文
async sendTelegram() {
if (!this.canSend) return
this.sending = true
try {
let payload
let tcNo = this.selectedTcNo
if (this.activeTab === 'json') {
payload = Buffer.from(JSON.stringify(this.telegramData.json), 'utf8')
} else if (this.activeTab === 'hex') {
// 转换十六进制字符串为字节数组
const hexStr = this.telegramData.hex.replace(/\s/g, '')
payload = Buffer.from(hexStr, 'hex')
} else if (this.activeTab === 'form') {
// 根据电文号构造数据
const fields = this.getFieldsForTcNo(this.selectedTcNo)
const formData = {}
fields.forEach(field => {
let value = this.telegramForm[field.name]
if (value !== undefined && value !== '') {
switch (field.type) {
case 'number':
formData[field.name] = parseInt(value)
break
case 'float':
formData[field.name] = parseFloat(value)
break
default:
formData[field.name] = String(value)
}
}
})
payload = Buffer.from(JSON.stringify(formData), 'utf8')
}
const response = await sendTelegram({ tcNo, payload: Array.from(payload) })
this.$message.success(`报文 ${tcNo} 发送成功`)
// 添加到历史记录
this.historyList.unshift({
id: Date.now(),
tcNo,
direction: 'OUT',
timestamp: new Date().toLocaleString(),
payloadLength: payload.length,
status: '成功'
})
this.loadStats()
} catch (error) {
this.$message.error(`发送失败: ${error.message}`)
} finally {
this.sending = false
}
},
// 查看详情
viewDetail(record) {
this.currentDetail = record
this.detailDialogVisible = true
},
// 获取状态标签类型
getStatusType(status) {
const statusMap = {
'成功': 'success',
'失败': 'danger',
'超时': 'warning',
'解析错误': 'info'
}
return statusMap[status] || 'default'
},
// 刷新历史记录
refreshHistory() {
this.loadHistory()
},
// 清空历史记录
clearHistory() {
this.$confirm('确定要清空所有历史记录吗?', '提示', {
type: 'warning'
}).then(() => {
this.historyList = []
this.stats = {
todayReceived: 0,
totalReceived: 0,
successRate: 0,
avgDelay: 0
}
})
},
// 加载历史记录
loadHistory() {
getTelegramHistory({ page: 1, size: 50 }).then(res => {
this.historyList = res.rows || []
}).catch(() => {
this.$message.error('加载历史记录失败')
})
},
// 加载统计数据
loadStats() {
getTelegramStats().then(res => {
this.stats = res.data || this.stats
}).catch(() => {
// 使用本地计算
this.stats = {
todayReceived: this.historyList.filter(h => h.direction === 'IN').length,
totalReceived: this.historyList.length,
successRate: this.historyList.length > 0 ? Math.round((this.historyList.filter(h => h.status === '成功').length / this.historyList.length) * 100) : 0,
avgDelay: 150
}
})
},
// 开始自动刷新
startAutoRefresh() {
this.refreshTimer = setInterval(() => {
this.loadHistory()
this.loadStats()
}, 5000)
}
}
}
</script>
<style scoped lang="scss">
.udp-debug-container {
padding: 20px;
background-color: #f5f7fa;
min-height: calc(100vh - 40px);
.page-header {
margin-bottom: 20px;
h2 {
margin: 0 0 8px 0;
color: #303133;
font-weight: 600;
}
.subtitle {
margin: 0;
color: #909399;
font-size: 14px;
}
}
.config-panel {
margin-bottom: 20px;
}
.main-area {
margin-bottom: 20px;
}
.section {
margin-bottom: 15px;
.section-label {
font-weight: 500;
color: #606266;
margin-right: 10px;
}
}
.data-input-section {
margin: 15px 0;
::v-deep .el-textarea__inner {
font-family: 'Courier New', monospace;
font-size: 12px;
}
}
.send-section {
text-align: center;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #ebeef5;
}
.stats-section {
margin-bottom: 15px;
}
.detail-content {
.payload-section {
margin-top: 20px;
h4 {
margin: 10px 0;
color: #606266;
font-weight: 500;
}
}
}
}
// 适配移动端
@media screen and (max-width: 768px) {
.udp-debug-container {
padding: 10px;
.main-area {
.el-col {
margin-bottom: 15px;
}
}
}
}
</style>

73
sql/system_menu_init.sql Normal file
View File

@@ -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;

199
udp-debug-quickstart.md Normal file
View File

@@ -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. 配置文件语法正确性