机械设备-java解析JT808协议相关流程-netty

  • 背景
    • 1.开发过程
      • 1.1认识JT808协议
      • 1.2 构建编/解码器
      • 1.3构建业务Handler
      • 1.4Channel的高效管理方式
    • 2.解析协议
      • 2.1 0200报文数据的构成与解析思路
      • 2.2 代码中解析0200报文信息
      • 2.3 解析完报文信息后会到相应的Handler处理
      • 2.4 0704内容解析
    • 3. 相关数据库表及OBD安装过程
      • 3.1 相关数据库表及作用描述
      • 3.2 OBD安装过程以及设备与终端关联步骤描述
    • 4.目前OBD存在的问题汇总

背景

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

开发环境:IDEA+JDK1.8+Maven
技术框架: Netty + Spring Boot+Mybatis
其他工具: lombok

1.开发过程

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

1.1认识JT808协议

下面简单介绍一下JT808协议的格式说明

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

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

@Data
public class DataPacket {protected Header header = new Header(); //消息头protected ByteBuf byteBuf; //消息流@Datapublic 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);}//获取加密类型 3bitspublic byte getEncryptionType() {return (byte) ((msgBodyProps & 0x1c00) >> 10);}//是否分包public boolean hasSubPackage() {return ((msgBodyProps & 0x2000) >> 13) == 1;}}
}

我们可以先将Header解析出来,然后由子类自己解析包体

 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和msgBodyPropsbb.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获取,跟前面获取的是同一个(不特别配置的话)。
**

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

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

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

建立结构如下:

@Data
public class LocationMsg 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字节BCDpublic LocationMsg(ByteBuf byteBuf) {super(byteBuf);}@Overridepublic void parseBody() {ByteBuf bb = this.byteBuf;this.setAlarm(bb.readInt());this.setStatusField(bb.readInt());this.setLatitude(bb.readUnsignedInt() * 1.0F / 1000000);this.setLongitude(bb.readUnsignedInt() * 1.0F / 1000000);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);}@Overridepublic 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自带的不能满足需求,再自己自定义)

经过DelimiterBasedFrameDecoder帮我们截断之后,信息就到了我们自己的解码器中了,我们的目的是将ByteBuf转化为我们前面定义的数据结构。
定义解码器

@Slf4j
public class MessageDecoder extends ByteToMessageDecoder {@Overrideprotected 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);}}
}

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

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);@Overrideprotected 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);}
}

第一步:转换

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);@AutowiredGisService gisService;@Autowired@Qualifier("workerGroup")private NioEventLoopGroup workerGroup;@Overrideprotected 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());}}
}

BaseHandler继承SimpleChannelInboundHandler ,里面定义了一些通用的方法,例如getSerialNumber()获取应答的流水号
我们将流水号存入Channel内部,方便维护。

    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;}

1.4Channel的高效管理方式

假设现在出现了一个需求,我们需要找到一个特定的连接发送一条消息,在我们这个项目里,特定指的是根据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时的数据内容如下,十六进制表示:

7E020000C1018230440164096A00000000000000020246091906A6DE13060200000082210617082515FA03000200EA71000307E50700F80CFA00040501010053240005040019E2E10006040009A2C50007040009540800100E000400FA0035003E00320030004F001202011C00130100001401150015020000001601F20017021EA20018011B001902006E001D0101001E0400A89B93002002000000210400B5ED2FEC2B60C002000060D001006050016C6490010060A00200F05112010050050200195101018251020182510A0100E67E

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

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

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

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

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

