diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/domain/PowerFrame.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/domain/PowerFrame.java new file mode 100644 index 0000000..818a6ac --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/domain/PowerFrame.java @@ -0,0 +1,20 @@ +package org.dromara.sis.sdk.meter.domain; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * @author lsm + * @apiNote PowerFrame + * @since 2025/7/20 + */ +@Data +@AllArgsConstructor +public class PowerFrame { + + private byte[] address; + + private byte controlCode; + + private byte[] data; +} diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/PowerMeterUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/PowerMeterUtil.java new file mode 100644 index 0000000..aa16979 --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/PowerMeterUtil.java @@ -0,0 +1,179 @@ +package org.dromara.sis.sdk.meter.utils; + +import org.dromara.sis.sdk.meter.domain.PowerFrame; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * @author lsm + * @apiNote PowerMeterUtil + * @since 2025/7/20 + */ +public class PowerMeterUtil { + + // 协议常量定义 + public static final byte FRAME_START = 0x68; + public static final byte FRAME_END = 0x16; + public static final byte[] PREAMBLE = {(byte) 0xFE, (byte) 0xFE, (byte) 0xFE, (byte) 0xFE}; + public static final int ADDR_LENGTH = 6; + public static final int MAX_READ_DATA_LEN = 200; + public static final int MAX_WRITE_DATA_LEN = 50; + + // 控制码功能定义 + public static final byte CTRL_BROADCAST_TIME = 0x08; + public static final byte CTRL_READ_DATA = 0x11; + public static final byte CTRL_READ_FOLLOW_DATA = 0x12; + public static final byte CTRL_WRITE_DATA = 0x14; + public static final byte CTRL_TRIP_CONTROL = 0x1C; + public static final byte CTRL_OUTPUT_CONTROL = 0x1D; + + // 地址通配符 + public static final byte ADDR_WILDCARD = (byte) 0xAA; + + /** + * 构建基础帧结构 + * + * @param address 6字节地址(高位在前,低位在后) + * @param ctrlCode 控制码 + * @param data 原始数据域(未加33H) + * @param isEncode 是否进行数据域处理 + * @return 完整帧数据 + */ + public byte[] buildFrame(byte[] address, byte ctrlCode, byte[] data, boolean isEncode) { + if (address.length != ADDR_LENGTH) { + throw new IllegalArgumentException("Address must be 6 bytes"); + } + + // 处理数据域:每个字节加0x33 + byte[] processedData = processDataDomain(data, isEncode); + + // 计算数据域长度 + int dataLen = (data != null) ? data.length : 0; + if (dataLen > MAX_READ_DATA_LEN) { + throw new IllegalArgumentException("Data length exceeds max limit"); + } + + // 计算总帧长度: 起始符(1) + 地址(6) + 起始符(1) + 控制码(1) + 长度(1) + 数据域 + 校验(1) + 结束符(1) + int totalLength = 11 + dataLen; + ByteBuffer buffer = ByteBuffer.allocate(totalLength) + .order(ByteOrder.LITTLE_ENDIAN); + + // 地址域处理 (传输顺序: 低字节在前) + byte[] reversedAddr = reverseAddress(address); + + // 构建帧 + buffer.put(FRAME_START) + .put(reversedAddr) + .put(FRAME_START) + .put(ctrlCode) + .put((byte) dataLen); + + if (dataLen > 0) { + buffer.put(processedData); + } + + // 计算校验码 (从第一个0x68到数据域结束) + byte[] frameWithoutCs = Arrays.copyOf(buffer.array(), buffer.position()); + byte cs = calculateChecksum(frameWithoutCs); + + buffer.put(cs) + .put(FRAME_END); + + return buffer.array(); + } + + /** + * 解析接收到的帧 + * @param frame 完整帧数据(包含前导符) + * @return 解析结果对象 + */ + public PowerFrame parseFrame(byte[] frame) { + // 跳过前导符 (0-3) + int startIndex = findFrameStart(frame); + if (startIndex == -1) { + throw new IllegalArgumentException("无效帧:未找到起始标记"); + } + + // 基本长度检查 + if (frame.length < startIndex + 12) { + throw new IllegalArgumentException("接受帧太短"); + } + + // 提取地址域 (传输顺序: 低字节在前) + byte[] reversedAddr = Arrays.copyOfRange(frame, startIndex + 1, startIndex + 7); + byte[] address = reverseAddress(reversedAddr); + + // 控制码 + byte ctrlCode = frame[startIndex + 8]; + + // 数据域长度 + int dataLen = frame[startIndex + 9] & 0xFF; + + // 数据域位置 + int dataStart = startIndex + 10; + int dataEnd = dataStart + dataLen; + + // 校验位位置 + int endPos = dataEnd + 1; + + // 验证结束符 + if (frame[endPos] != FRAME_END) { + throw new IllegalArgumentException("无效的帧结束标记"); + } + + // 提取原始数据域 (含33H处理) + byte[] rawData = Arrays.copyOfRange(frame, dataStart, dataEnd); + byte[] processedData = processDataDomain(rawData, false); + + // 验证校验和 + byte calculatedCs = calculateChecksum(Arrays.copyOfRange(frame, startIndex, dataEnd)); + byte receivedCs = frame[dataEnd]; + + if (calculatedCs != receivedCs) { + throw new IllegalArgumentException("校验和不匹配"); + } + + return new PowerFrame(address, ctrlCode, processedData); + } + + // 数据处理域:加/减33H + private byte[] processDataDomain(byte[] data, boolean isEncode) { + if (data == null || data.length == 0) return data; + + byte[] result = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + result[i] = (byte) (isEncode ? (data[i] + 0x33) : (data[i] - 0x33)); + } + return result; + } + + // 地址反转 (传输顺序处理) + private byte[] reverseAddress(byte[] address) { + byte[] reversed = new byte[address.length]; + for (int i = 0; i < address.length; i++) { + reversed[i] = address[address.length - 1 - i]; + } + return reversed; + } + + // 计算校验和 (模256和) + private byte calculateChecksum(byte[] data) { + int sum = 0; + for (byte b : data) { + sum = (sum + (b & 0xFF)) & 0xFF; + } + return (byte) sum; + } + + // 在帧数据中查找起始符 + private int findFrameStart(byte[] data) { + for (int i = 0; i < data.length - 1; i++) { + if (data[i] == FRAME_START && data[i + 1] != FRAME_START) { + return i; + } + } + return -1; + } +} diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/WaterMeterUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/WaterMeterUtil.java new file mode 100644 index 0000000..5b294b1 --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/meter/utils/WaterMeterUtil.java @@ -0,0 +1,168 @@ +package org.dromara.sis.sdk.meter.utils; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * @author lsm + * @apiNote WaterMeterUtil + * @since 2025/7/20 + */ +public class WaterMeterUtil { + + // 协议常量定义 + public static final byte PREAMBLE = (byte) 0xFE; + public static final byte FRAME_START = 0x68; + public static final byte FRAME_END = 0x16; + public static final byte WATER_METER_TYPE = 0x10; + public static final byte CTRL_READ = 0x01; + public static final byte CTRL_RESPONSE = (byte) 0x81; + public static final byte UNIT_TON = 0x2C; + public static final int ADDRESS_LENGTH = 7; + + /** + * 构建读表数据命令帧 + * + * @param meterAddress 12位表计地址字符串(如"000000000000012") + * @param diHighFirst 数据标识字节序:true=901Fh(高字节在前), false=1F90h(低字节在前) + * @return 完整的命令帧字节数组 + */ + public static byte[] buildReadCommand(String meterAddress, boolean diHighFirst) { + // 1. 地址转换:12位字符串 -> 7字节BCD码(逆序分组) + byte[] addressBytes = convertAddress(meterAddress); + + // 2. 构建帧主体(不含前导符和帧尾) + ByteBuffer buffer = ByteBuffer.allocate(32); + buffer.put(FRAME_START); + buffer.put(WATER_METER_TYPE); + buffer.put(addressBytes); + buffer.put(CTRL_READ); + buffer.put((byte) 0x03); // 数据域长度 + + // 数据标识处理 + if (diHighFirst) { + buffer.put((byte) 0x90); + buffer.put((byte) 0x1F); + } else { + buffer.put((byte) 0x1F); + buffer.put((byte) 0x90); + } + + buffer.put((byte) 0x00); // 序列号 + + // 3. 计算校验码(从FRAME_START到序列号) + byte[] frameBody = Arrays.copyOf(buffer.array(), buffer.position()); + byte cs = calculateChecksum(frameBody, 0, frameBody.length); + + // 4. 组装完整帧 + buffer.put(cs); + buffer.put(FRAME_END); + + // 5. 添加前导符 + byte[] fullFrame = Arrays.copyOf(buffer.array(), buffer.position()); + return addPreamble(fullFrame); + } + + /** + * 解析读表响应数据 + * + * @param response 完整响应帧(含前导符) + * @return 解析后的累积流量值(单位:吨) + * @throws IllegalArgumentException 响应格式错误 + */ + public static double parseReadResponse(byte[] response) { + // 1. 跳过前导符(0xFE x3) + int startIndex = 3; + if (response[startIndex] != FRAME_START) { + throw new IllegalArgumentException("无效帧起始符"); + } + + // 2. 基础信息解析 + int pos = startIndex + 1; + byte meterType = response[pos++]; + byte[] address = Arrays.copyOfRange(response, pos, pos + ADDRESS_LENGTH); + pos += ADDRESS_LENGTH; + + byte ctrlCode = response[pos++]; + if (ctrlCode != CTRL_RESPONSE) { + throw new IllegalArgumentException("无效控制码"); + } + + // 3. 数据域解析 + int dataLen = response[pos++] & 0xFF; + byte[] di = {response[pos++], response[pos++]}; // 数据标识 + byte ser = response[pos++]; // 序列号 + + // 4. 累积流量解析 (4字节BCD) + byte[] currentFlow = Arrays.copyOfRange(response, pos, pos + 4); + pos += 4; + + // 5. 单位校验 + if (response[pos++] != UNIT_TON) { + throw new IllegalArgumentException("无效计量单位"); + } + + // 6. 流量值转换 + return parseFlowValue(currentFlow); + } + + /** + * 计算校验码 (CJ/T188-2004标准) + * + * @param data 待计算数据 + * @param offset 起始位置 + * @param length 数据长度 + * @return 校验码 + */ + public static byte calculateChecksum(byte[] data, int offset, int length) { + int sum = 0; + for (int i = offset; i < offset + length; i++) { + sum += (data[i] & 0xFF); + } + return (byte) (sum % 256); + } + + // 地址转换:12位字符串 -> 7字节BCD码(逆序分组) + private static byte[] convertAddress(String address) { + if (address.length() != 12) { + throw new IllegalArgumentException("地址长度必须为12位"); + } + + // 填充为14位(7字节*2) + String padded = "00" + address; + byte[] result = new byte[ADDRESS_LENGTH]; + + // 逆序分组转换 + for (int i = 0; i < ADDRESS_LENGTH; i++) { + int end = padded.length() - i * 2; + int start = end - 2; + String segment = padded.substring(start, end); + result[i] = (byte) Integer.parseInt(segment, 16); + } + return result; + } + + // 添加前导符 0xFE x3 + private static byte[] addPreamble(byte[] frame) { + byte[] result = new byte[frame.length + 3]; + result[0] = PREAMBLE; + result[1] = PREAMBLE; + result[2] = PREAMBLE; + System.arraycopy(frame, 0, result, 3, frame.length); + return result; + } + + // 解析BCD流量值(4字节 -> 浮点数) + private static double parseFlowValue(byte[] data) { + // 拼接BCD数字串 + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(String.format("%02X", b)); + } + + // 转换为数值(最后2位是小数位) + String numStr = sb.toString(); + return Double.parseDouble(numStr.substring(0, numStr.length() - 2) + + Double.parseDouble(numStr.substring(numStr.length() - 2)) / 100.0); + } +}