首页 - 关于研博 - 技术笔记 - 水文SL651协议解析
水文SL651协议解析
2025.01.17

  SL 651是国家水文局制定的用于水文监测数据通信的协议,它规定了水文监测数据采集与传输的格式、内容、传输方式等技术参数,支持通过TCP(安全性低,不推荐)和TLS两种方式接入设备。本文重点介绍通过TCP方式接入使用HEX格式传输报文方式的协议解析。

示例报文

链路维持报2F
上行报文:
7E7E01001234567812342F0008020003591011155111036BCA

7E7E00987654321012342F0008027E0822101110074703C3AE
7E7E                              起始符SOH(2个字节)
01                                中心站(1个字节)
0012345678                        遥测站(5个字节)
1234                              密码(2个字节)
2F                                功能码(1个字节)
0008                              数据长度(2个字节,第一个0表示上行,8表示下行,剩下3位表示16进制的数据长度)
02                                数据起始符(1个字节)                              
0003                              流水号(2个字节)
591011155111                      时间(6个字节,yyMMddHHmmss)
03                                结束符(1个字节)
6BCA                              crc(2个字节)

7E7E                         起始符SOH(2个字节)						
01                           中心站(1个字节)
0012345678                   遥测站地址(5个字节)
1234                         密码(2个字节)
30                           功能码(1个字节)
002B                         数据长度(2个字节,第一个0表示上行,8表示下行,剩下3位表示16进制的数据长度)
02                           数据起始符(1个字节)   
0003                         流水号(2个字节)
591011154947                 时间(6个字节,yyMMddHHmmss)
F1F10012345678               站号标识符(F1F1)+测站地址(7个字节)
48                           遥测站类型(1个字节)
F0F05910111549               观测时间标识符(F0F0)+时间(yyMMddHHmm)(7个字节)
2019000005					 当前降水量:0.5mm(20要素标识符;19【其中高5位表示数据长度,低3位表示小数位】表示数据报文帧的长度字节为3个字节和1位小数位;000005表示具体数据)
2619000005					 降水量累计值:0.5mm
392300000127				 瞬时河道水位:0.127m
38121115					 电源电压:11.15V
03                           结束符(1个字节)
20FA                         crc(2个字节)

由示例可看出每条报文头部固定为起始符、中心站、遥测站、密码;尾部固定为结束符、crc校验。每条报文包含的数据由功能码决定。我们在解析时根据功能码进行对不同报文的不同解析。

实现物联网平台协议包

完整工程代码联系小助理获取

主要代码

public class SL651Codec implements ProtocolCodec {

    private static final Logger log = LoggerFactory.getLogger(SL651Codec.class);

    // 指定协议支撑信息
    @Override
    public ProtocolSupport support() {
        return new ProtocolSupport(TransportProtocol.TCP)
        .name("SL651")
        .id("SL651")
        .description("水文SL651")
        .feature(new ProtocolFeature().keepOnline(true).keepOnlineTimeoutSeconds(180));
    }

    // 协议数据解析方法,将设备上报的消息解析为平台设备消息。
    @Override
    public void decode(OperatorSupplier supplier, DeviceSession deviceSession, ProtocolMessage<?> message, MessageExporter<DeviceMessage<?>> messageExporter) {
        TcpProtocolMessage message1 = (TcpProtocolMessage) message;
        ByteBuf payload = message1.getPayload();
        byte[] bytes = new byte[payload.readableBytes()];
        payload.readBytes(bytes);
        String payloadStr = HexStringUtil.bytes2HexStr(bytes);
        boolean b = FrameUtil.verifyCRC16Code(HexStringUtil.hexStr2CharArray(payloadStr));
        if (!b) {
            throw new ServiceException("数据校验失败");
        }
        FrameReqWrapper frameReqWrapper = SL651Resolver.decodeHex(payloadStr);
        FrameReq reqDto = frameReqWrapper.getReqDto();
        sendMessage(supplier, reqDto, messageExporter);
        // 回复消息
        ByteBuf byteBuf = encodeM24Rsp(frameReqWrapper);
        TcpProtocolMessage protocolMessage = new TcpProtocolMessage();
        protocolMessage.payload(byteBuf);
        deviceSession.send(protocolMessage);
    }

