MessageHandler.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. package com.zksy.gas.utils;
  2. import static com.zksy.common.utils.CollectionSideEffects.*;
  3. import cn.hutool.core.lang.UUID;
  4. import com.alibaba.fastjson.JSONObject;
  5. import com.zksy.api.domain.WarningThreshold;
  6. import com.zksy.api.service.EarlyWarningAutoService;
  7. import com.zksy.api.service.WarningThresholdService;
  8. import com.zksy.api.domain.AlarmData;
  9. import com.zksy.api.service.AlarmDataService;
  10. import com.zksy.api.utils.AlarmLevelUtil;
  11. import com.zksy.api.utils.SmsUtil;
  12. import com.zksy.common.exception.InvalidMessageException;
  13. import com.zksy.gas.domain.GasMonitorData;
  14. import com.zksy.gas.service.GasMonitorDataService;
  15. import com.zksy.api.service.DeviceContactService;
  16. import io.netty.buffer.ByteBuf;
  17. import io.netty.buffer.Unpooled;
  18. import io.netty.channel.ChannelHandler;
  19. import io.netty.channel.ChannelHandlerContext;
  20. import io.netty.channel.ChannelInboundHandlerAdapter;
  21. import io.netty.handler.timeout.ReadTimeoutException;
  22. import io.netty.util.ReferenceCountUtil;
  23. import lombok.extern.slf4j.Slf4j;
  24. import org.slf4j.Logger;
  25. import org.slf4j.LoggerFactory;
  26. import org.springframework.beans.factory.annotation.Autowired;
  27. import org.springframework.beans.factory.annotation.Value;
  28. import org.springframework.http.HttpEntity;
  29. import org.springframework.http.HttpHeaders;
  30. import org.springframework.http.MediaType;
  31. import org.springframework.stereotype.Component;
  32. import org.springframework.web.client.RestTemplate;
  33. import java.math.BigDecimal;
  34. import java.time.LocalDateTime;
  35. import java.util.HashMap;
  36. import java.util.List;
  37. import java.util.Map;
  38. @ChannelHandler.Sharable
  39. @Slf4j
  40. @Component
  41. public class MessageHandler extends ChannelInboundHandlerAdapter {
  42. private static Logger logger = LoggerFactory.getLogger(MessageHandler.class);
  43. private final GasMonitorDataService service;
  44. @Autowired
  45. private WarningThresholdService warningThresholdService;
  46. @Autowired
  47. private AlarmDataService alarmDataService;
  48. @Autowired
  49. private EarlyWarningAutoService earlyWarningAutoService;
  50. @Autowired
  51. private SmsUtil smsUtil;
  52. @Autowired
  53. private DeviceContactService devicePhoneFetchUtil;
  54. @Autowired
  55. private AlarmUtil alarmUtil;
  56. @Autowired
  57. private RestTemplate restTemplate;
  58. @Autowired
  59. private DeviceOfflineCheckTask deviceOfflineCheckTask;
  60. @Value("${device.status.api.url:http://zk-api-service/equipmentStatus/updateAlarmStatus}")
  61. private String deviceStatusApiUrl;
  62. @Autowired
  63. public MessageHandler(GasMonitorDataService firefightingPressureService) {
  64. this.service = firefightingPressureService;
  65. }
  66. @Override
  67. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  68. super.channelActive(ctx);
  69. //sendDataToDevice(ctx);
  70. }
  71. @Override
  72. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  73. try {
  74. ByteBuf msgByteBuf = (ByteBuf) msg;
  75. if (msgByteBuf == null || !msgByteBuf.isReadable()) {
  76. logger.warn("接收到无效的消息");
  77. return;
  78. }
  79. int readable = msgByteBuf.readableBytes();
  80. byte[] msgBytes = new byte[readable];
  81. msgByteBuf.readBytes(msgBytes);
  82. logger.info("接收到 {} 字节的数据: {}, 来自: {}",
  83. readable, ProtocolUtils.bytesToHex(msgBytes), ctx.channel().remoteAddress());
  84. int checksumTotalLength = 2; // 末尾2字节校验码(如ED34)
  85. int bodyStartOffset = 16; // 校验和07的主体起始索引
  86. int fullDataStartOffset = 3; // 校验和16的完整数据起始索引
  87. // 校验报文长度合法性
  88. if (readable < fullDataStartOffset + checksumTotalLength) {
  89. throw new InvalidMessageException("报文长度不足,无法完成校验");
  90. }
  91. int bodyEndOffset = msgBytes.length - checksumTotalLength; // 避开末尾2字节校验码
  92. int bodyLength = bodyEndOffset - bodyStartOffset;
  93. if (bodyLength <= 0) {
  94. throw new InvalidMessageException("主体数据长度无效,无法计算校验和07");
  95. }
  96. int checksum07 = DataCheckUtil.calcSumOnly(msgBytes, bodyStartOffset, bodyLength);
  97. int fullDataEndOffset = msgBytes.length - 1; // 包含倒数第二个字节
  98. int fullDataLength = fullDataEndOffset - fullDataStartOffset;
  99. if (fullDataLength <= 0) {
  100. throw new InvalidMessageException("完整数据长度无效,无法计算校验和16");
  101. }
  102. int checksum16 = DataCheckUtil.calcChecksum(msgBytes, fullDataStartOffset, fullDataLength);
  103. int calculatedChecksum = (checksum07 << 8) | checksum16;
  104. int receivedChecksum = ((msgBytes[msgBytes.length - 2] & 0xFF) << 8)
  105. | (msgBytes[msgBytes.length - 1] & 0xFF);
  106. // 打印调试信息,确认拼接结果
  107. logger.debug("校验和07(高8位,仅求和): {}", Integer.toHexString(checksum07).toUpperCase());
  108. logger.debug("校验和16(低8位,FF-求和): {}", Integer.toHexString(checksum16).toUpperCase());
  109. logger.debug("修改后拼接校验码: {}", Integer.toHexString(calculatedChecksum).toUpperCase());
  110. logger.debug("报文携带校验码: {}", Integer.toHexString(receivedChecksum).toUpperCase());
  111. if (calculatedChecksum != receivedChecksum) {
  112. throw new InvalidMessageException(
  113. String.format("数据校验失败:本地计算(07+16)=%04X(07=%02X, 16=%02X),报文携带=%04X",
  114. calculatedChecksum, checksum07, checksum16, receivedChecksum));
  115. }
  116. com.zksy.gas.domain.GasMonitorData resultData = DataParser.parseMessage(msgBytes);
  117. resultData.setId(java.util.UUID.randomUUID().toString());
  118. requireSaved(service.save(resultData));
  119. // 更新设备最后接收时间,并标记设备在线
  120. run(resultData.getMacAddress(), "接收时间更新", () -> DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(resultData.getMacAddress(), new java.util.Date()));
  121. run(resultData.getMacAddress(), "在线状态同步", () -> deviceOfflineCheckTask.markDeviceOnline(resultData.getMacAddress()));
  122. run(null, "报警预警评估", () -> checkIfSmsAlertNeeded(resultData));
  123. logger.info("数据解析入库成功: {}", resultData);
  124. } catch (InvalidMessageException e) {
  125. logger.error("数据校验失败: {}", e.getMessage());
  126. ctx.writeAndFlush(Unpooled.copiedBuffer("数据校验失败".getBytes()));
  127. } catch (Exception e) {
  128. logger.error("数据解析/入库异常", e);
  129. ctx.writeAndFlush(Unpooled.copiedBuffer("数据处理异常".getBytes()));
  130. } finally {
  131. ReferenceCountUtil.release(msg);
  132. }
  133. }
  134. private String printHexBinary(byte[] bytes) {
  135. StringBuilder sb = new StringBuilder();
  136. for (byte b : bytes) {
  137. sb.append(String.format("%02X ", b));
  138. }
  139. System.out.println("Received raw data: " + sb.toString());
  140. logger.info("Received raw data: " + sb.toString());
  141. return sb.toString();
  142. }
  143. @Override
  144. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  145. if (cause instanceof ReadTimeoutException) {
  146. logger.info("来自" + ctx.channel().remoteAddress() + "的连接超时断开");
  147. } else {
  148. logger.error("设备连接异常", cause);
  149. logger.info("来自" + ctx.channel().remoteAddress() + "的连接异常断开");
  150. ctx.close();
  151. }
  152. }
  153. @Override
  154. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  155. ctx.flush();
  156. }
  157. @Override
  158. public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
  159. logger.info("来自" + ctx.channel().remoteAddress() + "的连接主动断开");
  160. ctx.fireChannelUnregistered();
  161. }
  162. // 主动向设备发送数据
  163. public void sendDataToDevice(ChannelHandlerContext ctx) {
  164. try {
  165. // 将字符串形式的十六进制数据转换为字节数组
  166. String hexData = "01 03 00 00 00 01 84 0A ";
  167. String[] hexArray = hexData.split(" ");
  168. byte[] dataBytes = new byte[hexArray.length];
  169. for (int i = 0; i < hexArray.length; i++) {
  170. dataBytes[i] = (byte) Integer.parseInt(hexArray[i], 16);
  171. }
  172. ByteBuf byteBuf = Unpooled.copiedBuffer(dataBytes);
  173. ctx.writeAndFlush(byteBuf);
  174. logger.info("已向设备发送数据: {}", printHexBinary(dataBytes));
  175. } catch (Exception e) {
  176. logger.error("发送数据失败: {}", e.getMessage());
  177. }
  178. }
  179. public void checkIfSmsAlertNeeded(GasMonitorData resultData){
  180. String deviceId = resultData == null ? null : resultData.getMacAddress();
  181. try {
  182. if (resultData == null || deviceId == null || deviceId.trim().isEmpty()) {
  183. log.warn("可燃气体报警数据缺少设备编码,跳过状态覆盖");
  184. return;
  185. }
  186. boolean hasAlarm = evaluateAlarmStatus(resultData);
  187. updateDeviceAlarmStatus(deviceId, hasAlarm ? 1 : 0);
  188. } catch (Exception e) {
  189. // 评估不完整时不覆盖上一状态,避免异常数据误清除报警
  190. log.error("可燃气体设备报警处理失败,保留原报警状态:deviceCode={}", deviceId, e);
  191. }
  192. }
  193. /** 评估单条采集数据是否存在报警。只有完整评估成功才允许同步设备状态。 */
  194. private boolean evaluateAlarmStatus(GasMonitorData resultData){
  195. //用MAC地址作为设备的设备编号
  196. String deviceId = resultData.getMacAddress();
  197. boolean hasAlarm = false;
  198. //获取气体浓度阈值(按气体类型只监测浓度,当前仅CH4;且仅按上限判定)
  199. String gasWarningCode = "WARN-GAS-CONCENTRATION";
  200. WarningThreshold gasThreshold = checkThreshold(deviceId, gasWarningCode);
  201. Double gasMaxValue = gasThreshold != null ? gasThreshold.getMaxValue() : null;
  202. String gasWarningType = "气体浓度预警";
  203. String gasRemark = null;
  204. //判断气体浓度:仅当浓度达到或超过上限时视为越限;浓度低(接近0)属正常,不告警
  205. BigDecimal gasConcentration = resultData.getGasConcentration();
  206. if (gasConcentration == null) { throw new IllegalArgumentException("缺少气体浓度值"); }
  207. if (gasConcentration != null && gasMaxValue != null) {
  208. BigDecimal gasMaxBd = BigDecimal.valueOf(gasMaxValue);
  209. if (gasConcentration.floatValue() >= gasMaxValue) {
  210. // 越限报警:等级计算以0为下限基线(气体浓度不会低于0),保证仅上限时也能正确分级
  211. int alarmLevel = saveAlarmData(deviceId, gasWarningType, gasWarningCode,
  212. BigDecimal.ZERO, gasMaxBd, gasConcentration,
  213. gasRemark != null ? gasRemark : "可燃气体浓度报警");
  214. run(deviceId, gasWarningCode + ":关闭预警", () -> earlyWarningAutoService.closeAutoWarning(deviceId, gasWarningCode, "已达报警阈值,转报警处理"));
  215. hasAlarm = true;
  216. if (alarmLevel == 1) {
  217. run(deviceId, gasWarningCode + ":短信通知", () -> sendMessage(deviceId, gasWarningType, resultData.getLongitude(), resultData.getLatitude()));
  218. }
  219. } else {
  220. // 未越限:接近上限时生成自动预警(min传null,仅按上限评估预警区)
  221. run(deviceId, gasWarningCode + ":告警恢复", () -> alarmDataService.recoverThresholdAlarm(
  222. deviceId, gasWarningCode, null, gasMaxBd, gasConcentration));
  223. run(deviceId, gasWarningCode + ":生成预警", () -> earlyWarningAutoService.checkAndGenerate(deviceId, gasWarningType, gasWarningCode,
  224. null, gasMaxBd, gasConcentration, gasRemark));
  225. }
  226. }
  227. return hasAlarm;
  228. }
  229. private WarningThreshold checkThreshold(String deviceCode, String warningCode) {
  230. return warningThresholdService.getWarningThresholdByDeviceAndCode(deviceCode, warningCode);
  231. }
  232. private int saveAlarmData(String deviceCode, String warningType, String warningCode,
  233. BigDecimal minValue, BigDecimal maxValue,
  234. BigDecimal actualValue, String remark) {
  235. int alarmLevel = 4;
  236. try {
  237. AlarmData alarmData = new AlarmData();
  238. alarmData.setDeviceCode(deviceCode);
  239. alarmData.setWarningCode(warningCode);
  240. alarmData.setMinValue(minValue);
  241. alarmData.setMaxValue(maxValue);
  242. alarmData.setActualValue(actualValue);
  243. alarmLevel = AlarmLevelUtil.calculateLevel(actualValue, minValue, maxValue);
  244. BigDecimal deviationRatio = AlarmLevelUtil.calculateDeviationRatio(actualValue, minValue, maxValue);
  245. alarmData.setAlarmLevel(alarmLevel);
  246. alarmData.setDeviationRatio(deviationRatio);
  247. alarmData.setAlarmStatus(0);
  248. alarmData.setCreateTime(LocalDateTime.now());
  249. alarmData.setCreateTime(LocalDateTime.now());
  250. boolean saved = run(deviceCode, warningCode + ":报警入库", () -> requireSaved(alarmDataService.saveAlarmData(alarmData)));
  251. if (alarmLevel == 1) {
  252. log.warn("【1级紧急告警】deviceCode={}, warningCode={}, deviationRatio={}%",
  253. deviceCode, warningCode, deviationRatio);
  254. }
  255. if (saved) { log.info("告警数据入库成功: deviceCode={}, warningCode={}, level={}级", deviceCode, warningCode, alarmLevel); }
  256. } catch (Exception e) {
  257. log.error("保存告警数据失败", e);
  258. }
  259. return alarmLevel;
  260. }
  261. //构造发送短信参数&&发送短信
  262. public void sendMessage(String deviceNo, String alarmType, BigDecimal lng, BigDecimal lat){
  263. // 构造短信参数
  264. JSONObject params = new JSONObject();
  265. params.put("deviceNo", deviceNo);
  266. params.put("alarmType", alarmType);
  267. // 经纬度处理(位置)
  268. params.put("location", String.format("经纬度:%.6f,%.8f", lng, lat));
  269. List<String> phoneListByDeviceId = devicePhoneFetchUtil.getPhoneListByDeviceCode(deviceNo);
  270. Map<String, Boolean> sendResults = smsUtil.sendBatchSms(phoneListByDeviceId, params.toJSONString());
  271. log.info("发送信息完成");
  272. }
  273. /**
  274. * 更新设备报警状态
  275. * @param deviceCode 设备编码
  276. * @param alarmStatus 报警状态:0-正常,1-报警
  277. */
  278. private void updateDeviceAlarmStatus(String deviceCode, int alarmStatus) {
  279. try {
  280. Map<String, Object> params = new HashMap<>();
  281. params.put("deviceCode", deviceCode);
  282. params.put("alarmStatus", alarmStatus);
  283. HttpHeaders headers = new HttpHeaders();
  284. headers.setContentType(MediaType.APPLICATION_JSON);
  285. HttpEntity<Map<String, Object>> request = new HttpEntity<>(params, headers);
  286. Map<?, ?> response = restTemplate.postForObject(deviceStatusApiUrl, request, Map.class);
  287. if (response == null || !Boolean.TRUE.equals(response.get("success"))) {
  288. log.warn("设备报警状态更新未成功: deviceCode={}, alarmStatus={}, response={}", deviceCode, alarmStatus, response);
  289. return;
  290. }
  291. log.info("设备报警状态更新成功: deviceCode={}, alarmStatus={}", deviceCode, alarmStatus);
  292. } catch (Exception e) {
  293. log.error("更新设备报警状态失败: deviceCode={}, alarmStatus={}", deviceCode, alarmStatus, e);
  294. }
  295. }
  296. }