部分数据 释义
FA 报警标识(只有触发报警才会有,下面有对报警编码作出解释)
03 长度
000200 报警状态,(该数据为熄火上报),报警数据包结束
EA 车辆基础数据报文数据
71 长度
000307E50700F80CFA 里程;0003,附加信息长度;07,附加信息,1/10km,对应车上里程表读数
0004050101005324 油量;;0004,附加信息长度:05,附加信息,1/10L,对应车上油量表读数
0005040019E2E1 总运行时长;0005,附加信息长度:04,安装OBD后统计总运行时长
0006040009A2C5 总熄火时长;0006,附加信息长度:04,安装OBD后统计总熄火时长
00070400095408 总怠速时长;0007,附加信息长度:04,安装OBD后统计总怠速时长)
00100E000400FA0035003E00320030004F 加速度表(目前没用到该数据),详情可参考文档
001202011C 车辆电压 0-36V
00130100 终端内置电池电压 0-5V
00140115 CSQ值 网络信号强度
0015020000 车型ID(OBD车型)
001601F2 OBD协议类型 协议类型表
0017021EA2 驾驶循环标签
0018011B GPS收星数 GPS定位收星数
001902006E GPS位置精度 0.01 GPS位置精度
001E0400A89B93 累计里程 米 当0003总里程数据中里程类型为仪表里程时,往往只能精确到1KM或者10KM,这样不利于统计里程,为了便于平台统计里程,增加一项累计里程
0020020000 点火类型
00210400B5ED2F OBD 转速 rpm 精度:1偏移:0范围:0 ~ 8000
EC 货车扩展数据流
2B 数据长度)
60C0020000 设备拔出状态(定制)
60D00100 OBD 车速 Km/h 精度:1偏移:0范围:0 ~ 240
6050016C OBD 冷却液温度 ℃ 精度:1℃偏移:-40.0℃范围:-40.0℃ ~ +210℃
64900100 OBD 加速踏板位置(油门踏板) % 精度:1偏移:0范围:0% ~ 100%
60A00200F0 OBD 燃油压力 kPa 精度:1偏移:0范围:0 ~ 500kpa
51120100 MIL状态 有效范围 0~1,“0”代表未点亮,“1”代表点亮。“0xFE”表示无效。
5005020019 OBD 油料使用率发动机燃油流量 L/h 精度:0.05L/h偏移:0取值范围:0 ~ 3212.75L/h
51010182 发动机净输出扭矩 % 精度:1偏移:-125取值范围:-125% ~+125%
51020182 摩擦扭矩 % 精度: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://0200packet = new LocationMessage(bb);break;case Const.TERNIMAL_MSG_LOCATION_BATCH://0704packet = 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字节BCDprivate MemMachineStatusBean machineStatus;// 设备状态数据private MemMachineSpeedBean memMachineSpeedBean;//设备怠速超速记录表private MachineRunDetail machineRunDetail;//设备运转记录详情表@Overridepublic 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 / 1000000);//纬度,以度为单位的纬度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际纬度数this.setLongitude(bb.readUnsignedInt() * 1.0F / 1000000);//经度,以度为单位的经度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际经度数this.setElevation(bb.readShort());//高程,海拔高度,单位为米(m)this.setSpeed((short) (bb.readShort() / 10));//速度, 1/10km/hresult = "0000000000000003015914321406121209111004140000000020110920302315100401060101141061400030508000039140000405011000006152000504000007030006040000071112000704000002915001001400040015100000000000000000000000120200813001301260014011900150200000016011100170200080018010110019020121100110012200113010100114040000391400020020010141157601200208138601300100621500200006050014116015001286011001646330010064600128649001006010002000060140100601001006100020091611002011326115002028962100400000000604001056070020410360140020280"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));//功能IDreadBytes(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));//功能idswitch ((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));//功能idswitch ((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));//功能idswitch ((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/HreadBytes(2);//平均速度 0.1KM/HreadBytes(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);//怠速耗油量   单位:MLreadBytes(2);//怠速转速最大值 单位:RPMreadBytes(2);//怠速转速最小值 单位:RPMsetRundetail(1);//报警解除后改为运行状态logger.info("Device idling warning cleared:{}", this.header.getTerminalPhone());}}}

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

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

@Overridepublic 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 / 1000000);//纬度,以度为单位的纬度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际纬度数this.setLongitude(bb.readUnsignedInt() * 1.0F / 1000000);//经度,以度为单位的经度值乘以10的六次方,精确到百万分之一度,化为十进制,即实际经度数this.setElevation(bb.readShort());//高程,海拔高度,单位为米(m)this.setSpeed((short) (bb.readShort() / 10));//速度, 1/10km/hresult = "0000000000000003015914321406121209111004140000000020110920302315100401060101141061400030508000039140000405011000006152000504000007030006040000071112000704000002915001001400040015100000000000000000000000120200813001301260014011900150200000016011100170200080018010110019020121100110012200113010100114040000391400020020010141157601200208138601300100621500200006050014116015001286011001646330010064600128649001006010002000060140100601001006100020091611002011326115002028962100400000000604001056070020410360140020280"this.setDirection(bb.readShort());//方向,0-359,正北为0,顺时针this.setTime(BCD.toBcdTimeString(readBytes(6)));//时间,YY-MM-DD-hh-mm-ss(GMT+8 时间,本标准中之后涉及的时间均采用此时区)saveMachineStatus();}

