|
@@ -1,379 +1,138 @@
|
|
|
package com.zksy.audio.utils;
|
|
package com.zksy.audio.utils;
|
|
|
|
|
|
|
|
import static com.zksy.common.utils.CollectionSideEffects.*;
|
|
import static com.zksy.common.utils.CollectionSideEffects.*;
|
|
|
-
|
|
|
|
|
-import cn.hutool.core.convert.Convert;
|
|
|
|
|
-import cn.hutool.core.date.DateField;
|
|
|
|
|
-import cn.hutool.core.date.DatePattern;
|
|
|
|
|
-import cn.hutool.core.date.DateUtil;
|
|
|
|
|
-import cn.hutool.core.lang.Validator;
|
|
|
|
|
-import cn.hutool.core.thread.ThreadUtil;
|
|
|
|
|
-import cn.hutool.core.util.StrUtil;
|
|
|
|
|
import com.zksy.audio.domain.ChannelInfo;
|
|
import com.zksy.audio.domain.ChannelInfo;
|
|
|
import com.zksy.audio.domain.NoiseInfo;
|
|
import com.zksy.audio.domain.NoiseInfo;
|
|
|
import com.zksy.audio.service.NoiseInfoService;
|
|
import com.zksy.audio.service.NoiseInfoService;
|
|
|
import com.zksy.audio.service.NoiseLeakAlarmService;
|
|
import com.zksy.audio.service.NoiseLeakAlarmService;
|
|
|
-import com.zksy.audio.utils.CrtHelper;
|
|
|
|
|
import io.netty.buffer.ByteBuf;
|
|
import io.netty.buffer.ByteBuf;
|
|
|
-import io.netty.util.ReferenceCountUtil;
|
|
|
|
|
import io.netty.buffer.Unpooled;
|
|
import io.netty.buffer.Unpooled;
|
|
|
-import io.netty.channel.ChannelHandler;
|
|
|
|
|
-import io.netty.channel.ChannelHandlerContext;
|
|
|
|
|
-import io.netty.channel.ChannelId;
|
|
|
|
|
-import io.netty.channel.ChannelInboundHandlerAdapter;
|
|
|
|
|
-import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
|
+import io.netty.channel.*;
|
|
|
|
|
+import io.netty.util.ReferenceCountUtil;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.apache.commons.io.FileUtils;
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Component;
|
|
import org.springframework.stereotype.Component;
|
|
|
-
|
|
|
|
|
-import java.io.File;
|
|
|
|
|
-import java.io.IOException;
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.nio.charset.StandardCharsets;
|
|
|
-import java.util.Arrays;
|
|
|
|
|
-import java.util.concurrent.ConcurrentHashMap;
|
|
|
|
|
-import java.util.concurrent.ConcurrentMap;
|
|
|
|
|
-import java.util.regex.Matcher;
|
|
|
|
|
-import java.util.regex.Pattern;
|
|
|
|
|
|
|
+import java.nio.file.*;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+import java.util.concurrent.*;
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * 消息处理器
|
|
|
|
|
- * 负责处理各种请求和响应
|
|
|
|
|
- */
|
|
|
|
|
|
|
+/** 只处理已组好的文本行或完整WAV,一份音频只计算和保存一次。 */
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@Component
|
|
@Component
|
|
|
@ChannelHandler.Sharable
|
|
@ChannelHandler.Sharable
|
|
|
public class MessageHandler extends ChannelInboundHandlerAdapter {
|
|
public class MessageHandler extends ChannelInboundHandlerAdapter {
|
|
|
|
|
+ public final ConcurrentMap<ChannelId, ChannelInfo> channelGroup = new ConcurrentHashMap<>();
|
|
|
|
|
+ public final ConcurrentMap<String, ChannelInfo> deviceGroup = new ConcurrentHashMap<>();
|
|
|
|
|
+ @Value("${wlds.fileUploadBaseDir}") private String fileUploadBaseDir;
|
|
|
|
|
+ // 正常/异常录音每24小时上报;连续重传按滑动静默窗口去重,下一份录音独立处理。
|
|
|
|
|
+ @Value("${wlds.retransmission-window-seconds:120}") private long retransmissionWindowSeconds = 120;
|
|
|
|
|
+ private final AudioUploadDeduplicator uploads = new AudioUploadDeduplicator();
|
|
|
|
|
+ private final NoiseInfoService service;
|
|
|
|
|
+ @Autowired private DeviceOfflineCheckTask deviceOfflineCheckTask;
|
|
|
|
|
+ @Autowired private NoiseLeakAlarmService noiseLeakAlarmService;
|
|
|
|
|
+ @Autowired public MessageHandler(NoiseInfoService service) { this.service = service; }
|
|
|
|
|
+
|
|
|
|
|
+ @javax.annotation.PostConstruct
|
|
|
|
|
+ public void validateAudioTiming() {
|
|
|
|
|
+ if (retransmissionWindowSeconds <= 0 || retransmissionWindowSeconds > 86400) {
|
|
|
|
|
+ throw new IllegalArgumentException("音频重传静默窗口必须为1~86400秒");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- public ConcurrentMap<ChannelId, ChannelInfo> channelGroup = new ConcurrentHashMap<>();
|
|
|
|
|
-
|
|
|
|
|
- public ConcurrentMap<String, ChannelInfo> deviceGroup = new ConcurrentHashMap<>();
|
|
|
|
|
-
|
|
|
|
|
- @Value("${wlds.fileUploadBaseDir}")
|
|
|
|
|
- private String fileUploadBaseDir;
|
|
|
|
|
-
|
|
|
|
|
- private final static String head = "524946462440010057415645666D742010000000010001000020000000400000020010006461746100400100";
|
|
|
|
|
- private final NoiseInfoService service;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- private DeviceOfflineCheckTask deviceOfflineCheckTask;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- private NoiseLeakAlarmService noiseLeakAlarmService;
|
|
|
|
|
- @Autowired
|
|
|
|
|
- public MessageHandler(NoiseInfoService noiseInfoService) {
|
|
|
|
|
- this.service = noiseInfoService;
|
|
|
|
|
- }
|
|
|
|
|
- @Override
|
|
|
|
|
- public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
|
|
|
- //log.info("==收到客户端:{}|{}发送的消息:{}============:", ctx.channel().id().asLongText(), ctx.channel().remoteAddress(), msg);
|
|
|
|
|
- try {
|
|
|
|
|
- ByteBuf byteBuffer = (ByteBuf) msg;
|
|
|
|
|
- if (byteBuffer == null) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- int readableLength = byteBuffer.readableBytes();
|
|
|
|
|
- byte[] arrayWithOutHead = new byte[readableLength];//去掉包头标识的数据
|
|
|
|
|
- byteBuffer.getBytes(byteBuffer.readerIndex(), arrayWithOutHead);
|
|
|
|
|
-
|
|
|
|
|
- String hex = CrtHelper.byteToHexStr1(arrayWithOutHead);
|
|
|
|
|
- log.info("收到消息:{}", hex);
|
|
|
|
|
- double power = 0;
|
|
|
|
|
- String deviceNo;
|
|
|
|
|
- double gain = 0;
|
|
|
|
|
-
|
|
|
|
|
- byte[] bytes;
|
|
|
|
|
- String asciiString;
|
|
|
|
|
- String path;
|
|
|
|
|
-
|
|
|
|
|
- ChannelInfo info;
|
|
|
|
|
-
|
|
|
|
|
- ChannelInfo channelInfo = new ChannelInfo("puqi", ctx.channel());
|
|
|
|
|
-
|
|
|
|
|
- if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
- // 加入客户端
|
|
|
|
|
- channelGroup.put(ctx.channel().id(), channelInfo);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
-
|
|
|
|
|
- // 申报身份
|
|
|
|
|
- if (hex.startsWith("242424")) {
|
|
|
|
|
- String reativePath = "";
|
|
|
|
|
- String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
- log.info("hexArray.length:{}", hexArray.length);
|
|
|
|
|
- if (hexArray.length >= 1) {
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(hexArray[0]);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- deviceNo = asciiString.replaceAll("\\$\\$\\$", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
- path = getPathName(deviceNo, reativePath);
|
|
|
|
|
-
|
|
|
|
|
- info.setDeviceNo(deviceNo);
|
|
|
|
|
- info.setPath(path);
|
|
|
|
|
- info.setFirst(true);
|
|
|
|
|
- info.setFilePath(path);
|
|
|
|
|
- info.setLength(0);
|
|
|
|
|
-
|
|
|
|
|
- deviceGroup.put(deviceNo, info);
|
|
|
|
|
- channelGroup.put(ctx.channel().id(), info);
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- }
|
|
|
|
|
- if (hexArray.length >= 2) {
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(hexArray[1]);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- asciiString = asciiString.replaceAll("VBAT:", "").replaceAll("V", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
- if (Validator.isNumber(asciiString)) {
|
|
|
|
|
- power = Convert.toDouble(asciiString);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (hexArray.length >= 3) {
|
|
|
|
|
- String gainHex = hexArray[3];
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
- if (Validator.isNumber(asciiString)) {
|
|
|
|
|
- gain = Convert.toDouble(asciiString);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- channelGroup.get(ctx.channel().id()).setPower(power);
|
|
|
|
|
- channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
-
|
|
|
|
|
- if (hexArray.length >= 4) {
|
|
|
|
|
- String videoHex = hexArray[3];
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- path = info.getPath();
|
|
|
|
|
- info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
- String finalPath1 = path;
|
|
|
|
|
- ThreadUtil.execAsync(() -> {
|
|
|
|
|
- saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
- // 申报电量
|
|
|
|
|
- else if (hex.startsWith("56424154")) {
|
|
|
|
|
- if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
- if (hexArray.length >= 1) {
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(hexArray[0]);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- asciiString = asciiString.replaceAll("VBAT:", "").replaceAll("V", "").replaceAll("\r\n", "").trim();
|
|
|
|
|
- if (Validator.isNumber(asciiString)) {
|
|
|
|
|
- power = Double.parseDouble(asciiString);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- if (hexArray.length >= 2) {
|
|
|
|
|
- String gainHex = hexArray[1];
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
- if (Validator.isNumber(asciiString)) {
|
|
|
|
|
- gain = Double.parseDouble(asciiString);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
- channelGroup.get(ctx.channel().id()).setPower(power);
|
|
|
|
|
-
|
|
|
|
|
- if (hexArray.length >= 3) {
|
|
|
|
|
- String videoHex = hexArray[2];
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- path = info.getPath();
|
|
|
|
|
- info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
- String finalPath1 = path;
|
|
|
|
|
- ThreadUtil.execAsync(() -> {
|
|
|
|
|
- saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- // 增益值
|
|
|
|
|
- else if (hex.startsWith("4761696E")) {
|
|
|
|
|
- if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- String[] hexArray = Arrays.stream(hex.split("0D0A")).filter(StrUtil::isNotEmpty).toArray(String[]::new);
|
|
|
|
|
- if (hexArray.length >= 1) {
|
|
|
|
|
- String gainHex = hexArray[0];
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(gainHex);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- asciiString = asciiString.replaceAll("Gain:", "").replaceAll("\r\n", "");
|
|
|
|
|
- if (Validator.isNumber(asciiString)) {
|
|
|
|
|
- gain = Double.parseDouble(asciiString);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- channelGroup.get(ctx.channel().id()).setGain(gain);
|
|
|
|
|
- if (hexArray.length >= 2) {
|
|
|
|
|
- String videoHex = hexArray[1];
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- path = info.getPath();
|
|
|
|
|
- info.setLength(info.getLength() + (videoHex.length() / 2));
|
|
|
|
|
- String finalPath1 = path;
|
|
|
|
|
- ThreadUtil.execAsync(() -> {
|
|
|
|
|
- saveHexStringToFile(finalPath1, videoHex);
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
- } else if (hex.startsWith("4F4B")) {
|
|
|
|
|
- bytes = CrtHelper.strToToHexByte1(hex);
|
|
|
|
|
- asciiString = new String(bytes, StandardCharsets.US_ASCII);
|
|
|
|
|
- log.info("==收到客户端:{},GPS信息:{}", ctx.channel().id().asLongText(), asciiString);
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- String pattern = ",(\\d+\\.\\d+),\\d+\\.\\d+";
|
|
|
|
|
- Pattern regex = Pattern.compile(pattern);
|
|
|
|
|
- Matcher match = regex.matcher(asciiString);
|
|
|
|
|
- if (match.matches()) {
|
|
|
|
|
- // 提取经纬度,注意第一个捕获组是纬度,第二个捕获组(如果有的话)是经度
|
|
|
|
|
- // 但由于我们只关心第一个逗号后的经纬度,所以可以直接拆分
|
|
|
|
|
- String[] coords = match.group(0).replaceFirst(",", "").split(",");
|
|
|
|
|
- if (coords.length == 2) {
|
|
|
|
|
- double lat = Double.parseDouble(coords[0]);
|
|
|
|
|
- double lon = Double.parseDouble(coords[1]);
|
|
|
|
|
- log.info("经纬度:{},{}", lat, lon);
|
|
|
|
|
- //-----------
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- log.info("==收到客户端:{}的设备调试信息:{}", ctx.channel().id().asLongText(), asciiString);
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- if (!channelGroup.containsKey(ctx.channel().id())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- if (StrUtil.isEmpty(channelGroup.get(ctx.channel().id()).getDeviceNo())) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- info = channelGroup.get(ctx.channel().id());
|
|
|
|
|
- path = channelGroup.get(ctx.channel().id()).getPath();
|
|
|
|
|
- String finalPath1 = path;
|
|
|
|
|
- String headHex;
|
|
|
|
|
- byte[] length = new byte[4];
|
|
|
|
|
- if (info.getFirst() && !hex.startsWith("52494646") ){
|
|
|
|
|
- //用于没带头文件的音频数据
|
|
|
|
|
- headHex = head + hex;
|
|
|
|
|
- long videoMaxSize = 80 * 1024 + 44;
|
|
|
|
|
- info.setVideoMaxSize(videoMaxSize);
|
|
|
|
|
-
|
|
|
|
|
- } else {
|
|
|
|
|
- headHex = hex;
|
|
|
|
|
- if (hex.startsWith("52494646")){
|
|
|
|
|
- //24001900 ====> 163840
|
|
|
|
|
- length[0] = (byte) Integer.parseInt(hex.substring(14, 16), 16);
|
|
|
|
|
- length[1] = (byte) Integer.parseInt(hex.substring(12, 14), 16);
|
|
|
|
|
- length[2] = (byte) Integer.parseInt(hex.substring(10, 12), 16);
|
|
|
|
|
- length[3] = (byte) Integer.parseInt(hex.substring(8, 10), 16);
|
|
|
|
|
- long videoMaxSize = (Long.parseLong(CrtHelper.byteToHexStr1(length), 16) - 36) / 10 + 44;
|
|
|
|
|
- info.setVideoMaxSize(videoMaxSize);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- info.setFirst(false);
|
|
|
|
|
- info.setLength(info.getLength() + headHex.length() / 2);
|
|
|
|
|
- ThreadUtil.execAsync(() -> {
|
|
|
|
|
- saveHexStringToFile(finalPath1, headHex);
|
|
|
|
|
- });
|
|
|
|
|
- log.info("======收到客户端总字节{}-----videoMaxSize:{}",info.getLength(),info.getVideoMaxSize());
|
|
|
|
|
- // 如果接收音频文件字节大于头文件标识长度;
|
|
|
|
|
- if (info.getLength() >= info.getVideoMaxSize()) {
|
|
|
|
|
- if (info.getGatheTime() == null) {
|
|
|
|
|
- info.setGatheTime(DateUtil.date());
|
|
|
|
|
- }
|
|
|
|
|
- // TODO: 2023/9/5 音频文件信息入库
|
|
|
|
|
- int rms = DataParser.calculateRMS(fileUploadBaseDir+path);
|
|
|
|
|
- double centerFrequency = DataParser.calcCenterFrequency(fileUploadBaseDir+path);
|
|
|
|
|
- NoiseInfo noise = new NoiseInfo();
|
|
|
|
|
- noise.setEncode(info.getDeviceNo());
|
|
|
|
|
- noise.setPower(info.getPower());
|
|
|
|
|
- noise.setGain(info.getGain());
|
|
|
|
|
- noise.setRms(rms);
|
|
|
|
|
- noise.setCenterFrequency(centerFrequency);
|
|
|
|
|
- noise.setFilepath(info.getFilePath());
|
|
|
|
|
- noise.setTs(DateUtil.format(DateUtil.offset(info.getGatheTime(), DateField.HOUR, -8),
|
|
|
|
|
- DatePattern.NORM_DATETIME_PATTERN));
|
|
|
|
|
- //log.info(JsonUtils.toJsonString(noise));
|
|
|
|
|
- requireSaved(service.save(noise));
|
|
|
|
|
- run(noise.getEncode(), "采集应答", () ->
|
|
|
|
|
- ctx.writeAndFlush(Unpooled.copiedBuffer("OK\r\n".getBytes(StandardCharsets.US_ASCII))));
|
|
|
|
|
- run(noise.getEncode(), "接收时间更新", () ->
|
|
|
|
|
- DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(noise.getEncode(), new java.util.Date()));
|
|
|
|
|
- run(noise.getEncode(), "在线状态同步", () -> deviceOfflineCheckTask.markDeviceOnline(noise.getEncode()));
|
|
|
|
|
- run(noise.getEncode(), "漏失报警预警", () -> noiseLeakAlarmService.evaluate(noise));
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.error(e.getMessage(), e);
|
|
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void channelRead(ChannelHandlerContext ctx, Object msg) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ ByteBuf input = (ByteBuf) msg;
|
|
|
|
|
+ if (!input.isReadable()) return;
|
|
|
|
|
+ ChannelInfo info = channelGroup.computeIfAbsent(ctx.channel().id(), k -> new ChannelInfo("puqi", ctx.channel()));
|
|
|
|
|
+ if (input.readableBytes() < 4 || input.getInt(input.readerIndex()) != 0x52494646) {
|
|
|
|
|
+ String line = input.toString(StandardCharsets.US_ASCII).trim();
|
|
|
|
|
+ if (line.startsWith("$$$")) {
|
|
|
|
|
+ String device = line.substring(3).trim();
|
|
|
|
|
+ if (!device.matches("[A-Za-z0-9_-]{1,80}")) throw new IllegalArgumentException("音频设备编号无效");
|
|
|
|
|
+ if (info.getDeviceNo() != null) deviceGroup.remove(info.getDeviceNo(), info);
|
|
|
|
|
+ info.setDeviceNo(device);
|
|
|
|
|
+ deviceGroup.put(device, info);
|
|
|
|
|
+ } else if (line.startsWith("VBAT:")) {
|
|
|
|
|
+ info.setPower(parseNumber(line.substring(5).replace("V", "")));
|
|
|
|
|
+ } else if (line.startsWith("Gain:")) {
|
|
|
|
|
+ info.setGain(parseNumber(line.substring(5)));
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (info.getDeviceNo() == null) throw new IllegalStateException("音频上传前必须申报设备身份");
|
|
|
|
|
+ byte[] bytes = new byte[input.readableBytes()];
|
|
|
|
|
+ input.readBytes(bytes);
|
|
|
|
|
+ boolean fresh = uploads.receive(info.getDeviceNo(), bytes, retransmissionWindowSeconds,
|
|
|
|
|
+ () -> saveAudio(ctx, info, bytes));
|
|
|
|
|
+ if (!fresh) {
|
|
|
|
|
+ // 已落库的同一份音频仅补发确认和更新在线状态,不重复漏失计数。
|
|
|
|
|
+ acknowledge(ctx, info.getDeviceNo());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("音频采集处理失败,继续接收后续采样", e);
|
|
|
} finally {
|
|
} finally {
|
|
|
ReferenceCountUtil.release(msg);
|
|
ReferenceCountUtil.release(msg);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private String getPathName(String deviceNo, String tempPath) {
|
|
|
|
|
- String ext = ".wav";
|
|
|
|
|
- tempPath = "/devices/" + deviceNo + "/" + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATE_FORMAT);
|
|
|
|
|
- File desc = new File(fileUploadBaseDir, tempPath);
|
|
|
|
|
- if (!desc.exists()) {
|
|
|
|
|
- if (!desc.getParentFile().exists()) {
|
|
|
|
|
- desc.getParentFile().mkdirs();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- String fileName = deviceNo + "_" + DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_FORMAT) + ext;
|
|
|
|
|
- return tempPath + "/" + fileName;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void saveHexStringToFile(String filePath, String hexString) {
|
|
|
|
|
- if (StrUtil.isEmpty(hexString)) {
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
- log.info("音频字节:{}",hexString);
|
|
|
|
|
- byte[] bytes = CrtHelper.strToToHexByte1(hexString);
|
|
|
|
|
- try {
|
|
|
|
|
- FileUtils.writeByteArrayToFile(new File(fileUploadBaseDir + filePath), bytes, true);
|
|
|
|
|
- } catch (IOException e) {
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Override
|
|
|
|
|
- public void channelActive(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
- super.channelActive(ctx);
|
|
|
|
|
- log.info("==客户端:{},连接成功", ctx.channel().remoteAddress());
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Override
|
|
|
|
|
- public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
- ctx.flush();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- @Override
|
|
|
|
|
- public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
|
|
|
|
- log.error("TCP异常:{}", cause.getMessage());
|
|
|
|
|
- super.exceptionCaught(ctx, cause);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private void saveAudio(ChannelHandlerContext ctx, ChannelInfo info, byte[] bytes) throws Exception {
|
|
|
|
|
+ Path file = null;
|
|
|
|
|
+ boolean saved = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
+ String relative = "/devices/" + info.getDeviceNo() + "/" + now.toLocalDate().toString().replace("-", "")
|
|
|
|
|
+ + "/" + UUID.randomUUID() + ".wav";
|
|
|
|
|
+ Path base = Paths.get(fileUploadBaseDir).toAbsolutePath().normalize();
|
|
|
|
|
+ file = base.resolve(relative.substring(1)).normalize();
|
|
|
|
|
+ if (!file.startsWith(base)) throw new IllegalArgumentException("音频路径无效");
|
|
|
|
|
+ Files.createDirectories(file.getParent());
|
|
|
|
|
+ // 完整写入后再计算,避免异步追加造成不存在/不完整的文件。
|
|
|
|
|
+ Files.write(file, bytes, StandardOpenOption.CREATE_NEW);
|
|
|
|
|
+ NoiseInfo noise = new NoiseInfo();
|
|
|
|
|
+ noise.setEncode(info.getDeviceNo());
|
|
|
|
|
+ noise.setPower(info.getPower());
|
|
|
|
|
+ noise.setGain(info.getGain());
|
|
|
|
|
+ noise.setRms(DataParser.calculateRMS(file.toString()));
|
|
|
|
|
+ noise.setCenterFrequency(DataParser.calcCenterFrequency(file.toString()));
|
|
|
|
|
+ noise.setFilepath(relative);
|
|
|
|
|
+ noise.setTs(now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
|
|
|
|
+ requireSaved(service.save(noise));
|
|
|
|
|
+ saved = true;
|
|
|
|
|
+ info.setPath(relative);
|
|
|
|
|
+ info.setFilePath(relative);
|
|
|
|
|
+ acknowledge(ctx, noise.getEncode());
|
|
|
|
|
+ run(noise.getEncode(), "漏失报警预警", () -> noiseLeakAlarmService.evaluate(noise));
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (!saved && file != null) {
|
|
|
|
|
+ try { Files.deleteIfExists(file); } catch (Exception e) { log.warn("清理未入库音频失败", e); }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- @Override
|
|
|
|
|
- public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
- log.info("==客户端:{},掉线了", ctx.channel().remoteAddress());
|
|
|
|
|
- ChannelInfo channelInfo = channelGroup.remove(ctx.channel().id());
|
|
|
|
|
- if (channelInfo != null && StrUtil.isNotEmpty(channelInfo.getDeviceNo())) {
|
|
|
|
|
- deviceGroup.remove(channelInfo.getDeviceNo());
|
|
|
|
|
- }
|
|
|
|
|
- super.channelInactive(ctx);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private void acknowledge(ChannelHandlerContext ctx, String device) {
|
|
|
|
|
+ run(device, "采集应答", () -> ctx.writeAndFlush(Unpooled.copiedBuffer("OK\r\n", StandardCharsets.US_ASCII)));
|
|
|
|
|
+ run(device, "接收时间更新", () -> DeviceOfflineCheckTask.deviceLastReceiveTimeMap.put(device, new Date()));
|
|
|
|
|
+ run(device, "在线状态同步", () -> deviceOfflineCheckTask.markDeviceOnline(device));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- @Override
|
|
|
|
|
- public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
- log.info("==客户端:{},掉线了,ChannelUnregistered", ctx.channel().remoteAddress());
|
|
|
|
|
- super.channelUnregistered(ctx);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private double parseNumber(String value) {
|
|
|
|
|
+ double result = Double.parseDouble(value.trim());
|
|
|
|
|
+ if (!Double.isFinite(result)) throw new IllegalArgumentException("音频参数不是有效数值");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- @Override
|
|
|
|
|
- public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
- log.info("==服务端:{},下线了", ctx.toString());
|
|
|
|
|
- super.handlerRemoved(ctx);
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception {
|
|
|
|
|
+ ChannelInfo info = channelGroup.remove(ctx.channel().id());
|
|
|
|
|
+ if (info != null && info.getDeviceNo() != null) deviceGroup.remove(info.getDeviceNo(), info);
|
|
|
|
|
+ super.channelInactive(ctx);
|
|
|
|
|
+ }
|
|
|
|
|
+ @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
|
|
|
|
|
+ log.error("音频连接异常", cause);
|
|
|
|
|
+ ctx.close();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|