    private void sendMessage(OperatorSupplier supplier, FrameReq reqDto, MessageExporter<DeviceMessage<?>> messageExporter) {
        FrameReqBody body = reqDto.getBody();
        String remoteStationAddr = body.getRemoteStationAddr();
        HashMap<String, List<FrameReqBodyElement>> elementMap = body.getElementMap();
        Set<Map.Entry<String, List<FrameReqBodyElement>>> entries = elementMap.entrySet();
        for (Map.Entry<String, List<FrameReqBodyElement>> entry : entries) {
            ReportPropertyMessage message = new ReportPropertyMessage();
            message.deviceId(remoteStationAddr);
            String funCode = entry.getKey();
            if ("f4".equals(funCode) || "f5".equals(funCode)) {
                // todo 具体场景
            }
            FrameReqBodyElement first = entry.getValue().getFirst();
            message.addProperty(funCode, first.calDoubleVal());
            message.timestamp(LocalDateTime.parse(first.getObserveTime()).toInstant(ZoneOffset.UTC).getEpochSecond());
            messageExporter.export(message);
        }
    }


    @Override
    public void encode(OperatorSupplier supplier, DeviceMessage<?> message, MessageExporter<ProtocolMessage<?>> messageExporter) {

    }

    // 消息响应
    private ByteBuf encodeM24Rsp(FrameReqWrapper frameReqWrapper) {
        try {
            FrameReqHeader header = frameReqWrapper.getReqDto().getHeader();
            StringBuilder sb = new StringBuilder();
            sb.append(FrameConstant.HEADER_START_HEX).append(header.getRemoteStationAddr())
                    .append(header.getCenterStationAddr()).append(header.getPwd()).append(header.getFuncCode())
                    .append(FrameConstant.HEADER_RSP_DOWN_SYMBOL_AND_ZERO_LEN_HEX)
                    .append(FrameConstant.HEADER_RSP_BODY_END_STX_HEX).append(FrameConstant.HEADER_RSP_BODY_END_EOT_HEX);
            sb.append(Crc16Util.crc16(Hex.decodeHex(sb.toString()), false).toUpperCase());
            byte[] bytes = DatatypeConverter.parseHexBinary(sb.toString());
            return Unpooled.copiedBuffer(bytes);// 将十六进制转换为二进制发送
//            return Unpooled.copiedBuffer(sb.toString().getBytes());
        } catch (Exception e) {
            log.info("出现异常了{}", e.getMessage());
        }
        return null;
    }
}

数据解析器

@Slf4j
public class SL651Resolver {

    private static boolean crcVerifyFlag = false;

    public static Map<String, Object> resolve(String hexStr) {
        // 取得帧头
        return null;
    }

    public static FrameReqWrapper decodeHex(String frameHexStr) {
        FrameReqWrapper wrapper = new FrameReqWrapper();
        FrameReq frameReq = new FrameReq();
        wrapper.setReqDto(frameReq);

        wrapper.setOriginalFrame(frameHexStr);
        char[] frame = HexStringUtil.hexStr2CharArray(frameHexStr);
        if (crcVerifyFlag && !FrameUtil.verifyCRC16Code(frame)) {
            log.warn("sl651-2014 frame crc verify fail>>>" + frameHexStr);
            wrapper.setWrapCodeEnum(ServiceCodeEnum.SERVICE_CRC_VERIFY_FAIL);
            return wrapper;
        }
        FrameReqHeader header = decodeHeader(frame);
        frameReq.setHeader(header);
        int bodyLen = header.getBodyLen().intValue();
        if (header.isM3Mode()) {
            frameReq.setBody(decodeM3Body(FrameUtil.getM3Body(frame, bodyLen)));
        } else {
            frameReq.setBody(decodeM124Body(FrameUtil.getM124Body(frame, bodyLen),
                    SL651FunCode.getByValue(header.getFuncCode())));
        }
        frameReq.setBodyEndSymbol(FrameUtil.getBodyEndSymbol(frame));
        frameReq.setCrcCode(FrameUtil.getCRC16Code(frame));
        wrapper.setWrapCodeEnum(ServiceCodeEnum.SERVICE_SUCCESS);
        return wrapper;
    }


    public static FrameReqHeader decodeHeader(char[] frame) {
        FrameReqHeader header = new FrameReqHeader();
        // 跳过帧起始的固定两字节
        // 中心站
        header.setCenterStationAddr(FrameUtil.getHeaderCenterStationAddr(frame));
        // 遥测站
        header.setRemoteStationAddr(FrameUtil.getHeaderRemoteStationAddr(frame));
        // 密码
        header.setPwd(FrameUtil.getHeaderPwd(frame));
        // 功能码
        header.setFuncCode(FrameUtil.getHeaderFuncCode(frame));
        // 报文正文长度
        header.setBodyLen(FrameUtil.getBodyLen(frame));
        // 传输是否为M3多包模式
        header.setM3Mode(FrameUtil.isM3Mode(frame));
        if (header.isM3Mode()) {
            // TODO 解析m3模式报文头特殊参数
            header.setFrameCnt(0);
            header.setFrameSerialNo(null);
        }
        return header;
    }

