Explorar o código

feat(alarm): 扩展报警指标并重构遥测报警评估逻辑

- 统一警告编码格式为连字符分隔(下划线改为连字符)
- 新增供水瞬时流量和供水流速两个报警指标枚举
- 雷达报警处理新增水位指标评估
- 重构遥测报警评估,将单一压力评估扩展为压力、瞬时流量、流速三项独立评估
- 提取 evaluateMetric 和 evaluateThreshold 方法,统一汇总后更新设备报警状态
- 调整 radar、telemetry、water-level 三个服务的 netty 端口分配
- 新增 RadarTestFrame 和 TelemetryTestFrame 测试报文构造夹具
林仔 hai 1 semana
pai
achega
257df7c32f

+ 7 - 1
flammable-gas-service/src/main/java/com/zksy/gas/domain/GasMonitorData.java

@@ -139,6 +139,12 @@ public class GasMonitorData implements Serializable {
     @TableField(value = "latitude")
     private BigDecimal latitude;
 
+    /**
+     * 报警项中文描述(逗号分隔),由报警信息位解析得出,便于直接看出触发了哪些报警
+     */
+    @TableField(value = "alarm_names")
+    private String alarmNames;
+
     /**
      * 入库时间
      */
@@ -154,7 +160,7 @@ public class GasMonitorData implements Serializable {
         if (alarmInfo == null) {
             alarmInfo = 0;
         }
-        alarmBits.put("unknownAlarm", (alarmInfo & (1 << 0)) != 0); // Bit0:未知报警
+        alarmBits.put("lowAlarm", (alarmInfo & (1 << 0)) != 0);      // Bit0:低报警
         alarmBits.put("highAlarm", (alarmInfo & (1 << 1)) != 0);     // Bit1:高报警
         alarmBits.put("overRange", (alarmInfo & (1 << 2)) != 0);     // Bit2:超量程
         alarmBits.put("calibrationCycle", (alarmInfo & (1 << 3)) != 0); // Bit3:标定周期

+ 66 - 54
flammable-gas-service/src/main/java/com/zksy/gas/utils/DataParser.java

@@ -44,9 +44,10 @@ public class DataParser {
         data.setProtocolVersion(String.valueOf(frame[offset] & 0xFF));
         offset++;
 
-        // 报警信息 (21~22)
-        int alarmInfo = (frame[offset] & 0xFF ) | ((frame[offset + 1] & 0xFF) << 8);
+        // 报警信息 (21~22):2 字节低字节在前(18 00 → 0x0018),与文档“01 00 低报警、04 00 超量程”一致
+        int alarmInfo = (frame[offset] & 0xFF) | ((frame[offset + 1] & 0xFF) << 8);
         data.setAlarmInfo(alarmInfo);
+        data.setAlarmNames(alarmNames(alarmInfo));
         offset += 2;
 
         // 时间戳 (23~28)
@@ -59,60 +60,49 @@ public class DataParser {
         offset++;
 
         // ------------------- 数据段 -------------------
-
-        // 1. 气体浓度 + 类型 (6 字节)
-        int gasTypeCode = frame[offset] & 0xFF; // 第1字节:气体类型
-        int unitCode = frame[offset + 1] & 0xFF; // 第2字节:单位
-        offset += 2;
-
-        long gasValueRaw = ((long)(frame[offset + 3] & 0xFF) << 24)
-                | ((long)(frame[offset + 2] & 0xFF) << 16)
-                | ((long)(frame[offset + 1] & 0xFF) << 8)
-                | ((long)(frame[offset] & 0xFF));
-        offset += 4;
-
-        String gasType;
-        switch (gasTypeCode) {
-            case 0x04: gasType = "CH4"; break;
-            case 0x05: gasType = "CO"; break;
-            case 0x06: gasType = "O2"; break;
-            default: gasType = "未知";
+        // 数据项总数由 dataCount 决定,数量随设备上报项变化(5 项无经纬度、7 项有经纬度),
+        // 不能按固定 6 项读取,否则短帧会越界。
+
+        // 第 1 项固定为气体浓度:气体类型 1 字节 + 单位 1 字节 + 值 4 字节
+        if (offset + 6 <= frame.length - 2) {
+            int gasTypeCode = frame[offset] & 0xFF;
+            int unitCode = frame[offset + 1] & 0xFF;
+            offset += 2;
+
+            long gasValueRaw = ((long)(frame[offset + 3] & 0xFF) << 24)
+                    | ((long)(frame[offset + 2] & 0xFF) << 16)
+                    | ((long)(frame[offset + 1] & 0xFF) << 8)
+                    | ((long)(frame[offset] & 0xFF));
+            offset += 4;
+
+            String gasType;
+            switch (gasTypeCode) {
+                case 0x04: gasType = "CH4"; break;
+                case 0x05: gasType = "CO"; break;
+                case 0x06: gasType = "O2"; break;
+                default: gasType = "未知";
+            }
+
+            data.setGasType(gasType);
+            data.setGasConcentration(BigDecimal.valueOf(gasValueRaw / 10.0));
+            data.setGasUnit(unitCode == 0x10 ? "%LEL" : "未知单位");
         }
 
-        BigDecimal gasConcentration = BigDecimal.valueOf(gasValueRaw / 10.0);
-        data.setGasType(gasType);
-        data.setGasConcentration(gasConcentration);
-        data.setGasUnit(unitCode == 0x10 ? "%LEL" : "未知单位");
-
-        // 2. 电池电量
-        float battery = ProtocolUtils.readFloat6(frame, offset);
-        data.setBattery(BigDecimal.valueOf(battery));
-        offset += 6;
-
-        // 3. 信号强度
-        float signal = ProtocolUtils.readFloat6(frame, offset);
-        data.setSignalStrength(signal);
-        offset += 6;
-
-        // 4. 温度(保留两位小数)
-        float temperature = ProtocolUtils.readFloat6(frame, offset);
-        data.setTemperature(new BigDecimal(temperature).setScale(2, RoundingMode.HALF_UP).floatValue());
-        offset += 6;
-
-        // 5. 湿度(保留两位小数)
-        float humidity = ProtocolUtils.readFloat6(frame, offset);
-        data.setHumidity(new BigDecimal(humidity).setScale(2, RoundingMode.HALF_UP).floatValue());
-        offset += 6;
-
-        // 6. 经度
-        float lon = ProtocolUtils.readFloat6(frame, offset);
-        data.setLongitude(BigDecimal.valueOf(lon));
-        offset += 6;
-
-        // 7. 纬度
-        float lat = ProtocolUtils.readFloat6(frame, offset);
-        data.setLatitude(BigDecimal.valueOf(lat));
-        offset += 6;
+        // 其余项:[2 字节数据标识][4 字节小端浮点],按标识分派,与上报顺序无关
+        for (int i = 1; i < dataCount && offset + 6 <= frame.length - 2; i++) {
+            int tag = ((frame[offset] & 0xFF) << 8) | (frame[offset + 1] & 0xFF);
+            float value = ProtocolUtils.readFloat6(frame, offset);
+            switch (tag) {
+                case 0xF96F: data.setBattery(BigDecimal.valueOf(value)); break;
+                case 0xF76F: data.setSignalStrength(value); break;
+                case 0xFF6F: data.setTemperature(round2(value)); break;
+                case 0xFE6F: data.setHumidity(round2(value)); break;
+                case 0xFA6F: data.setLongitude(BigDecimal.valueOf(value)); break;
+                case 0xFB6F: data.setLatitude(BigDecimal.valueOf(value)); break;
+                default: break; // 未知标识按 6 字节长度跳过
+            }
+            offset += 6;
+        }
 
         // 入库时间
         data.setCreateTime(LocalDateTime.now());
@@ -120,6 +110,28 @@ public class DataParser {
         return data;
     }
 
+    private static float round2(float value) {
+        return new BigDecimal(value).setScale(2, RoundingMode.HALF_UP).floatValue();
+    }
+
+    /** 报警位名称,下标即位号,顺序与厂商协议一致 */
+    private static final String[] ALARM_NAMES = {
+            "低报警", "高报警", "超量程", "标定周期", "超寿命", "跌倒报警", "欠压报警", "区间报警",
+            "按键报警", "震动报警", "水位报警", "断电报警", "传感器故障", "超湿报警", "超温报警", "系统故障"
+    };
+
+    /** 报警位转可读描述,无报警返回 null */
+    private static String alarmNames(int alarmInfo) {
+        StringBuilder names = new StringBuilder();
+        for (int i = 0; i < ALARM_NAMES.length; i++) {
+            if ((alarmInfo & (1 << i)) != 0) {
+                if (names.length() > 0) names.append(',');
+                names.append(ALARM_NAMES[i]);
+            }
+        }
+        return names.length() == 0 ? null : names.toString();
+    }
+
     public static void main(String[] args) {
         String hex = "7E0046902509250560010000FFFE01AA37FA350813180025101614432007041000000000F96F00004442F76F00000000FF6F102CE841FE6F96646642FA6FEEB3E142FB6FF368E141C08E";
         byte[] frame = ProtocolUtils.hexStringToBytes(hex);

+ 4 - 2
flammable-gas-service/src/main/java/com/zksy/gas/utils/MessageHandler.java

@@ -297,8 +297,10 @@ public class MessageHandler extends ChannelInboundHandlerAdapter {
 		JSONObject params = new JSONObject();
 		params.put("deviceNo", deviceNo);
 		params.put("alarmType", alarmType);
-		// 经纬度处理(位置)
-		params.put("location", String.format("经纬度:%.6f,%.8f", lng, lat));
+		// 经纬度处理(位置):5 项上报帧不含经纬度,此时不能格式化 null
+		params.put("location", (lng != null && lat != null)
+				? String.format("经纬度:%.6f,%.8f", lng, lat)
+				: "位置未知");
 		List<String> phoneListByDeviceId = devicePhoneFetchUtil.getPhoneListByDeviceCode(deviceNo);
 		Map<String, Boolean> sendResults = smsUtil.sendBatchSms(phoneListByDeviceId, params.toJSONString());
 		log.info("发送信息完成");

+ 3 - 2
flammable-gas-service/src/main/resources/mapper/GasMonitorDataMapper.xml

@@ -25,6 +25,7 @@
             <result property="humidity" column="humidity" jdbcType="FLOAT"/>
             <result property="longitude" column="longitude" jdbcType="DECIMAL"/>
             <result property="latitude" column="latitude" jdbcType="DECIMAL"/>
+            <result property="alarmNames" column="alarm_names" jdbcType="VARCHAR"/>
             <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
     </resultMap>
 
@@ -33,7 +34,7 @@
         short_address,auto_reply_option,sequence_no,
         command,device_attr,protocol_version,
         alarm_info,report_time,gas_concentration,
-        gas_type,gas_unit,battery,signal_strength,temperature,humidity
-        longitude,latitude,create_time
+        gas_type,gas_unit,battery,signal_strength,temperature,humidity,
+        longitude,latitude,alarm_names,create_time
     </sql>
 </mapper>

+ 7 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/gas/domain/GasMonitorData.java

@@ -122,6 +122,12 @@ public class GasMonitorData implements Serializable {
     @TableField(value = "latitude")
     private BigDecimal latitude;
 
+    /**
+     * 报警项中文描述(逗号分隔),由报警信息位解析得出,便于直接看出触发了哪些报警
+     */
+    @TableField(value = "alarm_names")
+    private String alarmNames;
+
     /**
      * 入库时间
      */
@@ -137,7 +143,7 @@ public class GasMonitorData implements Serializable {
         if (alarmInfo == null) {
             alarmInfo = 0;
         }
-        alarmBits.put("unknownAlarm", (alarmInfo & (1 << 0)) != 0); // Bit0:未知报警
+        alarmBits.put("lowAlarm", (alarmInfo & (1 << 0)) != 0);      // Bit0:低报警
         alarmBits.put("highAlarm", (alarmInfo & (1 << 1)) != 0);     // Bit1:高报警
         alarmBits.put("overRange", (alarmInfo & (1 << 2)) != 0);     // Bit2:超量程
         alarmBits.put("calibrationCycle", (alarmInfo & (1 << 3)) != 0); // Bit3:标定周期

+ 2 - 1
pipe-network-service/zksy-system/src/main/resources/mapper/base/gas/GasMonitorDataMapper.xml

@@ -22,6 +22,7 @@
             <result property="signalStrength" column="signal_strength" jdbcType="FLOAT"/>
             <result property="longitude" column="longitude" jdbcType="DECIMAL"/>
             <result property="latitude" column="latitude" jdbcType="DECIMAL"/>
+            <result property="alarmNames" column="alarm_names" jdbcType="VARCHAR"/>
             <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
     </resultMap>
 
@@ -31,6 +32,6 @@
         command,device_attr,protocol_version,
         alarm_info,report_time,gas_concentration,
         gas_type,battery,signal_strength,
-        longitude,latitude,create_time
+        longitude,latitude,alarm_names,create_time
     </sql>
 </mapper>

+ 19 - 10
zk-common/src/main/java/com/zksy/common/netty/DeviceFrameDecoder.java

@@ -15,7 +15,7 @@ public final class DeviceFrameDecoder extends ByteToMessageDecoder {
 
     @Override
     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
-        int minimum = protocol == Protocol.MANHOLE ? 72 : protocol == Protocol.GAS ? 74 : 5;
+        int minimum = protocol == Protocol.MANHOLE ? 72 : 5;
         while (in.readableBytes() >= minimum) {
             int start = in.readerIndex();
             int length;
@@ -38,10 +38,19 @@ public final class DeviceFrameDecoder extends ByteToMessageDecoder {
                 length = 72;
                 if (crc(in, start, 70) != in.getUnsignedShortLE(start + 70)) { in.skipBytes(1); continue; }
             } else if (protocol == Protocol.GAS) {
-                // 当前已支持的7指标上报结构固定为74字节,内外两层校验与接收器一致。
-                length = 74;
-                if (in.getUnsignedByte(start) != 0x7E || in.getUnsignedByte(start + 15) != 0xAA
-                        || !gasChecksum(in, start)) { in.skipBytes(1); continue; }
+                // 协议:7E + 2 字节长度(帧类型→校验07)+ 数据 + 校验07 + 校验16,总长 = 长度字段 + 4。
+                // 数据项数量随设备上报内容变化(5 项无经纬度、7 项含经纬度),帧长不能写死。
+                if (in.getUnsignedByte(start) != 0x7E) { in.skipBytes(1); continue; }
+                length = in.getUnsignedShort(start + 1) + 4;
+                if (length < 20 || length > MAX_FRAME) { in.skipBytes(1); continue; }
+                if (in.readableBytes() < length) {
+                    if (in.readableBytes() >= MAX_FRAME) { in.skipBytes(1); continue; }
+                    return;
+                }
+                if (in.getUnsignedByte(start + 15) != 0xAA || !gasChecksum(in, start, length)) {
+                    in.skipBytes(1);
+                    continue;
+                }
             } else {
                 if (in.getUnsignedShort(start) != 0x7E7E) { in.skipBytes(1); continue; }
                 // 现有消防设备长度字段不作为可信边界,使用结束符 + 完整CRC确认边界。
@@ -66,12 +75,12 @@ public final class DeviceFrameDecoder extends ByteToMessageDecoder {
         }
     }
 
-    private static boolean gasChecksum(ByteBuf in, int start) {
+    private static boolean gasChecksum(ByteBuf in, int start, int frameLen) {
         int body = 0, full = 0;
-        for (int i = 16; i < 72; i++) body += in.getUnsignedByte(start + i);
-        for (int i = 3; i < 73; i++) full += in.getUnsignedByte(start + i);
-        return (body & 255) == in.getUnsignedByte(start + 72)
-                && ((255 - full) & 255) == in.getUnsignedByte(start + 73);
+        for (int i = 16; i < frameLen - 2; i++) body += in.getUnsignedByte(start + i);
+        for (int i = 3; i < frameLen - 1; i++) full += in.getUnsignedByte(start + i);
+        return (body & 255) == in.getUnsignedByte(start + frameLen - 2)
+                && ((255 - full) & 255) == in.getUnsignedByte(start + frameLen - 1);
     }
 
     private static int nextPingshengFrame(ByteBuf in, int from) {