From 9afeed3108a40ce8826cbca502565e13616d76a0 Mon Sep 17 00:00:00 2001 From: zcxlsm Date: Thu, 31 Jul 2025 15:12:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(sis):=20=E9=87=8D=E6=9E=84=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E8=AE=BE=E5=A4=87=E9=80=9A=E4=BF=A1=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-modules/Sis/pom.xml | 6 +- .../sdk/smartDevices/utils/LightingUtil.java | 299 +++++++++++------- .../sis/sdk/smartDevices/utils/MeterUtil.java | 126 ++++++++ .../smartDevices/utils/PowerMeterUtil.java | 179 ----------- .../smartDevices/utils/WaterMeterUtil.java | 168 ---------- 5 files changed, 318 insertions(+), 460 deletions(-) create mode 100644 ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/MeterUtil.java delete mode 100644 ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/PowerMeterUtil.java delete mode 100644 ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/WaterMeterUtil.java diff --git a/ruoyi-modules/Sis/pom.xml b/ruoyi-modules/Sis/pom.xml index ba3949e..9aea97f 100644 --- a/ruoyi-modules/Sis/pom.xml +++ b/ruoyi-modules/Sis/pom.xml @@ -130,9 +130,9 @@ - org.eclipse.paho - org.eclipse.paho.client.mqttv3 - 1.2.5 + com.ghgande + j2mod + 3.0.0 diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/LightingUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/LightingUtil.java index d4fa654..61ca06e 100644 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/LightingUtil.java +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/LightingUtil.java @@ -1,14 +1,16 @@ package org.dromara.sis.sdk.smartDevices.utils; - -import com.google.gson.Gson; -import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; -import org.eclipse.paho.client.mqttv3.MqttException; -import org.eclipse.paho.client.mqttv3.MqttMessage; -import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.springframework.stereotype.Service; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static com.ghgande.j2mod.modbus.Modbus.WRITE_SINGLE_REGISTER; /** * @author lsm @@ -16,124 +18,201 @@ import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; * @since 2025/7/20 */ @Slf4j +@Service public class LightingUtil { - private final MqttClient mqttClient; - private final String productKey; - private final String deviceName; - private final Gson gson = new Gson(); + // Modbus TCP默认端口 + private static final int MODBUS_PORT = 502; + // 功能码03(读保持寄存器) + private static final byte FUNCTION_CODE = 0x03; + // 采集寄存器范围(协议地址) + private static final int START_ADDRESS = 42; // 40043 - 40001 = 42 + private static final int REGISTER_COUNT = 4; // 40046 - 40043 + 1 = 4 + private Socket socket; + private DataInputStream input; + private DataOutputStream output; + private int transactionId = 0; // 事务ID计数器 - // 初始化连接参数 - public LightingUtil(String brokerUrl, String productKey, String deviceName, - String username, String password) throws MqttException { - this.productKey = productKey; - this.deviceName = deviceName; - - MqttConnectOptions options = new MqttConnectOptions(); - options.setUserName(username); - options.setPassword(password.toCharArray()); - options.setCleanSession(true); - - mqttClient = new MqttClient(brokerUrl, deviceName, new MemoryPersistence()); - mqttClient.connect(options); - - // 订阅网关上报主题 - String subscribeTopic = "/sys/" + productKey + "/+/thing/event/+/post"; - mqttClient.subscribe(subscribeTopic, this::handleIncomingMessage); + /** + * 连接到Modbus TCP设备 + */ + public void connect(String host) throws IOException { + socket = new Socket(host, MODBUS_PORT); + input = new DataInputStream(socket.getInputStream()); + output = new DataOutputStream(socket.getOutputStream()); } - // 基础指令构造 - private JsonObject createBaseCommand(int code, String area, String address, String action) { - JsonObject command = new JsonObject(); - command.addProperty("code", code); - command.addProperty("deviceName", deviceName); - command.addProperty("area", area); - command.addProperty("address", address); - command.addProperty("action", action); - command.addProperty("identity", ""); - return command; - } - - // 灯具控制指令 - public void sendLightCommand(int code, String area, String address, String action, String params) - throws MqttException { - JsonObject command = createBaseCommand(code, area, address, action); - if (params != null) command.addProperty("params", params); - - String topic = "/" + productKey + "/" + deviceName + "/user/get"; - mqttClient.publish(topic, new MqttMessage(gson.toJson(command).getBytes())); - } - - // 常用快捷方法 - public void turnOnLight(String area, String groupAddress) throws MqttException { - sendLightCommand(200, area, groupAddress, "lightOn", null); - } - - public void turnOffLight(String area, String groupAddress) throws MqttException { - sendLightCommand(200, area, groupAddress, "lightOff", null); - } - - public void setBrightness(String area, String address, int brightness) throws MqttException { - sendLightCommand(200, area, address, "setHighBright", String.valueOf(brightness)); - } - - // 上报数据处理 - private void handleIncomingMessage(String topic, MqttMessage message) { + /** + * 断开连接 + */ + public void disconnect() { try { - JsonObject payload = gson.fromJson(new String(message.getPayload()), JsonObject.class); - String method = payload.get("method").getAsString(); - - switch (method) { - case "thing.event.heartbeat.post": - processHeartbeat(payload.getAsJsonObject("params")); - break; - case "thing.event.consumption.post": - processEnergyData(payload.getAsJsonObject("params")); - break; - case "thing.event.trigger.post": - processSensorTrigger(payload.getAsJsonObject("params")); - break; - // 添加其他事件处理... - } - } catch (Exception e) { - log.error("MQTT消息处理异常,topic: {}", topic, e); + if (input != null) input.close(); + if (output != null) output.close(); + if (socket != null) socket.close(); + } catch (IOException e) { + System.err.println("关闭连接时出错: " + e.getMessage()); } } - // 心跳处理 - private void processHeartbeat(JsonObject params) { - JsonObject value = params.getAsJsonObject("value"); - String uuid = value.get("uuid").getAsString(); - String area = value.get("area").getAsString(); - System.out.println("设备在线: " + uuid + " | 区域: " + area); + private byte[] initParse() throws IOException { + // 读取头(7字节) + byte[] header = new byte[7]; + input.readFully(header); + + // 验证事务ID + int receivedTid = ByteBuffer.wrap(header, 0, 2) + .order(ByteOrder.BIG_ENDIAN).getShort() & 0xFFFF; + if (receivedTid != transactionId - 1) { + throw new IOException("事务ID不匹配"); + } + + return header; } - // 能耗处理 - private void processEnergyData(JsonObject params) { - JsonObject value = params.getAsJsonObject("value"); - String uuid = value.get("uuid").getAsString(); - double power = value.get("power").getAsDouble(); - System.out.println("能耗报告: " + uuid + " | 功率: " + power + "W"); + /** + * 读取40043-40046寄存器数据 + * + * @return 包含4个寄存器值的int数组 + */ + public int[] readRegisters() throws IOException { + // 发送读取请求 + sendRequest(); + + // 接收并解析响应 + return parseResponse(); } - // 传感器触发处理 - private void processSensorTrigger(JsonObject params) { - JsonObject value = params.getAsJsonObject("value"); - long trigTime = value.get("trig_time").getAsLong(); - String area = value.get("area").getAsString(); - System.out.println("传感器触发: 区域=" + area + " | 时间=" + trigTime); + /** + * 构造并发送Modbus TCP请求帧 + */ + private void sendRequest() throws IOException { + // 事务ID(递增) + int currentTransactionId = transactionId++; + + // 创建请求帧(12字节) + ByteBuffer buffer = ByteBuffer.allocate(12) + .order(ByteOrder.BIG_ENDIAN); + + // Header(7字节) + buffer.putShort((short) currentTransactionId); // 事务ID + buffer.putShort((short) 0); // 协议ID(0=Modbus) + buffer.putShort((short) 6); // 长度(后续字节数) + buffer.put((byte) 1); // 单元ID + + // PDU(协议数据单元) + buffer.put(FUNCTION_CODE); // 功能码 + buffer.putShort((short) START_ADDRESS); // 起始地址 + buffer.putShort((short) REGISTER_COUNT); // 寄存器数量 + + output.write(buffer.array()); + output.flush(); } - // 网关管理 - public void rebootGateway(int delaySeconds) throws MqttException { - JsonObject command = createBaseCommand(400, "00 00", "FF FF", "reboot"); - command.addProperty("params", String.valueOf(delaySeconds)); - String topic = "/" + productKey + "/" + deviceName + "/user/get"; - mqttClient.publish(topic, new MqttMessage(gson.toJson(command).getBytes())); + /** + * 解析Modbus TCP响应 + */ + private int[] parseResponse() throws IOException { + // 读取头(7字节) + byte[] header = initParse(); + + // 读取PDU(协议数据单元) + int pduLength = ByteBuffer.wrap(header, 4, 2) + .getShort() & 0xFFFF - 1; // 减去单元ID长度 + byte[] pdu = new byte[pduLength]; + input.readFully(pdu); + + // 检查异常响应(功能码高位为1) + if ((pdu[0] & 0xFF) == (FUNCTION_CODE | 0x80)) { + throw new IOException("Modbus异常响应,错误码: " + (pdu[1] & 0xFF)); + } + + // 验证功能码和字节数 + if (pdu[0] != FUNCTION_CODE || pdu[1] != REGISTER_COUNT * 2) { + throw new IOException("无效响应格式"); + } + + // 提取寄存器数据(每个寄存器2字节) + int[] values = new int[REGISTER_COUNT]; + for (int i = 0; i < REGISTER_COUNT; i++) { + int offset = 2 + i * 2; + values[i] = ByteBuffer.wrap(pdu, offset, 2) + .order(ByteOrder.BIG_ENDIAN).getShort() & 0xFFFF; + } + return values; } - // 关闭连接 - public void disconnect() throws MqttException { - mqttClient.disconnect(); + /** + * 写单个保持寄存器(功能码06) + * + * @param registerAddress 寄存器地址(协议地址,如40044对应0x0043) + * @param value 要写入的值(0-65535) + * @return true表示写入成功 + */ + public boolean writeSingleRegister(int registerAddress, int value) throws IOException { + // 发送写请求 + sendWriteRequest(registerAddress, value); + + // 接收并验证响应 + return parseWriteResponse(); } + + /** + * 构造并发送写寄存器请求帧 + */ + private void sendWriteRequest(int registerAddress, int value) throws IOException { + int currentTransactionId = transactionId++; + + // 创建请求帧(12字节) + ByteBuffer buffer = ByteBuffer.allocate(12) + .order(ByteOrder.BIG_ENDIAN); + + // MBAP Header(7字节) + buffer.putShort((short) currentTransactionId); // 事务ID + buffer.putShort((short) 0); // 协议ID(0=Modbus) + buffer.putShort((short) 6); // 长度(后续字节数) + buffer.put((byte) 1); // 单元ID + + // PDU(协议数据单元) + buffer.put((byte) WRITE_SINGLE_REGISTER); // 功能码06 + buffer.putShort((short) registerAddress); // 寄存器地址 + buffer.putShort((short) value); // 写入的值 + + output.write(buffer.array()); + output.flush(); + } + + /** + * 解析写寄存器响应 + */ + private boolean parseWriteResponse() throws IOException { + // 读取头(7字节) + this.initParse(); + + // 读取PDU(5字节) + byte[] pdu = new byte[5]; + input.readFully(pdu); + + // 检查异常响应(功能码高位为1) + if ((pdu[0] & 0xFF) == (WRITE_SINGLE_REGISTER | 0x80)) { + throw new IOException("Modbus异常响应,错误码: " + (pdu[1] & 0xFF)); + } + + // 验证功能码和字节数 + if (pdu[0] != WRITE_SINGLE_REGISTER) { + throw new IOException("无效响应格式"); + } + + // 响应应回显写入的地址和值 + int respAddress = ByteBuffer.wrap(pdu, 1, 2).getShort() & 0xFFFF; + System.out.println("传输指令后----" + respAddress); + int respValue = ByteBuffer.wrap(pdu, 3, 2).getShort() & 0xFFFF; + System.out.println("传输指令后----" + respValue); + + // 这里可以根据需要验证回显的值是否与写入一致 + // 通常只需确认功能码正确即可认为成功 + return true; + } + + } diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/MeterUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/MeterUtil.java new file mode 100644 index 0000000..47e5a6c --- /dev/null +++ b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/MeterUtil.java @@ -0,0 +1,126 @@ +package org.dromara.sis.sdk.smartDevices.utils; + +import com.ghgande.j2mod.modbus.ModbusException; +import com.ghgande.j2mod.modbus.facade.ModbusTCPMaster; +import com.ghgande.j2mod.modbus.procimg.InputRegister; +import com.ghgande.j2mod.modbus.util.ModbusUtil; +import org.springframework.stereotype.Service; + + +/** + * @author lsm + * @apiNote MeterUtil + * @since 2025/7/31 + */ +@Service +public class MeterUtil { + private ModbusTCPMaster master; + + private static final int PORT = 502; + + // 寄存器区域定义 (基于0的起始地址) + private static final int CONSTANT_AREA_START = 0; // 常数区起始地址 (30001) + private static final int COLLECTION_AREA_START = 42; // 采集区起始地址 (30043) + private static final int REPORT_AREA_START = 4002; // 上报区起始地址 (34003) + + // 区域大小 + private static final int CONSTANT_AREA_SIZE = 21; // 0-20 共21个浮点数 + private static final int COLLECTION_AREA_SIZE = 1980; // 21-2000 共1980个浮点数 + private static final int REPORT_AREA_SIZE = 1000; // 2001-3000 共1000个浮点数 + + + /** + * 连接到Modbus TCP服务器 + * + * @throws Exception 连接失败时抛出异常 + */ + public void connect(String host) throws Exception { + if (master != null && master.isConnected()) { + return; + } + master = new ModbusTCPMaster(host, PORT); + master.setTimeout(3000); // 设置3秒超时 + master.connect(); + } + + /** + * 断开连接 + */ + public void disconnect() { + if (master != null && master.isConnected()) { + master.disconnect(); + } + } + + /** + * 从寄存器读取浮点数 + * + * @param register 寄存器起始地址 (0-based) + * @return 读取到的浮点数值 + * @throws ModbusException Modbus通信异常 + */ + private float readFloat(int register) throws ModbusException { + InputRegister[] registers = master.readInputRegisters(register, 2); + byte[] bytes = { + registers[0].toBytes()[0], + registers[0].toBytes()[1], + registers[1].toBytes()[0], + registers[1].toBytes()[1] + }; + return ModbusUtil.registersToFloat(bytes); + } + + /** + * 读取常数区数据 + * + * @param index 常数区索引 (0-20) + * @return 浮点数值 + * @throws ModbusException Modbus通信异常 + * @throws IllegalArgumentException 索引越界 + */ + public float readConstantValue(int index) throws ModbusException { + if (index < 0 || index >= CONSTANT_AREA_SIZE) { + throw new IllegalArgumentException("常数区索引范围应为 0-20"); + } + return readFloat(CONSTANT_AREA_START + index * 2); + } + + /** + * 读取采集区数据 + * + * @param index 采集区索引 (0-1979) + * @return 浮点数值 + * @throws ModbusException Modbus通信异常 + * @throws IllegalArgumentException 索引越界 + */ + public float readCollectionValue(int index) throws ModbusException { + if (index < 0 || index >= COLLECTION_AREA_SIZE) { + throw new IllegalArgumentException("采集区索引范围应为 0-1979"); + } + return readFloat(COLLECTION_AREA_START + index * 2); + } + + /** + * 读取上报区数据 + * + * @param index 上报区索引 (0-999) + * @return 浮点数值 + * @throws ModbusException Modbus通信异常 + * @throws IllegalArgumentException 索引越界 + */ + public float readReportValue(int index) throws ModbusException { + if (index < 0 || index >= REPORT_AREA_SIZE) { + throw new IllegalArgumentException("上报区索引范围应为 0-999"); + } + return readFloat(REPORT_AREA_START + index * 2); + } + + /** + * 检查连接状态 + * + * @return 是否已连接 + */ + public boolean isConnected() { + return master != null && master.isConnected(); + } +} diff --git a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/PowerMeterUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/PowerMeterUtil.java deleted file mode 100644 index 8f4a1f0..0000000 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/PowerMeterUtil.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.dromara.sis.sdk.smartDevices.utils; - -import org.dromara.sis.sdk.smartDevices.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/smartDevices/utils/WaterMeterUtil.java b/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/WaterMeterUtil.java deleted file mode 100644 index 31fb071..0000000 --- a/ruoyi-modules/Sis/src/main/java/org/dromara/sis/sdk/smartDevices/utils/WaterMeterUtil.java +++ /dev/null @@ -1,168 +0,0 @@ -package org.dromara.sis.sdk.smartDevices.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); - } -}