java解析JT808协议-netty-机械设备

java解析JT808协议-netty-机械设备机械设备 解析 JT808 协议相关流程背景 1 开发过程 1 1 认识 JT808 协议 1 2 构建编 解码器 1 3 构建业务 Handler1 4Channel 的高效管理方式 2 解析协议 2 10200 报文数

大家好,欢迎来到IT知识分享网。

背景

JT808网关作为部标终端连接的服务端,承载了终端登录、心跳、位置、拍照等基础业务以及信令交互,是整个系统最核心的模块,一旦崩溃,则所有部标终端都会离线,所有信令交互包括1078和主动安全的信令交互也会大受影响。所以,JT808网关的并发性稳定性健壮性成为整个系统最重要的考量之一。

1.开发过程

  1. 认识JT808协议
  2. 构建编/解码器
  3. 构建业务Handler
  4. Channel的高效管理方式

1.1认识JT808协议

其中消息体属性中我们先只关注消息体长度,不关注其他,分包情况先不考虑。

根据消息头和消息体我们可以抽象出一个最基本的数据结构

@Data public class DataPacket { 
    protected Header header = new Header(); //消息头 protected ByteBuf byteBuf; //消息流 @Data public static class Header { 
    private short msgId;// 消息ID 2字节 private short msgBodyProps;//消息体属性 2字节 private String terminalPhone; // 终端手机号 6字节 private short flowId;// 流水号 2字节 //获取包体长度 public short getMsgBodyLength() { 
    return (short) (msgBodyProps & 0x3ff); } //获取加密类型 3bits public byte getEncryptionType() { 
    return (byte) ((msgBodyProps & 0x1c00) >> 10); } //是否分包 public boolean hasSubPackage() { 
    return ((msgBodyProps & 0x2000) >> 13) == 1; } } } 
 public void parse() { 
    try{ 
    this.parseHead(); //验证包体长度 if (this.header.getMsgBodyLength() != this.byteBuf.readableBytes()) { 
    throw new RuntimeException("包体长度有误"); } this.parseBody();//由子类重写 }finally { 
    ReferenceCountUtil.safeRelease(this.byteBuf);//注意释放 } } protected void parseHead() { 
    header.setMsgId(byteBuf.readShort()); header.setMsgBodyProps(byteBuf.readShort()); header.setTerminalPhone(BCD.BCDtoString(readBytes(6))); header.setFlowId(byteBuf.readShort()); } protected void parseBody() { 
    } 

其中readByte(int length)方法是对ByteBuf.readBytes(byte[] dst)的一个简单封装

public byte[] readBytes(int length) { 
    byte[] bytes = new byte[length]; this.byteBuf.readBytes(bytes); return bytes; } 

因为没有在Netty官方的Api中找到类似的方法,所以自己定义了一个

另外定义一个方法用于响应重写。

响应重写:

 public ByteBuf toByteBufMsg() { 
    ByteBuf bb = ByteBufAllocator.DEFAULT.heapBuffer(); bb.writeInt(0);//先占4字节用来写msgId和msgBodyProps bb.writeBytes(BCD.toBcdBytes(StringUtils.leftPad(this.header.getTerminalPhone(), 12, "0"))); bb.writeShort(this.header.getFlowId()); return bb; } ** "最佳实践":尽量使用内存池分配ByteBuf,效率相比非池化Unpooled.buffer()高很多,但是得注意释放,否则会内存泄漏 在ChannelPipeLine中我们可以使用ctx.alloc()或者channel.alloc()获取Netty默认内存分配器, 其他地方不一定要建立独有的内存分配器,可以通过ByteBufAllocator.DEFAULT获取,跟前面获取的是同一个(不特别配置的话)** 

DataPacket 完整代码

@Data public class DataPacket { 
    protected Header header = new Header(); //消息头 protected ByteBuf body; //消息体 public DataPacket() { 
    } public DataPacket(ByteBuf body) { 
    this.body = body; } public void parse() { 
    try { 
    this.parseHead(); //验证包体长度 if (this.header.getMsgBodyLength() != this.body.readableBytes()) { 
    throw new RuntimeException("包体长度有误"); } this.parseBody(); } finally { 
    //ReferenceCountUtil.safeRelease(this.body); } } public void parseHead() { 
    header.setMsgId(body.readShort()); header.setMsgBodyProps(body.readShort()); header.setTerminalPhone(BCD.toString(readBytes(6))); header.setFlowId(body.readShort()); if (header.hasSubPackage()) { 
    //TODO 处理分包 body.readInt(); } } / * 请求报文重写 */ protected void parseBody() { 
    } / * 响应报文重写 并调用父类 * * @return */ public ByteBuf toByteBufMsg() { 
    ByteBuf bb = ByteBufAllocator.DEFAULT.heapBuffer();//在JT808Encoder escape()方法处回收 bb.writeInt(0);//先占4字节用来写msgId和msgBodyProps,JT808Encoder中覆盖回来 bb.writeBytes(BCD.toBcdBytes(StringUtils.leftPad(this.header.getTerminalPhone(), 12, "0"))); bb.writeShort(this.header.getFlowId()); //TODO 处理分包 return bb; } / * 从ByteBuf中read固定长度的数组,相当于ByteBuf.readBytes(byte[] dst)的简单封装 * * @param length * @return */ public byte[] readBytes(int length) { 
    byte[] bytes = new byte[length]; this.body.readBytes(bytes); return bytes; } / * 从ByteBuf中读出固定长度的数组 ,根据808默认字符集构建字符串 * * @param length * @return */ public String readString(int length) { 
    return new String(readBytes(length), Const.DEFAULT_CHARSET); } / * 消息头对象 */ @Data public static class Header { 
    private short msgId;// 功能ID 2字节 private short msgBodyProps;//消息属性 2字节 private String terminalPhone; // 终端手机号 6字节 private short flowId;// 流水号 2字节 //获取包体长度 public short getMsgBodyLength() { 
    return (short) (msgBodyProps & 0x3ff); } //获取加密类型 3bits public byte getEncryptionType() { 
    return (byte) ((msgBodyProps & 0x1c00) >> 10); } //是否分包 public boolean hasSubPackage() { 
    return ((msgBodyProps & 0x2000) >> 13) == 1; } } } 

这里当我们将响应转化为ByteBuf写出去的时候,此时并不知道消息体的具体长度,所有此时我们先占住位置,回头再来写。

所有的消息都继承自DataPacket,我们挑出一个字段相对较多的-》 位置上报消息

然后我们建立位置上报消息的数据结构,先看位置消息的格式

在这里插入图片描述

/ * */ @Data public class LocationMessage extends DataPacket { 
    private int alarm; //告警信息 4字节 private int statusField;//状态 4字节 private float latitude;//纬度 4字节 private float longitude;//经度 4字节 private short elevation;//海拔高度 2字节 private short speed; //速度 2字节 private short direction; //方向 2字节 private String time; //时间 6字节BCD public LocationMsg(ByteBuf byteBuf) { 
    super(byteBuf); } @Override public void parseBody() { 
    //解码器中的MessageDecoder.parse方法会把body消息体传到这里,在这里根据自己的业务进行解析数据 ByteBuf bb= this.body; this.setAlarm(bb.readInt()); this.setStatusField(bb.readInt()); this.setLatitude(bb.readUnsignedInt() * 1.0F / ); this.setLongitude(bb.readUnsignedInt() * 1.0F / ); this.setElevation(bb.readShort()); this.setSpeed(bb.readShort()); this.setDirection(bb.readShort()); this.setTime(BCD.toBcdTimeString(readBytes(6))); } } 
@Data public class CommonResponse extends DataPacket { 
    public static final byte SUCCESS = 0;//成功/确认 public static final byte FAILURE = 1;//失败 public static final byte MSG_ERROR = 2;//消息有误 public static final byte UNSUPPORTED = 3;//不支持 public static final byte ALARM_PROCESS_ACK = 4;//报警处理确认 private short replyFlowId; //应答流水号 2字节 private short replyId; //应答 ID 2字节 private byte result; //结果 1字节 public CommonResponse() { 
    this.getHeader().setMsgId(Const.SERVER_RESP_COMMON); } @Override public ByteBuf toByteBufMsg() { 
    ByteBuf bb = super.toByteBufMsg(); bb.writeShort(replyFlowId); bb.writeShort(replyId); bb.writeByte(result); return bb; } public static CommonResponse success(DataPacket msg, short flowId) { 
    CommonResponse resp = new CommonResponse(); resp.getHeader().setTerminalPhone(msg.getHeader().getTerminalPhone()); resp.getHeader().setFlowId(flowId); resp.setReplyFlowId(msg.getHeader().getFlowId()); resp.setReplyId(msg.getHeader().getMsgId()); resp.setResult(SUCCESS); return resp; } public static CommonResponse success(DataPacket msg, short flowId,byte result) { 
    CommonResponse resp = new CommonResponse(); resp.getHeader().setTerminalPhone(msg.getHeader().getTerminalPhone()); resp.getHeader().setFlowId(flowId); resp.setReplyFlowId(msg.getHeader().getFlowId()); resp.setReplyId(msg.getHeader().getMsgId()); resp.setResult(result); return resp; } } 

1.2 构建编/解码器

解码器
前面协议可以看到,标识位为0x7e,所以我们第一个解码器可以用Netty自带的DelimiterBasedFrameDecoder,其中的delimiters自然就是0x7e了。(Netty有很多自带的编解码器,建议先确认Netty自带的不能满足需求,再自己自定义)

