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协议的解析。如有出入之处欢迎大家指导交流。