feat(sis): 重构智能设备通信模块
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
Some checks are pending
Gitea Actions Demo / Explore-Gitea-Actions (push) Waiting to run
This commit is contained in:
parent
7af0cee8e9
commit
9afeed3108
@ -130,9 +130,9 @@
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<groupId>com.ghgande</groupId>
|
||||
<artifactId>j2mod</artifactId>
|
||||
<version>3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user