saveMachineStatus 方法中处理的数据每次都可能不一样,所以要做相应的判断
该方法为解析报文核心部分
1.获取ByteBuf ,判断是否有可读字节数据流,没有则直接返回
2.获取命令ID,判断是否为FA(报警命令),如果有该数据会存在于EA前边,没有则直接处理EA(车辆基础数据信息)
3.machineStatus.getStatus该方法判断是否有报警信息,如果有,该行读取的为EA
4.处理完每个单独的业务数据都要做判断
5.读取EA(车辆基础数据)、EB(轿车相关数据)、EC数据(货车相关数据),矿山中的车辆目前获取的都是EA/EC数据

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}

下面以EA数据解析为例作出说明,其他EB/EC都和此方法相同,只需字段作出调整
1.获取报文长度
2.当前指针长度 + 报文长度 获取该段数据总长度,用于判断读取截止字节
3.获取报文中的功能id、switch 中获取对应的数据即可,如需添加EA中其他字段信息,添加case即可。EB/EC数据也是相同的思路

/*** @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));//功能idswitch ((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/HreadBytes(2);//平均速度 0.1KM/HreadBytes(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);//怠速耗油量   单位:MLreadBytes(2);//怠速转速最大值 单位:RPMreadBytes(2);//怠速转速最小值 单位:RPMsetRundetail(1);//报警解除后改为运行状态logger.info("Device idling warning cleared:{}", this.header.getTerminalPhone());}}

解析内容为:

018230440164 0200 2418 - 2021-06-17 08:25:36 1 38.144281 111.599123 1 1540 3 170 EA OBD仪表里程(KM) 16256.375 J1939油耗算法1(L) 16798.5 总运行时长 1696497 总熄火时长 631499 总怠速时长 611343 车辆电压 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处理

官方建议效验码判断通过后,应立刻给出应答,防止重复请求服务器
workerGroup.execute(() -> write(ctx, response));//通用回答

@Component
@ChannelHandler.Sharable
public class LocationMessageHandler extends BaseHandler<LocationMessage> {private static final Logger logger = LoggerFactory.getLogger(LocationMessageHandler.class);@AutowiredGisService gisService;@Autowired@Qualifier("workerGroup")private NioEventLoopGroup workerGroup;@Overrideprotected 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());}}
}

业务处理代码 gisService.saveLocation
以下代码通过备注作出说明

 @Override@Transactionalpublic 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);//保存位置信息 mogodbString 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中昨天的数据,没有则默认都为0String 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())));//保存运转记录表 mysqlsaveMachineRun(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());//修改上一条数据结束时间 mogodbmachineRunDetailDao.updateMachineRunDetailById(machineRunDetail1);//插入最新的数据 mogodbmachineRunDetailDao.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。

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

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插口插入即可,车辆型号不同,插口所在的位置也不相同,这块需要自己寻找

2.插入接口后,指示灯必须黄、绿、蓝一直到常亮,不亮或者常闪均说明有问题。(正常等2-3分钟会常亮)
黄、绿、蓝分别代表电源、信号、OBD数据信息

2队豪沃车辆OBD插孔所在位置

指示灯常亮即安装成功

3.此时OBD会向服务器发送数据,mem_terminal 表中会获取该设备的信息,code字段对应是设备上的ID值作为唯一标识
4.手动关联设备与终端信息,mem_machine_terminal维护两者的关系,通过两者ID关联,mem_machine表中没有车辆信息则需要手动添加,到此,车辆与OBD关联步骤结束。

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

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

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

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

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

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

里程跳变

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

运行时间会重置

java解析JT808协议-netty相关推荐

  1. java解析JT808协议

    JT808协议扫盲 1 数据类型 2 消息结构 3 消息头 解析 1 消息体实体类 2 字节数组到消息体实体类的转换 21 消息转换器 22 用到的工具类 221 BCD操作工具类 222 位操作工具 ...

  2. Java解析JT808协议示例

    原文地址:https://blog.csdn.net/hylexus/article/details/54987786 JT808协议扫盲 1 数据类型 2 消息结构 3 消息头 解析 1 消息体实体 ...

  3. Netty解析JT808协议

    原文地址:http://blog.csdn.net/hylexus/article/details/54987786 1 JT808协议扫盲 1.1 数据类型 数据类型 描述及要求 BYTE 无符号单 ...

  4. java解析 电力协议_DLT645解析JAVA JAVA解析DLT645电表通信协议 - 下载 - 搜珍网

    压缩包 : d49c1cc60d68dfaef4a4b19567ae9131.zip 列表 ProtocolAnalysls-master/ ProtocolAnalysls-master/.giti ...

  5. java解析bt协议详解_【转】bt协议的详细分析

    一 BT系统的组成结构 1 普通的Web服务器:        例如Apache或IIS服务器 2 一个静态的种子文件:     即.Torrent文件,采用Bencoding编码 3  Tracke ...

  6. java解析bt协议详解_BT(带中心Tracker)通信协议的分析

    BT通信协议举例分析 现在的很多BT下载都采用了DHT网络,这样进行BT下载就不需要中心服务器了.本文针对的是需要中心服务器的BT下载. 小弟我最近正在研究BT通信协议,网上的资料很全,但是不是那事详 ...

  7. java解析bt协议详解_BT下载原理分析

    BT全名为BitTorrent,是一个p2p软件,你在下载download的同时,也在为其他用户提供上传upload,因为大家是"互相帮助",所以不会随着用户数的增加而降低下载速度 ...

  8. JAVA实现报文解析:协议的数据类型和完整的报文结构解析

    **JAVA实现GBT32960报文解析系列文章链接:** JAVA实现GBT32960报文解析(一):协议的数据类型和完整的报文结构解析 JAVA实现GBT32960报文解析(二):数据包结构解析源 ...

  9. app及Java服务端实现JT808协议的定义解析

    app及Java服务端实现JT808协议的定义解析,可以用做行车记录仪等的app. 网上很多帖子已经对JT808协议做了说明,我在这里就不做过多的赘述了. 最近接到一个项目是打车类的,所以对这协议做了 ...

  10. java jt808协议解读_jt808 协议服务端解析 - 下载 - 搜珍网

    文件名大小更新时间 jt808\.git\config3022019-11-09 jt808\.git\description732019-11-09 jt808\.git\HEAD232019-11 ...

最新文章

  1. app、H5、safari、appstore应用主页评分页之间拉起调用、打开手机某些系统功能、app打开文档
  2. Mac下搭建solr搜索引擎与PHP扩展开发(上)
  3. TOMCAT服务器概述
  4. linux入门与常用指令
  5. java实现程序输出以下5*5数字方格
  6. 合成迪丽热巴下海_热巴生图颜值回春,被韩媒吹上天,她是找了杨幂的医生?...
  7. 4安全框_压花玻璃与安全玻璃有哪些特点?玻璃隔断的介绍
  8. HTTP::Request
  9. micropython入门指南-电子工业出版社-网上书店
  10. 自考计算机哪个简单,自考哪个专业好考?
  11. POSIX 与 CMSIS 标准
  12. 交互设计的本质是什么?交互设计哪些意义?
  13. 爱尔兰房产泡沫破灭带给我们…
  14. Rockchip DRM RK628 Porting Guide
  15. Java项目——云R记
  16. 用css3 3d效果做一个立体盒子
  17. 公众号php关键词回复小程序,微信自定义关键词回复信息
  18. linux踢人命令 pkill踢人用法
  19. 计算机应用Office2016,Office 2016
  20. 物联网项目——具体分工

热门文章

  1. Apache Jena TDB 常用API
  2. pytorch.chunk
  3. html5 div 3d悬浮效果,html5悬浮球效果
  4. SCI论文写作(一) | SCI论文的文献综述(Literature Review)部分
  5. MySQL数据库与JDBC编程
  6. 股票价格中后取复权的计算
  7. 2022-8-03 第七小组 黄均睿 学习日记 (day27)线程2
  8. Unity 灯光及光照烘焙
  9. CarEye GT1078 转GB28181 方案
  10. ISO26262-道路车辆功能安全