java解析JT808协议相关流程-netty-机械设备
背景
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; //消息流
@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,我们挑出一个字段相对较多的-》 位置上报消息
然后我们建立位置上报消息的数据结构,先看位置消息的格式
建立结构如下:
解码器中的MessageDecoder.parse方法会把body消息体传到这里,在这里根据自己的业务进行解析数据
/**
*
*/
@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 / 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);
}
@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自带的不能满足需求,再自己自定义)
经过DelimiterBasedFrameDecoder帮我们截断之后,信息就到了我们自己的解码器中了,我们的目的是将ByteBuf转化为我们前面定义的数据结构。
定义解码器 完整代码
@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());
}
}
}
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;
}
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时的数据内容如下,十六进制表示:
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://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 / 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));//功能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 / 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));//功能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());
}
}
解析内容为:
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);
@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());
}
}
}
业务处理代码 gisService.saveLocation
以下代码通过备注作出说明
@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 = "0123456789ABCDEF";
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插口插入即可,车辆型号不同,插口所在的位置也不相同,这块需要自己寻找
2.插入接口后,指示灯必须黄、绿、蓝一直到常亮,不亮或者常闪均说明有问题。(正常等2-3分钟会常亮)
黄、绿、蓝分别代表电源、信号、OBD数据信息
![](https://i-blog.csdnimg.cn/blog_migrate/80a728bed37426b8db3cf6446b16e529.png)
![](https://i-blog.csdnimg.cn/blog_migrate/48955b721d479ea6f94362290cc098d7.png)
3.此时OBD会向服务器发送数据,mem_terminal 表中会获取该设备的信息,code字段对应是设备上的ID值作为唯一标识
4.手动关联设备与终端信息,mem_machine_terminal维护两者的关系,通过两者ID关联,mem_machine表中没有车辆信息则需要手动添加,到此,车辆与OBD关联步骤结束。
4.目前OBD存在的问题汇总
每日可通过查看 mem_machine_run表中数据观察存在的问题
1.里程问题,油耗正常,大约20台设备左右,目前已和厂家沟通解决中。
![](https://i-blog.csdnimg.cn/blog_migrate/1761485cd5ff2bd4d5c538fb1ce86471.png)
2.油耗问题,里程正常,目前2队有三台豪沃存在问题,目前已和厂家沟通解决中。
![](https://i-blog.csdnimg.cn/blog_migrate/8222a4b197d81f20b148a44e37d49fc5.png)
3.里程跳变,目前已和厂家沟通解决中。
![](https://i-blog.csdnimg.cn/blog_migrate/82ac453945770318b006cd16878bb16e.png)
4.运行时间过一段时间会重置
![](https://i-blog.csdnimg.cn/blog_migrate/3e849d3795845389dc391aad9d8cf783.png)