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

背景

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

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

1.开发过程

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

1.1认识JT808协议

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

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

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

@Data
public class DataPacket {

    protected Header header = new Header(); //消息头
    protected ByteBuf byteBuf; //消息流

    @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)
00001/10km/h
00000-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
00140115CSQ值 网络信号强度
0015020000车型ID(OBD车型)
001601F2OBD协议类型 协议类型表
0017021EA2驾驶循环标签
0018011BGPS收星数 GPS定位收星数
001902006EGPS位置精度 0.01 GPS位置精度
001E0400A89B93累计里程 米 当0003总里程数据中里程类型为仪表里程时,往往只能精确到1KM或者10KM,这样不利于统计里程,为了便于平台统计里程,增加一项累计里程
0020020000点火类型
00210400B5ED2FOBD 转速 rpm 精度:1偏移:0范围:0 ~ 8000
EC货车扩展数据流
2B数据长度)
60C0020000设备拔出状态(定制)
60D00100OBD 车速 Km/h 精度:1偏移:0范围:0 ~ 240
6050016COBD 冷却液温度 ℃ 精度:1℃偏移:-40.0℃范围:-40.0℃ ~ +210℃
64900100OBD 加速踏板位置(油门踏板) % 精度:1偏移:0范围:0% ~ 100%
60A00200F0OBD 燃油压力 kPa 精度:1偏移:0范围:0 ~ 500kpa
51120100MIL状态 有效范围 0~1,“0”代表未点亮,“1”代表点亮。“0xFE”表示无效。
5005020019OBD 油料使用率发动机燃油流量 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设备怠速超速记录表;有怠速、超速报警后数据会存储该表
machinemogodb数据库; 该集合存储设备的位置、高度、速度等信息
machineRunDetailmogodb数据库; 该集合存储设备的运行状态及结束时间

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

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

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

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

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

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

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

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

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

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


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

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

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

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

里程跳变

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

运行时间会重置

  • 21
    点赞
  • 86
    收藏
    觉得还不错? 一键收藏
  • 19
    评论
评论 19
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值