java解析JT808协议-netty
机械设备-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.开发过程
- 认识JT808协议
- 构建编/解码器
- 构建业务Handler
- 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数据信息
3.此时OBD会向服务器发送数据,mem_terminal 表中会获取该设备的信息,code字段对应是设备上的ID值作为唯一标识
4.手动关联设备与终端信息,mem_machine_terminal维护两者的关系,通过两者ID关联,mem_machine表中没有车辆信息则需要手动添加,到此,车辆与OBD关联步骤结束。
4.目前OBD存在的问题汇总
每日可通过查看 mem_machine_run表中数据观察存在的问题
1.里程问题,油耗正常,大约20台设备左右,目前已和厂家沟通解决中。
2.油耗问题,里程正常,目前2队有三台豪沃存在问题,目前已和厂家沟通解决中。
3.里程跳变,目前已和厂家沟通解决中。
4.运行时间过一段时间会重置
java解析JT808协议-netty相关推荐
- java解析JT808协议
JT808协议扫盲 1 数据类型 2 消息结构 3 消息头 解析 1 消息体实体类 2 字节数组到消息体实体类的转换 21 消息转换器 22 用到的工具类 221 BCD操作工具类 222 位操作工具 ...
- Java解析JT808协议示例
原文地址:https://blog.csdn.net/hylexus/article/details/54987786 JT808协议扫盲 1 数据类型 2 消息结构 3 消息头 解析 1 消息体实体 ...
- Netty解析JT808协议
原文地址:http://blog.csdn.net/hylexus/article/details/54987786 1 JT808协议扫盲 1.1 数据类型 数据类型 描述及要求 BYTE 无符号单 ...
- java解析 电力协议_DLT645解析JAVA JAVA解析DLT645电表通信协议 - 下载 - 搜珍网
压缩包 : d49c1cc60d68dfaef4a4b19567ae9131.zip 列表 ProtocolAnalysls-master/ ProtocolAnalysls-master/.giti ...
- java解析bt协议详解_【转】bt协议的详细分析
一 BT系统的组成结构 1 普通的Web服务器: 例如Apache或IIS服务器 2 一个静态的种子文件: 即.Torrent文件,采用Bencoding编码 3 Tracke ...
- java解析bt协议详解_BT(带中心Tracker)通信协议的分析
BT通信协议举例分析 现在的很多BT下载都采用了DHT网络,这样进行BT下载就不需要中心服务器了.本文针对的是需要中心服务器的BT下载. 小弟我最近正在研究BT通信协议,网上的资料很全,但是不是那事详 ...
- java解析bt协议详解_BT下载原理分析
BT全名为BitTorrent,是一个p2p软件,你在下载download的同时,也在为其他用户提供上传upload,因为大家是"互相帮助",所以不会随着用户数的增加而降低下载速度 ...
- JAVA实现报文解析:协议的数据类型和完整的报文结构解析
**JAVA实现GBT32960报文解析系列文章链接:** JAVA实现GBT32960报文解析(一):协议的数据类型和完整的报文结构解析 JAVA实现GBT32960报文解析(二):数据包结构解析源 ...
- app及Java服务端实现JT808协议的定义解析
app及Java服务端实现JT808协议的定义解析,可以用做行车记录仪等的app. 网上很多帖子已经对JT808协议做了说明,我在这里就不做过多的赘述了. 最近接到一个项目是打车类的,所以对这协议做了 ...
- java jt808协议解读_jt808 协议服务端解析 - 下载 - 搜珍网
文件名大小更新时间 jt808\.git\config3022019-11-09 jt808\.git\description732019-11-09 jt808\.git\HEAD232019-11 ...
最新文章
- app、H5、safari、appstore应用主页评分页之间拉起调用、打开手机某些系统功能、app打开文档
- Mac下搭建solr搜索引擎与PHP扩展开发(上)
- TOMCAT服务器概述
- linux入门与常用指令
- java实现程序输出以下5*5数字方格
- 合成迪丽热巴下海_热巴生图颜值回春,被韩媒吹上天,她是找了杨幂的医生?...
- 4安全框_压花玻璃与安全玻璃有哪些特点?玻璃隔断的介绍
- HTTP::Request
- micropython入门指南-电子工业出版社-网上书店
- 自考计算机哪个简单,自考哪个专业好考?
- POSIX 与 CMSIS 标准
- 交互设计的本质是什么?交互设计哪些意义?
- 爱尔兰房产泡沫破灭带给我们…
- Rockchip DRM RK628 Porting Guide
- Java项目——云R记
- 用css3 3d效果做一个立体盒子
- 公众号php关键词回复小程序,微信自定义关键词回复信息
- linux踢人命令 pkill踢人用法
- 计算机应用Office2016,Office 2016
- 物联网项目——具体分工