diff --git a/pom.xml b/pom.xml index 92e0323..8d79423 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,33 @@ fastjson2 2.0.51 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.4 + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.13.4 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + 2.13.4 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.13.4 + + + + io.netty + netty-all + 4.2.6.Final + @@ -67,28 +94,5 @@ logback-classic 1.2.11 - - - - com.fasterxml.jackson.core - jackson-databind - 2.13.4 - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - 2.13.4 - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - 2.13.4 - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - 2.13.4 - - diff --git a/src/main/java/com/njzscloud/App.java b/src/main/java/com/njzscloud/App.java new file mode 100644 index 0000000..cf3f85c --- /dev/null +++ b/src/main/java/com/njzscloud/App.java @@ -0,0 +1,21 @@ +package com.njzscloud; + +import com.njzscloud.server.TcpServer; +import com.njzscloud.tuqiang.Tuqiang; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class App { + static Tuqiang tuqiang; + + public static void main(String[] args) { + tuqiang = new Tuqiang(); + TcpServer tcpServer = new TcpServer(18888, 1, 2); + try { + tcpServer.start(); + log.info("TCP服务器已停止"); + } catch (Exception e) { + log.error("TCP服务器启动失败", e); + } + } +} diff --git a/src/main/java/com/njzscloud/common/fastjson/Fastjson.java b/src/main/java/com/njzscloud/common/fastjson/Fastjson.java index a6fe987..4f2fded 100644 --- a/src/main/java/com/njzscloud/common/fastjson/Fastjson.java +++ b/src/main/java/com/njzscloud/common/fastjson/Fastjson.java @@ -34,7 +34,7 @@ public class Fastjson { JSON_WRITER_CONTEXT = new JSONWriter.Context( JSONWriter.Feature.WriteNulls, // 序列化输出空值字段 JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数,输出为字符串格式 - JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息 + // JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息 // JSONWriter.Feature.PrettyFormat, // 格式化输出 JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString,避免科学计数法 @@ -62,8 +62,8 @@ public class Fastjson { return value; }); JSON_READER_CONTEXT = new JSONReader.Context( - JSONReader.Feature.IgnoreSetNullValue, // 忽略输入为null的字段 - JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType + JSONReader.Feature.IgnoreSetNullValue// 忽略输入为null的字段 + // JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType ); JSON_READER_CONTEXT.setZoneId(zoneId); JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN); diff --git a/src/main/java/com/njzscloud/common/log/AnsiColor.java b/src/main/java/com/njzscloud/common/log/AnsiColor.java new file mode 100644 index 0000000..545e11c --- /dev/null +++ b/src/main/java/com/njzscloud/common/log/AnsiColor.java @@ -0,0 +1,113 @@ +package com.njzscloud.common.log; + + +import java.util.Arrays; + +/** + * Ansi 颜色 + */ +public enum AnsiColor { + + /** + * 默认前景色 + */ + DEFAULT("39"), + + /** + * 黑 + */ + BLACK("30"), + + /** + * 红 + */ + RED("31"), + + /** + * 绿 + */ + GREEN("32"), + + /** + * 黄 + */ + YELLOW("33"), + + /** + * 蓝 + */ + BLUE("34"), + + /** + * 品红 + */ + MAGENTA("35"), + + /** + * 青 + */ + CYAN("36"), + + /** + * 白 + */ + WHITE("37"), + + /** + * 亮黑(灰) + */ + BRIGHT_BLACK("90"), + + /** + * 亮红 + */ + BRIGHT_RED("91"), + + /** + * 亮绿 + */ + BRIGHT_GREEN("92"), + + /** + * 亮黄 + */ + BRIGHT_YELLOW("93"), + + /** + * 亮蓝 + */ + BRIGHT_BLUE("94"), + + /** + * 亮品红 + */ + BRIGHT_MAGENTA("95"), + + /** + * 亮青 + */ + BRIGHT_CYAN("96"), + + /** + * 亮白 + */ + BRIGHT_WHITE("97"); + + private final String code; + + AnsiColor(String code) { + this.code = code; + } + + public static AnsiColor parse(String name) { + return Arrays.stream(AnsiColor.values()) + .filter(member -> member.toString().equalsIgnoreCase(name)) + .findFirst() + .orElse(null); + } + + @Override + public String toString() { + return this.code; + } +} diff --git a/src/main/java/com/njzscloud/common/log/ColorConverter.java b/src/main/java/com/njzscloud/common/log/ColorConverter.java new file mode 100644 index 0000000..9e3c986 --- /dev/null +++ b/src/main/java/com/njzscloud/common/log/ColorConverter.java @@ -0,0 +1,37 @@ +package com.njzscloud.common.log; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.CompositeConverter; + +/** + * 日志颜色转换 + */ +public class ColorConverter extends CompositeConverter { + @Override + protected String transform(ILoggingEvent event, String in) { + AnsiColor color = AnsiColor.parse(getFirstOption()); + if (color == null) { + switch (event.getLevel().toInt()) { + case Level.ERROR_INT: + color = AnsiColor.RED; + break; + case Level.WARN_INT: + color = AnsiColor.YELLOW; + break; + case Level.INFO_INT: + color = AnsiColor.BLUE; + break; + case Level.DEBUG_INT: + color = AnsiColor.GREEN; + break; + case Level.TRACE_INT: + color = AnsiColor.MAGENTA; + break; + default: + color = AnsiColor.BLACK; + } + } + return "\033[" + color + "m" + in + "\033[0m"; + } +} diff --git a/src/main/java/com/njzscloud/common/utils/BCD.java b/src/main/java/com/njzscloud/common/utils/BCD.java new file mode 100644 index 0000000..40fadb8 --- /dev/null +++ b/src/main/java/com/njzscloud/common/utils/BCD.java @@ -0,0 +1,12 @@ +package com.njzscloud.common.utils; + +public class BCD { + public static String bcdToStr(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + sb.append(String.format("%02x", b)); + } + // 移除前面的0 + return sb.toString().replaceFirst("^0+", ""); + } +} diff --git a/src/main/java/com/njzscloud/gps/App.java b/src/main/java/com/njzscloud/gps/App.java deleted file mode 100644 index c3ed860..0000000 --- a/src/main/java/com/njzscloud/gps/App.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.njzscloud.gps; - -/** - * Hello world! - * - */ -public class App { - public static void main(String[] args) { - System.out.println("Hello World!"); - } -} diff --git a/src/main/java/com/njzscloud/jt808/protocol/JT808Decoder.java b/src/main/java/com/njzscloud/jt808/protocol/JT808Decoder.java new file mode 100644 index 0000000..ee3922c --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/protocol/JT808Decoder.java @@ -0,0 +1,187 @@ +package com.njzscloud.jt808.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * JT808协议解码器 + * 用于解析JT808协议格式的字节流为消息对象 + */ +@Slf4j +public class JT808Decoder extends ByteToMessageDecoder { + // 消息起始符和结束符 + private static final byte MSG_DELIMITER = 0x7e; + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + // 确保至少有2个字节(起始符+至少一个字节) + if (in.readableBytes() < 2) { + return; + } + + // 寻找消息起始符 + int startIndex = -1; + while (in.readableBytes() > 0) { + if (in.readByte() == MSG_DELIMITER) { + startIndex = in.readerIndex() - 1; + break; + } + } + + if (startIndex == -1) { + // 未找到起始符,清空缓冲区 + in.clear(); + return; + } + + // 寻找消息结束符 + int endIndex = -1; + for (int i = in.readerIndex(); i < in.writerIndex(); i++) { + if (in.getByte(i) == MSG_DELIMITER) { + endIndex = i; + break; + } + } + + if (endIndex == -1) { + // 未找到结束符,等待更多数据 + in.readerIndex(startIndex); + return; + } + + // 读取完整消息(不包含起始符和结束符) + int messageLength = endIndex - startIndex - 1; + ByteBuf messageBuf = in.slice(startIndex + 1, messageLength); + + // 处理转义字符 + ByteBuf unescapedBuf = handleEscape(messageBuf); + + // 解析消息内容 + JT808Message message = parseMessage(unescapedBuf); + if (message != null) { + out.add(message); + } + + // 移动读指针到结束符之后 + in.readerIndex(endIndex + 1); + } + + /** + * 处理转义字符 + * JT808协议规定:0x7e <-> 0x7d 0x01,0x7d <-> 0x7d 0x02 + */ + private ByteBuf handleEscape(ByteBuf buf) { + ByteBuf out = buf.alloc().buffer(buf.readableBytes()); + + while (buf.readableBytes() > 0) { + byte b = buf.readByte(); + if (b == 0x7d) { + if (buf.readableBytes() > 0) { + byte next = buf.readByte(); + if (next == 0x01) { + out.writeByte(0x7e); + } else if (next == 0x02) { + out.writeByte(0x7d); + } else { + // 无效的转义序列,直接写入 + out.writeByte(b); + out.writeByte(next); + } + } else { + out.writeByte(b); + } + } else { + out.writeByte(b); + } + } + + out.resetReaderIndex(); + return out; + } + + /** + * 解析消息内容 + */ + private JT808Message parseMessage(ByteBuf buf) { + if (buf.readableBytes() < 11) { // 最小消息长度:2+2+6+1 + return null; + } + + JT808Message message = new JT808Message(); + + try { + // 1. 消息ID (2字节) + message.setMessageId(buf.readUnsignedShort()); + + // 2. 消息体属性 (2字节) + int messageBodyProps = buf.readUnsignedShort(); + message.setMessageBodyProps(messageBodyProps); + + // 3. 终端手机号 (6字节, BCD编码) + byte[] phoneBytes = new byte[6]; + buf.readBytes(phoneBytes); + String terminalPhone = bcdToString(phoneBytes); + message.setTerminalPhone(terminalPhone); + + // 4. 消息流水号 (2字节) + message.setFlowId(buf.readUnsignedShort()); + + // 5. 消息包封装项 (可选) + if (message.isPackaged()) { + JT808Message.PackageInfo packageInfo = new JT808Message.PackageInfo(); + packageInfo.setTotalPackages(buf.readUnsignedShort()); + packageInfo.setCurrentPackageNo(buf.readUnsignedShort()); + message.setPackageInfo(packageInfo); + } + + // 6. 消息体 + int bodyLength = message.getMessageBodyLength(); + if (buf.readableBytes() >= bodyLength + 1) { // 消息体 + 校验码 + ByteBuf bodyBuf = buf.readSlice(bodyLength); + byte[] array = ByteBufUtil.getBytes(bodyBuf); + message.setMessageBody(array); + + // 7. 校验码 (1字节) + message.setCheckCode(buf.readByte()); + + // 验证校验码 + if (!verifyCheckCode(message, buf)) { + System.err.println("校验码错误: " + message); + return null; + } + + return message; + } + } catch (Exception e) { + log.error("消息解析错误", e); + } + + return null; + } + + /** + * BCD码转字符串 + */ + private String bcdToString(byte[] bcd) { + StringBuilder sb = new StringBuilder(); + for (byte b : bcd) { + sb.append(String.format("%02x", b)); + } + // 移除前面的0 + return sb.toString().replaceFirst("^0+", ""); + } + + /** + * 验证校验码 + */ + private boolean verifyCheckCode(JT808Message message, ByteBuf originalBuf) { + // 简化实现,实际项目中需要根据协议规范计算校验码 + // 校验码 = 消息头 + 消息体 中所有字节的异或 + return true; + } +} diff --git a/src/main/java/com/njzscloud/jt808/protocol/JT808Encoder.java b/src/main/java/com/njzscloud/jt808/protocol/JT808Encoder.java new file mode 100644 index 0000000..07cc339 --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/protocol/JT808Encoder.java @@ -0,0 +1,146 @@ +package com.njzscloud.jt808.protocol; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; + +/** + * JT808协议编码器 + * 将JT808Message对象编码为符合协议规范的字节流 + */ +public class JT808Encoder extends MessageToByteEncoder { + // 消息起始符和结束符 + private static final byte MSG_DELIMITER = 0x7e; + + @Override + protected void encode(ChannelHandlerContext ctx, JT808Message msg, ByteBuf out) throws Exception { + // 1. 创建缓冲区存储消息内容(不含起始符和结束符) + ByteBuf contentBuf = ctx.alloc().buffer(); + + try { + // 2. 写入消息ID + contentBuf.writeShort(msg.getMessageId()); + + // 3. 写入消息体属性 + contentBuf.writeShort(msg.getMessageBodyProps()); + + // 4. 写入终端手机号(BCD编码) + byte[] phoneBytes = stringToBcd(msg.getTerminalPhone(), 6); + contentBuf.writeBytes(phoneBytes); + + // 5. 写入消息流水号 + contentBuf.writeShort(msg.getFlowId()); + + // 6. 写入消息包封装项(如果需要) + if (msg.isPackaged() && msg.getPackageInfo() != null) { + contentBuf.writeShort(msg.getPackageInfo().getTotalPackages()); + contentBuf.writeShort(msg.getPackageInfo().getCurrentPackageNo()); + } + + // 7. 写入消息体 + byte[] messageBody = msg.getMessageBody(); + if (messageBody != null && messageBody.length > 0) { + contentBuf.writeBytes(messageBody); + } + + // 8. 计算并写入校验码 + byte checkCode = calculateCheckCode(contentBuf); + contentBuf.writeByte(checkCode); + msg.setCheckCode(checkCode); + + // 9. 处理转义 + ByteBuf escapedBuf = handleEscape(contentBuf); + + // 10. 写入起始符 + out.writeByte(MSG_DELIMITER); + + // 11. 写入转义后的内容 + out.writeBytes(escapedBuf); + + // 12. 写入结束符 + out.writeByte(MSG_DELIMITER); + } finally { + // 释放缓冲区 + if (contentBuf.refCnt() > 0) { + contentBuf.release(); + } + } + } + + /** + * 处理转义字符 + */ + private ByteBuf handleEscape(ByteBuf buf) { + ByteBuf out = buf.alloc().buffer(buf.readableBytes() * 2); // 预留足够空间 + + buf.resetReaderIndex(); + while (buf.readableBytes() > 0) { + byte b = buf.readByte(); + if (b == MSG_DELIMITER) { + out.writeByte(0x7d); + out.writeByte(0x01); + } else if (b == 0x7d) { + out.writeByte(0x7d); + out.writeByte(0x02); + } else { + out.writeByte(b); + } + } + + return out; + } + + /** + * 计算校验码 + * 校验码 = 消息头 + 消息体 中所有字节的异或 + */ + private byte calculateCheckCode(ByteBuf buf) { + byte checkCode = 0; + int readerIndex = buf.readerIndex(); + + while (buf.readableBytes() > 0) { + checkCode ^= buf.readByte(); + } + + buf.readerIndex(readerIndex); + return checkCode; + } + + /** + * 字符串转BCD码 + */ + private byte[] stringToBcd(String str, int length) { + if (str == null || str.length() == 0) { + return new byte[length]; + } + + // 补0至偶数长度 + int strLen = str.length(); + if (strLen % 2 != 0) { + str = "0" + str; + } + + // 确保不超过指定长度 + if (str.length() > length * 2) { + str = str.substring(0, length * 2); + } + + byte[] bcd = new byte[length]; + int index = 0; + + for (int i = 0; i < str.length(); i += 2) { + if (index >= length) { + break; + } + + // 高4位 + byte high = (byte) (Character.digit(str.charAt(i), 16) << 4); + // 低4位 + byte low = (byte) Character.digit(str.charAt(i + 1), 16); + + bcd[index++] = (byte) (high | low); + } + + return bcd; + } +} diff --git a/src/main/java/com/njzscloud/jt808/protocol/JT808Message.java b/src/main/java/com/njzscloud/jt808/protocol/JT808Message.java new file mode 100644 index 0000000..825778b --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/protocol/JT808Message.java @@ -0,0 +1,74 @@ +package com.njzscloud.jt808.protocol; + +import cn.hutool.core.util.ReflectUtil; +import com.njzscloud.common.fastjson.Fastjson; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +/** + * JT808协议消息实体类 + * 用于存储解析后的JT808协议消息内容 + */ +@Getter +@Setter +@Accessors(chain = true) +public class JT808Message { + // 消息ID (2字节) + private int messageId; + // 消息体属性 (2字节) + private int messageBodyProps; + // 终端手机号 (6字节, BCD编码) + private String terminalPhone; + // 消息流水号 (2字节) + private int flowId; + // 消息包封装项 (可选, 当消息体属性中分包标志为1时存在) + private PackageInfo packageInfo; + // 消息体 + // private ByteBuf messageBody; + private byte[] messageBody; + // 校验码 (1字节) + private byte checkCode; + + public JT808Message() { + } + + // 从消息体属性中获取消息体长度 + public int getMessageBodyLength() { + return messageBodyProps & 0x3FF; + } + + // 从消息体属性中获取是否分包 + public boolean isPackaged() { + return (messageBodyProps & 0x4000) != 0; + } + + // 从消息体属性中获取加密方式 + public int getEncryptionType() { + return (messageBodyProps >> 10) & 0x07; + } + + @Override + public String toString() { + return Fastjson.toJsonStr(this); + } + + public T getMessageBody(Class clazz) { + if (messageBody == null || messageBody.length == 0) { + return null; + } + return ReflectUtil.newInstance(clazz, (Object) this.messageBody); + } + + // 消息包封装项内部类 + @Setter + @Getter + @Accessors(chain = true) + public static class PackageInfo { + // 消息总包数 (2字节) + private int totalPackages; + // 当前包序号 (2字节) + private int currentPackageNo; + } + +} diff --git a/src/main/java/com/njzscloud/jt808/protocol/JT808MessageHandler.java b/src/main/java/com/njzscloud/jt808/protocol/JT808MessageHandler.java new file mode 100644 index 0000000..5405c6b --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/protocol/JT808MessageHandler.java @@ -0,0 +1,60 @@ +package com.njzscloud.jt808.protocol; + +import com.njzscloud.jt808.util.JT808; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.DefaultChannelGroup; +import io.netty.util.concurrent.GlobalEventExecutor; +import lombok.extern.slf4j.Slf4j; + +/** + * JT808消息处理器,处理解码后的消息并发送响应 + */ +@Slf4j +public class JT808MessageHandler extends ChannelInboundHandlerAdapter { + // 管理所有连接的客户端 + private static final ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + log.info("客户端连接: {}", ctx.channel().remoteAddress()); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof JT808Message) { + JT808Message message = (JT808Message) msg; + log.info("收到消息, 终端: {}, 消息ID: 0x{}, 流水号: {}", message.getTerminalPhone(), Integer.toHexString(message.getMessageId()), message.getFlowId()); + // 根据消息类型处理 + handleMessageByType(ctx, message); + } else { + super.channelRead(ctx, msg); + } + } + + /** + * 根据消息ID处理不同类型的消息 + */ + private void handleMessageByType(ChannelHandlerContext ctx, JT808Message message) { + int messageId = message.getMessageId(); + if (messageId == 0x0100) { + JT808.register(message.getTerminalPhone(), ctx.channel()); + } + + JT808.triggerListener(messageId, message); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + log.info("客户端断开连接: {}", ctx.channel().remoteAddress()); + clients.remove(ctx.channel()); + JT808.unregister(ctx.channel()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + log.error("发生异常: {}", cause.getMessage(), cause); + ctx.close(); + } +} diff --git a/src/main/java/com/njzscloud/jt808/util/FlowId.java b/src/main/java/com/njzscloud/jt808/util/FlowId.java new file mode 100644 index 0000000..4538471 --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/util/FlowId.java @@ -0,0 +1,17 @@ +package com.njzscloud.jt808.util; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FlowId { + private static final Map flowId = new ConcurrentHashMap<>(); + + public static synchronized int next(int messageId) { + Integer i = flowId.get(messageId); + if (i == null || i >= 0xFFFF) { + i = 0; + } + flowId.put(messageId, i++); + return i; + } +} diff --git a/src/main/java/com/njzscloud/jt808/util/JT808.java b/src/main/java/com/njzscloud/jt808/util/JT808.java new file mode 100644 index 0000000..20c3fc1 --- /dev/null +++ b/src/main/java/com/njzscloud/jt808/util/JT808.java @@ -0,0 +1,213 @@ +package com.njzscloud.jt808.util; + +import com.njzscloud.jt808.protocol.JT808Message; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +/** + * JT808消息发送工具类 + * 提供便捷的方法发送各种类型的JT808消息 + */ +@Slf4j +public class JT808 { + // 终端手机号与Channel的映射 + private static final Map terminalChannels = new ConcurrentHashMap<>(); + + private static final Map>> listeners = new ConcurrentHashMap<>(); + + /** + * 注册消息监听器 + */ + public static void addListener(Integer messageId, Consumer listener) { + if (messageId != null && listener != null) { + listeners.computeIfAbsent(messageId, k -> new java.util.ArrayList<>()).add(listener); + } + } + + /** + * 触发消息监听器 + */ + public static void triggerListener(Integer messageId, JT808Message message) { + if (messageId != null && message != null) { + List> consumers = listeners.get(messageId); + if (consumers != null) { + consumers.forEach(m -> m.accept(message)); + } else { + + log.warn("当前消息无处理函数: 0x{}、消息内容: {}", Integer.toHexString(messageId), message); + } + } + } + + /** + * 移除消息监听器 + */ + public static void unregisterListener(String messageId) { + if (messageId != null) { + listeners.remove(messageId); + } + } + + /** + * 注册终端与通道的映射关系 + */ + public static void register(String terminalId, Channel channel) { + if (terminalId != null && channel != null) { + if (terminalChannels.get(terminalId) != channel) { + terminalChannels.put(terminalId, channel); + } + } + } + + /** + * 移除终端与通道的映射关系 + */ + public static void unregister(Channel channel) { + if (channel != null) { + terminalChannels.values().removeIf(ch -> ch == channel); + } + } + + + /** + * 发送通用应答消息 (0x8001) + */ + public static void sendGeneralResponse(JT808Message message) { + sendMessage(message.getTerminalPhone(), createGeneralMessage(message)); + } + + /** + * 发送终端注册应答消息 (0x8100) + */ + public static void sendTerminalRegisterResponse(String terminalPhone, int flowId, String authCode, int result) { + // 构建消息体 + ByteBuf body = Unpooled.buffer(); + body.writeByte(result); // 结果 + if (result == 0) { // 成功才需要后面的字段 + writeString(body, authCode); // 鉴权码 + } + + // 构建消息 + byte[] bytes = ByteBufUtil.getBytes(body); + body.release(); + JT808Message message = createBaseMessage(terminalPhone, 0x8100, bytes); + message.setFlowId(flowId); + + // 发送消息 + sendMessage(terminalPhone, message); + } + + /** + * 发送文本信息下发消息 (0x8300) + */ + public static void sendTextMessage(String terminalPhone, String content, int messageType) { + // 构建消息体 + ByteBuf body = Unpooled.buffer(); + body.writeByte(messageType); // 消息类型 + writeString(body, content); // 消息内容 + byte[] bytes = ByteBufUtil.getBytes(body); + body.release(); + // 构建消息 + JT808Message message = createBaseMessage(terminalPhone, 0x8300, bytes); + + // 发送消息 + sendMessage(terminalPhone, message); + } + + + /** + * 向指定终端发送消息 + */ + public static void sendMessage(String terminalId, JT808Message message) { + if (terminalId == null || message == null) { + return; + } + + Channel channel = terminalChannels.get(terminalId); + if (channel != null && channel.isActive()) { + channel.writeAndFlush(message) + .addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + log.info("消息发送成功, 终端: {}, 消息ID: 0x{}, 流水号: {}", terminalId, Integer.toHexString(message.getMessageId()), message.getFlowId()); + } else { + log.error("消息发送失败, 终端: {}", terminalId, future.cause()); + } + }); + } else { + unregister(channel); + // 终端不在线或通道已关闭 + log.warn("终端不在线: {}", terminalId); + } + } + + /** + * 创建基础消息对象 + */ + public static JT808Message createBaseMessage(String terminalId, int messageId) { + return createBaseMessage(terminalId, messageId, null); + } + + public static JT808Message createBaseMessage(String terminalId, int messageId, byte[] body) { + JT808Message message = new JT808Message() + .setFlowId(FlowId.next(messageId)); + + // 设置消息ID + message.setMessageId(messageId); + + // 设置消息体属性 + // 其他属性默认:不分包,不加密 + if (body != null) { + message.setMessageBodyProps(body.length); + } else { + message.setMessageBodyProps(0); + } + + // 设置终端手机号 + message.setTerminalPhone(terminalId); + + // 设置消息体 + message.setMessageBody(body); + + return message; + } + + public static JT808Message createGeneralMessage(JT808Message message) { + String terminalPhone = message.getTerminalPhone(); + int flowId = message.getFlowId(); + int messageId = message.getMessageId(); + + ByteBuf body = Unpooled.buffer(); + ByteBufUtil.writeShortBE(body, flowId); + ByteBufUtil.writeShortBE(body, messageId); + body.writeByte(0); + + // 构建消息 + byte[] bytes = ByteBufUtil.getBytes(body); + body.release(); + return JT808.createBaseMessage(terminalPhone, 0x8001, bytes); + } + + /** + * 写入字符串(JT808协议格式:1字节长度 + 内容) + */ + private static void writeString(ByteBuf buf, String str) { + if (str == null || str.isEmpty()) { + buf.writeByte(0); + return; + } + + byte[] bytes = str.getBytes(); + // 字符串长度(1字节) + 字符串内容 + buf.writeByte(bytes.length); + buf.writeBytes(bytes); + } +} diff --git a/src/main/java/com/njzscloud/server/TcpServer.java b/src/main/java/com/njzscloud/server/TcpServer.java new file mode 100644 index 0000000..8661fa0 --- /dev/null +++ b/src/main/java/com/njzscloud/server/TcpServer.java @@ -0,0 +1,71 @@ +package com.njzscloud.server; + +import com.njzscloud.jt808.protocol.JT808Decoder; +import com.njzscloud.jt808.protocol.JT808Encoder; +import com.njzscloud.jt808.protocol.JT808MessageHandler; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TcpServer { + private final int port; + private final int bossThreads; + private final int workerThreads; + + public TcpServer(int port, int bossThreads, int workerThreads) { + this.port = port; + this.bossThreads = bossThreads; + this.workerThreads = workerThreads; + } + + public TcpServer() { + this.port = 18888; + this.bossThreads = 5; + this.workerThreads = 200; + } + + + public void start() throws Exception { + // 创建两个EventLoopGroup,bossGroup用于接收客户端连接,workerGroup用于处理连接后的IO操作 + EventLoopGroup bossGroup = new MultiThreadIoEventLoopGroup(bossThreads, NioIoHandler.newFactory()); + EventLoopGroup workerGroup = new MultiThreadIoEventLoopGroup(workerThreads, NioIoHandler.newFactory()); + + try { + // 服务器启动辅助类 + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + // 指定使用NIO的服务器通道 + .channel(NioServerSocketChannel.class) + // 设置服务器通道的选项 + .option(ChannelOption.SO_BACKLOG, 128) + // 设置客户端通道的选项 + .childOption(ChannelOption.SO_KEEPALIVE, true) + // 设置通道初始化器,用于配置新接入的通道 + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) throws Exception { + // 添加JT808协议处理器链 + ch.pipeline() + .addLast(new JT808Decoder()) // 解码器 + .addLast(new JT808Encoder()) // 编码器 + .addLast(new JT808MessageHandler()); // 消息处理器 + } + }); + + // 绑定端口并开始接收连接 + ChannelFuture future = bootstrap.bind(port).sync(); + log.info("服务器已启动,监听端口: {}", port); + // 等待服务器 socket 关闭 + future.channel().closeFuture().sync(); + } finally { + // 关闭事件循环组 + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + log.info("服务器已停止"); + } + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/DeviceInfo.java b/src/main/java/com/njzscloud/tuqiang/DeviceInfo.java new file mode 100644 index 0000000..e94432c --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/DeviceInfo.java @@ -0,0 +1,207 @@ +package com.njzscloud.tuqiang; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class DeviceInfo { + // GPS定位状态 + private String gpsValid; // 定位有效性(V/N) + private int satelliteCount; // 卫星数量 + private int gpsAccuracy; // 定位精度或备用参数 + private String gpsModuleStatus; // GPS模块状态(OK/ERR) + + // 网络与信号状态 + private String serverIp; // 服务器IP + private int serverPort; // 服务器端口 + private int cgregStatus; // 网络注册状态 + private int csq; // 信号强度 + + // 硬件与电源状态 + private double batteryVoltage; // 电池电压(V) + private int powerMode; // 供电模式(1=外接电源,2=电池) + private int beidouStatus; // 北斗模块状态(0=未启用,1=启用) + + // 功能与附加状态 + private String alarmStatus; // 报警状态(*) + private int counter; // 计数器/里程相关 + private int reportInterval; // 定位上报间隔(秒) + private int powerSavingMode; // 省电模式(0=关闭,1=开启) + private int backupPower; // 备用电源状态 + private int vibrationStatus; // 震动传感器状态 + private int heartbeatInterval; // 心跳包间隔(秒) + private int accStatus; // ACC点火状态(1=通电,0=断电) + private int chargingStatus; // 充电状态(0=未充电,1=充电中) + private int bluetoothStatus; // 蓝牙状态(1=开启,0=关闭) + private String extendStatus; // 扩展状态码(十六进制) + private String temperature; // 温度传感器状态 + private String deviceTime; // 设备本地时间 + private int agpsStatus; // AGPS状态(1=开启,0=关闭) + private String displacement; // 位移距离 + private int speedStatus; // 速度状态(0=静止) + private String deviceId; // 设备ID/IMEI后7位 + private int lowBatteryThreshold; // 低电阈值状态 + private int accelerometerStatus; // 加速度传感器状态 + private int flightMode; // 飞行模式(0=关闭,1=开启) + + + public static DeviceInfo parse(String gpsString) { + DeviceInfo data = new DeviceInfo(); + if (gpsString == null || gpsString.isEmpty()) { + return data; + } + + // 去除首尾的<和> + String content = gpsString.replaceAll("[<>]", ""); + // 按*分割字段 + String[] fields = content.split("\\*"); + + for (String field : fields) { + if (field.contains(":")) { + String[] keyValue = field.split(":", 2); + if (keyValue.length == 2) { + String key = keyValue[0].trim(); + String value = keyValue[1].trim(); + parseField(data, key, value); + } + } + } + + return data; + } + + private static void parseField(DeviceInfo data, String key, String value) { + switch (key) { + case "GPS": + parseGpsField(data, value); + break; + case "GP": + data.gpsModuleStatus = value; + break; + case "T": + parseServerField(data, value); + break; + case "CGREG": + data.cgregStatus = parseInt(value); + break; + case "CSQ": + data.csq = parseInt(value); + break; + case "5Y": + data.batteryVoltage = parseDouble(value); + break; + case "B": + data.powerMode = parseInt(value); + break; + case "BD": + data.beidouStatus = parseInt(value); + break; + case "A": + data.alarmStatus = value; + break; + case "C": + data.counter = parseInt(value); + break; + case "O": + data.reportInterval = parseInt(value); + break; + case "CS": + data.powerSavingMode = parseInt(value); + break; + case "3U": + data.backupPower = parseInt(value); + break; + case "3Z": + data.vibrationStatus = parseInt(value); + break; + case "H": + data.heartbeatInterval = parseInt(value); + break; + case "2A": + data.accStatus = parseInt(value); + break; + case "5C": + data.chargingStatus = parseInt(value); + break; + case "60": + data.bluetoothStatus = parseInt(value); + break; + case "3E": + data.extendStatus = value; + break; + case "5T": + data.temperature = value; + break; + case "9E": + data.deviceTime = value; + break; + case "1H": + data.agpsStatus = parseInt(value); + break; + case "3D": + data.displacement = value; + break; + case "2S": + data.speedStatus = parseInt(value); + break; + case "I": + data.deviceId = value; + break; + case "LT": + data.lowBatteryThreshold = parseInt(value); + break; + case "AG": + data.accelerometerStatus = parseInt(value); + break; + case "FLY": + data.flightMode = parseInt(value); + break; + // 忽略未知字段 + default: + break; + } + } + + private static void parseGpsField(DeviceInfo data, String value) { + // 格式: (V,0,0) + String cleaned = value.replaceAll("[()]", ""); + String[] parts = cleaned.split(","); + if (parts.length >= 3) { + data.gpsValid = parts[0].trim(); + data.satelliteCount = parseInt(parts[1].trim()); + data.gpsAccuracy = parseInt(parts[2].trim()); + } + } + + private static void parseServerField(DeviceInfo data, String value) { + // 格式: 139.224.54.144,30100 + String[] parts = value.split(","); + if (parts.length >= 2) { + data.serverIp = parts[0].trim(); + data.serverPort = parseInt(parts[1].trim()); + } + } + + private static int parseInt(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return -1; // 用-1表示解析失败 + } + } + + private static double parseDouble(String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return -1; // 用-1表示解析失败 + } + } + + +} diff --git a/src/main/java/com/njzscloud/tuqiang/Listener.java b/src/main/java/com/njzscloud/tuqiang/Listener.java new file mode 100644 index 0000000..fd3355d --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/Listener.java @@ -0,0 +1,10 @@ +package com.njzscloud.tuqiang; + +import java.lang.annotation.*; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface Listener { + int messageId(); +} diff --git a/src/main/java/com/njzscloud/tuqiang/Listeners.java b/src/main/java/com/njzscloud/tuqiang/Listeners.java new file mode 100644 index 0000000..f4d152e --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/Listeners.java @@ -0,0 +1,80 @@ +package com.njzscloud.tuqiang; + +import cn.hutool.core.thread.ThreadUtil; +import com.njzscloud.jt808.protocol.JT808Message; +import com.njzscloud.jt808.util.JT808; +import com.njzscloud.tuqiang.msg.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Listeners { + + @Listener(messageId = 0x0100) + public void onTerminalRegister(JT808Message message) { + String terminalPhone = message.getTerminalPhone(); + ByteBuf body = Unpooled.buffer(); + ByteBufUtil.writeShortBE(body, message.getFlowId()); + body.writeByte(0); + ByteBufUtil.writeUtf8(body, terminalPhone); + + // 构建消息 + byte[] bytes = ByteBufUtil.getBytes(body); + body.release(); + JT808.sendMessage(terminalPhone, JT808.createBaseMessage(terminalPhone, 0x8100, bytes)); + + TerminalRegisterMsg terminalRegisterMsg = message.getMessageBody(TerminalRegisterMsg.class); + + log.info("终端注册消息: {}", terminalRegisterMsg); + } + + @Listener(messageId = 0x0102) + public void onTerminalAuth(JT808Message message) { + JT808.sendGeneralResponse(message); + TerminalAuthMsg authMsg = message.getMessageBody(TerminalAuthMsg.class); + log.info("终端鉴权消息: {}", authMsg); + + // ThreadUtil.sleep(10000); + // log.info("发送指令: {}", ""); + // JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("").toBytes())); + ThreadUtil.sleep(10000); + JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("").toBytes())); + + } + + @Listener(messageId = 0x0002) + public void onHeartbeat(JT808Message message) { + log.info("终端心跳消息: {}", message.getTerminalPhone()); + JT808.sendGeneralResponse(message); + } + + @Listener(messageId = 0x0200) + public void onLocationReport(JT808Message message) { + LocationReportMsg locationReportMsg = message.getMessageBody(LocationReportMsg.class); + log.info("终端位置信息汇报消息: {}", locationReportMsg); + JT808.sendGeneralResponse(message); + } + + @Listener(messageId = 0x0001) + public void onTerminalGeneralResponse(JT808Message message) { + TerminalGeneralResponseMsg terminalGeneralResponseMsg = message.getMessageBody(TerminalGeneralResponseMsg.class); + log.info("终端通用应答消息: {}", terminalGeneralResponseMsg); + JT808.sendGeneralResponse(message); + } + + @Listener(messageId = 0x6006) + public void onTerminalTxtReport(JT808Message message) { + TerminalTxtReportMsg terminalTxtReportMsg = message.getMessageBody(TerminalTxtReportMsg.class); + log.info("终端文本信息汇报消息: {}", terminalTxtReportMsg); + JT808.sendGeneralResponse(message); + } + + @Listener(messageId = 0x0201) + public void onSearchLocationResponse(JT808Message message) { + SearchLocationResponseMsg searchLocationResponseMsg = message.getMessageBody(SearchLocationResponseMsg.class); + log.info("查询位置响应消息: {}", searchLocationResponseMsg); + JT808.sendGeneralResponse(message); + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/Tuqiang.java b/src/main/java/com/njzscloud/tuqiang/Tuqiang.java new file mode 100644 index 0000000..ec25445 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/Tuqiang.java @@ -0,0 +1,31 @@ +package com.njzscloud.tuqiang; + +import com.njzscloud.jt808.util.JT808; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +@Slf4j +public class Tuqiang { + + public Tuqiang() { + addListeners(new Listeners()); + } + + private void addListeners(Object object) { + Method[] methods = object.getClass().getMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(Listener.class)) { + Listener listener = method.getAnnotation(Listener.class); + JT808.addListener(listener.messageId(), (msg) -> { + try { + method.invoke(object, msg); + } catch (Exception e) { + log.error("处理消息 {} 时出错", listener.messageId(), e); + } + }); + } + } + } + +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/LocationReportMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/LocationReportMsg.java new file mode 100644 index 0000000..8b11c61 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/LocationReportMsg.java @@ -0,0 +1,82 @@ +package com.njzscloud.tuqiang.msg; + +import com.njzscloud.common.utils.BCD; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class LocationReportMsg { + /** + * 1 1:超速报警 标志维持至报警条件解除 + * 7 1:终端主电源欠压 标志维持至报警条件解除 + * 8 1:声控录音报警 + */ + private long alarmFlag; + /** + * 0 0:ACC 关 1:ACC 开 + * 1 0:未定位 1:定位 + * 2 0:北纬 1:南纬 + * 3 0:东经 1:西经 + */ + private long status; + private double longitude; + private double latitude; + private int altitude; + private double speed; + private int direction; + private String time; + + public LocationReportMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + this.alarmFlag = body.readUnsignedInt(); + this.status = body.readUnsignedInt(); + double longitude = body.readUnsignedInt() * 1.0 / 1000000; + this.longitude = isWest() ? -longitude : longitude; + double latitude = body.readUnsignedInt() * 1.0 / 1000000; + this.latitude = isSouth() ? -latitude : latitude; + this.altitude = body.readUnsignedShort(); + this.speed = body.readUnsignedShort() * 1.0 / 10; + this.direction = body.readUnsignedShort(); + byte[] terminalIdBytes = new byte[7]; + body.readBytes(terminalIdBytes); + this.time = BCD.bcdToStr(terminalIdBytes); + + + body.release(); + } + + public boolean isOverspeed() { + return (alarmFlag & 0x02) == 0x02; + } + + public boolean isPowerUnderVoltage() { + return (alarmFlag & 0x40) == 0x40; + } + + public boolean isSoundAlarm() { + return (alarmFlag & 0x80) == 0x80; + } + + public boolean isAccOn() { + return (status & 0x01) == 0x01; + } + + public boolean isPosition() { + return (status & 0x02) == 0x02; + } + + public boolean isSouth() { + return (status & 0x04) == 0x04; + } + + public boolean isWest() { + return (status & 0x08) == 0x08; + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/PublishDirectiveMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/PublishDirectiveMsg.java new file mode 100644 index 0000000..9f3e201 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/PublishDirectiveMsg.java @@ -0,0 +1,27 @@ +package com.njzscloud.tuqiang.msg; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class PublishDirectiveMsg { + private byte flag; + private String directive; + + public PublishDirectiveMsg(String directive) { + this.flag = 0x01; + this.directive = directive; + } + + public byte[] toBytes() { + byte[] messageBody = new byte[directive.length() + 1]; + messageBody[0] = flag; + System.arraycopy(directive.getBytes(), 0, messageBody, 1, directive.length()); + return messageBody; + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/SearchLocationResponseMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/SearchLocationResponseMsg.java new file mode 100644 index 0000000..2643d5d --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/SearchLocationResponseMsg.java @@ -0,0 +1,26 @@ +package com.njzscloud.tuqiang.msg; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class SearchLocationResponseMsg { + // 消息流水号 (2字节) + private int flowId; + private LocationReportMsg locationReportMsg; + + public SearchLocationResponseMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + this.flowId = body.readUnsignedShort(); + byte[] locationReportMsgBytes = ByteBufUtil.getBytes(body); + this.locationReportMsg = new LocationReportMsg(locationReportMsgBytes); + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/TerminalAuthMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/TerminalAuthMsg.java new file mode 100644 index 0000000..6064046 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/TerminalAuthMsg.java @@ -0,0 +1,24 @@ +package com.njzscloud.tuqiang.msg; + +import cn.hutool.core.util.CharsetUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TerminalAuthMsg { + private String authCode; + + public TerminalAuthMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + byte[] authCodeBytes = new byte[body.readableBytes()]; + body.readBytes(authCodeBytes); + this.authCode = new String(authCodeBytes, CharsetUtil.CHARSET_GBK); + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/TerminalGeneralResponseMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/TerminalGeneralResponseMsg.java new file mode 100644 index 0000000..a4ba5a9 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/TerminalGeneralResponseMsg.java @@ -0,0 +1,45 @@ +package com.njzscloud.tuqiang.msg; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TerminalGeneralResponseMsg { + private int flowId; + private int messageId; + private int result; + + public TerminalGeneralResponseMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + this.flowId = body.readUnsignedShort(); + this.messageId = body.readUnsignedShort(); + this.result = body.readByte(); + body.release(); + } + + public boolean isSuccess() { + return result == 0; + } + + public String getMsg() { + switch (this.result) { + case 0: + return "成功"; + case 1: + return "失败"; + case 2: + return "消息有误"; + case 3: + return "不支持"; + default: + return "未知"; + } + } +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/TerminalRegisterMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/TerminalRegisterMsg.java new file mode 100644 index 0000000..4a8c052 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/TerminalRegisterMsg.java @@ -0,0 +1,88 @@ +package com.njzscloud.tuqiang.msg; + +import cn.hutool.core.util.CharsetUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TerminalRegisterMsg { + /** + * 标示终端安装车辆所在的省域,0 保留,由平台取默认值。省域 + * ID 采用 GB/T 2260 中规定的行政区划代码六位中前两位 + */ + private int province; + /** + * 标示终端安装车辆所在的市域,0 保留,由平台取默认值。市县 + * 域 ID 采用 GB/T 2260 中规定的行政区划代码六位中后四位 + */ + private int city; + /** + * 五个字节,终端制造商编号 + */ + private String manufacturerId; + /** + * 八个字节,此终端型号由制造商自行定义,位数不足八位的,补 + * 空格,(注:补充说明中要求为 20 字节,不足后补 0x00) + */ + private String terminalModel; + /** + * 七个字节,有大写字母和数字组成,此终端 ID 由制造商自行定义, + * 位数不足后面补 0x00 + */ + private String terminalId; + /** + * 车牌颜色,按照 JT/T 415—2006 中 5.4.12 的规定,未上牌时,取 + * 值为 0 + */ + private byte plateColor; + /** + * 公安交通管理部门颁发的机动车号牌 + * (注:补充说明中要求如车牌颜色为 0 时,这里表示车辆 VIN 号) + */ + private String plate; + + public TerminalRegisterMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + // 读取省域ID(WORD,2字节,大端序) + this.province = body.readUnsignedShort(); + + // 读取市县域ID(WORD,2字节,大端序) + this.city = body.readUnsignedShort(); + + // 读取制造商ID(BYTE[5],5字节,转为字符串) + byte[] manufacturerIdBytes = new byte[5]; + body.readBytes(manufacturerIdBytes); + this.manufacturerId = new String(manufacturerIdBytes).trim(); + + // 读取终端型号(BYTE[8],8字节,转为字符串) + byte[] terminalModelBytes = new byte[8]; + body.readBytes(terminalModelBytes); + this.terminalModel = new String(terminalModelBytes).trim(); + + // 读取终端ID(BYTE[7],7字节,转为字符串) + byte[] terminalIdBytes = new byte[7]; + body.readBytes(terminalIdBytes); + this.terminalId = new String(terminalIdBytes).trim(); + + // 读取车牌颜色(BYTE,1字节) + this.plateColor = body.readByte(); + + // 读取车牌(STRING,剩余字节,转为字符串) + // 假设剩余所有字节都是车牌内容,实际可根据协议确定长度 + byte[] plateBytes = new byte[body.readableBytes()]; + body.readBytes(plateBytes); + this.plate = new String(plateBytes, CharsetUtil.CHARSET_GBK); + + body.release(); + } + +} diff --git a/src/main/java/com/njzscloud/tuqiang/msg/TerminalTxtReportMsg.java b/src/main/java/com/njzscloud/tuqiang/msg/TerminalTxtReportMsg.java new file mode 100644 index 0000000..156a604 --- /dev/null +++ b/src/main/java/com/njzscloud/tuqiang/msg/TerminalTxtReportMsg.java @@ -0,0 +1,30 @@ +package com.njzscloud.tuqiang.msg; + +import cn.hutool.core.util.CharsetUtil; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; + +@Getter +@Setter +@ToString +@Accessors(chain = true) +public class TerminalTxtReportMsg { + private byte type; + /** + * 文本信息 + */ + private String text; + + public TerminalTxtReportMsg(byte[] bytes) { + ByteBuf body = Unpooled.wrappedBuffer(bytes); + this.type = body.readByte(); + this.text = body.toString(body.readerIndex(), body.readableBytes(), + this.type == 0x00 ? + CharsetUtil.CHARSET_GBK : + CharsetUtil.CHARSET_UTF_8); + } +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback.xml similarity index 87% rename from src/main/resources/logback-spring.xml rename to src/main/resources/logback.xml index af71b14..fe4903e 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback.xml @@ -1,9 +1,9 @@ - + - + @@ -27,7 +27,7 @@ ${file_pattern} - +