From da2620f17d44e529a3dc219509e87b3fbe24829a Mon Sep 17 00:00:00 2001 From: wangyu <823267011@qq.com> Date: Fri, 15 May 2026 14:27:49 +0800 Subject: [PATCH] =?UTF-8?q?udp=E8=B0=83=E8=AF=95=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E5=AE=8C=E5=85=A8=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mill/controller/MillDataController.java | 261 ++++++ .../ruoyi/mill/protocol/MillDataCodec.java | 178 ++++ .../ruoyi/mill/protocol/MillDataField.java | 79 ++ .../ruoyi/mill/protocol/MillDataSchema.java | 155 ++++ .../com/ruoyi/mill/udp/MillDataRecord.java | 78 ++ .../com/ruoyi/mill/udp/MillDataStore.java | 75 ++ .../java/com/ruoyi/mill/udp/UdpSender.java | 38 + .../java/com/ruoyi/mill/udp/UdpServer.java | 94 ++- ruoyi-ui/src/api/mill/millData.js | 45 + ruoyi-ui/src/views/mill/mill-debug.vue | 796 ++++++++++++++++++ ruoyi-ui/src/views/tool/udp-debug.vue | 2 +- 11 files changed, 1773 insertions(+), 28 deletions(-) create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/controller/MillDataController.java create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataCodec.java create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataField.java create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataSchema.java create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataRecord.java create mode 100644 ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataStore.java create mode 100644 ruoyi-ui/src/api/mill/millData.js create mode 100644 ruoyi-ui/src/views/mill/mill-debug.vue diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/MillDataController.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/MillDataController.java new file mode 100644 index 00000000..ef957203 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/controller/MillDataController.java @@ -0,0 +1,261 @@ +package com.ruoyi.mill.controller; + +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.mill.protocol.MillDataCodec; +import com.ruoyi.mill.protocol.MillDataField; +import com.ruoyi.mill.protocol.MillDataSchema; +import com.ruoyi.mill.udp.MillDataRecord; +import com.ruoyi.mill.udp.MillDataStore; +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.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetAddress; +import java.util.*; + +/** + * 新协议报文调试控制器 + * 帧格式:[4字节LE ID][4字节LE 数据体长度][数据体] + */ +@Api(tags = "轧线 - 新协议报文调试") +@RestController +@RequestMapping("/mill/data") +public class MillDataController extends BaseController { + + private static final Logger log = LoggerFactory.getLogger(MillDataController.class); + + @Autowired + private MillDataStore millDataStore; + + // ── Schema 查询 ──────────────────────────────────────────────────── + + @ApiOperation("获取所有报文Schema定义") + @GetMapping("/schemas") + public AjaxResult getSchemas() { + Map result = new LinkedHashMap<>(); + Map descs = MillDataSchema.getIdDescriptions(); + + List> schemaList = new ArrayList<>(); + for (Map.Entry> entry : MillDataSchema.getAllSchemas().entrySet()) { + int id = entry.getKey(); + List schema = entry.getValue(); + + Map item = new LinkedHashMap<>(); + item.put("id", id); + item.put("description", descs.getOrDefault(id, "")); + item.put("totalBytes", schema.stream().mapToInt(MillDataField::byteLength).sum()); + + List> fields = new ArrayList<>(); + for (MillDataField f : schema) { + Map fd = new LinkedHashMap<>(); + fd.put("name", f.getName()); + fd.put("description", f.getDescription()); + fd.put("unit", f.getUnit()); + fd.put("type", f.getType().name()); + fd.put("byteLength", f.byteLength()); + if (f.hasBits()) { + List> bits = new ArrayList<>(); + for (Map.Entry be : f.getBitNames().entrySet()) { + Map bit = new LinkedHashMap<>(); + bit.put("index", be.getKey()); + bit.put("name", be.getValue()); + bit.put("description", f.getBitDescriptions().getOrDefault(be.getKey(), "")); + bits.add(bit); + } + fd.put("bits", bits); + } + fields.add(fd); + } + item.put("fields", fields); + schemaList.add(item); + } + result.put("schemas", schemaList); + return success(result); + } + + // ── 发送 ────────────────────────────────────────────────────────── + + @ApiOperation("发送新协议报文到指定IP:Port") + @PostMapping("/send") + public AjaxResult send(@RequestBody Map req) { + try { + // 目标地址 + String host = (String) req.get("host"); + Object portObj = req.get("port"); + if (host == null || host.isEmpty()) return error("host不能为空"); + if (portObj == null) return error("port不能为空"); + int port = ((Number) portObj).intValue(); + + // 报文ID + Object idObj = req.get("id"); + if (idObj == null) return error("id不能为空"); + int id = ((Number) idObj).intValue(); + + List schema = MillDataSchema.getSchema(id); + if (schema == null) return error("未知报文ID: " + id); + + // 字段值 + @SuppressWarnings("unchecked") + Map values = req.containsKey("fields") + ? (Map) req.get("fields") + : Collections.emptyMap(); + + byte[] frame = MillDataCodec.encodePacket(id, schema, values); + + // UDP发送 + boolean ok = udpSend(host, port, frame); + + // 解码已发送的数据体用于记录 + byte[] body = Arrays.copyOfRange(frame, 8, frame.length); + Map decoded = MillDataCodec.decodeBody(schema, body); + millDataStore.addOutbound(id, frame, decoded, ok, host, port); + + if (ok) { + log.info("[MILL-DATA] 发送成功 id={} -> {}:{} frameLen={}", id, host, port, frame.length); + Map resp = new LinkedHashMap<>(); + resp.put("frameLength", frame.length); + resp.put("dataLength", frame.length - 8); + resp.put("hexPreview", MillDataCodec.toHexString(Arrays.copyOf(frame, Math.min(frame.length, 32)))); + return success(resp); + } else { + return error("UDP发送失败"); + } + + } catch (Exception e) { + log.error("[MILL-DATA] 发送异常", e); + return error("发送异常: " + e.getMessage()); + } + } + + @ApiOperation("发送原始Hex报文到指定IP:Port") + @PostMapping("/sendRaw") + public AjaxResult sendRaw(@RequestBody Map req) { + try { + String host = (String) req.get("host"); + Object portObj = req.get("port"); + String hexStr = (String) req.get("hex"); + if (host == null || host.isEmpty()) return error("host不能为空"); + if (portObj == null) return error("port不能为空"); + if (hexStr == null || hexStr.isEmpty()) return error("hex不能为空"); + + int port = ((Number) portObj).intValue(); + hexStr = hexStr.replaceAll("[\\s\\-]", ""); + if (hexStr.length() % 2 != 0) return error("hex字符串长度必须为偶数"); + + byte[] frame = new byte[hexStr.length() / 2]; + for (int i = 0; i < frame.length; i++) { + frame[i] = (byte) Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16); + } + + int packetId = MillDataCodec.peekId(frame); + List schema = MillDataSchema.getSchema(packetId); + + boolean ok = udpSend(host, port, frame); + + Map decoded = null; + if (schema != null && frame.length >= 8) { + byte[] body = Arrays.copyOfRange(frame, 8, frame.length); + decoded = MillDataCodec.decodeBody(schema, body); + } + millDataStore.addOutbound(packetId, frame, decoded, ok, host, port); + + if (ok) { + Map r = new LinkedHashMap<>(); + r.put("frameLength", frame.length); + return success(r); + } else { + return error("UDP发送失败"); + } + + } catch (Exception e) { + log.error("[MILL-DATA] 原始发送异常", e); + return error("发送异常: " + e.getMessage()); + } + } + + // ── 解析 ────────────────────────────────────────────────────────── + + @ApiOperation("解析Hex字节为字段值") + @PostMapping("/parse") + public AjaxResult parse(@RequestBody Map req) { + try { + String hexStr = (String) req.get("hex"); + if (hexStr == null || hexStr.isEmpty()) return error("hex不能为空"); + + hexStr = hexStr.replaceAll("[\\s\\-]", ""); + byte[] data = new byte[hexStr.length() / 2]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16); + } + + Map result = MillDataCodec.decodePacket(data, MillDataSchema.getAllSchemas()); + + // 将rawBody转为hex展示 + byte[] rawBody = (byte[]) result.get("rawBody"); + if (rawBody != null) { + result.put("rawBodyHex", MillDataCodec.toHexString(rawBody)); + result.remove("rawBody"); + } + return success(result); + + } catch (Exception e) { + log.error("[MILL-DATA] 解析异常", e); + return error("解析异常: " + e.getMessage()); + } + } + + // ── 历史记录 ────────────────────────────────────────────────────── + + @ApiOperation("获取报文历史记录") + @GetMapping("/history") + public AjaxResult history( + @RequestParam(defaultValue = "1") Integer pageNum, + @RequestParam(defaultValue = "50") Integer pageSize) { + List rows = millDataStore.getHistory(pageNum, pageSize); + Map result = new LinkedHashMap<>(); + result.put("rows", rows); + result.put("total", millDataStore.getTotalCount()); + return success(result); + } + + @ApiOperation("获取统计信息") + @GetMapping("/stats") + public AjaxResult stats() { + Map stats = new LinkedHashMap<>(); + int total = millDataStore.getTotalCount(); + long success = millDataStore.countSuccess(); + stats.put("total", total); + stats.put("inbound", millDataStore.countInbound()); + stats.put("outbound", millDataStore.countOutbound()); + stats.put("successRate", total > 0 ? Math.round(success * 100.0 / total) : 100); + return success(stats); + } + + @ApiOperation("清空历史记录") + @DeleteMapping("/history") + public AjaxResult clearHistory() { + millDataStore.clear(); + return success(); + } + + // ── 内部工具 ────────────────────────────────────────────────────── + + private boolean udpSend(String host, int port, byte[] data) { + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(3000); + InetAddress addr = InetAddress.getByName(host); + DatagramPacket pkt = new DatagramPacket(data, data.length, addr, port); + socket.send(pkt); + return true; + } catch (Exception e) { + log.warn("[MILL-DATA] UDP发送失败 {}:{} : {}", host, port, e.getMessage()); + return false; + } + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataCodec.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataCodec.java new file mode 100644 index 00000000..153c4bc0 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataCodec.java @@ -0,0 +1,178 @@ +package com.ruoyi.mill.protocol; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.*; + +/** + * 新协议编解码器 + * + * 帧格式: + * [4字节 LITTLE_ENDIAN] ID (uint32) + * [4字节 LITTLE_ENDIAN] 数据体字节数 (uint32) + * [N字节] 数据体 + * + * 数据体字段编码规则(全部小端存储): + * I4 → 4字节有符号整数 + * I2 → 2字节有符号整数 + * F4 → 4字节 IEEE-754 单精度浮点 + * 位字段从所属 I4 字段中按位提取(bit0 = LSB) + */ +public final class MillDataCodec { + + private MillDataCodec() {} + + // ── 编码 ────────────────────────────────────────────────────────── + + /** + * 编码完整报文(含8字节头) + */ + public static byte[] encodePacket(int id, List schema, Map values) { + byte[] body = encodeBody(schema, values); + ByteBuffer buf = ByteBuffer.allocate(8 + body.length).order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(id); + buf.putInt(body.length); + buf.put(body); + return buf.array(); + } + + /** + * 仅编码数据体(不含头) + */ + public static byte[] encodeBody(List schema, Map values) { + int totalLen = schema.stream().mapToInt(MillDataField::byteLength).sum(); + ByteBuffer buf = ByteBuffer.allocate(totalLen).order(ByteOrder.LITTLE_ENDIAN); + + for (MillDataField field : schema) { + switch (field.getType()) { + case I4: { + int intVal; + if (field.hasBits()) { + intVal = 0; + for (Map.Entry e : field.getBitNames().entrySet()) { + Object bitVal = values.get(e.getValue()); + if (isTruthy(bitVal)) { + intVal |= (1 << e.getKey()); + } + } + // 如果也直接提供了整数值,以整数值为准 + Object raw = values.get(field.getName()); + if (raw instanceof Number) intVal = ((Number) raw).intValue(); + } else { + Object raw = values.getOrDefault(field.getName(), 0); + intVal = raw instanceof Number ? ((Number) raw).intValue() : 0; + } + buf.putInt(intVal); + break; + } + case I2: { + Object raw = values.getOrDefault(field.getName(), 0); + short shortVal = raw instanceof Number ? ((Number) raw).shortValue() : 0; + buf.putShort(shortVal); + break; + } + case F4: { + Object raw = values.getOrDefault(field.getName(), 0f); + float floatVal = raw instanceof Number ? ((Number) raw).floatValue() : 0f; + buf.putFloat(floatVal); + break; + } + } + } + return buf.array(); + } + + // ── 解码 ────────────────────────────────────────────────────────── + + /** + * 解码完整报文,返回头信息 + 字段值Map + * @return id→packetId, dataLength→body字节数, fields→字段Map + */ + public static Map decodePacket(byte[] data, Map> schemas) { + Map result = new LinkedHashMap<>(); + if (data.length < 8) { + result.put("error", "报文过短,至少需要8字节头"); + return result; + } + ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN); + int id = buf.getInt(); + int dataLen = buf.getInt(); + + result.put("id", id); + result.put("dataLength", dataLen); + + int bodyLen = Math.min(dataLen, data.length - 8); + byte[] body = new byte[bodyLen]; + buf.get(body); + result.put("rawBody", body); + + List schema = schemas != null ? schemas.get(id) : null; + if (schema != null) { + Map fields = decodeBody(schema, body); + result.put("fields", fields); + result.put("schemaMatched", true); + } else { + result.put("schemaMatched", false); + } + return result; + } + + /** + * 仅解码数据体 + */ + public static Map decodeBody(List schema, byte[] body) { + ByteBuffer buf = ByteBuffer.wrap(body).order(ByteOrder.LITTLE_ENDIAN); + Map result = new LinkedHashMap<>(); + + for (MillDataField field : schema) { + if (buf.remaining() < field.byteLength()) break; + switch (field.getType()) { + case I4: { + int val = buf.getInt(); + result.put(field.getName(), val); + if (field.hasBits()) { + for (Map.Entry e : field.getBitNames().entrySet()) { + result.put(e.getValue(), (val >> e.getKey()) & 1); + } + } + break; + } + case I2: { + result.put(field.getName(), (int) buf.getShort()); + break; + } + case F4: { + result.put(field.getName(), buf.getFloat()); + break; + } + } + } + return result; + } + + // ── 工具方法 ────────────────────────────────────────────────────── + + /** 将字节数组转为16进制字符串(含空格) */ + public static String toHexString(byte[] data) { + if (data == null || data.length == 0) return ""; + StringBuilder sb = new StringBuilder(data.length * 3); + for (byte b : data) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + + /** 读取报文头中的ID(前4字节LE),不足则返回-1 */ + public static int peekId(byte[] data) { + if (data == null || data.length < 4) return -1; + return ByteBuffer.wrap(data, 0, 4).order(ByteOrder.LITTLE_ENDIAN).getInt(); + } + + private static boolean isTruthy(Object val) { + if (val == null) return false; + if (val instanceof Boolean) return (Boolean) val; + if (val instanceof Number) return ((Number) val).intValue() != 0; + if (val instanceof String) return "1".equals(val) || "true".equalsIgnoreCase((String) val); + return false; + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataField.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataField.java new file mode 100644 index 00000000..06254298 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataField.java @@ -0,0 +1,79 @@ +package com.ruoyi.mill.protocol; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 新协议字段定义 + * 报文格式:[4字节LE ID][4字节LE 数据长度][数据体] + * 数据体:每个字段小端存储 + */ +public class MillDataField { + + public enum DataType { + I4, // 4字节有符号整数(小端) + I2, // 2字节有符号整数(小端) + F4 // 4字节IEEE-754单精度浮点(小端) + } + + private final String name; + private final String description; + private final String unit; + private final DataType type; + // I4类型可含位字段:index→bitName,index→bitDescription + private final Map bitNames; + private final Map bitDescriptions; + + private MillDataField(String name, String description, String unit, DataType type, + Map bitNames, Map bitDescriptions) { + this.name = name; + this.description = description; + this.unit = unit; + this.type = type; + this.bitNames = bitNames != null ? Collections.unmodifiableMap(bitNames) : Collections.emptyMap(); + this.bitDescriptions = bitDescriptions != null ? Collections.unmodifiableMap(bitDescriptions) : Collections.emptyMap(); + } + + public static MillDataField i4(String name, String description, String unit) { + return new MillDataField(name, description, unit, DataType.I4, null, null); + } + + public static MillDataField i4WithBits(String name, String description, + Map bitNames, + Map bitDescriptions) { + return new MillDataField(name, description, "", DataType.I4, bitNames, bitDescriptions); + } + + public static MillDataField i2(String name, String description, String unit) { + return new MillDataField(name, description, unit, DataType.I2, null, null); + } + + public static MillDataField f4(String name, String description, String unit) { + return new MillDataField(name, description, unit, DataType.F4, null, null); + } + + public int byteLength() { + return type == DataType.I2 ? 2 : 4; + } + + public boolean hasBits() { + return !bitNames.isEmpty(); + } + + public String getName() { return name; } + public String getDescription() { return description; } + public String getUnit() { return unit; } + public DataType getType() { return type; } + public Map getBitNames() { return bitNames; } + public Map getBitDescriptions() { return bitDescriptions; } + + /** 构建位字段Map的便捷方法 */ + public static Map bits(Object... indexAndName) { + Map map = new LinkedHashMap<>(); + for (int i = 0; i + 1 < indexAndName.length; i += 2) { + map.put((Integer) indexAndName[i], (String) indexAndName[i + 1]); + } + return map; + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataSchema.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataSchema.java new file mode 100644 index 00000000..bf86376d --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/protocol/MillDataSchema.java @@ -0,0 +1,155 @@ +package com.ruoyi.mill.protocol; + +import java.util.*; + +import static com.ruoyi.mill.protocol.MillDataField.*; + +/** + * 新协议报文 Schema 定义 + * + * 报文帧结构: + * [4字节 LE] ID + * [4字节 LE] 数据体长度 + * [N字节] 数据体(各字段小端存储) + * + * ID=1202 (0x000004B2):100ms周期发送,数据体138字节 + * no.1 counter I4 4B offset=0 + * no.2 passNo I2 2B offset=4 (道次号) + * no.3 rolledLength F4 4B offset=6 (当前道次轧制长度 m) + * no.4 entryCoilerLen F4 4B offset=10 + * no.5 exitCoilerLen F4 4B offset=14 + * no.6 millStatus I4 4B offset=18 (含9个位字段 bit0~bit8) + * no.7 entryThick F4 4B offset=22 (入口厚度设定值 mm) + * no.8 entryThickDev F4 4B offset=26 (入口厚度偏差 mm) + * no.9 exitThick F4 4B offset=30 + * no.10 exitThickDev F4 4B offset=34 + * no.11 topLimit F4 4B offset=38 (偏差上限 %) + * no.12 botLimit F4 4B offset=42 (偏差下限 %) + * no.13 ffSc F4 4B offset=46 (前馈辊缝修正) + * no.14 spare14 F4 4B offset=50 (备用) + * no.15 fbSc F4 4B offset=54 (反馈辊缝修正) + * no.16 mfSc F4 4B offset=58 (秒流量辊缝修正) + * no.17 entryTension F4 4B offset=62 (kN) + * no.18 entryTensionDiff F4 4B offset=66 + * no.19 exitTension F4 4B offset=70 + * no.20 exitTensionDiff F4 4B offset=74 + * no.21 entrySpeed F4 4B offset=78 (m/min) + * no.22 exitSpeed F4 4B offset=82 + * no.23 standSpeed F4 4B offset=86 + * no.24 rollForce F4 4B offset=90 (kN) + * no.25 rollForceDiff F4 4B offset=94 + * no.26 rollgap F4 4B offset=98 (mm) + * no.27 rollgapDiff F4 4B offset=102 + * no.28 forwardslip F4 4B offset=106 + * no.29 power F4 4B offset=110 (kW) + * no.30 torque F4 4B offset=114 (kNm) + * no.31 irbendMeasure F4 4B offset=118 (kN) + * no.32 irbendReference F4 4B offset=122 + * no.33 wrbendMeasure F4 4B offset=126 + * no.34 wrbendReference F4 4B offset=130 + * no.35 irshift F4 4B offset=134 (mm) + * Total: 138 bytes + */ +public final class MillDataSchema { + + private MillDataSchema() {} + + public static final int ID_1202 = 1202; + + public static final List SCHEMA_1202 = Collections.unmodifiableList(Arrays.asList( + // no.1 + i4("counter", "计数器", "-"), + // no.2 I2 = 2字节 + i2("passNo", "道次号", "-"), + // no.3~5 + f4("rolledLength", "当前道次轧制长度", "m"), + f4("entryCoilerLen", "入口卷取机长度", "m"), + f4("exitCoilerLen", "出口卷取机长度", "m"), + // no.6 I4含位字段 + i4WithBits("millStatus", "轧机状态", + bits( + 0, "millDecelerating", + 1, "millAccelerating", + 2, "entryGaugeMeterHealthy", + 3, "exitGaugeMeterHealthy", + 4, "entrySpeedMeterHealthy", + 5, "exitSpeedMeterHealthy" + ), + bits( + 0, "轧机减速中", + 1, "轧机加速中", + 2, "入口测厚仪健康", + 3, "出口测厚仪健康", + 4, "入口测速仪健康", + 5, "出口测速仪健康" + ) + ), + // no.7~13 + f4("entryThick", "入口厚度设定值", "mm"), + f4("entryThickDev", "入口厚度偏差", "mm"), + f4("exitThick", "出口厚度", "mm"), + f4("exitThickDev", "出口厚度偏差", "mm"), + f4("topLimit", "偏差上限", "%"), + f4("botLimit", "偏差下限", "%"), + f4("ffSc", "前馈辊缝修正", ""), + // no.14 备用 + f4("spare14", "备用字段14", ""), + // no.15~16 + f4("fbSc", "反馈辊缝修正", ""), + f4("mfSc", "秒流量辊缝修正", ""), + // no.17~20 + f4("entryTension", "入口张力", "kN"), + f4("entryTensionDiff", "入口张力差", "kN"), + f4("exitTension", "出口张力", "kN"), + f4("exitTensionDiff", "出口张力差", "kN"), + // no.21~23 + f4("entrySpeed", "入口速度", "m/min"), + f4("exitSpeed", "出口速度", "m/min"), + f4("standSpeed", "机架速度", "m/min"), + // no.24~27 + f4("rollForce", "轧制力", "kN"), + f4("rollForceDiff", "轧制力差", "kN"), + f4("rollgap", "辊缝", "mm"), + f4("rollgapDiff", "辊缝差", "mm"), + // no.28~30 + f4("forwardslip", "前滑值", ""), + f4("power", "功率", "kW"), + f4("torque", "扭矩", "kNm"), + // no.31~35 + f4("irbendMeasure", "工作辊弯辊实测", "kN"), + f4("irbendReference", "工作辊弯辊设定", "kN"), + f4("wrbendMeasure", "支撑辊弯辊实测", "kN"), + f4("wrbendReference", "支撑辊弯辊设定", "kN"), + f4("irshift", "工作辊横移", "mm") + )); + + // ── Schema 注册表 ────────────────────────────────────────────────── + private static final Map> SCHEMA_MAP; + private static final Map ID_DESCRIPTIONS; + + static { + Map> m = new LinkedHashMap<>(); + m.put(ID_1202, SCHEMA_1202); + SCHEMA_MAP = Collections.unmodifiableMap(m); + + Map d = new LinkedHashMap<>(); + d.put(ID_1202, "100ms周期数据(轧机实时状态)"); + ID_DESCRIPTIONS = Collections.unmodifiableMap(d); + } + + public static List getSchema(int id) { + return SCHEMA_MAP.get(id); + } + + public static Map> getAllSchemas() { + return SCHEMA_MAP; + } + + public static Map getIdDescriptions() { + return ID_DESCRIPTIONS; + } + + public static boolean isKnownId(int id) { + return SCHEMA_MAP.containsKey(id); + } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataRecord.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataRecord.java new file mode 100644 index 00000000..6b8ef7f8 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataRecord.java @@ -0,0 +1,78 @@ +package com.ruoyi.mill.udp; + +import com.ruoyi.mill.protocol.MillDataCodec; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +/** + * 新协议报文记录(发送/接收) + */ +public class MillDataRecord { + + private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private long id; + private int packetId; + private String direction; // IN / OUT + private String timestamp; + private int dataLength; + private String rawHex; // 完整报文16进制 + private Map fields; // 解析后字段 + private boolean success; + private String targetHost; + private int targetPort; + private String sourceHost; + private int sourcePort; + + private MillDataRecord() {} + + public static MillDataRecord inbound(long id, int packetId, byte[] rawFrame, + Map fields, + String sourceHost, int sourcePort) { + MillDataRecord r = new MillDataRecord(); + r.id = id; + r.packetId = packetId; + r.direction = "IN"; + r.timestamp = LocalDateTime.now().format(FMT); + r.dataLength = rawFrame.length; + r.rawHex = MillDataCodec.toHexString(rawFrame); + r.fields = fields; + r.success = true; + r.sourceHost = sourceHost; + r.sourcePort = sourcePort; + return r; + } + + public static MillDataRecord outbound(long id, int packetId, byte[] rawFrame, + Map fields, + boolean success, + String targetHost, int targetPort) { + MillDataRecord r = new MillDataRecord(); + r.id = id; + r.packetId = packetId; + r.direction = "OUT"; + r.timestamp = LocalDateTime.now().format(FMT); + r.dataLength = rawFrame.length; + r.rawHex = MillDataCodec.toHexString(rawFrame); + r.fields = fields; + r.success = success; + r.targetHost = targetHost; + r.targetPort = targetPort; + return r; + } + + public long getId() { return id; } + public int getPacketId() { return packetId; } + public String getDirection() { return direction; } + public String getTimestamp() { return timestamp; } + public int getDataLength() { return dataLength; } + public String getRawHex() { return rawHex; } + public Map getFields() { return fields; } + public boolean isSuccess() { return success; } + public String getTargetHost() { return targetHost; } + public int getTargetPort() { return targetPort; } + public String getSourceHost() { return sourceHost; } + public int getSourcePort() { return sourcePort; } +} diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataStore.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataStore.java new file mode 100644 index 00000000..8dfd6404 --- /dev/null +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/MillDataStore.java @@ -0,0 +1,75 @@ +package com.ruoyi.mill.udp; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 新协议报文记录仓库(线程安全,最多保留1000条) + */ +@Component +public class MillDataStore { + + private static final int MAX_RECORDS = 1000; + + private final CopyOnWriteArrayList records = new CopyOnWriteArrayList<>(); + private final AtomicLong idGen = new AtomicLong(0); + + public MillDataRecord addInbound(int packetId, byte[] rawFrame, + java.util.Map fields, + String sourceHost, int sourcePort) { + MillDataRecord r = MillDataRecord.inbound(idGen.incrementAndGet(), packetId, rawFrame, + fields, sourceHost, sourcePort); + records.add(0, r); + trim(); + return r; + } + + public MillDataRecord addOutbound(int packetId, byte[] rawFrame, + java.util.Map fields, + boolean success, + String targetHost, int targetPort) { + MillDataRecord r = MillDataRecord.outbound(idGen.incrementAndGet(), packetId, rawFrame, + fields, success, targetHost, targetPort); + records.add(0, r); + trim(); + return r; + } + + public List getHistory(int pageNum, int pageSize) { + int total = records.size(); + int from = (pageNum - 1) * pageSize; + if (from >= total) return Collections.emptyList(); + return new ArrayList<>(records.subList(from, Math.min(from + pageSize, total))); + } + + public int getTotalCount() { + return records.size(); + } + + public long countInbound() { + return records.stream().filter(r -> "IN".equals(r.getDirection())).count(); + } + + public long countOutbound() { + return records.stream().filter(r -> "OUT".equals(r.getDirection())).count(); + } + + public long countSuccess() { + return records.stream().filter(MillDataRecord::isSuccess).count(); + } + + public void clear() { + records.clear(); + } + + private void trim() { + while (records.size() > MAX_RECORDS) { + records.remove(records.size() - 1); + } + } +} 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 4f95c369..5f27f95b 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 @@ -1,5 +1,8 @@ package com.ruoyi.mill.udp; +import com.ruoyi.mill.protocol.MillDataCodec; +import com.ruoyi.mill.protocol.MillDataField; +import com.ruoyi.mill.protocol.MillDataSchema; import com.ruoyi.mill.protocol.TelegramCodec; import com.ruoyi.mill.protocol.TelegramSchema; import org.slf4j.Logger; @@ -12,6 +15,7 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; import java.util.Map; /** @@ -92,6 +96,40 @@ public class UdpSender { return false; } + // ── 新协议发送 ──────────────────────────────────────────────────── + + /** + * 新协议:发送报文到指定IP:Port + * 帧格式 = [4字节LE ID][4字节LE 数据体长度][数据体] + */ + public boolean sendMillData(int id, String host, int port, Map values) { + List schema = MillDataSchema.getSchema(id); + if (schema == null) { + log.error("[MILL-DATA] 未知报文ID: {}", id); + return false; + } + byte[] frame = MillDataCodec.encodePacket(id, schema, values); + return sendMillDataRaw(id, host, port, frame, schema, values); + } + + public boolean sendMillDataRaw(int id, String host, int port, byte[] frame, + List schema, + Map values) { + try (DatagramSocket socket = new DatagramSocket()) { + socket.setSoTimeout(props.getTimeout()); + InetAddress addr = InetAddress.getByName(host); + DatagramPacket pkt = new DatagramPacket(frame, frame.length, addr, port); + socket.send(pkt); + log.info("[MILL-DATA] 发送成功 id={} -> {}:{} frameLen={}", id, host, port, frame.length); + return true; + } catch (Exception e) { + log.warn("[MILL-DATA] 发送失败 id={} -> {}:{} : {}", id, host, port, e.getMessage()); + return false; + } + } + + // ── 旧协议内部方法 ──────────────────────────────────────────────── + private Map decodePayload(String tcNo, byte[] payload) { try { java.util.List schema = TelegramSchema.getSchema(tcNo); diff --git a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpServer.java b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpServer.java index bcf736f6..40dd720b 100644 --- a/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpServer.java +++ b/ruoyi-mill/src/main/java/com/ruoyi/mill/udp/UdpServer.java @@ -1,5 +1,8 @@ package com.ruoyi.mill.udp; +import com.ruoyi.mill.protocol.MillDataCodec; +import com.ruoyi.mill.protocol.MillDataField; +import com.ruoyi.mill.protocol.MillDataSchema; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -11,16 +14,22 @@ import java.net.DatagramPacket; import java.net.DatagramSocket; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** - * UDP 接收服务 - * 监听来自 L3 的下行电文,解析电文号后交 TelegramDispatcher 处理 + * UDP接收服务,支持两种帧格式自动识别: * - * 电文帧结构(iXComPCS 第18章): - * 前6字节 ASCII = 电文号 (TC_NO) - * 后续字节 = 电文体 (payload) + * 新协议(优先判断): + * [4字节 LITTLE_ENDIAN] 报文ID (uint32,已知ID在MillDataSchema中) + * [4字节 LITTLE_ENDIAN] 数据体长度 + * [N字节] 数据体(各字段小端存储) + * + * 旧协议(iXComPCS 第18章): + * [6字节 ASCII] 电文号 (TC_NO) + * [N字节] 电文体 */ @Component public class UdpServer { @@ -34,6 +43,9 @@ public class UdpServer { @Autowired private TelegramDispatcher dispatcher; + @Autowired + private MillDataStore millDataStore; + private DatagramSocket socket; private volatile boolean running; private final ExecutorService executor = Executors.newSingleThreadExecutor( @@ -62,34 +74,28 @@ public class UdpServer { try { DatagramPacket pkt = new DatagramPacket(buf, buf.length); socket.receive(pkt); - - // 打印接收到的原始数据信息 + String senderAddr = pkt.getAddress().getHostAddress(); int senderPort = pkt.getPort(); byte[] data = Arrays.copyOf(pkt.getData(), pkt.getLength()); - - log.info("[UDP-RECV] <<<< 收到UDP数据包 - 来源: {}:{}, 长度: {} bytes", - senderAddr, senderPort, data.length); - - if (data.length < TC_NO_LEN) { - log.warn("[UDP-RECV] 收到过短数据包,长度={}, 忽略", data.length); + + log.info("[UDP-RECV] <<<< {}:{} len={}", senderAddr, senderPort, data.length); + + if (data.length < 4) { + log.warn("[UDP-RECV] 数据包过短({} bytes),丢弃", data.length); continue; } - - String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim(); - byte[] payload = Arrays.copyOfRange(data, TC_NO_LEN, data.length); - - log.info("[UDP-RECV] 电文号: '{}', Payload长度: {} bytes", tcNo, payload.length); - - // 打印前32字节的十六进制数据 - StringBuilder hexDump = new StringBuilder(); - for (int i = 0; i < Math.min(data.length, 32); i++) { - hexDump.append(String.format("%02X ", data[i])); + + // 优先尝试新协议:前4字节LE读取ID,判断是否已注册 + int candidateId = MillDataCodec.peekId(data); + if (data.length >= 8 && MillDataSchema.isKnownId(candidateId)) { + handleNewProtocol(data, candidateId, senderAddr, senderPort); + } else if (data.length >= TC_NO_LEN) { + handleOldProtocol(data, senderAddr, senderPort); + } else { + log.warn("[UDP-RECV] 无法识别的短包({} bytes),丢弃", data.length); } - log.debug("[UDP-RECV] 数据预览: {}", hexDump.toString()); - - dispatcher.dispatch(tcNo, data, payload); - + } catch (Exception e) { if (running) { log.error("[UDP-SERVER] 接收异常", e); @@ -97,4 +103,38 @@ public class UdpServer { } } } + + /** 处理新协议帧 */ + private void handleNewProtocol(byte[] data, int packetId, String senderAddr, int senderPort) { + int dataLen = java.nio.ByteBuffer.wrap(data, 4, 4) + .order(java.nio.ByteOrder.LITTLE_ENDIAN).getInt(); + int bodyLen = Math.min(dataLen, data.length - 8); + byte[] body = Arrays.copyOfRange(data, 8, 8 + bodyLen); + + List schema = MillDataSchema.getSchema(packetId); + Map fields = null; + if (schema != null) { + try { + fields = MillDataCodec.decodeBody(schema, body); + } catch (Exception e) { + log.warn("[UDP-RECV][NEW] 解码数据体失败 id={}: {}", packetId, e.getMessage()); + } + } + + millDataStore.addInbound(packetId, data, fields, senderAddr, senderPort); + log.info("[UDP-RECV][NEW] id={} dataLen={} bodyDecoded={}", packetId, dataLen, fields != null); + + // 打印前32字节hex + if (log.isDebugEnabled()) { + log.debug("[UDP-RECV][NEW] hex: {}", MillDataCodec.toHexString(Arrays.copyOf(data, Math.min(data.length, 32)))); + } + } + + /** 处理旧协议帧(iXComPCS) */ + private void handleOldProtocol(byte[] data, String senderAddr, int senderPort) { + String tcNo = new String(data, 0, TC_NO_LEN, StandardCharsets.US_ASCII).trim(); + byte[] payload = Arrays.copyOfRange(data, TC_NO_LEN, data.length); + log.info("[UDP-RECV][OLD] tcNo='{}' payloadLen={}", tcNo, payload.length); + dispatcher.dispatch(tcNo, data, payload); + } } diff --git a/ruoyi-ui/src/api/mill/millData.js b/ruoyi-ui/src/api/mill/millData.js new file mode 100644 index 00000000..bd0cce16 --- /dev/null +++ b/ruoyi-ui/src/api/mill/millData.js @@ -0,0 +1,45 @@ +import request from '@/utils/request' + +/** 获取所有报文Schema定义 */ +export function getSchemas() { + return request({ url: '/mill/data/schemas', method: 'get' }) +} + +/** + * 发送报文(字段表单模式) + * @param {Object} data - { host, port, id, fields: {} } + */ +export function sendPacket(data) { + return request({ url: '/mill/data/send', method: 'post', data }) +} + +/** + * 发送原始Hex报文 + * @param {Object} data - { host, port, hex: '...' } + */ +export function sendRawPacket(data) { + return request({ url: '/mill/data/sendRaw', method: 'post', data }) +} + +/** + * 解析Hex为字段值 + * @param {Object} data - { hex: '...' } + */ +export function parsePacket(data) { + return request({ url: '/mill/data/parse', method: 'post', data }) +} + +/** 获取报文历史 */ +export function getHistory(query) { + return request({ url: '/mill/data/history', method: 'get', params: query }) +} + +/** 获取统计信息 */ +export function getStats() { + return request({ url: '/mill/data/stats', method: 'get' }) +} + +/** 清空历史记录 */ +export function clearHistory() { + return request({ url: '/mill/data/history', method: 'delete' }) +} diff --git a/ruoyi-ui/src/views/mill/mill-debug.vue b/ruoyi-ui/src/views/mill/mill-debug.vue new file mode 100644 index 00000000..789c120e --- /dev/null +++ b/ruoyi-ui/src/views/mill/mill-debug.vue @@ -0,0 +1,796 @@ + + + + + diff --git a/ruoyi-ui/src/views/tool/udp-debug.vue b/ruoyi-ui/src/views/tool/udp-debug.vue index 655c8f85..1e809d1d 100644 --- a/ruoyi-ui/src/views/tool/udp-debug.vue +++ b/ruoyi-ui/src/views/tool/udp-debug.vue @@ -267,7 +267,7 @@ export default { // 配置表单 configForm: { - localPort: 8080, + localPort: 8090, remotePort: 8081, remoteHost: '192.168.1.100', bufferSize: 8192,