MessageHandler.java 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. package com.zksy.audio.utils;
  2. import static com.zksy.common.utils.CollectionSideEffects.*;
  3. import com.zksy.audio.domain.ChannelInfo;
  4. import com.zksy.audio.domain.NoiseInfo;
  5. import com.zksy.audio.service.NoiseInfoService;
  6. import com.zksy.audio.service.NoiseLeakAlarmService;
  7. import io.netty.buffer.ByteBuf;
  8. import io.netty.buffer.Unpooled;
  9. import io.netty.channel.*;
  10. import io.netty.util.ReferenceCountUtil;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.beans.factory.annotation.Value;
  14. import org.springframework.stereotype.Component;
  15. import java.nio.charset.StandardCharsets;
  16. import java.nio.file.*;
  17. import java.time.LocalDateTime;
  18. import java.time.format.DateTimeFormatter;
  19. import java.util.*;
  20. import java.util.concurrent.*;
  21. /** 只处理已组好的文本行或完整WAV,一份音频只计算和保存一次。 */
  22. @Slf4j
  23. @Component
  24. @ChannelHandler.Sharable
  25. public class MessageHandler extends ChannelInboundHandlerAdapter {
  26. public final ConcurrentMap<ChannelId, ChannelInfo> channelGroup = new ConcurrentHashMap<>();
  27. public final ConcurrentMap<String, ChannelInfo> deviceGroup = new ConcurrentHashMap<>();
  28. @Value("${wlds.fileUploadBaseDir}") private String fileUploadBaseDir;
  29. // 正常/异常录音每24小时上报;连续重传按滑动静默窗口去重,下一份录音独立处理。
  30. @Value("${wlds.retransmission-window-seconds:120}") private long retransmissionWindowSeconds = 120;
  31. private final AudioUploadDeduplicator uploads = new AudioUploadDeduplicator();
  32. private final NoiseInfoService service;
  33. @Autowired private DeviceOfflineCheckTask deviceOfflineCheckTask;
  34. @Autowired private NoiseLeakAlarmService noiseLeakAlarmService;
  35. @Autowired public MessageHandler(NoiseInfoService service) { this.service = service; }
  36. @javax.annotation.PostConstruct
  37. public void validateAudioTiming() {
  38. if (retransmissionWindowSeconds <= 0 || retransmissionWindowSeconds > 86400) {
  39. throw new IllegalArgumentException("音频重传静默窗口必须为1~86400秒");
  40. }
  41. }
  42. @Override
  43. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  44. try {
  45. ByteBuf input = (ByteBuf) msg;
  46. if (!input.isReadable()) return;
  47. ChannelInfo info = channelGroup.computeIfAbsent(ctx.channel().id(), k -> new ChannelInfo("puqi", ctx.channel()));
  48. if (input.readableBytes() < 4 || input.getInt(input.readerIndex()) != 0x52494646) {
  49. String line = input.toString(StandardCharsets.US_ASCII).trim();
  50. if (line.startsWith("$$$")) {
  51. String device = line.substring(3).trim();
  52. if (!device.matches("[A-Za-z0-9_-]{1,80}")) throw new IllegalArgumentException("音频设备编号无效");
  53. if (info.getDeviceNo() != null) deviceGroup.remove(info.getDeviceNo(), info);
  54. info.setDeviceNo(device);
  55. deviceGroup.put(device, info);
  56. } else if (line.startsWith("VBAT:")) {
  57. info.setPower(parseNumber(line.substring(5).replace("V", "")));
  58. } else if (line.startsWith("Gain:")) {
  59. info.setGain(parseNumber(line.substring(5)));
  60. }
  61. return;
  62. }
  63. if (info.getDeviceNo() == null) throw new IllegalStateException("音频上传前必须申报设备身份");
  64. byte[] bytes = new byte[input.readableBytes()];
  65. input.readBytes(bytes);
  66. boolean fresh = uploads.receive(info.getDeviceNo(), bytes, retransmissionWindowSeconds,
  67. () -> saveAudio(ctx, info, bytes));
  68. if (!fresh) {
  69. // 已落库的同一份音频仅补发确认和更新在线状态,不重复漏失计数。
  70. acknowledge(ctx, info.getDeviceNo());
  71. }
  72. } catch (Exception e) {
  73. log.error("音频采集处理失败,继续接收后续采样", e);
  74. } finally {
  75. ReferenceCountUtil.release(msg);
  76. }
  77. }
  78. private void saveAudio(ChannelHandlerContext ctx, ChannelInfo info, byte[] bytes) throws Exception {
  79. Path file = null;
  80. boolean saved = false;
  81. try {
  82. LocalDateTime now = LocalDateTime.now();
  83. String relative = "/devices/" + info.getDeviceNo() + "/" + now.toLocalDate().toString().replace("-", "")
  84. + "/" + UUID.randomUUID() + ".wav";
  85. Path base = Paths.get(fileUploadBaseDir).toAbsolutePath().normalize();
  86. file = base.resolve(relative.substring(1)).normalize();
  87. if (!file.startsWith(base)) throw new IllegalArgumentException("音频路径无效");
  88. Files.createDirectories(file.getParent());
  89. // 完整写入后再计算,避免异步追加造成不存在/不完整的文件。
  90. Files.write(file, bytes, StandardOpenOption.CREATE_NEW);
  91. NoiseInfo noise = new NoiseInfo();
  92. noise.setEncode(info.getDeviceNo());
  93. noise.setPower(info.getPower());
  94. noise.setGain(info.getGain());
  95. noise.setRms(DataParser.calculateRMS(file.toString()));
  96. noise.setCenterFrequency(DataParser.calcCenterFrequency(file.toString()));
  97. noise.setFilepath(relative);
  98. noise.setTs(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
  99. requireSaved(service.save(noise));
  100. saved = true;
  101. info.setPath(relative);
  102. info.setFilePath(relative);
  103. acknowledge(ctx, noise.getEncode());
  104. run(noise.getEncode(), "漏失报警预警", () -> noiseLeakAlarmService.evaluate(noise));
  105. } finally {
  106. if (!saved && file != null) {
  107. try { Files.deleteIfExists(file); } catch (Exception e) { log.warn("清理未入库音频失败", e); }
  108. }
  109. }
  110. }
  111. private void acknowledge(ChannelHandlerContext ctx, String device) {
  112. run(device, "采集应答", () -> ctx.writeAndFlush(Unpooled.copiedBuffer("OK\r\n", StandardCharsets.US_ASCII)));
  113. run(device, "接收时间更新", () -> DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(device, new Date()));
  114. run(device, "在线状态同步", () -> deviceOfflineCheckTask.markDeviceOnline(device));
  115. }
  116. private double parseNumber(String value) {
  117. double result = Double.parseDouble(value.trim());
  118. if (!Double.isFinite(result)) throw new IllegalArgumentException("音频参数不是有效数值");
  119. return result;
  120. }
  121. @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  122. ChannelInfo info = channelGroup.remove(ctx.channel().id());
  123. if (info != null && info.getDeviceNo() != null) deviceGroup.remove(info.getDeviceNo(), info);
  124. super.channelInactive(ctx);
  125. }
  126. @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  127. log.error("音频连接异常", cause);
  128. ctx.close();
  129. }
  130. }