master
parent
2b3e56352c
commit
d70a562d95
50
pom.xml
50
pom.xml
|
|
@ -18,6 +18,33 @@
|
||||||
<artifactId>fastjson2</artifactId>
|
<artifactId>fastjson2</artifactId>
|
||||||
<version>2.0.51</version>
|
<version>2.0.51</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--<editor-fold desc="jackson">-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||||
|
<artifactId>jackson-dataformat-xml</artifactId>
|
||||||
|
<version>2.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jdk8</artifactId>
|
||||||
|
<version>2.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||||
|
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||||
|
<version>2.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<!--</editor-fold>-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.netty</groupId>
|
||||||
|
<artifactId>netty-all</artifactId>
|
||||||
|
<version>4.2.6.Final</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!--<editor-fold desc="hutool">-->
|
<!--<editor-fold desc="hutool">-->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
@ -67,28 +94,5 @@
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>1.2.11</version>
|
<version>1.2.11</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--<editor-fold desc="jackson">-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
|
||||||
<artifactId>jackson-databind</artifactId>
|
|
||||||
<version>2.13.4</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
|
||||||
<artifactId>jackson-dataformat-xml</artifactId>
|
|
||||||
<version>2.13.4</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
|
||||||
<artifactId>jackson-datatype-jdk8</artifactId>
|
|
||||||
<version>2.13.4</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
|
||||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
|
||||||
<version>2.13.4</version>
|
|
||||||
</dependency>
|
|
||||||
<!--</editor-fold>-->
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,7 +34,7 @@ public class Fastjson {
|
||||||
JSON_WRITER_CONTEXT = new JSONWriter.Context(
|
JSON_WRITER_CONTEXT = new JSONWriter.Context(
|
||||||
JSONWriter.Feature.WriteNulls, // 序列化输出空值字段
|
JSONWriter.Feature.WriteNulls, // 序列化输出空值字段
|
||||||
JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数,输出为字符串格式
|
JSONWriter.Feature.BrowserCompatible, // 在大范围超过JavaScript支持的整数,输出为字符串格式
|
||||||
JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息
|
// JSONWriter.Feature.WriteClassName, // 序列化时输出类型信息
|
||||||
// JSONWriter.Feature.PrettyFormat, // 格式化输出
|
// JSONWriter.Feature.PrettyFormat, // 格式化输出
|
||||||
JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature
|
JSONWriter.Feature.SortMapEntriesByKeys, // 对Map中的KeyValue按照Key做排序后再输出。在有些验签的场景需要使用这个Feature
|
||||||
JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString,避免科学计数法
|
JSONWriter.Feature.WriteBigDecimalAsPlain, // 序列化BigDecimal使用toPlainString,避免科学计数法
|
||||||
|
|
@ -62,8 +62,8 @@ public class Fastjson {
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
JSON_READER_CONTEXT = new JSONReader.Context(
|
JSON_READER_CONTEXT = new JSONReader.Context(
|
||||||
JSONReader.Feature.IgnoreSetNullValue, // 忽略输入为null的字段
|
JSONReader.Feature.IgnoreSetNullValue// 忽略输入为null的字段
|
||||||
JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType
|
// JSONReader.Feature.SupportAutoType // 支持自动类型,要读取带"@type"类型信息的JSON数据,需要显示打开SupportAutoType
|
||||||
);
|
);
|
||||||
JSON_READER_CONTEXT.setZoneId(zoneId);
|
JSON_READER_CONTEXT.setZoneId(zoneId);
|
||||||
JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN);
|
JSON_READER_CONTEXT.setDateFormat(DatePattern.NORM_DATETIME_PATTERN);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ILoggingEvent> {
|
||||||
|
@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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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+", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
package com.njzscloud.gps;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hello world!
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public class App {
|
|
||||||
public static void main(String[] args) {
|
|
||||||
System.out.println("Hello World!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<JT808Message> {
|
||||||
|
// 消息起始符和结束符
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> T getMessageBody(Class<T> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Integer, Integer> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String, Channel> terminalChannels = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private static final Map<Integer, List<Consumer<JT808Message>>> listeners = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册消息监听器
|
||||||
|
*/
|
||||||
|
public static void addListener(Integer messageId, Consumer<JT808Message> 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<Consumer<JT808Message>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<SocketChannel>() {
|
||||||
|
@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("服务器已停止");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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表示解析失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.njzscloud.tuqiang;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.METHOD})
|
||||||
|
public @interface Listener {
|
||||||
|
int messageId();
|
||||||
|
}
|
||||||
|
|
@ -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("发送指令: {}", "<SPBSJ*P:BSJGPS*C:0*H:300>");
|
||||||
|
// JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("<SPBSJ*P:BSJGPS*C:0*H:300>").toBytes()));
|
||||||
|
ThreadUtil.sleep(10000);
|
||||||
|
JT808.sendMessage("61000602070", JT808.createBaseMessage("61000602070", 0x8300, new PublishDirectiveMsg("<CKBSJ>").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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 "未知";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<configuration debug="false">
|
<configuration debug="false">
|
||||||
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
|
<conversionRule conversionWord="clr" converterClass="com.njzscloud.common.log.ColorConverter"/>
|
||||||
|
|
||||||
<property name="log_path" value="logs"/>
|
<property name="log_path" value="logs"/>
|
||||||
<property name="service_name" value="${project.artifactId}"/>
|
<property name="service_name" value="gps"/>
|
||||||
|
|
||||||
<property name="console_pattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr(%-5p) %blue([%t]) %cyan(%c) %blue([%M:%L]): %clr(%m%n)"/>
|
<property name="console_pattern" value="%magenta(%d{yyyy-MM-dd HH:mm:ss.SSS}) %clr(%-5p) %blue([%t]) %cyan(%c) %blue([%M:%L]): %clr(%m%n)"/>
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
<pattern>${file_pattern}</pattern>
|
<pattern>${file_pattern}</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
<logger name="com.njzscloud" level="DEBUG"/>
|
||||||
<!-- TRACE < DEBUG < INFO < WARN < ERROR -->
|
<!-- TRACE < DEBUG < INFO < WARN < ERROR -->
|
||||||
<root level="WARN">
|
<root level="WARN">
|
||||||
<appender-ref ref="console_appender"/>
|
<appender-ref ref="console_appender"/>
|
||||||
Loading…
Reference in New Issue