定位设备相关代码
This commit is contained in:
parent
8f7067c77f
commit
f00a9ee3af
|
|
@ -21,6 +21,7 @@ import com.bonus.sgzb.material.domain.*;
|
|||
import com.bonus.sgzb.material.mapper.IotMachineMapper;
|
||||
import com.bonus.sgzb.material.mapper.ReportAlarmMapper;
|
||||
import com.bonus.sgzb.material.service.IotMachineService;
|
||||
import com.bonus.sgzb.material.utils.JTT808Protocol;
|
||||
import com.bonus.sgzb.system.api.domain.SysUser;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
|
@ -638,4 +639,29 @@ public class IotMachineServiceImpl implements IotMachineService {
|
|||
}
|
||||
return iotLocationVo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置5分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = 300000)
|
||||
public void dingWei(){
|
||||
try {
|
||||
// 创建协议实例
|
||||
JTT808Protocol protocol = new JTT808Protocol("868120322495257", "36.33.26.201", 21100);
|
||||
|
||||
// 连接服务器
|
||||
protocol.connect();
|
||||
|
||||
// 注册终端
|
||||
protocol.register();
|
||||
|
||||
// 等待一段时间后断开连接
|
||||
Thread.sleep(60000);
|
||||
protocol.disconnect();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
@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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.SimpleChannelInboundHandler;
|
||||
import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelId;
|
||||
import io.netty.channel.group.ChannelGroup;
|
||||
import io.netty.channel.group.DefaultChannelGroup;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.GlobalEventExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
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℃
|
||||
|
||||
//新能源
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
import lombok.Data;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/5
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/6
|
||||
*/
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class JTT808Parser {
|
||||
|
||||
// 常见附加信息ID
|
||||
private static final int INFO_ID_MILEAGE = 0x01; // 里程
|
||||
private static final int INFO_ID_FUEL = 0x02; // 油量
|
||||
private static final int INFO_ID_SPEED = 0x03; // 行驶记录速度
|
||||
private static final int INFO_ID_ENGINE_RPM = 0x04; // 发动机转速
|
||||
private static final int INFO_ID_SATELLITES = 0x11; // 卫星数
|
||||
private static final int INFO_ID_TERMINAL_TEMP = 0x25; // 终端温度
|
||||
|
||||
public LocationInfo parseLocationData(byte[] data) throws Exception {
|
||||
if (data == null || data.length < 28) {
|
||||
throw new IllegalArgumentException("数据长度不足,至少需要28字节");
|
||||
}
|
||||
|
||||
LocationInfo info = new LocationInfo();
|
||||
int offset = 0;
|
||||
|
||||
// 解析报警标志
|
||||
info.setAlarmFlag(readInt32(data, offset));
|
||||
offset += 4;
|
||||
|
||||
// 解析状态
|
||||
info.setStatus(readInt32(data, offset));
|
||||
offset += 4;
|
||||
|
||||
// 解析经纬度
|
||||
info.setLatitude(readInt32(data, offset) / 1e6);
|
||||
offset += 4;
|
||||
|
||||
info.setLongitude(readInt32(data, offset) / 1e6);
|
||||
offset += 4;
|
||||
|
||||
// 解析高程
|
||||
info.setAltitude(readInt16(data, offset));
|
||||
offset += 2;
|
||||
|
||||
// 解析速度
|
||||
info.setSpeed(readInt16(data, offset) / 10.0);
|
||||
offset += 2;
|
||||
|
||||
// 解析方向
|
||||
info.setDirection(readInt16(data, offset));
|
||||
offset += 2;
|
||||
|
||||
// 解析时间
|
||||
info.setLocationTime(parseTime(data, offset));
|
||||
offset += 6;
|
||||
|
||||
// 解析附加信息
|
||||
if (offset < data.length) {
|
||||
info.setAdditionalInfo(parseAdditionalInfo(Arrays.copyOfRange(data, offset, data.length)));
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private Map<Integer, Object> parseAdditionalInfo(byte[] data) {
|
||||
Map<Integer, Object> additionalInfo = new HashMap<>();
|
||||
int offset = 0;
|
||||
|
||||
while (offset + 2 <= data.length) {
|
||||
int infoId = data[offset] & 0xFF;
|
||||
int infoLength = data[offset + 1] & 0xFF;
|
||||
offset += 2;
|
||||
|
||||
if (offset + infoLength > data.length) {
|
||||
break; // 数据不足,退出解析
|
||||
}
|
||||
|
||||
byte[] infoData = Arrays.copyOfRange(data, offset, offset + infoLength);
|
||||
|
||||
// 根据ID解析不同类型的附加信息
|
||||
switch (infoId) {
|
||||
case INFO_ID_MILEAGE:
|
||||
additionalInfo.put(infoId, readInt32(infoData, 0) / 10.0); // 0.1km
|
||||
break;
|
||||
case INFO_ID_FUEL:
|
||||
additionalInfo.put(infoId, readInt16(infoData, 0) / 10.0); // 0.1L
|
||||
break;
|
||||
case INFO_ID_SPEED:
|
||||
additionalInfo.put(infoId, readInt16(infoData, 0) / 10.0); // 0.1km/h
|
||||
break;
|
||||
case INFO_ID_ENGINE_RPM:
|
||||
additionalInfo.put(infoId, readInt16(infoData, 0)); // rpm
|
||||
break;
|
||||
case INFO_ID_SATELLITES:
|
||||
additionalInfo.put(infoId, infoData[0] & 0xFF); // 卫星数
|
||||
break;
|
||||
case INFO_ID_TERMINAL_TEMP:
|
||||
additionalInfo.put(infoId, (int) infoData[0]); // 温度
|
||||
break;
|
||||
default:
|
||||
// 未知类型,保存原始数据的十六进制字符串
|
||||
additionalInfo.put(infoId, bytesToHex(infoData));
|
||||
}
|
||||
|
||||
offset += infoLength;
|
||||
}
|
||||
|
||||
return additionalInfo;
|
||||
}
|
||||
|
||||
// 读取32位整数(大端序)
|
||||
private int readInt32(byte[] data, int offset) {
|
||||
return ((data[offset] & 0xFF) << 24) |
|
||||
((data[offset + 1] & 0xFF) << 16) |
|
||||
((data[offset + 2] & 0xFF) << 8) |
|
||||
(data[offset + 3] & 0xFF);
|
||||
}
|
||||
|
||||
// 读取16位整数(大端序)
|
||||
private int readInt16(byte[] data, int offset) {
|
||||
return ((data[offset] & 0xFF) << 8) |
|
||||
(data[offset + 1] & 0xFF);
|
||||
}
|
||||
|
||||
// 解析BCD格式时间
|
||||
private LocalDateTime parseTime(byte[] data, int offset) {
|
||||
int year = ((data[offset] >> 4) & 0x0F) * 10 + (data[offset] & 0x0F);
|
||||
int month = ((data[offset + 1] >> 4) & 0x0F) * 10 + (data[offset + 1] & 0x0F);
|
||||
int day = ((data[offset + 2] >> 4) & 0x0F) * 10 + (data[offset + 2] & 0x0F);
|
||||
int hour = ((data[offset + 3] >> 4) & 0x0F) * 10 + (data[offset + 3] & 0x0F);
|
||||
int minute = ((data[offset + 4] >> 4) & 0x0F) * 10 + (data[offset + 4] & 0x0F);
|
||||
int second = ((data[offset + 5] >> 4) & 0x0F) * 10 + (data[offset + 5] & 0x0F);
|
||||
|
||||
// 假设当前世纪为21世纪
|
||||
year += 2000;
|
||||
|
||||
return LocalDateTime.of(year, month, day, hour, minute, second);
|
||||
}
|
||||
|
||||
// 字节数组转十六进制字符串
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
result.append(String.format("%02X", b));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
// 定位信息类
|
||||
public static class LocationInfo {
|
||||
private long alarmFlag;
|
||||
private long status;
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private int altitude;
|
||||
private double speed;
|
||||
private int direction;
|
||||
private LocalDateTime locationTime;
|
||||
private Map<Integer, Object> additionalInfo = new HashMap<>();
|
||||
|
||||
// Getters and setters
|
||||
public long getAlarmFlag() { return alarmFlag; }
|
||||
public void setAlarmFlag(long alarmFlag) { this.alarmFlag = alarmFlag; }
|
||||
|
||||
public long getStatus() { return status; }
|
||||
public void setStatus(long status) { this.status = status; }
|
||||
|
||||
public double getLatitude() { return latitude; }
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
||||
public int getAltitude() { return altitude; }
|
||||
public void setAltitude(int altitude) { this.altitude = altitude; }
|
||||
|
||||
public double getSpeed() { return speed; }
|
||||
public void setSpeed(double speed) { this.speed = speed; }
|
||||
|
||||
public int getDirection() { return direction; }
|
||||
public void setDirection(int direction) { this.direction = direction; }
|
||||
|
||||
public LocalDateTime getLocationTime() { return locationTime; }
|
||||
public void setLocationTime(LocalDateTime locationTime) { this.locationTime = locationTime; }
|
||||
|
||||
public Map<Integer, Object> getAdditionalInfo() { return additionalInfo; }
|
||||
public void setAdditionalInfo(Map<Integer, Object> additionalInfo) { this.additionalInfo = additionalInfo; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LocationInfo{" +
|
||||
"alarmFlag=" + alarmFlag +
|
||||
", status=" + status +
|
||||
", latitude=" + latitude +
|
||||
", longitude=" + longitude +
|
||||
", altitude=" + altitude +
|
||||
", speed=" + speed +
|
||||
", direction=" + direction +
|
||||
", locationTime=" + locationTime +
|
||||
", additionalInfo=" + additionalInfo +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
// 示例数据 (根据实际情况替换)
|
||||
byte[] sampleData = hexStringToByteArray(
|
||||
"01000000" + // 报警标志: 紧急报警
|
||||
"00000001" + // 状态: ACC开, 定位有效
|
||||
"01CDD6A1" + // 纬度: 30.545633°
|
||||
"771A1A54" + // 经度: 114.293492°
|
||||
"0064" + // 高程: 100米
|
||||
"004B" + // 速度: 7.5km/h
|
||||
"00C8" + // 方向: 200°
|
||||
"250605143000" + // 时间: 2025-06-05 14:30:00
|
||||
// 附加信息: 里程(0x01) = 12345.6km
|
||||
"010400017D90" +
|
||||
// 附加信息: 卫星数(0x11) = 12颗
|
||||
"11010C"
|
||||
);
|
||||
|
||||
JTT808Parser parser = new JTT808Parser();
|
||||
LocationInfo result = parser.parseLocationData(sampleData);
|
||||
|
||||
// 打印解析结果
|
||||
System.out.println("解析结果:");
|
||||
System.out.printf("报警标志: %d%n", result.getAlarmFlag());
|
||||
System.out.printf("状态: %d%n", result.getStatus());
|
||||
System.out.printf("纬度: %.6f°%n", result.getLatitude());
|
||||
System.out.printf("经度: %.6f°%n", result.getLongitude());
|
||||
System.out.printf("高程: %d米%n", result.getAltitude());
|
||||
System.out.printf("速度: %.1fkm/h%n", result.getSpeed());
|
||||
System.out.printf("方向: %d°%n", result.getDirection());
|
||||
System.out.printf("定位时间: %s%n", result.getLocationTime());
|
||||
|
||||
System.out.println("\n附加信息:");
|
||||
Map<Integer, Object> additionalInfo = result.getAdditionalInfo();
|
||||
for (Map.Entry<Integer, Object> entry : additionalInfo.entrySet()) {
|
||||
String infoName = getAdditionalInfoName(entry.getKey());
|
||||
System.out.printf("%s: %s%n", infoName, entry.getValue());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 获取附加信息名称
|
||||
private static String getAdditionalInfoName(int infoId) {
|
||||
switch (infoId) {
|
||||
case INFO_ID_MILEAGE: return "里程";
|
||||
case INFO_ID_FUEL: return "油量";
|
||||
case INFO_ID_SPEED: return "行驶记录速度";
|
||||
case INFO_ID_ENGINE_RPM: return "发动机转速";
|
||||
case INFO_ID_SATELLITES: return "卫星数";
|
||||
case INFO_ID_TERMINAL_TEMP: return "终端温度";
|
||||
default: return String.format("未知信息ID(0x%02X)", infoId);
|
||||
}
|
||||
}
|
||||
|
||||
// 十六进制字符串转字节数组
|
||||
private static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,412 @@
|
|||
package com.bonus.sgzb.material.utils;
|
||||
|
||||
/**
|
||||
* @author 马三炮
|
||||
* @date 2025/6/6
|
||||
*/
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* JT/T 808协议实现 - 从请求到解析的完整流程
|
||||
*/
|
||||
public class JTT808Protocol {
|
||||
|
||||
// 消息ID定义
|
||||
private static final int MSG_ID_TERMINAL_REGISTER = 0x0100; // 终端注册
|
||||
private static final int MSG_ID_TERMINAL_AUTH = 0x0102; // 终端鉴权
|
||||
private static final int MSG_ID_TERMINAL_HEARTBEAT = 0x0002; // 终端心跳
|
||||
private static final int MSG_ID_TERMINAL_LOCATION = 0x0200; // 终端位置信息汇报
|
||||
private static final int MSG_ID_PLATFORM_GENERAL_ACK = 0x8001; // 平台通用应答
|
||||
|
||||
// 终端信息
|
||||
private String terminalPhone; // 终端手机号(12位数字)
|
||||
private String serverIp; // 服务器IP
|
||||
private int serverPort; // 服务器端口
|
||||
private Socket socket; // 通信套接字
|
||||
private int flowId = 0; // 流水号
|
||||
|
||||
// 定位信息解析器
|
||||
private JTT808Parser parser = new JTT808Parser();
|
||||
|
||||
public JTT808Protocol(String terminalPhone, String serverIp, int serverPort) {
|
||||
this.terminalPhone = terminalPhone;
|
||||
this.serverIp = serverIp;
|
||||
this.serverPort = serverPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到服务器
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
socket = new Socket(serverIp, serverPort);
|
||||
System.out.println("已连接到服务器: " + serverIp + ":" + serverPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开与服务器的连接
|
||||
*/
|
||||
public void disconnect() {
|
||||
try {
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
System.out.println("已断开与服务器的连接");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端注册
|
||||
*/
|
||||
public void register() throws IOException {
|
||||
// 构建注册消息体
|
||||
ByteArrayOutputStream msgBody = new ByteArrayOutputStream();
|
||||
// 省域ID(2字节)
|
||||
msgBody.write(0x00);
|
||||
msgBody.write(0x01);
|
||||
// 市县域ID(2字节)
|
||||
msgBody.write(0x00);
|
||||
msgBody.write(0x01);
|
||||
// 制造商ID(5字节)
|
||||
msgBody.write("TEST1".getBytes(StandardCharsets.UTF_8));
|
||||
// 终端型号(20字节)
|
||||
byte[] model = "TEST_MODEL".getBytes(StandardCharsets.UTF_8);
|
||||
msgBody.write(model);
|
||||
for (int i = model.length; i < 20; i++) {
|
||||
msgBody.write(0x00);
|
||||
}
|
||||
// 终端ID
|
||||
byte[] terminalId = terminalPhone.getBytes(StandardCharsets.UTF_8);
|
||||
msgBody.write(terminalId);
|
||||
for (int i = terminalId.length; i < 7; i++) {
|
||||
msgBody.write(0x00);
|
||||
}
|
||||
// 发送注册消息
|
||||
sendMessage(MSG_ID_TERMINAL_REGISTER, msgBody.toByteArray());
|
||||
|
||||
// 接收注册应答
|
||||
byte[] response = receiveMessage();
|
||||
|
||||
if (response != null) {
|
||||
int msgId = ((response[1] & 0xFF) << 8) | (response[2] & 0xFF);
|
||||
if (msgId == 0x8100) { // 注册应答
|
||||
int result = response[10] & 0xFF;
|
||||
if (result == 0) { // 成功
|
||||
int authCodeLen = response.length - 11;
|
||||
byte[] authCodeBytes = Arrays.copyOfRange(response, 11, 11 + authCodeLen);
|
||||
String authCode = new String(authCodeBytes, StandardCharsets.UTF_8);
|
||||
System.out.println("注册成功,鉴权码: " + authCode);
|
||||
|
||||
// 发送鉴权消息
|
||||
authenticate(authCode);
|
||||
} else {
|
||||
System.out.println("注册失败,错误码: " + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端鉴权
|
||||
*/
|
||||
public void authenticate(String authCode) throws IOException {
|
||||
// 发送鉴权消息
|
||||
sendMessage(MSG_ID_TERMINAL_AUTH, authCode.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 接收鉴权应答
|
||||
byte[] response = receiveMessage();
|
||||
if (response != null) {
|
||||
int msgId = ((response[1] & 0xFF) << 8) | (response[2] & 0xFF);
|
||||
if (msgId == 0x8001) { // 通用应答
|
||||
int replyFlowId = ((response[3] & 0xFF) << 8) | (response[4] & 0xFF);
|
||||
int replyMsgId = ((response[5] & 0xFF) << 8) | (response[6] & 0xFF);
|
||||
int result = response[7] & 0xFF;
|
||||
|
||||
if (replyMsgId == MSG_ID_TERMINAL_AUTH && result == 0) {
|
||||
System.out.println("鉴权成功");
|
||||
|
||||
// 发送心跳
|
||||
sendHeartbeat();
|
||||
//发送定位请求
|
||||
//requestLocation();
|
||||
// 开始接收定位信息
|
||||
startReceivingLocation();
|
||||
} else {
|
||||
System.out.println("鉴权失败,错误码: " + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳
|
||||
*/
|
||||
public void sendHeartbeat() throws IOException {
|
||||
// 心跳消息体为空
|
||||
sendMessage(MSG_ID_TERMINAL_HEARTBEAT, new byte[0]);
|
||||
System.out.println("发送心跳消息");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送位置信息请求
|
||||
*/
|
||||
public void requestLocation() throws IOException {
|
||||
// 构建位置信息请求消息体(实际应用中可能不需要请求,终端会主动上报)
|
||||
sendMessage(MSG_ID_TERMINAL_LOCATION, new byte[0]);
|
||||
System.out.println("发送位置信息请求");
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始接收定位信息
|
||||
*/
|
||||
public void startReceivingLocation() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
while (socket != null && !socket.isClosed()) {
|
||||
byte[] response = receiveMessage();
|
||||
if (response != null) {
|
||||
int msgId = ((response[1] & 0xFF) << 8) | (response[2] & 0xFF);
|
||||
|
||||
if (msgId == MSG_ID_TERMINAL_LOCATION) {
|
||||
// 解析位置信息
|
||||
int bodyOffset = 13; // 消息头长度
|
||||
int bodyLength = response.length - bodyOffset - 1; // 减去标识位和校验码
|
||||
byte[] locationData = Arrays.copyOfRange(response, bodyOffset, bodyOffset + bodyLength);
|
||||
|
||||
JTT808Parser.LocationInfo locationInfo = parser.parseLocationData(locationData);
|
||||
System.out.println("收到位置信息:");
|
||||
System.out.printf("时间: %s, 经纬度: %.6f, %.6f, 速度: %.1fkm/h%n",
|
||||
locationInfo.getLocationTime(),
|
||||
locationInfo.getLatitude(),
|
||||
locationInfo.getLongitude(),
|
||||
locationInfo.getSpeed());
|
||||
|
||||
// 发送位置信息接收应答
|
||||
sendGeneralAck(flowId - 1, MSG_ID_TERMINAL_LOCATION, 0);
|
||||
} else if (msgId == MSG_ID_PLATFORM_GENERAL_ACK) {
|
||||
System.out.println("收到平台通用应答");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("接收消息出错: " + e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送通用应答
|
||||
*/
|
||||
private void sendGeneralAck(int flowId, int msgId, int result) throws IOException {
|
||||
ByteArrayOutputStream msgBody = new ByteArrayOutputStream();
|
||||
|
||||
// 应答流水号
|
||||
msgBody.write((flowId >> 8) & 0xFF);
|
||||
msgBody.write(flowId & 0xFF);
|
||||
|
||||
// 应答消息ID
|
||||
msgBody.write((msgId >> 8) & 0xFF);
|
||||
msgBody.write(msgId & 0xFF);
|
||||
|
||||
// 结果
|
||||
msgBody.write(result);
|
||||
|
||||
sendMessage(MSG_ID_PLATFORM_GENERAL_ACK, msgBody.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*/
|
||||
private void sendMessage(int msgId, byte[] msgBody) throws IOException {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
throw new IOException("未连接到服务器");
|
||||
}
|
||||
|
||||
// 构建消息头
|
||||
ByteArrayOutputStream header = new ByteArrayOutputStream();
|
||||
|
||||
// 消息ID
|
||||
header.write((msgId >> 8) & 0xFF);
|
||||
header.write(msgId & 0xFF);
|
||||
|
||||
// 消息体属性
|
||||
int bodyLength = msgBody.length;
|
||||
int property = bodyLength & 0x03FF; // 消息体长度(10位)
|
||||
header.write((property >> 8) & 0xFF);
|
||||
header.write(property & 0xFF);
|
||||
|
||||
// 终端手机号
|
||||
byte[] phoneBytes = terminalPhone.getBytes(StandardCharsets.UTF_8);
|
||||
for (int i = 0; i < 12 - phoneBytes.length; i++) {
|
||||
header.write(0x30); // 补0(ASCII码48)
|
||||
}
|
||||
header.write(phoneBytes);
|
||||
|
||||
// 流水号
|
||||
header.write((flowId >> 8) & 0xFF);
|
||||
header.write(flowId & 0xFF);
|
||||
flowId = (flowId + 1) % 65536;
|
||||
|
||||
// 构建完整消息
|
||||
ByteArrayOutputStream fullMessage = new ByteArrayOutputStream();
|
||||
|
||||
// 标识位
|
||||
fullMessage.write(0x7E);
|
||||
|
||||
// 消息头
|
||||
fullMessage.write(header.toByteArray());
|
||||
|
||||
// 消息体
|
||||
fullMessage.write(msgBody);
|
||||
|
||||
// 校验码
|
||||
byte checksum = calculateChecksum(fullMessage.toByteArray(), 1, fullMessage.size());
|
||||
fullMessage.write(checksum);
|
||||
|
||||
// 标识位
|
||||
fullMessage.write(0x7E);
|
||||
|
||||
// 转义处理
|
||||
byte[] escapedMessage = escapeMessage(fullMessage.toByteArray());
|
||||
|
||||
// 发送消息
|
||||
OutputStream out = socket.getOutputStream();
|
||||
out.write(escapedMessage);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收消息
|
||||
*/
|
||||
private byte[] receiveMessage() throws IOException {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
throw new IOException("未连接到服务器");
|
||||
}
|
||||
|
||||
InputStream in = socket.getInputStream();
|
||||
|
||||
// 查找起始标识位
|
||||
int b;
|
||||
while ((b = in.read()) != -1) {
|
||||
if (b == 0x7E) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (b == -1) {
|
||||
System.out.println("连接已关闭");
|
||||
return null; // 连接已关闭
|
||||
}
|
||||
|
||||
// 读取消息内容(直到下一个标识位)
|
||||
ByteArrayOutputStream message = new ByteArrayOutputStream();
|
||||
message.write(0x7E);
|
||||
|
||||
boolean escaped = false;
|
||||
while ((b = in.read()) != -1) {
|
||||
if (escaped) {
|
||||
if (b == 0x01) {
|
||||
message.write(0x7D);
|
||||
} else if (b == 0x02) {
|
||||
message.write(0x7E);
|
||||
} else {
|
||||
message.write(b); // 不合法的转义,按原始数据处理
|
||||
}
|
||||
escaped = false;
|
||||
} else {
|
||||
if (b == 0x7D) {
|
||||
escaped = true;
|
||||
} else if (b == 0x7E) {
|
||||
message.write(b);
|
||||
break; // 结束标识位
|
||||
} else {
|
||||
message.write(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("返回信息"+message.toString());
|
||||
if (message.size() < 5) { // 至少需要标识位+消息头(4字节)+校验码+标识位
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] msgBytes = message.toByteArray();
|
||||
|
||||
// 校验校验码
|
||||
int dataLength = msgBytes.length - 3; // 减去前后两个标识位和最后一个字节的校验码
|
||||
byte checksum = calculateChecksum(msgBytes, 1, dataLength + 1);
|
||||
if (checksum != msgBytes[dataLength + 1]) {
|
||||
System.out.println("校验码错误");
|
||||
return null;
|
||||
}
|
||||
|
||||
return msgBytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算校验码
|
||||
*/
|
||||
private byte calculateChecksum(byte[] data, int start, int length) {
|
||||
byte checksum = 0;
|
||||
System.out.print("长度{}"+data.length);
|
||||
if (data.length<1){
|
||||
return checksum;
|
||||
}
|
||||
for (int i = start; i < length; i++) {
|
||||
checksum ^= data[i];
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息转义处理
|
||||
*/
|
||||
private byte[] escapeMessage(byte[] data) {
|
||||
ByteArrayOutputStream escaped = new ByteArrayOutputStream();
|
||||
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
if (data[i] == 0x7E) {
|
||||
escaped.write(0x7D);
|
||||
escaped.write(0x02);
|
||||
} else if (data[i] == 0x7D) {
|
||||
escaped.write(0x7D);
|
||||
escaped.write(0x01);
|
||||
} else {
|
||||
escaped.write(data[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return escaped.toByteArray();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
// 创建协议实例
|
||||
JTT808Protocol protocol = new JTT808Protocol("868120322495257", "36.33.26.201", 21100);
|
||||
|
||||
// 连接服务器
|
||||
protocol.connect();
|
||||
|
||||
// 注册终端
|
||||
protocol.register();
|
||||
|
||||
// 等待一段时间后断开连接
|
||||
Thread.sleep(60000);
|
||||
protocol.disconnect();
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue