MessageHandler.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. package com.zksy.pressure.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.aliyun.apache.hc.client5.http.utils.Hex;
  6. import com.zksy.api.domain.AlarmData;
  7. import com.zksy.api.domain.WarningThreshold;
  8. import com.zksy.api.service.AlarmDataService;
  9. import com.zksy.api.service.EarlyWarningAutoService;
  10. import com.zksy.api.service.WarningThresholdService;
  11. import com.zksy.api.utils.AlarmLevelUtil;
  12. import com.zksy.api.utils.EarlyWarningLevelUtil;
  13. import com.zksy.api.utils.SmsUtil;
  14. import com.zksy.common.exception.InvalidMessageException;
  15. import com.zksy.pressure.domain.FirefightingPressure;
  16. import com.zksy.pressure.service.FirefightingPressureService;
  17. import com.zksy.api.service.DeviceContactService;
  18. import io.netty.buffer.ByteBuf;
  19. import io.netty.util.ReferenceCountUtil;
  20. import io.netty.buffer.Unpooled;
  21. import io.netty.channel.ChannelHandler;
  22. import io.netty.channel.ChannelHandlerContext;
  23. import io.netty.channel.ChannelInboundHandlerAdapter;
  24. import io.netty.handler.timeout.ReadTimeoutException;
  25. import lombok.extern.slf4j.Slf4j;
  26. import org.slf4j.Logger;
  27. import org.slf4j.LoggerFactory;
  28. import org.springframework.beans.factory.annotation.Autowired;
  29. import org.springframework.beans.factory.annotation.Value;
  30. import org.springframework.http.HttpEntity;
  31. import org.springframework.http.HttpHeaders;
  32. import org.springframework.http.MediaType;
  33. import org.springframework.stereotype.Component;
  34. import org.springframework.web.client.RestTemplate;
  35. import java.math.BigDecimal;
  36. import java.time.LocalDateTime;
  37. import java.util.Date;
  38. import java.util.HashMap;
  39. import java.util.List;
  40. import java.util.Map;
  41. @ChannelHandler.Sharable
  42. @Slf4j
  43. @Component
  44. public class MessageHandler extends ChannelInboundHandlerAdapter {
  45. private static Logger logger = LoggerFactory.getLogger(MessageHandler.class);
  46. /** 压力值预警编码,阈值取自 app_user.warning_threshold(device_code + warning_code) */
  47. private static final String PRESSURE_WARNING_CODE = "WARN-FIRE-PRESSURE";
  48. private static final String PRESSURE_WARNING_TYPE = "消防压力监测";
  49. private final FirefightingPressureService firefightingPressureService;
  50. @Autowired
  51. private AlarmDataService alarmDataService;
  52. @Autowired
  53. private WarningThresholdService warningThresholdService;
  54. @Autowired
  55. private EarlyWarningAutoService earlyWarningAutoService;
  56. @Autowired
  57. private SmsUtil smsUtil;
  58. @Autowired
  59. private DeviceContactService devicePhoneFetchUtil;
  60. @Autowired
  61. private RestTemplate restTemplate;
  62. @Autowired
  63. private DeviceOfflineCheckTask deviceOfflineCheckTask;
  64. @Value("${device.status.api.url}")
  65. private String deviceStatusApiUrl;
  66. @Autowired
  67. public MessageHandler(FirefightingPressureService firefightingPressureService) {
  68. this.firefightingPressureService = firefightingPressureService;
  69. }
  70. @Override
  71. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  72. super.channelActive(ctx);
  73. //sendDataToDevice(ctx);
  74. }
  75. @Override
  76. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  77. try {
  78. ByteBuf msgByteBuf = (ByteBuf) msg;
  79. if (msgByteBuf == null || !msgByteBuf.isReadable()) {
  80. logger.warn("接收到无效的消息");
  81. return;
  82. }
  83. logger.info("接收到 {} 字节的数据,来自: {}", msgByteBuf.readableBytes(), ctx.channel().remoteAddress());
  84. byte[] msgBytes = new byte[msgByteBuf.readableBytes()];
  85. msgByteBuf.readBytes(msgBytes);
  86. String result = printHexBinary(msgBytes);
  87. String msgString = result.replaceAll("\\s", "");
  88. String CRCString = msgString.substring(msgString.length() - 4);
  89. String bodyResult = msgString.substring(0, msgString.length() - 4);
  90. String codeString = DataCheckUtil.crc16Tall(bodyResult);
  91. System.out.println("校验码:" + CRCString + "-----------生成的校验码:" + codeString);
  92. if (!CRCString.equals(codeString)) {
  93. throw new InvalidMessageException("数据校验不成功");
  94. } else {
  95. String hexString = Hex.encodeHexString(msgBytes).toUpperCase();
  96. String uploadEndFlag = hexString.substring(hexString.length() - 6, hexString.length() - 4);
  97. logger.info("成功!!!接收到 {} 字节的数据,来自: {}", msgString, ctx.channel().remoteAddress());
  98. FirefightingPressure resultData = DataParser.parseMessage(msgString);
  99. String functionCode = msgString.substring(20, 22);
  100. logger.info("生成的回复报文功能码: {}", functionCode);
  101. if("EF".equals(functionCode)){
  102. String responseMsg = buildResponseMessage(resultData.getTelemeteringStation(), resultData.getCentralStation(), resultData.getPassword(),resultData.getFunctionCode(), "0000", uploadEndFlag);
  103. logger.info("生成的回复报文: {}", responseMsg);
  104. byte[] messageBytes = hexStringToByteArray(responseMsg);
  105. ctx.channel().writeAndFlush(Unpooled.copiedBuffer(messageBytes));
  106. return;
  107. }
  108. resultData.setId(UUID.randomUUID().toString());
  109. // 更新设备最后一次接收数据的时间,并标记设备在线
  110. run(resultData.getTelemeteringStation(), "接收时间更新", () -> DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(resultData.getTelemeteringStation(), new Date()));
  111. requireSaved(firefightingPressureService.save(resultData));
  112. run(resultData.getTelemeteringStation(), "采集应答", () -> {
  113. // 4. 构建回复报文
  114. String responseMsg = buildResponseMessage(resultData.getTelemeteringStation(), resultData.getCentralStation(), resultData.getPassword(), resultData.getFunctionCode(), resultData.getSerialNumber(), uploadEndFlag);
  115. logger.info("生成的回复报文: {}", responseMsg);
  116. byte[] messageBytes = hexStringToByteArray(responseMsg);
  117. ctx.channel().writeAndFlush(Unpooled.copiedBuffer(messageBytes));
  118. });
  119. run(resultData.getTelemeteringStation(), "在线状态同步", () -> deviceOfflineCheckTask.markDeviceOnline(resultData.getTelemeteringStation()));
  120. // 阈值监测:报警 + 接近阈值自动预警
  121. run(null, "报警预警评估", () -> checkIfSmsAlertNeeded(resultData));
  122. }
  123. } catch (InvalidMessageException e) {
  124. logger.error("数据入库失败: {}", e.getMessage());
  125. ctx.writeAndFlush(Unpooled.copiedBuffer("数据入库失败".getBytes()));
  126. } catch (Exception e) {
  127. logger.error("当前采集报文处理失败,连接继续接收后续数据", e);
  128. } finally {
  129. ReferenceCountUtil.release(msg);
  130. }
  131. }
  132. private byte[] hexStringToByteArray(String hexString) {
  133. // 校验输入合法性
  134. if (hexString == null || hexString.length() % 2 != 0) {
  135. throw new IllegalArgumentException("十六进制字符串长度必须为偶数,输入:" + hexString);
  136. }
  137. int len = hexString.length();
  138. byte[] data = new byte[len / 2];
  139. // 每2个十六进制字符转换为1个字节
  140. for (int i = 0; i < len; i += 2) {
  141. data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
  142. + Character.digit(hexString.charAt(i + 1), 16));
  143. }
  144. return data;
  145. }
  146. /**
  147. * 构建回复报文
  148. * @param telemetryAddr 遥测站地址(来自上传报文)
  149. * @param centerAddr 中心站地址(来自上传报文)
  150. * @param password 密码(来自上传报文)
  151. * @param funcCode 功能码(来自上传报文)
  152. * @param serialNo 流水号(来自上传报文)
  153. * @param uploadEndFlag 上传报文结束符(03/17)
  154. * @return 完整的回复报文十六进制字符串(包含CRC)
  155. */
  156. private String buildResponseMessage(String telemetryAddr, String centerAddr, String password,
  157. String funcCode, String serialNo, String uploadEndFlag) {
  158. // 1. 固定字段
  159. String frameHeader = "7E7E"; // 帧头
  160. String downLen = "8008"; // 下行/载荷长度
  161. String msgStartFlag = "02"; // 报文起始符
  162. // 2. 动态字段 - 发报时间(当前时间,格式:年月日时分秒,十六进制)
  163. String sendTime = DataCheckUtil.getCurrentTimeHex();
  164. // 3. 回复报文结束符(上传是03则回复04,上传是17则回复06)
  165. String responseEndFlag = "04";
  166. if ("17".equals(uploadEndFlag)) {
  167. responseEndFlag = "06";
  168. }
  169. // 4. 拼接回复报文主体(不含CRC)
  170. StringBuilder bodyBuilder = new StringBuilder();
  171. bodyBuilder.append(frameHeader) // 帧头 7E7E
  172. .append(telemetryAddr) // 遥测站地址
  173. .append(centerAddr) // 中心站地址
  174. .append(password) // 密码
  175. .append(funcCode) // 功能码
  176. .append(downLen) // 下行/载荷长度
  177. .append(msgStartFlag) // 报文起始符
  178. .append(serialNo) // 流水号
  179. .append(sendTime) // 发报时间
  180. .append(responseEndFlag); // 报文结束符
  181. String body = bodyBuilder.toString();
  182. System.out.println("回复报文主体:" + body);
  183. // 5. 计算CRC16校验码
  184. String crc = DataCheckUtil.crc16Tall(body);
  185. // 6. 拼接最终报文(主体 + CRC)
  186. return body + crc;
  187. }
  188. private String printHexBinary(byte[] bytes) {
  189. StringBuilder sb = new StringBuilder();
  190. for (byte b : bytes) {
  191. sb.append(String.format("%02X ", b));
  192. }
  193. System.out.println("Received raw data: " + sb.toString());
  194. logger.info("Received raw data: " + sb.toString());
  195. return sb.toString();
  196. }
  197. @Override
  198. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
  199. if (cause instanceof ReadTimeoutException) {
  200. logger.info("来自" + ctx.channel().remoteAddress() + "的连接超时断开");
  201. } else {
  202. logger.error("设备连接异常", cause);
  203. logger.info("来自" + ctx.channel().remoteAddress() + "的连接异常断开");
  204. ctx.close();
  205. }
  206. }
  207. @Override
  208. public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
  209. ctx.flush();
  210. }
  211. @Override
  212. public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
  213. logger.info("来自" + ctx.channel().remoteAddress() + "的连接主动断开");
  214. ctx.fireChannelUnregistered();
  215. }
  216. // 主动向设备发送数据
  217. public void sendDataToDevice(ChannelHandlerContext ctx) {
  218. try {
  219. // 将字符串形式的十六进制数据转换为字节数组
  220. String hexData = "01 03 00 00 00 01 84 0A ";
  221. String[] hexArray = hexData.split(" ");
  222. byte[] dataBytes = new byte[hexArray.length];
  223. for (int i = 0; i < hexArray.length; i++) {
  224. dataBytes[i] = (byte) Integer.parseInt(hexArray[i], 16);
  225. }
  226. ByteBuf byteBuf = Unpooled.copiedBuffer(dataBytes);
  227. ctx.writeAndFlush(byteBuf);
  228. logger.info("已向设备发送数据: {}", printHexBinary(dataBytes));
  229. } catch (Exception e) {
  230. logger.error("发送数据失败: {}", e.getMessage());
  231. }
  232. }
  233. /**
  234. * 压力值阈值评估:不再使用设备上报的 D14~D10 五种压力报警标志,
  235. * 统一按 pressure_value 与 WARN-FIRE-PRESSURE 配置阈值判定。
  236. */
  237. public void checkIfSmsAlertNeeded(FirefightingPressure data) {
  238. if (data == null) {
  239. log.warn("消防压力数据为空,跳过阈值预警评估");
  240. return;
  241. }
  242. String deviceCode = data.getTelemeteringStation() == null ? null : data.getTelemeteringStation().trim();
  243. if (deviceCode == null || deviceCode.isEmpty()) {
  244. log.warn("消防压力数据缺少遥测站地址,跳过阈值预警评估");
  245. return;
  246. }
  247. // 评估失败(阈值未配置、压力值缺失)时返回 null,保留设备上一报警状态
  248. Boolean status = evaluate(deviceCode, PRESSURE_WARNING_CODE + ":压力阈值评估",
  249. () -> evaluatePressure(deviceCode, data));
  250. if (status != null) {
  251. updateDeviceAlarmStatus(deviceCode, status ? 1 : 0);
  252. }
  253. }
  254. /**
  255. * 按阈值判定本次压力值:越限写报警并关闭预警;未越限则处理告警恢复并生成接近阈值预警。
  256. *
  257. * @return true 报警,false 正常/预警区
  258. */
  259. private boolean evaluatePressure(String deviceCode, FirefightingPressure data) {
  260. if (warningThresholdService == null || alarmDataService == null || earlyWarningAutoService == null) {
  261. throw new IllegalStateException("阈值预警依赖未注入");
  262. }
  263. if (data.getPressureValue() == null) {
  264. throw new IllegalArgumentException("采集报文缺少压力值,无法判定报警状态");
  265. }
  266. BigDecimal actual = BigDecimal.valueOf(data.getPressureValue());
  267. WarningThreshold threshold = warningThresholdService
  268. .getWarningThresholdByDeviceAndCode(deviceCode, PRESSURE_WARNING_CODE);
  269. if (threshold == null || (threshold.getMinValue() == null && threshold.getMaxValue() == null)) {
  270. throw new IllegalStateException("消防压力阈值未配置,无法判定报警状态: deviceCode=" + deviceCode);
  271. }
  272. BigDecimal min = threshold.getMinValue() == null ? null : BigDecimal.valueOf(threshold.getMinValue());
  273. BigDecimal max = threshold.getMaxValue() == null ? null : BigDecimal.valueOf(threshold.getMaxValue());
  274. // 单侧阈值时补齐等级/预警区计算基线:压力不会为负,仅上限以 0 作下限;仅下限以两倍下限作上限,
  275. // 保证只配一侧时等级计算仍有可比区间(与可燃气体仅按上限判定的处理保持一致)。
  276. BigDecimal levelMin = min != null ? min : BigDecimal.ZERO;
  277. BigDecimal levelMax = max != null ? max : min.multiply(BigDecimal.valueOf(2));
  278. if (EarlyWarningLevelUtil.isOverThreshold(actual, min, max)) {
  279. int level = saveAlarmData(deviceCode, PRESSURE_WARNING_TYPE, PRESSURE_WARNING_CODE,
  280. levelMin, levelMax, actual, "消防压力值越限报警");
  281. run(deviceCode, PRESSURE_WARNING_CODE + ":关闭预警",
  282. () -> earlyWarningAutoService.closeAutoWarning(deviceCode, PRESSURE_WARNING_CODE, "已达报警阈值,转报警处理"));
  283. if (level == 1) {
  284. run(deviceCode, PRESSURE_WARNING_CODE + ":一级告警短信",
  285. () -> sendMessage(deviceCode, PRESSURE_WARNING_TYPE, data.getLongitude(), data.getLatitude()));
  286. }
  287. return true;
  288. }
  289. run(deviceCode, PRESSURE_WARNING_CODE + ":告警恢复",
  290. () -> alarmDataService.recoverThresholdAlarm(deviceCode, PRESSURE_WARNING_CODE, levelMin, levelMax, actual));
  291. run(deviceCode, PRESSURE_WARNING_CODE + ":生成预警",
  292. () -> earlyWarningAutoService.checkAndGenerate(deviceCode, PRESSURE_WARNING_TYPE, PRESSURE_WARNING_CODE,
  293. levelMin, levelMax, actual, null));
  294. return false;
  295. }
  296. private int saveAlarmData(String deviceCode, String warningType, String warningCode,
  297. BigDecimal minValue, BigDecimal maxValue,
  298. BigDecimal actualValue, String remark) {
  299. int alarmLevel = 4;
  300. try {
  301. AlarmData alarmData = new AlarmData();
  302. alarmData.setDeviceCode(deviceCode);
  303. alarmData.setWarningCode(warningCode);
  304. alarmData.setMinValue(minValue);
  305. alarmData.setMaxValue(maxValue);
  306. alarmData.setActualValue(actualValue);
  307. alarmLevel = AlarmLevelUtil.calculateLevel(actualValue, minValue, maxValue);
  308. BigDecimal deviationRatio = AlarmLevelUtil.calculateDeviationRatio(actualValue, minValue, maxValue);
  309. alarmData.setAlarmLevel(alarmLevel);
  310. alarmData.setDeviationRatio(deviationRatio);
  311. alarmData.setAlarmContent(buildAlarmContent(warningType, actualValue, minValue, maxValue, remark));
  312. alarmData.setAlarmStatus(0);
  313. alarmData.setCreateTime(LocalDateTime.now());
  314. alarmData.setUpdateTime(LocalDateTime.now());
  315. boolean saved = run(deviceCode, warningCode + ":报警入库", () -> requireSaved(alarmDataService.saveAlarmData(alarmData)));
  316. if (alarmLevel == 1) {
  317. log.warn("【1级紧急告警】deviceCode={}, warningCode={}, deviationRatio={}%",
  318. deviceCode, warningCode, deviationRatio);
  319. }
  320. if (saved) { log.info("告警数据入库成功: deviceCode={}, warningCode={}, level={}级", deviceCode, warningCode, alarmLevel); }
  321. } catch (Exception e) {
  322. log.error("保存告警数据失败", e);
  323. }
  324. return alarmLevel;
  325. }
  326. /** 告警内容与其他采集服务保持一致:指标名称 + 越限方向,便于前端直接展示。 */
  327. private String buildAlarmContent(String warningType, BigDecimal actualValue,
  328. BigDecimal minValue, BigDecimal maxValue, String remark) {
  329. String content = warningType == null || warningType.trim().isEmpty() ? remark : warningType.trim();
  330. if (actualValue == null) {
  331. return content;
  332. }
  333. if (minValue != null && actualValue.compareTo(minValue) <= 0) {
  334. return content + "达到或低于下限";
  335. }
  336. if (maxValue != null && actualValue.compareTo(maxValue) >= 0) {
  337. return content + "达到或超过上限";
  338. }
  339. return content;
  340. }
  341. //构造发送短信参数&&发送短信
  342. public void sendMessage(String deviceNo,String alarmType,BigDecimal lng,BigDecimal lat){
  343. // 构造短信参数
  344. JSONObject params = new JSONObject();
  345. params.put("deviceNo", deviceNo);
  346. params.put("alarmType", alarmType);
  347. // 经纬度处理(为执行信息处理)
  348. params.put("location", (lng != null && lat != null)
  349. ? String.format("经纬度:%.6f,%.8f", lng, lat)
  350. : "位置未知");
  351. List<String> phoneListByDeviceId = devicePhoneFetchUtil.getPhoneListByDeviceCode(deviceNo);
  352. Map<String, Boolean> sendResults = smsUtil.sendBatchSms(phoneListByDeviceId, params.toJSONString());
  353. log.info("发送信息完成");
  354. }
  355. /**
  356. * 更新设备报警状态
  357. * @param deviceCode 设备编码
  358. * @param alarmStatus 报警状态:0-正常,1-报警
  359. */
  360. private void updateDeviceAlarmStatus(String deviceCode, int alarmStatus) {
  361. try {
  362. Map<String, Object> params = new HashMap<>();
  363. params.put("deviceCode", deviceCode);
  364. params.put("alarmStatus", alarmStatus);
  365. HttpHeaders headers = new HttpHeaders();
  366. headers.setContentType(MediaType.APPLICATION_JSON);
  367. HttpEntity<Map<String, Object>> request = new HttpEntity<>(params, headers);
  368. Map<?, ?> response = restTemplate.postForObject(deviceStatusApiUrl, request, Map.class);
  369. if (response == null || !Boolean.TRUE.equals(response.get("success"))) {
  370. log.warn("设备报警状态更新未成功: deviceCode={}, alarmStatus={}, response={}", deviceCode, alarmStatus, response);
  371. return;
  372. }
  373. log.info("设备报警状态更新成功: deviceCode={}, alarmStatus={}", deviceCode, alarmStatus);
  374. } catch (Exception e) {
  375. log.error("更新设备报警状态失败: deviceCode={}, alarmStatus={}", deviceCode, alarmStatus, e);
  376. }
  377. }
  378. }