@Slf4j public class MessageDecoder extends ByteToMessageDecoder { 
    @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { 
    log.info("<<<<< ip:{},hex:{}", ctx.channel().remoteAddress(), ByteBufUtil.hexDump(in).toLowerCase()); DataPacket msg = null; msg = decode(in); if (msg != null) { 
    out.add(msg); } } private DataPacket decode(ByteBuf in) { 
    if (in.readableBytes() < 12) { 
    //包头最小长度 return null; } //第一步 转义 byte[] raw = new byte[in.readableBytes()]; in.readBytes(raw); ByteBuf escape = revert(raw); //第二步 校验 byte pkgCheckSum = escape.getByte(escape.writerIndex() - 1); escape.writerIndex(escape.writerIndex() - 1);//排除校验码 byte calCheckSum = BCD.XorSumBytes(escape); if (pkgCheckSum != calCheckSum) { 
    log.warn("校验码错误,pkgCheckSum:{},calCheckSum:{}", pkgCheckSum, calCheckSum); ReferenceCountUtil.safeRelease(escape); return null; } //第三步:解码 return parse(escape); } / * 将接收到的原始转义数据还原 * * @param raw * @return */ public ByteBuf revert(byte[] raw) { 
    int len = raw.length; ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer(len);//DataPacket parse方法回收 for (int i = 0; i < len; i++) { 
    //这里如果最后一位是0x7d会导致index溢出,说明原始报文转义有误 if (raw[i] == 0x7d && raw[i + 1] == 0x01) { 
    buf.writeByte(0x7d); i++; } else if (raw[i] == 0x7d && raw[i + 1] == 0x02) { 
    buf.writeByte(0x7e); i++; } else { 
    buf.writeByte(raw[i]); } } return buf; } public DataPacket parse(ByteBuf bb) { 
    DataPacket packet = null; short msgId = bb.getShort(bb.readerIndex()); switch (msgId) { 
    case Const.TERNIMAL_MSG_HEARTBEAT: packet = new HeartBeatMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION://0200 packet = new LocationMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION_BATCH://0704 packet = new LocationBatchMessage(bb); break; case Const.TERNIMAL_MSG_REGISTER: packet = new RegisterMessage(bb); break; case Const.TERNIMAL_MSG_AUTH: packet = new AuthMessage(bb); break; // case Const.TERNIMAL_MSG_STATUS: 0900数据 // packet = new StatusMessage(bb); // break; default: packet = new DataPacket(bb); break; } packet.parse(); return packet; } } 

第一步:转义还原,转义规则如下

0x7d 0x01 -> 0x7d

0x7d 0x02 -> 0x7e

public ByteBuf revert(byte[] raw) { 
    int len = raw.length; ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer(len);//DataPacket parse方法回收 for (int i = 0; i < len; i++) { 
    if (raw[i] == 0x7d && raw[i + 1] == 0x01) { 
    buf.writeByte(0x7d); i++; } else if (raw[i] == 0x7d && raw[i + 1] == 0x02) { 
    buf.writeByte(0x7e); i++; } else { 
    buf.writeByte(raw[i]); } } return buf; } 

第二步:校验

 byte pkgCheckSum = escape.getByte(escape.writerIndex() - 1); escape.writerIndex(escape.writerIndex() - 1);//排除校验码 byte calCheckSum = JT808Util.XorSumBytes(escape); if (pkgCheckSum != calCheckSum) { 
    log.warn("校验码错误,pkgCheckSum:{},calCheckSum:{}", pkgCheckSum, calCheckSum); ReferenceCountUtil.safeRelease(escape);//一定不要漏了释放 return null; } 

第三步:解码 根据自己的业务设置

public DataPacket parse(ByteBuf bb) { 
    DataPacket packet = null; short msgId = bb.getShort(bb.readerIndex()); switch (msgId) { 
    case Const.TERNIMAL_MSG_HEARTBEAT: packet = new HeartBeatMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION: packet = new LocationMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION_BATCH: packet = new LocationBatchMessage(bb); break; case Const.TERNIMAL_MSG_REGISTER: packet = new RegisterMessage(bb); break; case Const.TERNIMAL_MSG_AUTH: packet = new AuthMessage(bb); break; // case Const.TERNIMAL_MSG_STATUS: 0900数据 // packet = new StatusMessage(bb); // break; default: packet = new DataPacket(bb); break; } packet.parse(); return packet; } 

switch里我们尽量将收到频率高的放在前面,避免过多的if判断

然后我们将消息out.add(msg)就可以让消息到我们的业务Handler中了。

编码器 完整代码
编码器需要讲我们的DataPacket转化为ByteBuf,然后再转义发送出去。
定义编码器

public class MessageEncoder extends MessageToByteEncoder<DataPacket> { 
    private static final Logger log = LoggerFactory.getLogger(MessageDecoder.class); @Override protected void encode(ChannelHandlerContext ctx, DataPacket msg, ByteBuf out) throws Exception { 
    log.debug(msg.toString()); //第一步:转换 ByteBuf bb = msg.toByteBufMsg(); bb.markWriterIndex();//标记一下,先到前面去写覆盖的,然后回到标记写校验码 short bodyLen = (short) (bb.readableBytes() - 12);//包体长度=总长度-头部长度 short bodyProps = createDefaultMsgBodyProperty(bodyLen); //覆盖占用的4字节 bb.writerIndex(0); bb.writeShort(msg.getHeader().getMsgId()); bb.writeShort(bodyProps); bb.resetWriterIndex(); bb.writeByte(BCD.XorSumBytes(bb)); //log.info(">>>>> ip:{},hex:{}\n", ctx.channel().remoteAddress(), ByteBufUtil.hexDump(bb).toLowerCase()); //第二步:转义 ByteBuf escape = escape(bb); out.writeBytes(escape); ReferenceCountUtil.safeRelease(escape); } / * 转义待发送数据 * * @param raw * @return */ public ByteBuf escape(ByteBuf raw) { 
    int len = raw.readableBytes(); ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer(len + 12); buf.writeByte(Const.PKG_DELIMITER); while (len > 0) { 
    byte b = raw.readByte(); if (b == 0x7e) { 
    buf.writeByte(0x7d); buf.writeByte(0x02); } else if (b == 0x7d) { 
    buf.writeByte(0x7d); buf.writeByte(0x01); } else { 
    buf.writeByte(b); } len--; } ReferenceCountUtil.safeRelease(raw); buf.writeByte(Const.PKG_DELIMITER); return buf; } / * 生成header中的消息体属性 * * @param bodyLen * @return */ public static short createDefaultMsgBodyProperty(short bodyLen) { 
    return createMsgBodyProperty(bodyLen, (byte) 0, false, (byte) 0); } public static short createMsgBodyProperty(short bodyLen, byte encType, boolean isSubPackage, byte reversed) { 
    int subPkg = isSubPackage ? 1 : 0; int ret = (bodyLen & 0x3FF) | ((encType << 10) & 0x1C00) | ((subPkg << 13) & 0x2000) | ((reversed << 14) & 0xC000); return (short) (ret & 0xffff); } } 

第一步:转换

ByteBuf bb = msg.toByteBufMsg(); 

第二步:转义

 public ByteBuf escape(ByteBuf raw) { 
    int len = raw.readableBytes(); ByteBuf buf = ByteBufAllocator.DEFAULT.directBuffer(len + 12);//假设最多有12个需要转义 buf.writeByte(JT808Const.PKG_DELIMITER); while (len > 0) { 
    byte b = raw.readByte(); if (b == 0x7e) { 
    buf.writeByte(0x7d); buf.writeByte(0x02); } else if (b == 0x7d) { 
    buf.writeByte(0x7d); buf.writeByte(0x01); } else { 
    buf.writeByte(b); } len--; } //转义完成,就直接发送出去了,当然不能忘了释放。 ReferenceCountUtil.safeRelease(raw); buf.writeByte(JT808Const.PKG_DELIMITER); return buf; } ** "最佳实践":我们这里返回ByteBuf是写出去的,所以采用directBuffer效率更高 ** 

转义完成,就直接发送出去了,当然不能忘了释放。

ReferenceCountUtil.safeRelease(raw); buf.writeByte(JT808Const.PKG_DELIMITER); 

1.3构建业务Handler

这里列举一个LocationMsgHandler的详细代码,将位置保存到数据库然后回复设备

@Component @ChannelHandler.Sharable public class LocationMessageHandler extends BaseHandler<LocationMessage> { 
    private static final Logger logger = LoggerFactory.getLogger(LocationMessageHandler.class); @Autowired GisService gisService; @Autowired @Qualifier("workerGroup") private NioEventLoopGroup workerGroup; @Override protected void channelRead0(ChannelHandlerContext ctx, LocationMessage message) { 
    try { 
    //官方建议效验码判断通过后,应立刻给出应答,防止重复请求服务器 CommonResponse response = CommonResponse.success(message, getSerialNumber(ctx.channel()), CommonResponse.SUCCESS); workerGroup.execute(() -> write(ctx, response)); Location location = Location.parseFromLocationMsg(message); Machine machine = new Machine(); // message.saveMachineStatus();//保存状态信息 machine.setLatitude(location.getLatitude().toString()); machine.setLongitude(location.getLongitude().toString()); machine.setAngle(location.getDirection().toString()); machine.setSpeed(location.getSpeed().toString()); machine.setElevation(location.getElevation().toString()); machine.setTime(UtilDate.formatDate(location.getTime()).getTime()); if (message.getMachineRunDetail() !=null && message.getMachineRunDetail().getRun_type() == -1){ 
    return;//振动报警过滤掉 (关门、开门操作) } //业务处理 gisService.saveLocation(machine, message, message.getHeader().getTerminalPhone()); } catch (Exception e) { 
    logger.error("LocationMessageHandler 解析报文信息发生错误", e); } finally { 
    ReferenceCountUtil.release(message.getBody()); } } } 
 private static final AttributeKey<Short> SERIAL_NUMBER = AttributeKey.newInstance("serialNumber"); public short getSerialNumber(Channel channel){ 
    Attribute<Short> flowIdAttr = channel.attr(SERIAL_NUMBER); Short flowId = flowIdAttr.get(); if (flowId == null) { 
    flowId = 0; } else { 
    flowId++; } flowIdAttr.set(flowId); return flowId; } 

BaseHandler完整代码

@Slf4j public abstract class BaseHandler<T> extends SimpleChannelInboundHandler<T> { 
    //消息流水号 private static final AttributeKey<Short> SERIAL_NUMBER = AttributeKey.newInstance("serialNumber"); / * 递增获取流水号 * * @return */ public short getSerialNumber(Channel channel) { 
    Attribute<Short> flowIdAttr = channel.attr(SERIAL_NUMBER); Short flowId = flowIdAttr.get(); if (flowId == null) { 
    flowId = 0; } else { 
    flowId++; } flowIdAttr.set(flowId); return flowId; } public void write(ChannelHandlerContext ctx, DataPacket msg) { 
    ctx.writeAndFlush(msg).addListener(future -> { 
    if (!future.isSuccess()) { 
    log.error("发送失败", future.cause()); } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 
    log.error("exceptionCaught", cause); ctx.close(); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 
    if (evt instanceof IdleStateEvent) { 
    //此实例项目只设置了读取超时时间,可以通过state分别做处理,一般服务端在这里关闭连接节省资源,客户端发送心跳维持连接 IdleState state = ((IdleStateEvent) evt).state(); if (state == IdleState.READER_IDLE) { 
    log.warn("客户端{}读取超时,关闭连接", ctx.channel().remoteAddress()); ctx.close(); } else if (state == IdleState.WRITER_IDLE) { 
    log.warn("客户端{}写入超时", ctx.channel().remoteAddress()); } else if (state == IdleState.ALL_IDLE) { 
    log.warn("客户端{}读取写入超时", ctx.channel().remoteAddress()); } } else { 
    super.userEventTriggered(ctx, evt); } } } 

1.4 Channel的高效管理方式

假设现在出现了一个需求,我们需要找到一个特定的连接发送一条消息,在我们这个项目里,特定指的是根据header中的手机号找到连接并发送消息。我们可以自己维护一个Map用来存放所有Channel,但是这样就浪费了Netty自带的DefaultChannelGroup提供的一系列方法了。所以我们改进一下,定义一个ChannelManager,内部采用DefaultChannelGroup维护Channel,自己维护手机号->ChannelId的映射关系。

@Slf4j @Component public class ChannelManager { 
    private static final AttributeKey<String> TERMINAL_PHONE = AttributeKey.newInstance("terminalPhone"); private ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); private Map<String, ChannelId> channelIdMap = new ConcurrentHashMap<>(); private ChannelFutureListener remover = future -> { 
    String phone = future.channel().attr(TERMINAL_PHONE).get(); if (channelIdMap.get(phone) == future.channel().id()) { 
    channelIdMap.remove(phone); } }; public boolean add(String terminalPhone, Channel channel) { 
    boolean added = channelGroup.add(channel); if (added) { 
    if (channelIdMap.containsKey(terminalPhone)) { 
   //替换 Channel old = get(terminalPhone); old.closeFuture().removeListener(remover); old.close(); } channel.attr(TERMINAL_PHONE).set(terminalPhone); channel.closeFuture().addListener(remover); channelIdMap.put(terminalPhone, channel.id()); } return added; } public boolean remove(String terminalPhone) { 
    return channelGroup.remove(channelIdMap.remove(terminalPhone)); } public Channel get(String terminalPhone) { 
    return channelGroup.find(channelIdMap.get(terminalPhone)); } public ChannelGroup getChannelGroup() { 
    return channelGroup; } } 

我们定义了一个ChannelFutureListener,当channel关闭时,会执行这个回调,帮助我们维护自己的channelIdMap不至于太过臃肿,提升效率,DefaultChannelGroup中也是如此,所以不必担心Channel都不存在了 还占用着内存这种情况。另外我们可以将DefaultChannelGroup提供出去,以便某些时候进行广播。

2.解析协议

目前项目中以使用到的协议

协议编码 功能说明
0002 终端心跳包上报
0100 终端注册消息体数据
0003 终端注销消息体为空
0102 终端鉴权消息体数据
0200 位置信息汇报
0704 位置信息批量汇报(相当于多个0200)
0900 数据上行透传(目前没用)

以下为0200解析代码思路

2.1 0200报文数据的构成与解析思路

该数据为目前太钢项目一队车辆【坦克102】2021-06-17 08:25:36数据信息
一个终端号即SIM卡号命令ID为200时的数据内容如下,十六进制表示:

7E020000C64096A000000000000000A6DE0515FA0EAE50700F80CFA0009E2EA2CE000400FA0035003E00F00C0000001601FEAB00E001D0E0400A89B000B5ED2FEC2B60C00D00CA00200F00510A0100E67E

以上数据的消息头部分 含义对应如下:

部分数据 释义
7E 标志位:0x7e表示
0200 消息ID:0x0200
0051 消息体属性,00//0//0 00//00 0101 0001,保留//分包//数据加密方式//消息体长度,这里不加密,无消息包封装项
0 终端手机号(虚构,篡改了实际的)
0063 消息流水号

以上数据的位置基本信息部分 含义对应如下:

部分数据 释义
00000000 详细见,报警标志位定义附表
00000002 换成二进制(8421展开),状态位,ACC开,定位,使用北斗卫星进行定位,使用GLONASS 卫星进行定位
0159E331 纬度,以度为单位的纬度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际纬度数
06CC09C7 经度,以度为单位的经度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际经度数
00C8 海拔高度,单位为米(m)
0000 1/10km/h
0000 0-359,正北为0,顺时针
8 时间,YY-MM-DD-hh-mm-ss(GMT+8 时间,本标准中之后涉及的时间均采用此时区)

以上数据的位置附加信息项列表部分 含义对应如下:

部分数据 释义
FA 报警标识(只有触发报警才会有,下面有对报警编码作出解释)
03 长度
000200 报警状态,(该数据为熄火上报),报警数据包结束
EA 车辆基础数据报文数据
71 长度
000307E50700F80CFA 里程;0003,附加信息长度;07,附加信息,1/10km,对应车上里程表读数
00024 油量;;0004,附加信息长度:05,附加信息,1/10L,对应车上油量表读数
000E2E1 总运行时长;0005,附加信息长度:04,安装OBD后统计总运行时长
000A2C5 总熄火时长;0006,附加信息长度:04,安装OBD后统计总熄火时长
000 总怠速时长;0007,附加信息长度:04,安装OBD后统计总怠速时长)
00100E000400FA0035003E00F 加速度表(目前没用到该数据),详情可参考文档
00C 车辆电压 0-36V
00 终端内置电池电压 0-5V
00 CSQ值 网络信号强度
00 车型ID(OBD车型)
001601F2 OBD协议类型 协议类型表
0017021EA2 驾驶循环标签
0018011B GPS收星数 GPS定位收星数
00E GPS位置精度 0.01 GPS位置精度
001E0400A89B93 累计里程 米 当0003总里程数据中里程类型为仪表里程时,往往只能精确到1KM或者10KM,这样不利于统计里程,为了便于平台统计里程,增加一项累计里程
00 点火类型
00B5ED2F OBD 转速 rpm 精度:1偏移:0范围:0 ~ 8000
EC 货车扩展数据流
2B 数据长度)
60C0020000 设备拔出状态(定制)
60D00100 OBD 车速 Km/h 精度:1偏移:0范围:0 ~ 240
C OBD 冷却液温度 ℃ 精度:1℃偏移:-40.0℃范围:-40.0℃ ~ +210℃
OBD 加速踏板位置(油门踏板) % 精度:1偏移:0范围:0% ~ 100%
60A00200F0 OBD 燃油压力 kPa 精度:1偏移:0范围:0 ~ 500kpa
MIL状态 有效范围 0~1,“0”代表未点亮,“1”代表点亮。“0xFE”表示无效。
OBD 油料使用率发动机燃油流量 L/h 精度:0.05L/h偏移:0取值范围:0 ~ 3212.75L/h
发动机净输出扭矩 % 精度:1偏移:-125取值范围:-125% ~+125%
摩擦扭矩 % 精度:1偏移:-125取值范围:-125% ~+125%
510A0100 发动机扭矩模式 0:超速失效1:转速控制2:扭矩控制3:转速/扭矩控制9:正常
E6 效验码
7E 标志位结束

FA、EA、EC数据不是每条0200(0704)中都会包含的,代码中要做相应的判断

2.2 代码中解析0200报文信息

第一步:通过解码器找到对应的实体类并且解析报文信息

public DataPacket parse(ByteBuf bb) { 
    DataPacket packet = null; short msgId = bb.getShort(bb.readerIndex()); switch (msgId) { 
    case Const.TERNIMAL_MSG_HEARTBEAT: packet = new HeartBeatMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION://0200 packet = new LocationMessage(bb); break; case Const.TERNIMAL_MSG_LOCATION_BATCH://0704 packet = new LocationBatchMessage(bb); break; case Const.TERNIMAL_MSG_REGISTER: packet = new RegisterMessage(bb); break; case Const.TERNIMAL_MSG_AUTH: packet = new AuthMessage(bb); break; // case Const.TERNIMAL_MSG_STATUS: 0900数据 // packet = new StatusMessage(bb); // break; default: packet = new DataPacket(bb); break; } packet.parse();//执行报文解析 return packet; } 

通过解码器中的parse方法获取到0200对应的LocationMessage实体类,将业务在实体类里面进行处理

packet.parse();//该方法执行报文解析

以下贴整个代码处理方式、后面做会做每段代码释义

@Data @NoArgsConstructor public class LocationMessage extends DataPacket { 
    private static final Logger logger = LoggerFactory.getLogger(LocationMessage.class); public LocationMessage(ByteBuf byteBuf) { 
    super(byteBuf); } private int alarm; //告警信息 4字节 private int statusField;//状态,目前判断ACC开关 1开,0关 private float latitude;//纬度 4字节 private float longitude;//经度 4字节 private short elevation;//海拔高度 2字节 private short speed; //速度 2字节 private short direction; //方向 2字节 private String time; //时间 6字节BCD private MemMachineStatusBean machineStatus;// 设备状态数据 private MemMachineSpeedBean memMachineSpeedBean;//设备怠速超速记录表 private MachineRunDetail machineRunDetail;//设备运转记录详情表 @Override public void parseBody() { 
    ByteBuf bb = this.body; this.setAlarm(bb.readInt());//报警标志 // this.setStatusField(bb.readInt());//换成二进制(8421展开),状态位,ACC开,定位,使用北斗卫星进行定位,使用GLONASS 卫星进行定位 String s = Integer.toBinaryString(bb.readInt()); this.setStatusField(Integer.parseInt(s.substring(s.length()-1))); this.setLatitude(bb.readUnsignedInt() * 1.0F / );//纬度,以度为单位的纬度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际纬度数 this.setLongitude(bb.readUnsignedInt() * 1.0F / );//经度,以度为单位的经度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际经度数 this.setElevation(bb.readShort());//高程,海拔高度,单位为米(m) this.setSpeed((short) (bb.readShort() / 10));//速度, 1/10km/hresult = "0000000000000000000000000000000000000000000000000000000000000000000000000000020280" this.setDirection(bb.readShort());//方向,0-359,正北为0,顺时针 this.setTime(BCD.toBcdTimeString(readBytes(6)));//时间,YY-MM-DD-hh-mm-ss(GMT+8 时间,本标准中之后涉及的时间均采用此时区) saveMachineStatus(); } public void saveMachineStatus() { 
    ByteBuf bb = this.body; if (bb.readableBytes() == 0) return; machineStatus = new MemMachineStatusBean(); long alarm_ID = BCD.toLong(readBytes(1));//FA 报警命令ID及描述项 1字节 //报警命令 if (alarm_ID == Const.ALARM_COMMAND_INFORMED) { 
    //报警信息处理 AlarmInformation(); } if (this.machineRunDetail == null) setRundetail(1); logger.info("machineRunDetail status:{}", machineRunDetail.getRun_type()); if (machineStatus.getStatus() != null) readBytes(1);//数据包涵子ID EA68 2字节 if (bb.readableBytes() == 0) return; //读取EA数据 ReadEAData(bb); if (bb.readableBytes() == 0) return; alarm_ID = BCD.toLong(readBytes(1));//EB EC等报警附加数据 1字节 if (alarm_ID == Const.ALARM_COMMAND_EXTEND) { 
    //读取EB数据 轿车附加数据 ReadEBData(bb); } else if (alarm_ID == Const.ALARM_COMMAND_TRUCK) { 
    //读取EC数据 货车附加数据 ReadECData(bb); } machineStatus.setUpdate_time(UtilDate.formatDate(time, "yyyy-MM-dd HH:mm:ss"));//更新时间 if (machineStatus.getStatus() == null) machineStatus.setStatus(1);//当前状态 TODO } //报警信息处理 private void AlarmInformation() { 
    int acc = BCD.toInteger(readBytes(1));//长度 1字节 long functionID = BCD.toLong(readBytes(2));//功能ID readBytes(1);//长度 1字节 switch ((int) functionID) { 
    //振动报警 case Const.ALARM_COMMAND_ABNORMAL_VIBRATION: setRundetail(-1); machineStatus.setStatus(MyEnum.machine_run_status.run_status0.getValue());//点火后改为静止 break; case Const.ALARM_COMMAND_IGNITION: //点火上报 setRundetail(MyEnum.machine_run_status.run_status0.getValue()); machineStatus.setStatus(MyEnum.machine_run_status.run_status0.getValue());//点火后改为静止 break; case Const.ALARM_COMMAND_FLAMEOUT: //熄火上报 setRundetail(MyEnum.machine_run_status.run_status3.getValue()); machineStatus.setStatus(-1);//熄火后改为离线 break; case Const.ALARM_COMMAND_START: //系统启动 machineStatus.setStatus(MyEnum.machine_run_status.run_status0.getValue()); //eadEAData();//系统启动后没有EB数据,读取完EA数据直接返回 break; case Const.ALARM_COMMAND_IDLING: //怠速报警 long status = BCD.toLong(readBytes(1)); setSpeedldling(status);//设置怠速报警 break; case Const.ALARM_COMMAND_SPEEDING: //超速报警 long status1 = BCD.toLong(readBytes(1)); setOverSpeed(status1);//设置超速报警 break; default: //其他报警信息 machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); break; } } / * @Description: TODO 读取EA数据 */ public void ReadEAData(ByteBuf bb) { 
    int acc = (int) BCD.toLong(readBytes(1));//报文长度 int i3 = bb.readerIndex() + acc; while (i3 > bb.readerIndex()) { 
    long l = BCD.toLong(readBytes(2));//功能id switch ((short) l) { 
    case Const.ALARM_COMMAND_0X0003: machineStatus.setTotal_mileage(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//总里程数(单位:米) 4字节 break; case Const.ALARM_COMMAND_0X0004: machineStatus.setTotal_oil(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//总耗油量(累计油耗)(单位:毫升) 4字节 break; case Const.ALARM_COMMAND_0X0005: machineStatus.setTotle_run_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆运行累计总时长(单位:秒) 4字节 break; case Const.ALARM_COMMAND_0X0006: machineStatus.setTotal_power_off_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆熄火累计总时长 4字节 break; case Const.ALARM_COMMAND_0X0007: machineStatus.setTotal_idling_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆怠速累计总时长 4字节 break; case Const.ALARM_COMMAND_0X00012: double voltage = (double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1))));//车辆电压(单位:0.1V) 2字节 machineStatus.setVoltage(DoubleCalculate.div(voltage, 10, 2)); //车辆电压 break; default: readBytes(BCD.toInteger(readBytes(1))); break; } } } / * @Description: TODO 读取EB数据 */ private void ReadEBData(ByteBuf bb) { 
    int acc = (int) BCD.toLong(readBytes(1));//报文长度 int i3 = bb.readerIndex() + acc; while (i3 > bb.readerIndex()) { 
    long l = BCD.toLong(readBytes(2));//功能id switch ((short) l) { 
    case Const.ALARM_COMMAND_0x60C0: machineStatus.setRotate_speed(BCD.toInteger(readBytes(BCD.toInteger(readBytes(1)))));//转速 2字节 break; case Const.ALARM_COMMAND_0x60D0: machineStatus.setSpeed((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车速 1字节 break; case Const.ALARM_COMMAND_0x62F0: long last = BCD.toLong(readBytes(BCD.toInteger(readBytes(1))));//剩余油量计算单位 1字节(0:百分比;128:升) if (last == 0) { 
   //百分比 //TODO 依据百分比计算剩余油量(油箱容积)根据设备code获取油箱容积 } else { 
   //升 machineStatus.setLast_oil((double) last);//剩余油量 1字节 } if (machineStatus.getLast_oil() == null) machineStatus.setLast_oil(0d); break; case Const.ALARM_COMMAND_0x6050: machineStatus.setCoolant_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//冷却液温度 1字节 break; case Const.ALARM_COMMAND_0x60F0: machineStatus.setAir_inlet_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//进气口温度 1字节 break; case Const.ALARM_COMMAND_0x60B0: machineStatus.setAir_inlet_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//进气压力 1字节 break; case Const.ALARM_COMMAND_0x6330: machineStatus.setAtmosphere_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//大气压力 1字节 break; case Const.ALARM_COMMAND_0x6460: machineStatus.setEnvironment_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//环境温度 1字节 break; case Const.ALARM_COMMAND_0x6490: machineStatus.setFootboard_position((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//加速踏板位置 1字节 break; case Const.ALARM_COMMAND_0x60A0: machineStatus.setFuel_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//燃油压力 2字节 break; case Const.ALARM_COMMAND_0x6014: machineStatus.setFault_status(BCD.toInteger(readBytes(BCD.toInteger(readBytes(1)))));//故障码状态 1字节 break; case Const.ALARM_COMMAND_0X6010: machineStatus.setFault(BCD.toInteger(readBytes(BCD.toInteger(readBytes(1)))));//故障码个数 1字节 break; case Const.ALARM_COMMAND_0x6100: machineStatus.setAir_flow((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//空气流量1字节 break; case Const.ALARM_COMMAND_0x6110: machineStatus.setThrottle_position((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//节气门位置 2字节 break; case Const.ALARM_COMMAND_0x61F0: machineStatus.setRun_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//自发动机起动的时间(单位:秒) 2字节 break; case Const.ALARM_COMMAND_0x6210: machineStatus.setFault_mileage((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//故障行驶里程 4字节 break; case Const.ALARM_COMMAND_0x6040: machineStatus.setEngine_load((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//发动机负荷计算值 1字节 break; case Const.ALARM_COMMAND_0x6070: machineStatus.setFuel_correction((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//长期燃油修正 2字节 break; case Const.ALARM_COMMAND_0x60E0: machineStatus.setSpark_advance_angle((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//点火提前角 2字节 break; default: readBytes(BCD.toInteger(readBytes(1))); break; } } } / * @Description: TODO 读取EC数据 货车附加数据 */ private void ReadECData(ByteBuf bb) { 
    int acc = (int) BCD.toLong(readBytes(1));//报文长度 int i3 = bb.readerIndex() + acc; while (i3 > bb.readerIndex()) { 
    long l = BCD.toLong(readBytes(2));//功能id switch ((short) l) { 
    case Const.ALARM_COMMAND_0x60C0: Long speed = BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))); machineStatus.setRotate_speed(speed.intValue());//转速 2字节 break; case Const.ALARM_COMMAND_0x60D0: machineStatus.setSpeed((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车速 1字节 break; case Const.ALARM_COMMAND_0x62f0: long last = BCD.toLong(readBytes(BCD.toInteger(readBytes(1))));//剩余油量计算单位 1字节(0:百分比;128:升) if (last == 0) { 
   //百分比 //TODO 依据百分比计算剩余油量(油箱容积)根据设备code获取油箱容积 } else { 
   //升 machineStatus.setLast_oil((double) last);//剩余油量 1字节 } if (machineStatus.getLast_oil() == null) machineStatus.setLast_oil(0d); break; case Const.ALARM_COMMAND_0x6050: machineStatus.setCoolant_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))) - 40);//冷却液温度 1字节 精度:1℃偏移:-40.0℃范围:-40.0℃ ~ +210℃ break; case Const.ALARM_COMMAND_0x60F0: machineStatus.setAir_inlet_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))) - 40);//进气口温度 1字节 break; case Const.ALARM_COMMAND_0x60B0: machineStatus.setAir_inlet_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//进气压力 1字节 break; case Const.ALARM_COMMAND_0x6330: machineStatus.setAtmosphere_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//大气压力 1字节 break; case Const.ALARM_COMMAND_0x6460: machineStatus.setEnvironment_temp((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))) - 40);//环境温度 1字节 break; case Const.ALARM_COMMAND_0x6490: machineStatus.setFootboard_position((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//加速踏板位置 1字节 break; case Const.ALARM_COMMAND_0x60A0: machineStatus.setFuel_pressure((double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//燃油压力 2字节 break; case Const.ALARM_COMMAND_0x5001: System.out.println("离合器开关:" + BCD.toLong(readBytes(BCD.toInteger(readBytes(1))))); break; case Const.ALARM_COMMAND_0x5002: System.out.println("OBD 制动刹车开关:" + BCD.toLong(readBytes(BCD.toInteger(readBytes(1))))); break; case Const.ALARM_COMMAND_0x5003: System.out.println("OBD 驻车刹车开关:" + BCD.toLong(readBytes(BCD.toInteger(readBytes(1))))); break; case Const.ALARM_COMMAND_0x5006: System.out.println("OBD 燃油温度:" + BCD.toLong(readBytes(BCD.toInteger(readBytes(1))))); break; case Const.ALARM_COMMAND_0x5007: System.out.println("OBD 机油温度:" + BCD.toLong(readBytes(BCD.toInteger(readBytes(1))))); break; default: readBytes(BCD.toInteger(readBytes(1))); break; } } } private void setRundetail(Integer run_type) { 
    machineRunDetail = new MachineRunDetail(); //行为类型(0:点火;1:运行;2:怠速;3:熄火;) machineRunDetail.setRun_type(run_type); } / * @Description: TODO 设置超速报警 */ private void setOverSpeed(long status1) { 
    if (status1 == 1) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); logger.info("Device overspeed warning :{}", this.header.getTerminalPhone()); //超速逻辑处理 } else if (status1 == 0) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); //超速解除报警处理 memMachineSpeedBean = new MemMachineSpeedBean(); memMachineSpeedBean.setType(1); memMachineSpeedBean.setDuration(BCD.toInteger(readBytes(2)));//报警持续时长 单位:秒 readBytes(2);//超速最大速度 0.1KM/H readBytes(2);//平均速度 0.1KM/H readBytes(2);//超速行驶距离 米 logger.info("Device overspeed warning cleared:{}", this.header.getTerminalPhone()); } } / * @Description: TODO 设置怠速报警 */ private void setSpeedldling(long status) { 
    if (status == 1) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status2.getValue());//超速状态 setRundetail(2); logger.info("Device idling warning :{}", this.header.getTerminalPhone()); } else if (status == 0) { 
    //报警解除 machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); memMachineSpeedBean = new MemMachineSpeedBean(); memMachineSpeedBean.setType(0); memMachineSpeedBean.setDuration(BCD.toInteger(readBytes(2))); readBytes(2);//怠速耗油量 单位:ML readBytes(2);//怠速转速最大值 单位:RPM readBytes(2);//怠速转速最小值 单位:RPM setRundetail(1);//报警解除后改为运行状态 logger.info("Device idling warning cleared:{}", this.header.getTerminalPhone()); } } } 

从上往下看、依次对每个方法作出说明

parseBody 处理基本数据信息,该数据每条0200报文中数据都会包含

@Override public void parseBody() { 
    ByteBuf bb = this.body; this.setAlarm(bb.readInt());//报警标志 // this.setStatusField(bb.readInt());//换成二进制(8421展开),状态位,ACC开,定位,使用北斗卫星进行定位,使用GLONASS 卫星进行定位 String s = Integer.toBinaryString(bb.readInt()); this.setStatusField(Integer.parseInt(s.substring(s.length()-1))); this.setLatitude(bb.readUnsignedInt() * 1.0F / );//纬度,以度为单位的纬度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际纬度数 this.setLongitude(bb.readUnsignedInt() * 1.0F / );//经度,以度为单位的经度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际经度数 this.setElevation(bb.readShort());//高程,海拔高度,单位为米(m) this.setSpeed((short) (bb.readShort() / 10));//速度, 1/10km/hresult = "0000000000000000000000000000000000000000000000000000000000000000000000000000020280" this.setDirection(bb.readShort());//方向,0-359,正北为0,顺时针 this.setTime(BCD.toBcdTimeString(readBytes(6)));//时间,YY-MM-DD-hh-mm-ss(GMT+8 时间,本标准中之后涉及的时间均采用此时区) saveMachineStatus(); } 
public void saveMachineStatus() { 
    ByteBuf bb = this.body; if (bb.readableBytes() == 0) return; machineStatus = new MemMachineStatusBean(); long alarm_ID = BCD.toLong(readBytes(1));//FA 报警命令ID及描述项 1字节 //报警命令 if (alarm_ID == Const.ALARM_COMMAND_INFORMED) { 
    //报警信息处理 AlarmInformation(); } if (this.machineRunDetail == null) setRundetail(1); logger.info("machineRunDetail status:{}", machineRunDetail.getRun_type()); if (machineStatus.getStatus() != null) readBytes(1);//数据包涵子ID EA68 2字节 if (bb.readableBytes() == 0) return; //读取EA数据 ReadEAData(bb); if (bb.readableBytes() == 0) return; alarm_ID = BCD.toLong(readBytes(1));//EB EC等报警附加数据 1字节 if (alarm_ID == Const.ALARM_COMMAND_EXTEND) { 
    //读取EB数据 轿车附加数据 ReadEBData(bb); } else if (alarm_ID == Const.ALARM_COMMAND_TRUCK) { 
    //读取EC数据 货车附加数据 ReadECData(bb); } machineStatus.setUpdate_time(UtilDate.formatDate(time, "yyyy-MM-dd HH:mm:ss"));//更新时间 if (machineStatus.getStatus() == null) machineStatus.setStatus(1);//当前状态 TODO } 
/ * @Description: TODO 读取EA数据 */ public void ReadEAData(ByteBuf bb) { 
    int acc = (int) BCD.toLong(readBytes(1));//报文长度 int i3 = bb.readerIndex() + acc; while (i3 > bb.readerIndex()) { 
    long l = BCD.toLong(readBytes(2));//功能id switch ((short) l) { 
    case Const.ALARM_COMMAND_0X0003: machineStatus.setTotal_mileage(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//总里程数(单位:米) 4字节 break; case Const.ALARM_COMMAND_0X0004: machineStatus.setTotal_oil(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//总耗油量(累计油耗)(单位:毫升) 4字节 break; case Const.ALARM_COMMAND_0X0005: machineStatus.setTotle_run_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆运行累计总时长(单位:秒) 4字节 break; case Const.ALARM_COMMAND_0X0006: machineStatus.setTotal_power_off_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆熄火累计总时长 4字节 break; case Const.ALARM_COMMAND_0X0007: machineStatus.setTotal_idling_time(BCD.toLong(readBytes(BCD.toInteger(readBytes(1)))));//车辆怠速累计总时长 4字节 break; case Const.ALARM_COMMAND_0X00012: double voltage = (double) BCD.toLong(readBytes(BCD.toInteger(readBytes(1))));//车辆电压(单位:0.1V) 2字节 machineStatus.setVoltage(DoubleCalculate.div(voltage, 10, 2)); //车辆电压 break; default: readBytes(BCD.toInteger(readBytes(1))); break; } } } 

以下代码为报警数据的处理,车辆运行状态(0:点火;1:运行;2:怠速;3:熄火;)均在报警中处理。
超速、怠速会在相应的表中添加数据

private void setRundetail(Integer run_type) { 
    machineRunDetail = new MachineRunDetail(); //行为类型(0:点火;1:运行;2:怠速;3:熄火;) machineRunDetail.setRun_type(run_type); } / * @Description: TODO 设置超速报警 */ private void setOverSpeed(long status1) { 
    if (status1 == 1) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); logger.info("Device overspeed warning :{}", this.header.getTerminalPhone()); //超速逻辑处理 } else if (status1 == 0) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); //超速解除报警处理 memMachineSpeedBean = new MemMachineSpeedBean(); memMachineSpeedBean.setType(1); memMachineSpeedBean.setDuration(BCD.toInteger(readBytes(2)));//报警持续时长 单位:秒 readBytes(2);//超速最大速度 0.1KM/H readBytes(2);//平均速度 0.1KM/H readBytes(2);//超速行驶距离 米 logger.info("Device overspeed warning cleared:{}", this.header.getTerminalPhone()); } } / * @Description: TODO 设置怠速报警 */ private void setSpeedldling(long status) { 
    if (status == 1) { 
    machineStatus.setStatus(MyEnum.machine_run_status.run_status2.getValue());//超速状态 setRundetail(2); logger.info("Device idling warning :{}", this.header.getTerminalPhone()); } else if (status == 0) { 
    //报警解除 machineStatus.setStatus(MyEnum.machine_run_status.run_status1.getValue()); memMachineSpeedBean = new MemMachineSpeedBean(); memMachineSpeedBean.setType(0); memMachineSpeedBean.setDuration(BCD.toInteger(readBytes(2))); readBytes(2);//怠速耗油量 单位:ML readBytes(2);//怠速转速最大值 单位:RPM readBytes(2);//怠速转速最小值 单位:RPM setRundetail(1);//报警解除后改为运行状态 logger.info("Device idling warning cleared:{}", this.header.getTerminalPhone()); } } 

解析内容为:

0 0200 2418 – 2021-06-17 08:25:36 1 38. 111. 1 1540 3 170 EA OBD仪表里程(KM) 16256.375 J1939油耗算法1(L) 16798.5 总运行时长 总熄火时长 总怠速时长 车辆电压 28.4 电池电压 0 CSQ值 23 车型ID 0 OBD协议类型 f2 驾驶循环标签 7843 卫星数 27 GPS位置精度 1.1 定位标志 1 累计里程 11049.875 ERROR ERROR EC 转速 1608 车速 4 冷却液温度 68 加速踏板位置 35 燃油压力 460 MIL状态 0 油料使用率 16.35 净输出扭矩 25 摩擦扭矩 8 扭矩模式 1

2.3 解析完报文信息后会到相应的Handler处理

@Component @ChannelHandler.Sharable public class LocationMessageHandler extends BaseHandler<LocationMessage> { 
    private static final Logger logger = LoggerFactory.getLogger(LocationMessageHandler.class); @Autowired GisService gisService; @Autowired @Qualifier("workerGroup") private NioEventLoopGroup workerGroup; @Override protected void channelRead0(ChannelHandlerContext ctx, LocationMessage message) { 
    try { 
    //官方建议效验码判断通过后,应立刻给出应答,防止重复请求服务器 CommonResponse response = CommonResponse.success(message, getSerialNumber(ctx.channel()), CommonResponse.SUCCESS); workerGroup.execute(() -> write(ctx, response)); Location location = Location.parseFromLocationMsg(message); Machine machine = new Machine(); // message.saveMachineStatus();//保存状态信息 machine.setLatitude(location.getLatitude().toString()); machine.setLongitude(location.getLongitude().toString()); machine.setAngle(location.getDirection().toString()); machine.setSpeed(location.getSpeed().toString()); machine.setElevation(location.getElevation().toString()); machine.setTime(UtilDate.formatDate(location.getTime()).getTime()); if (message.getMachineRunDetail() !=null && message.getMachineRunDetail().getRun_type() == -1){ 
    return;//振动报警过滤掉 (关门、开门操作) } gisService.saveLocation(machine, message, message.getHeader().getTerminalPhone()); } catch (Exception e) { 
    logger.error("LocationMessageHandler 解析报文信息发生错误", e); } finally { 
    ReferenceCountUtil.release(message.getBody()); } } } 
 @Override @Transactional public void saveLocation(Machine machine, LocationMessage locationMessage, String code) { 
    //Integer machine_id = machineGisDao.getMachineIdByCode(code); Integer machine_id = this.getMachineIdByCode(code);//通过设备code关联的设备id //getStatusField ==1 为点火状态 if (machine_id != null && machine_id != -1) { 
    machine.setMachine_id(machine_id.toString()); gisDao.saveGis(machine);//保存位置信息 mogodb String keyword = CacheEnum.mem_machine_run_gis.getPrefix() + machine_id; Integer id = (Integer) redisService.get(keyword);//用于判断machine_gis表中有没有数据,有的话修改最新位置信息,没有则添加 查询mem_machine_gis表中数据信息 if (id == null) { 
    Integer gisId = machineGisDao.getCountByMachineID(machine_id.toString()); if (gisId == null) { 
    MemMachineGisBean memMachineGisBean = generatorMachineGisBean(machine); memMachineGisBeanMapper.insert(memMachineGisBean); gisId = memMachineGisBean.getId(); } redisService.set(keyword, gisId, Consts.RUN_TIME); } else { 
    machineGisDao.updateMachineGisByMachine(machine_id.toString(), machine.getLongitude(), machine.getLatitude(), machine.getAngle(), machine.getSpeed(), new Date(machine.getTime())); } //修改设备状态信息 UpdateMachineStatus(locationMessage, machine_id); } } private void UpdateMachineStatus(LocationMessage locationMessage, Integer machine_id) { 
    MachineRunDetail machineRunDetail = locationMessage.getMachineRunDetail(); if (locationMessage.getStatusField()==0 && machineRunDetail != null && machineRunDetail.getRun_type() !=3){ 
    return;//如果车辆为熄火状态,并且不是熄火报警状态数据过滤 } MemMachineStatusBean machineStatus = locationMessage.getMachineStatus();//获取解析的状态数据 if (machineStatus != null) { 
    machineStatus.setMachine_id(machine_id); //设备状态信息更新 if (machineStatusBeanMapper.updateByPrimaryKeySelective(machineStatus) == 0) machineStatusBeanMapper.insertSelective(machineStatus); //更新 mem_machine_run 表状态 试试更新 //获取redis中昨天的数据,没有则默认都为0 String keyword = CacheEnum.mem_machine_run.getPrefix() + UtilDate.getYesterday() + "_" + machine_id; MemMachineRunBean memMachineYestRunBean = getMachineRunYest(machine_id, keyword); MemMachineRunBean machineRunInfo = machineGisDao.getMachineRunInfoByMid(machine_id, UtilDate.getCurrentDate());//获取今天运行数据, if (machineRunInfo != null) { 
    //int mins = (int) Math.ceil(UtilDate.getDatePoor(UtilDate.transForDate(machineRunDetail.getBegin_time()), UtilDate.transForDate(machineRunDetail1.getBegin_time()))); //修改设备运转记录表信息 updateMachieRun(machineRunInfo, machineStatus, memMachineYestRunBean); } else { 
    //int mins = (int) Math.ceil(UtilDate.getDatePoor(UtilDate.transForDate(machineRunDetail.getBegin_time()), UtilDate.transForDate(machineRunDetail1.getBegin_time()))); //保存运转记录表 mysql saveMachineRun(machine_id, machineStatus, memMachineYestRunBean); } } //更新 mogodb中的数据状态 if (machineRunDetail != null ) { 
    //设备运转记录信息 mogodb //报文时间 Date date = UtilDate.formatDate(locationMessage.getTime(), "yyyy-MM-dd HH:mm:ss"); machineRunDetail.setMachine_id(machine_id); machineRunDetail.setBegin_time(Objects.requireNonNull(date).getTime()); MachineRunDetail machineRunDetail1 = machineRunDetailDao.getlatestData(machine_id, UtilDate.getCurrentDate());//获取mogo数据中最新一条纪录,已去掉今天查询条件,使用id获取最新一条数据 // machineRunDetail1.getRun_type() 行为类型(0:点火;1:运行;2:怠速;3:熄火;) //如果不是熄火、点火状态 if (machineRunDetail1 != null && machineRunDetail1.getRun_type() != 0 && machineRunDetail1.getRun_type() != 3) { 
    //当前状态与上一条数据状态不一致则进行处理,自动合并运行时间 if (!machineRunDetail1.getRun_type().equals(machineRunDetail.getRun_type())) { 
    machineRunDetail1.setEnd_time(date.getTime()); //修改上一条数据结束时间 mogodb machineRunDetailDao.updateMachineRunDetailById(machineRunDetail1); //插入最新的数据 mogodb machineRunDetailDao.saveMachineRunDetail(machineRunDetail); } //如果状态为熄火 obd可以获取今日油耗 //if (machineRunDetail.getRun_type().equals(MyEnum.machine_run_status.run_status3.getValue())) { 
    /* 保存设备油耗信息*/ //saveMachineOilGather(machineStatus, machine_id); //} } else { 
    machineRunDetailDao.saveMachineRunDetail(machineRunDetail); //mogodb // if (machineRunDetail1.getRun_type() == MyEnum.machine_run_status.run_status1.getValue()) { 
    // machineRunDetail1.setEnd_time(date.getTime()); // //修改上一条数据结束时间 // machineRunDetailDao.updateMachineRunDetailById(machineRunDetail1); // } else { 
    // machineRunDetailDao.saveMachineRunDetail(machineRunDetail); // } } //ACC开关为0并且为熄火状态 if (locationMessage.getStatusField() == 0 &&machineRunDetail.getRun_type()==3) { 
    } } //设备超速怠速记录信息 MemMachineSpeedBean memMachineSpeedBean = locationMessage.getMemMachineSpeedBean(); //保存怠速超速信息 if (!Objects.isNull(memMachineSpeedBean)) { 
    memMachineSpeedBean.setMachine_id(machine_id); memMachineSpeedBean.setRelieve_time(UtilDate.formatDate(locationMessage.getTime(), "yyyy-MM-dd HH:mm:ss")); memMachineSpeedBeanMapper.insertSelective(memMachineSpeedBean); } } 

以上为0200报文处理的整个流程

2.4 0704内容解析

0704是一个包含多个0200报文的数据信息,当服务器接收不到报文数据时,会默认存储到OBD设备中,待服务器正常时,0704会进行批量上报,其中会包含盲点数据等等。

解析过程与0200一直,这块只需要获取0704包含几个数据包,进行for循环即可,每条报文数据不大于1KB。

 @Override public void parseBody() { 
    ByteBuf bb = this.body; if (bb.readableBytes() == 0) return; int acc = (int) BCD.toLong(readBytes(2));//包涵的位置数据项(包)个数N,>0 readBytes(1);//0:正常批量数据 1:盲点补报 locationBatchMessages = new ArrayList<>(); for (int i = 0; i < acc; i++) { 
    LocationBatchMessage locationBatchMessage = new LocationBatchMessage(); saveMachineStatus(locationBatchMessage); locationBatchMessages.add(locationBatchMessage); } } 

2.5 BCD工具类

@Slf4j public class BCD { 
    public static long toLong(byte[] value) { 
    long result = 0; int len = value.length; int temp; for (int i = 0; i < len; i++) { 
    temp = (len - 1 - i) * 8; if (temp == 0) { 
    result += (value[i] & 0x0ff); } else { 
    result += (value[i] & 0x0ff) << temp; } } return result; } public static byte[] longToBytes(long value, int len) { 
    byte[] result = new byte[len]; int temp; for (int i = 0; i < len; i++) { 
    temp = (len - 1 - i) * 8; if (temp == 0) { 
    result[i] += (value & 0x0ff); } else { 
    result[i] += (value >>> temp) & 0x0ff; } } return result; } public static byte[] DecimalToBCD(long num) { 
    int digits = 0; long temp = num; while (temp != 0) { 
    digits++; temp /= 10; } int byteLen = digits % 2 == 0 ? digits / 2 : (digits + 1) / 2; byte bcd[] = new byte[byteLen]; for (int i = 0; i < digits; i++) { 
    byte tmp = (byte) (num % 10); if (i % 2 == 0) { 
    bcd[i / 2] = tmp; } else { 
    bcd[i / 2] |= (byte) (tmp << 4); } num /= 10; } for (int i = 0; i < byteLen / 2; i++) { 
    byte tmp = bcd[i]; bcd[i] = bcd[byteLen - i - 1]; bcd[byteLen - i - 1] = tmp; } return bcd; } public static long toDecimal(byte[] bcd) { 
    return Long.valueOf(BCD.toString(bcd)); } public static int toInteger(byte[] bcd) { 
    return Integer.parseInt(BCD.toString(bcd)); } public static String toString(byte bcd) { 
    StringBuffer sb = new StringBuffer(); byte high = (byte) (bcd & 0xf0); high >>>= (byte) 4; high = (byte) (high & 0x0f); byte low = (byte) (bcd & 0x0f); sb.append(high); sb.append(low); return sb.toString(); } public static String toString(byte[] bcd) { 
    StringBuffer sb = new StringBuffer(); for (int i = 0; i < bcd.length; i++) { 
    sb.append(toString(bcd[i])); } return sb.toString(); } private static final String HEX = "0ABCDEF"; private static byte toByte(char c) { 
    byte b = (byte) HEX.indexOf(c); return b; } public static byte[] toBcdBytes(String hex) { 
    int len = (hex.length() / 2); byte[] result = new byte[len]; char[] achar = hex.toCharArray(); for (int i = 0; i < len; i++) { 
    int pos = i * 2; result[i] = (byte) (toByte(achar[pos]) << 4 | toByte(achar[pos + 1])); } return result; } public static String toBcdDateString(byte[] bs) { 
    if (bs.length != 3 && bs.length != 4) { 
    log.error("无效BCD日期"); return "0000-00-00"; } StringBuffer sb = new StringBuffer(); int i = 0; if (bs.length == 3) { 
    sb.append("20"); } else { 
    sb.append(BCD.toString(bs[i++])); } sb.append(BCD.toString(bs[i++])); sb.append("-").append(BCD.toString(bs[i++])); sb.append("-").append(BCD.toString(bs[i++])); return sb.toString(); } public static String toBcdTimeString(byte[] bs) { 
    if (bs.length != 6 && bs.length != 7) { 
    log.error("无效BCD时间"); return "0000-00-00 00:00:00"; } StringBuffer sb = new StringBuffer(); int i = 0; if (bs.length == 6) { 
    sb.append("20"); } else { 
    sb.append(BCD.toString(bs[i++])); } sb.append(BCD.toString(bs[i++])); sb.append("-").append(BCD.toString(bs[i++])); sb.append("-").append(BCD.toString(bs[i++])); sb.append(" ").append(BCD.toString(bs[i++])); sb.append(":").append(BCD.toString(bs[i++])); sb.append(":").append(BCD.toString(bs[i])); return sb.toString(); } / * 根据byteBuf的readerIndex和writerIndex计算校验码 * 校验码规则:从消息头开始,同后一字节异或,直到校验码前一个字节,占用 1 个字节 * * @param byteBuf * @return */ public static byte XorSumBytes(ByteBuf byteBuf) { 
    byte sum = byteBuf.getByte(byteBuf.readerIndex()); for (int i = byteBuf.readerIndex() + 1; i < byteBuf.writerIndex(); i++) { 
    sum = (byte) (sum ^ byteBuf.getByte(i)); } return sum; } //取num字节的第几位 public static int getBit(int num, int i) { 
    return ((num & (1 << i)) != 0)?1:0;//true 表示第i位为1,否则为0 } } 

2.6 Const常量类(仅供参考)

public class Const { 
    //默认字符集为GBK public static final Charset DEFAULT_CHARSET = Charset.forName("GBK"); //消息分隔符 public static final byte PKG_DELIMITER = 0x7e; // 终端应答 public static final short TERNIMAL_RESP_COMMON_ = 0x0001; //通用应答 // 终端消息分类 public static final short TERNIMAL_MSG_HEARTBEAT = 0x0002; //心跳 public static final short TERNIMAL_MSG_REGISTER = 0x0100; //注册 public static final short TERNIMAL_MSG_LOGOUT = 0x0003;//注销 public static final short TERNIMAL_MSG_AUTH = 0x0102;//鉴权 public static final short TERNIMAL_MSG_LOCATION = 0x0200;//位置 public static final short TERNIMAL_MSG_LOCATION_BATCH = 0x0704;//批量位置上报 public static final short TERNIMAL_MSG_STATUS = 0x0900;//位置数据上行透传 //报警命令ID public static final short ALARM_COMMAND_INFORMED = 0xFA;// 报警命令ID及描述附表 public static final short ALARM_COMMAND_EXTEND = 0xEB;// 轿车扩展数据流 public static final short ALARM_COMMAND_BASICS = 0xEA;// 基础数据流附表 public static final short ALARM_COMMAND_TRUCK = 0xEC;// 火车扩展数据流 //数据上行透传消息体 public static final short STATUS_MSG_FLAMEOUT =0xF1;// 驾驶行程数据(熄火发送) 驾驶行程数据包 public static final short STATUS_MSG_FAULT =0xF2;// 故障码数据(状态改变发送) 故障码数据包 public static final short STATUS_MSG_DORMANCY =0xF3;// 休眠进入(进入休眠模式发送) 休眠进入数据包 public static final short STATUS_MSG_AWAKEN =0xF4;// 休眠唤醒(退出休眠模式发送) 休眠唤醒数据包 //public static final short STATUS_MSG_DATA =0xF5;// 车辆GPS精简数据包(货车版) 暂时未加入 //public static final short STATUS_MSG_FLAMEOUT =0xF6;// MCU升级状态反馈包 MCU升级状态反馈包 public static final short STATUS_MSG_COLLISION =0xF7;// 疑似碰撞报警描述包 疑似碰撞报警描述包 //FA报警信息 public static final short ALARM_COMMAND_SPEEDING = 0x0107;//超速报警 public static final short ALARM_COMMAND_IDLING = 0x0106;//怠速报警 public static final short ALARM_COMMAND_IGNITION = 0x0001;//点火上报 public static final short ALARM_COMMAND_FLAMEOUT = 0x0002;//熄火上报 public static final short ALARM_COMMAND_START = 0x0007;//系统启动 public static final short ALARM_COMMAND_ABNORMAL_VIBRATION = 0x0115;//异常振动报警,类似于点火操作 //服务器应答 public static final short SERVER_RESP_COMMON = (short) 0x8001;//通用应答 public static final short SERVER_RESP_REGISTER = (short) 0x8100;//注册应答 //readerIdleTime public static final int IDLESTATE_HANDLER_READTIMEOUT = 15; //包头最大长度16+包体最大长度1023+分隔符2+转义字符最大姑且算60 = 1100 public static final int MAX_FRAME_LENGTH = 2200; //_基础数据流 需要哪些数据,switch添加即可 public static final short ALARM_COMMAND_0X0003 = 0x0003;//总里程数据 米 public static final short ALARM_COMMAND_0X0004 = 0x0004;//总油耗数据 毫升 public static final short ALARM_COMMAND_0X0005 = 0x0005;//总运行时长 秒 public static final short ALARM_COMMAND_0X0006 = 0x0006;//总熄火时长 秒 public static final short ALARM_COMMAND_0X0007 = 0x0007;//总怠速时长 秒 public static final short ALARM_COMMAND_0X00010 = 0x0010;//加速度表 public static final short ALARM_COMMAND_0X00011 = 0x0011;//车辆状态表 public static final short ALARM_COMMAND_0X00012 = 0x0012;//车辆电压 0.1V public static final short ALARM_COMMAND_0X00013 = 0x0013;//终端内置电池电压 0.1V public static final short ALARM_COMMAND_0X00014 = 0x0014;// CSQ值 网络信号强度 //轿车扩展数据流 public static final short ALARM_COMMAND_0x60C0 = 0x60C0;//转速 精度:1偏移:0范围:0 ~ 8000 public static final short ALARM_COMMAND_0x60D0 = 0x60D0;//车速 精度:1偏移:0范围:0 ~ 240 public static final short ALARM_COMMAND_0x62F0 = 0x62F0;// 剩余油量 % L 剩余油量,单位L或% Bit15 ==0百分比% OBD都为百分比==1单位L public static final short ALARM_COMMAND_0x6050 = 0x6050;//冷却液温度 public static final short ALARM_COMMAND_0x60F0 = 0x60F0;//进气口温度 ℃ public static final short ALARM_COMMAND_0x60B0 = 0x60B0;//进气(岐管绝对)压力 kPa public static final short ALARM_COMMAND_0x6330 = 0x6330;//大气压力 kPa public static final short ALARM_COMMAND_0x6460 = 0x6460;//环境温度 ℃ public static final short ALARM_COMMAND_0x6490 = 0x6490;//加速踏板位置 public static final short ALARM_COMMAND_0x60A0 = 0x60A0;//燃油压力 public static final short ALARM_COMMAND_0x6014 = 0x6014;//故障码状态 public static final short ALARM_COMMAND_0X6010 = 0X6010;//故障码个数 public static final short ALARM_COMMAND_0x6100 = 0x6100;//空气流量 public static final short ALARM_COMMAND_0x6110 = 0x6110;//绝对节气门位置 public static final short ALARM_COMMAND_0x61F0 = 0x61F0;//自发动机起动的时间 public static final short ALARM_COMMAND_0x6210 = 0x6210;//故障行驶里程 public static final short ALARM_COMMAND_0x6040 = 0x6040;//计算负荷值 public static final short ALARM_COMMAND_0x6070 = 0x6070;//长期燃油修正(气缸列1和3) public static final short ALARM_COMMAND_0x60E0 = 0x60E0;//第一缸点火正时提前角 public static final short ALARM_COMMAND_0x6901 = 0x6901;//前刹车片磨损 0 正常/否则 显示对应数据,单位:级 public static final short ALARM_COMMAND_0x6902 = 0x6902;//后刹车片磨损 0 正常/否则 显示对应数据,单位:级 public static final short ALARM_COMMAND_0x6903 = 0x6903;//制动液液位 public static final short ALARM_COMMAND_0x6904 = 0x6904;//机油液位 mL 显示值为上传值/1000 单位 毫米 public static final short ALARM_COMMAND_0x6905 = 0x6905;//胎压报警 0:当前无警告 1:存在胎压失压 public static final short ALARM_COMMAND_0x6906 = 0x6906;//冷却液液位 public static final short ALARM_COMMAND_0x6907 = 0x6907;//续航里程 //货车扩展数据流 6部分与轿车类似 public static final short ALARM_COMMAND_0x5001 = 0x5001;//离合器开关 0x00/0x01 关/开 public static final short ALARM_COMMAND_0x5002 = 0x5002;//制动刹车开关 0x00/0x01 关/开 public static final short ALARM_COMMAND_0x5003 = 0x6110;//驻车刹车开关 0x00/0x01 关/开 public static final short ALARM_COMMAND_0x5004 = 0x61F0;//节流阀位置 精度:1偏移:0范围:0% ~ 100% public static final short ALARM_COMMAND_0x5005 = 0x5005;//油料使用率 精度:0.05L/h偏移:0取值范围:0 ~ 3212.75L/h 单位 L/h public static final short ALARM_COMMAND_0x5006 = 0x5006;//燃油温度 精度:0.03125℃偏移:-273.0℃范围:-273.0℃ ~ +1734.96875℃ 单位 ℃ public static final short ALARM_COMMAND_0x5007 = 0x5007;//机油温度 精度:0.03125℃偏移:-273.0℃范围:-273.0℃ ~ +1734.96875℃ public static final short ALARM_COMMAND_0x5008 = 0x5008;//OBD发动机润滑油压力 精度:4偏移:0范围:0 ~ 1000kpa public static final short ALARM_COMMAND_0x5009 = 0x6901;//OBD制动器踏板位置 精度:1偏移:0范围:0% ~ 100% public static final short ALARM_COMMAND_0x500A = 0x6902;//OBD 空气流量 精度:0.1偏移:0取值范围:0~6553.5 public static final short ALARM_COMMAND_0x62f0 = 0x62f0;//剩余油量,单位L或%Bit15 ==0百分比% OBD都为百分 ==1单位L 显示值为上传值/10 //能读取到以下数据 public static final short ALARM_COMMAND_0x5105 = 0x5105;//反应剂余量 % 精度:0.4偏移:0范围:0% ~ 100% public static final short ALARM_COMMAND_0x5101 = 0x5101;//发动机净输出扭矩 % 精度:1偏移:-125取值范围:-125% ~+125% public static final short ALARM_COMMAND_0x5102 = 0x5102;// 摩擦扭矩 % 精度:1偏移:-125取值范围:-125% ~+125% public static final short ALARM_COMMAND_0x510A = 0x510A;//发动机扭矩模式 0:超速失效1:转速控制2:扭矩控制3:转速/扭矩控制9:正常 public static final short ALARM_COMMAND_0x510C = 0x510C;//尿素箱温度 ℃ 精度:1℃偏移:-40.0℃范围:-40.0℃ ~ +210℃ //新能源 } 

分界线———————— 以下为自己项目中的应用————————-分界线


3. 相关数据库表及OBD安装过程

3.1 相关数据库表及作用描述

OBD数据获取整个过程设计到的表及作用描述

表名称 描述
mem_terminal 智能终端表;OBD自行上报数据后会在该表中添加一条数据,code对应虚拟卡号
mem_machine_gis 设备地理位置信息表;每次OBD上报位置后会更新此表;更新最新位置记录
mem_machine_status 设备实时状态信息表;车辆运行状态、里程、油耗等OBD获取的数据实时更新
mem_machine_run 设备运转记录表;记录设备每天运行的数据信息;(此表观察OBD存在的问题)
mem_machine_speed 设备怠速超速记录表;有怠速、超速报警后数据会存储该表
machine mogodb数据库; 该集合存储设备的位置、高度、速度等信息
machineRunDetail mogodb数据库; 该集合存储设备的运行状态及结束时间

3.2 OBD安装过程以及设备与终端关联步骤描述

OBD插线中有三根数据线,该线针对不同的车型插口不同,以下就目前太钢已安装车型做备注

车型 安装方式 备注
豪沃 开放 只有二队有该车型,该车型较老,只安装了 3台,获取油耗有问题
坦克、临工、徐工 标准 大部分车队都是这几种车型,安装相对简单

1.找到车辆OBD插口插入即可,车辆型号不同,插口所在的位置也不相同,这块需要自己寻找


java解析JT808协议-netty-机械设备

2队豪沃车辆OBD插孔所在位置(照片来自太钢现场车辆)

java解析JT808协议-netty-机械设备

指示灯常亮即安装成功(照片来自太钢现场车辆)




4.目前OBD存在的问题汇总

每日可通过查看 mem_machine_run表中数据观察存在的问题
1.里程问题,油耗正常,大约20台设备左右,目前已和厂家沟通解决中。


java解析JT808协议-netty-机械设备

里程 <= 0 均说明存在问题




2.油耗问题,里程正常,目前2队有三台豪沃存在问题,目前已和厂家沟通解决中。

java解析JT808协议-netty-机械设备

里程已有的情况下,油耗获取有误




3.里程跳变,目前已和厂家沟通解决中。

java解析JT808协议-netty-机械设备

里程跳变




4.运行时间过一段时间会重置

java解析JT808协议-netty-机械设备

运行时间会重置









免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/110625.html

(0)
上一篇 2026-01-30 12:26
下一篇 2026-01-30 12:45

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信