    public static FrameReqBody decodeM3Body(char[] bodyFrame) {
        return null;
    }

    public static FrameReqBody decodeM124Body(char[] bodyFrame, SL651FunCode funcEnum) {
        if (funcEnum == null) {
            return null;
        }
        switch (funcEnum) {
            // 心跳消息
            case F_2F -> {
                return decodeM124BodyByHeartBeat(bodyFrame);
            }
            // 测试报文
            case F_30 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            // 均匀时段水文信息 暂不支持
            case F_31 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            // 遥测站定时报
            case F_32 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            // 遥测站加报报
            case F_33 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            // 遥测站小时报 todo 重新写逻辑
            case F_34 -> {
                return decodeF_34BodyByRegularReport(bodyFrame);
            }
            // 遥测站人工置数报
            case F_35 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            // 遥测站图片报或中心站查询遥测站图片采集信息
            case F_36 -> {
                return decodeM124BodyByRegularReport(bodyFrame);
            }
            default -> {
                return null;
            }
        }
    }

    private static FrameReqBody decodeM124BodyByRegularReport(char[] bodyFrame) {
        FrameReqBody body = new FrameReqBody();
        body.setSerialNo(FrameM124Util.getBodySerialNo(bodyFrame));
        body.setSendTime(FrameM124Util.getBodySendTime(bodyFrame));
        body.setRemoteStationAddr(FrameM124Util.getBodyRemoteStationAddr(bodyFrame));
        body.setRemoteStationTypeCode(FrameM124Util.getRemoteStationTypeCode(bodyFrame));
        body.setObserveTime(FrameM124Util.getBodyObserveTime(bodyFrame));
        // TODO 解析自定义扩展参数
        body.setElementMap(FrameUtil.getBodyElement(FrameM124Util.getBodyElementByRegularReport(bodyFrame)));
        return body;
    }

    private static FrameReqBody decodeF_34BodyByRegularReport(char[] bodyFrame) {
        FrameReqBody body = new FrameReqBody();
        body.setSerialNo(FrameM124Util.getBodySerialNo(bodyFrame));
        body.setSendTime(FrameM124Util.getBodySendTime(bodyFrame));
        body.setRemoteStationAddr(FrameM124Util.getBodyRemoteStationAddr(bodyFrame));
        body.setRemoteStationTypeCode(FrameM124Util.getRemoteStationTypeCode(bodyFrame));
        body.setObserveTime(FrameM124Util.getBodyObserveTime(bodyFrame));
        // TODO 解析自定义扩展参数
        body.setElementMap(FrameUtil.getBodyElementQiZhu(FrameM124Util.getF_34BodyElementByRegularReport(bodyFrame)));
        return body;
    }

    private static FrameReqBody decodeM124BodyByHeartBeat(char[] bodyFrame) {
        FrameReqBody body = new FrameReqBody();
        body.setSerialNo(FrameM124Util.getBodySerialNo(bodyFrame));
        body.setSendTime(FrameM124Util.getBodySendTime(bodyFrame));
        return body;
    }


    public static void main(String[] args) {
        String test = "7E7E05001122334403E8340068020033170718110014F1F1001122334448F0F01707181005F4600500000014FFFFFFFFFF0000F0F017071811002619000040F0F01707181005F5C0000C000C001C00310031FFFFFFFFFFFFFFFFFFFF00310031F0F0170718110039230001049020190000403812109903F502";
        FrameReqWrapper frameReqWrapper = decodeHex(test);
        FrameReq reqDto = frameReqWrapper.getReqDto();
        System.out.println("reqDto = " + reqDto);
        HashMap<String, List<FrameReqBodyElement>> elementMap = reqDto.getBody().getElementMap();
        System.out.println("elementMap = " + elementMap);
    }
}

以上便是我们对水文SL651协议的解析。如有出入之处欢迎大家指导交流。

获取相关资料
下载地址将会发送至您填写的邮箱
相关新闻
HJ212环境监测数据传输协议
2025-02-14
电能表DLT645协议解析
2025-02-07
JT/T808 协议解析
2025-01-10
  • 在线客服
  • 电话咨询
  • 微信
  • 短视频