|
@@ -1,7 +1,11 @@
|
|
|
package com.usky.ems.protocol;
|
|
package com.usky.ems.protocol;
|
|
|
|
|
|
|
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
import com.usky.ems.util.AesUtil;
|
|
import com.usky.ems.util.AesUtil;
|
|
|
import org.dom4j.Document;
|
|
import org.dom4j.Document;
|
|
|
|
|
+import org.dom4j.DocumentException;
|
|
|
import org.dom4j.DocumentHelper;
|
|
import org.dom4j.DocumentHelper;
|
|
|
import org.dom4j.Element;
|
|
import org.dom4j.Element;
|
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.Logger;
|
|
@@ -11,6 +15,9 @@ import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
import java.io.InputStream;
|
|
|
import java.io.OutputStream;
|
|
import java.io.OutputStream;
|
|
|
import java.net.Socket;
|
|
import java.net.Socket;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.ZoneOffset;
|
|
|
|
|
+import java.util.Objects;
|
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -169,8 +176,22 @@ public class TcpClient {
|
|
|
try {
|
|
try {
|
|
|
String heartbeatXml = com.usky.ems.util.XmlBuilder.buildHeartbeatRequest(buildingId, gatewayId);
|
|
String heartbeatXml = com.usky.ems.util.XmlBuilder.buildHeartbeatRequest(buildingId, gatewayId);
|
|
|
sendPacket(NetworkPacket.TYPE_HEARTBEAT, heartbeatXml, false);
|
|
sendPacket(NetworkPacket.TYPE_HEARTBEAT, heartbeatXml, false);
|
|
|
- logger.debug("心跳发送成功");
|
|
|
|
|
- return true;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ NetworkPacket response1 = receivePacket();
|
|
|
|
|
+ if (response1 != null && response1.getType() == NetworkPacket.TYPE_HEARTBEAT) {
|
|
|
|
|
+ // 直接转换为可读XML(无需解密)
|
|
|
|
|
+ String heartbeatResponseXml = new String(response1.getData(), java.nio.charset.StandardCharsets.UTF_8);
|
|
|
|
|
+ System.out.println("可读报文:\n" + heartbeatResponseXml);
|
|
|
|
|
+ boolean success = this.parseHeartbeatResult(heartbeatResponseXml);
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ logger.info("发送心跳成功");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }else{
|
|
|
|
|
+ logger.info("发送心跳失败");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
logger.error("发送心跳失败", e);
|
|
logger.error("发送心跳失败", e);
|
|
|
return false;
|
|
return false;
|
|
@@ -191,69 +212,95 @@ public class TcpClient {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
sendPacket(NetworkPacket.TYPE_ENERGY_DATA, xmlData, true);
|
|
sendPacket(NetworkPacket.TYPE_ENERGY_DATA, xmlData, true);
|
|
|
- logger.info("能耗数据发送成功");
|
|
|
|
|
|
|
|
|
|
- // 接收服务端响应
|
|
|
|
|
NetworkPacket response = receivePacket();
|
|
NetworkPacket response = receivePacket();
|
|
|
- if (response != null && response.getType() == NetworkPacket.TYPE_ENERGY_DATA) {
|
|
|
|
|
- // 能耗数据响应需要解密
|
|
|
|
|
- try {
|
|
|
|
|
- byte[] responseData = response.getData();
|
|
|
|
|
- String responseXml = null;
|
|
|
|
|
- Exception lastException = null;
|
|
|
|
|
-
|
|
|
|
|
- // 方法1:首先尝试直接解密字节数组
|
|
|
|
|
- try {
|
|
|
|
|
- responseXml = AesUtil.decrypt(authKey,responseData);
|
|
|
|
|
- logger.debug("直接解密字节数组成功");
|
|
|
|
|
- } catch (Exception e1) {
|
|
|
|
|
- lastException = e1;
|
|
|
|
|
- logger.debug("直接解密字节数组失败: {}", e1.getMessage());
|
|
|
|
|
-
|
|
|
|
|
- // 方法2:如果直接解密失败,尝试作为Base64字符串处理
|
|
|
|
|
- // 先检查数据是否可能是Base64字符串(只包含Base64字符)
|
|
|
|
|
- if (responseXml == null) {
|
|
|
|
|
- try {
|
|
|
|
|
- String dataString = new String(responseData, java.nio.charset.StandardCharsets.UTF_8);
|
|
|
|
|
- // 检查是否只包含Base64字符(A-Z, a-z, 0-9, +, /, =)
|
|
|
|
|
- if (dataString.matches("^[A-Za-z0-9+/=]+$")) {
|
|
|
|
|
- // Base64解码后再解密
|
|
|
|
|
- byte[] decodedBytes = java.util.Base64.getDecoder().decode(dataString);
|
|
|
|
|
- responseXml = AesUtil.decrypt(authKey,decodedBytes);
|
|
|
|
|
- logger.debug("Base64解码后解密成功");
|
|
|
|
|
- } else {
|
|
|
|
|
- logger.debug("数据不是Base64格式,跳过Base64解码");
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e2) {
|
|
|
|
|
- lastException = e2;
|
|
|
|
|
- logger.debug("Base64解码后解密失败: {}", e2.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (responseXml != null) {
|
|
|
|
|
- logger.debug("服务端响应: {}", responseXml);
|
|
|
|
|
- } else {
|
|
|
|
|
- throw lastException != null ? lastException : new Exception("所有解密方法都失败");
|
|
|
|
|
- }
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- logger.warn("解密服务端响应失败", e);
|
|
|
|
|
- logger.debug("响应数据长度: {}, 前100字节: {}",
|
|
|
|
|
- response.getData() != null ? response.getData().length : 0,
|
|
|
|
|
- response.getData() != null && response.getData().length > 0
|
|
|
|
|
- ? java.util.Arrays.toString(java.util.Arrays.copyOf(response.getData(), Math.min(100, response.getData().length)))
|
|
|
|
|
- : "null");
|
|
|
|
|
|
|
+ if (response != null && response.getType() == NetworkPacket.TYPE_ENERGY_DATA) {
|
|
|
|
|
+ // 直接转换为可读XML(无需解密)
|
|
|
|
|
+ String readable = new String(response.getData(), java.nio.charset.StandardCharsets.UTF_8);
|
|
|
|
|
+ System.out.println("可读报文:\n" + readable);
|
|
|
|
|
+ boolean success = this.parseEnergyDataResult(readable);
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ logger.info("能耗数据发送成功");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }else{
|
|
|
|
|
+ logger.info("能耗数据发送失败");
|
|
|
|
|
+ return false;
|
|
|
}
|
|
}
|
|
|
- return true;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ return false;
|
|
|
|
|
|
|
|
- return true;
|
|
|
|
|
} catch (Exception e) {
|
|
} catch (Exception e) {
|
|
|
logger.error("发送能耗数据失败", e);
|
|
logger.error("发送能耗数据失败", e);
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 将TCP报文字节数组直接转换为可读格式(无需解密)
|
|
|
|
|
+ * @param dataBytes 原始报文字节数组
|
|
|
|
|
+ * @param charset 报文字符编码(默认UTF-8)
|
|
|
|
|
+ * @param format 报文格式(xml/json/plain,plain为普通文本)
|
|
|
|
|
+ * @return 可读的报文字符串
|
|
|
|
|
+ */
|
|
|
|
|
+ public String convertToReadablePacket(byte[] dataBytes, String charset, String format) {
|
|
|
|
|
+ if (dataBytes == null || dataBytes.length == 0) {
|
|
|
|
|
+ return "空报文";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 兜底处理字符编码,默认用UTF-8
|
|
|
|
|
+ String actualCharset = (charset == null || charset.isEmpty()) ? "UTF-8" : charset;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 第一步:直接将字节数组转字符串(核心,无需解密)
|
|
|
|
|
+ String rawText = new String(dataBytes, actualCharset);
|
|
|
|
|
+ logger.debug("原始文本报文:{}", rawText);
|
|
|
|
|
+
|
|
|
|
|
+ // 第二步:根据格式美化
|
|
|
|
|
+ switch (format.toLowerCase()) {
|
|
|
|
|
+ case "xml":
|
|
|
|
|
+ return formatXml(rawText); // XML格式化(带缩进)
|
|
|
|
|
+ case "json":
|
|
|
|
|
+ return formatJson(rawText); // JSON格式化(带缩进)
|
|
|
|
|
+ case "plain":
|
|
|
|
|
+ default:
|
|
|
|
|
+ return rawText; // 普通文本直接返回
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.warn("转换为文本格式失败,返回十六进制格式:{}", e.getMessage());
|
|
|
|
|
+ // 转换失败时,返回十六进制(所有二进制数据都能读)
|
|
|
|
|
+ return bytesToHex(dataBytes);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * XML格式化(美化缩进)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatXml(String xmlText) throws DocumentException {
|
|
|
|
|
+ Document document = DocumentHelper.parseText(xmlText);
|
|
|
|
|
+ return document.asXML();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * JSON格式化(美化缩进)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String formatJson(String jsonText) throws Exception {
|
|
|
|
|
+ ObjectMapper mapper = new ObjectMapper();
|
|
|
|
|
+ Object jsonObj = mapper.readValue(jsonText, Object.class);
|
|
|
|
|
+ return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 字节数组转十六进制字符串(兜底方案)
|
|
|
|
|
+ * 格式:00 1A 2B ... (便于排查二进制数据问题)
|
|
|
|
|
+ */
|
|
|
|
|
+ private String bytesToHex(byte[] bytes) {
|
|
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
|
|
+ for (byte b : bytes) {
|
|
|
|
|
+ sb.append(String.format("%02X ", b));
|
|
|
|
|
+ }
|
|
|
|
|
+ return "十六进制格式:" + sb.toString().trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 发送数据包
|
|
* 发送数据包
|
|
|
*
|
|
*
|
|
@@ -326,6 +373,7 @@ public class TcpClient {
|
|
|
return NetworkPacket.decode(packetBytes);
|
|
return NetworkPacket.decode(packetBytes);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 从XML中解析随机序列
|
|
* 从XML中解析随机序列
|
|
|
*/
|
|
*/
|
|
@@ -366,6 +414,56 @@ public class TcpClient {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从XML中解析推送能耗数据响应结果
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean parseEnergyDataResult(String xml) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Document document = DocumentHelper.parseText(xml);
|
|
|
|
|
+ Element root = document.getRootElement();
|
|
|
|
|
+ Element dataElement = root.element("data");
|
|
|
|
|
+ if (dataElement == null) {
|
|
|
|
|
+ logger.warn("XML中未找到<data>节点");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取<ack>子节点的文本值
|
|
|
|
|
+ Element ackElement = dataElement.element("ack");
|
|
|
|
|
+ if (ackElement == null) {
|
|
|
|
|
+ logger.warn("XML中未找到<ack>节点");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return "OK".equals(ackElement.getTextTrim());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.error("解析认证结果失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从XML中解析推送能耗数据响应结果
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean parseHeartbeatResult(String xml) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Document document = DocumentHelper.parseText(xml);
|
|
|
|
|
+ Element root = document.getRootElement();
|
|
|
|
|
+ Element dataElement = root.element("heart_beat");
|
|
|
|
|
+ if (dataElement == null) {
|
|
|
|
|
+ logger.warn("XML中未找到<heart_beat>节点");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 获取<ack>子节点的文本值
|
|
|
|
|
+ Element ackElement = dataElement.element("time");
|
|
|
|
|
+ if (ackElement == null) {
|
|
|
|
|
+ logger.warn("XML中未找到<time>节点");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return StringUtils.isNotBlank(ackElement.getTextTrim());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ logger.error("解析认证结果失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
public boolean isConnected() {
|
|
public boolean isConnected() {
|
|
|
return connected.get() && socket != null && !socket.isClosed();
|
|
return connected.get() && socket != null && !socket.isClosed();
|
|
|
}
|
|
}
|