diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/IotMachineServiceImpl.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/IotMachineServiceImpl.java index 5a5125d..26fdc58 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/IotMachineServiceImpl.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/IotMachineServiceImpl.java @@ -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(); + } + } } diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BCD.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BCD.java new file mode 100644 index 0000000..1c9d14a --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BCD.java @@ -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 + + } +} diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BaseHandler.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BaseHandler.java new file mode 100644 index 0000000..2be703e --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/BaseHandler.java @@ -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 extends SimpleChannelInboundHandler { + + //消息流水号 + private static final AttributeKey SERIAL_NUMBER = AttributeKey.newInstance("serialNumber"); + + /** + * 递增获取流水号 + * + * @return + */ + public short getSerialNumber(Channel channel) { + Attribute 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); + } + } + +} + diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/ChannelManager.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/ChannelManager.java new file mode 100644 index 0000000..13fdcd1 --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/ChannelManager.java @@ -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 TERMINAL_PHONE = AttributeKey.newInstance("terminalPhone"); + + private ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + + private Map 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; + } +} + diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CommonResponse.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CommonResponse.java new file mode 100644 index 0000000..cd4758c --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CommonResponse.java @@ -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; + } + +} + diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/Const.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/Const.java new file mode 100644 index 0000000..d5e3a06 --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/Const.java @@ -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℃ + + //新能源 + + +} + diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/DataPacket.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/DataPacket.java new file mode 100644 index 0000000..c24452d --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/DataPacket.java @@ -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; + } + } +} + diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Parser.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Parser.java new file mode 100644 index 0000000..c87195e --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Parser.java @@ -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 parseAdditionalInfo(byte[] data) { + Map 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 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 getAdditionalInfo() { return additionalInfo; } + public void setAdditionalInfo(Map 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 additionalInfo = result.getAdditionalInfo(); + for (Map.Entry 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; + } +} diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Protocol.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Protocol.java new file mode 100644 index 0000000..1c2f1b8 --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/JTT808Protocol.java @@ -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(); + } + } + +} +