first commit

This commit is contained in:
lzh
2025-12-12 11:45:17 +08:00
commit dcd409e5d0
49 changed files with 3642 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
package com.hua.transport.jt808;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Jt808Application {
public static void main(String[] args) {
SpringApplication.run(Jt808Application.class, args);
}
}

View File

@@ -0,0 +1,27 @@
package com.hua.transport.jt808.common;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult<T> {
private long code;
private String message;
private T data;
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(200, "Success", data);
}
public static <T> CommonResult<T> success(T data, String message) {
return new CommonResult<>(200, message, data);
}
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<>(500, message, null);
}
}

View File

@@ -0,0 +1,46 @@
package com.hua.transport.jt808.common;
import java.nio.charset.Charset;
public class Consts {
public static final String DEFAULT_ENCODE = "GBK";
public static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_ENCODE);
/** 标识位 **/
public static final int PKG_DELIMITER = 0x7e;
/** 客户端发呆15分钟后,服务器主动断开连接 **/
public static int TCP_CLIENT_IDLE = 15;
/** 终端通用应答 **/
public static final Integer MSGID_COMMON_RESP = 0x0001;
/** 终端心跳 **/
public static final Integer MSGID_HEART_BEAT = 0x0002;
/** 终端注册 **/
public static final Integer MSGID_REGISTER = 0x0100;
/** 终端注销 **/
public static final Integer MSGID_LOG_OUT = 0x0003;
/** 终端鉴权 **/
public static final Integer MSGID_AUTHENTICATION = 0x0102;
/** 位置信息汇报 **/
public static final Integer MSGID_LOCATION_UPLOAD = 0x0200;
/** 胎压数据透传 **/
public static final Integer MSGID_TRANSMISSION_TYPE_PRESSURE = 0x0600;
/** 查询终端参数应答 **/
public static final Integer MSGID_PARAM_QUERY_RESP = 0x0104;
/** 平台通用应答 **/
public static final int CMD_COMMON_RESP = 0x8001;
/** 终端注册应答 **/
public static final int CMD_REGISTER_RESP = 0x8100;
/** 设置终端参数 **/
public static final int CMD_PARAM_SETTINGS = 0X8103;
/** 查询终端参数 **/
public static final int CMD_PARAM_QUERY = 0x8104;
}

View File

@@ -0,0 +1,70 @@
package com.hua.transport.jt808.controller;
import com.hua.transport.jt808.common.CommonResult;
import com.hua.transport.jt808.entity.dto.LocationDto;
import com.hua.transport.jt808.service.ApiLogService;
import com.hua.transport.jt808.service.DeviceService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Date;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/api/v1/device")
public class DeviceController {
@Autowired
private DeviceService deviceService;
@Autowired
private ApiLogService apiLogService;
/**
* Standard Location Report (Typed)
*/
@PostMapping("/location")
public ResponseEntity<String> reportLocation(@RequestBody LocationDto location) {
if (location.getTime() == null) {
location.setTime(new Date());
}
deviceService.processLocation(
location.getImei(),
location.getLat(),
location.getLon(),
location.getSpeed(),
location.getTime()
);
return ResponseEntity.ok("Received");
}
/**
* 接收任意格式的设备上报数据
* 1. 打印日志到文件
* 2. 推送到前端页面
*/
@PostMapping("/upload")
public CommonResult<String> receiveDeviceData(@RequestBody Map<String, Object> payload) {
// 广播日志(同时会记录到服务器日志文件)
apiLogService.broadcastLog(payload);
return CommonResult.success("设备数据接收成功");
}
/**
* 前端 SSE 连接接口,用于实时接收日志
*/
@GetMapping("/logs/stream")
public SseEmitter streamLogs() {
return apiLogService.createEmitter();
}
@GetMapping("/health")
public String health() {
return "OK";
}
}

View File

@@ -0,0 +1,205 @@
package com.hua.transport.jt808.entity;
import java.util.Arrays;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.netty.channel.Channel;
/**
* 通用数据包
* @author huaxl
*
*/
public class DataPack {
/**
* 16byte 消息头
*/
protected PackHead packHead;
// 消息体字节数组
@JsonIgnore
protected byte[] bodyBytes;
/**
* 校验码 1byte
*/
protected int checkSum;
@JsonIgnore
protected Channel channel;
public PackHead getPackHead() {
return packHead;
}
public void setPackHead(PackHead packHead) {
this.packHead = packHead;
}
public byte[] getBodyBytes() {
return bodyBytes;
}
public void setBodyBytes(byte[] bodyBytes) {
this.bodyBytes = bodyBytes;
}
public int getCheckSum() {
return checkSum;
}
public void setCheckSum(int checkSum) {
this.checkSum = checkSum;
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
@Override
public String toString() {
return "PackageData [packHead=" + packHead + ", bodyBytes=" + Arrays.toString(bodyBytes) + ", checkSum=" + checkSum + ", address=" + channel + "]";
}
public static class PackHead {
// 消息ID
protected int id;
/////// ========消息体属性
// byte[2-3]
protected int bodyPropsField;
// 消息体长度
protected int bodyLength;
// 数据加密方式
protected int encryptionType;
// 是否分包,true==>有消息包封装项
protected boolean hasSubPackage;
// 保留位[14-15]
protected String reservedBit;
/////// ========消息体属性
// 终端手机号
protected String terminalPhone;
// 流水号
protected int flowId;
//////// =====消息包封装项
// byte[12-15]
protected int infoField;
// 消息包总数(word(16))
protected long subPackage;
// 包序号(word(16))这次发送的这个消息包是分包中的第几个消息包, 从 1 开始
protected long subPackageSequeue;
//////// =====消息包封装项
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getMsgBodyLength() {
return bodyLength;
}
public void setMsgBodyLength(int msgBodyLength) {
this.bodyLength = msgBodyLength;
}
public int getEncryptionType() {
return encryptionType;
}
public void setEncryptionType(int encryptionType) {
this.encryptionType = encryptionType;
}
public String getTerminalPhone() {
return terminalPhone;
}
public void setTerminalPhone(String terminalPhone) {
this.terminalPhone = terminalPhone;
}
public int getFlowId() {
return flowId;
}
public void setFlowId(int flowId) {
this.flowId = flowId;
}
public boolean isHasSubPackage() {
return hasSubPackage;
}
public void setHasSubPackage(boolean hasSubPackage) {
this.hasSubPackage = hasSubPackage;
}
public String getReservedBit() {
return reservedBit;
}
public void setReservedBit(String reservedBit) {
this.reservedBit = reservedBit;
}
public long getSubPackage() {
return subPackage;
}
public void setSubPackage(long totalPackage) {
this.subPackage = totalPackage;
}
public long getSubPackageSequeue() {
return subPackageSequeue;
}
public void setSubPackageSequeue(long packageSequeue) {
this.subPackageSequeue = packageSequeue;
}
public int getBodyPropsField() {
return bodyPropsField;
}
public void setBodyPropsField(int bodyPropsField) {
this.bodyPropsField = bodyPropsField;
}
public void setInfoField(int infoField) {
this.infoField = infoField;
}
public int getInfoField() {
return infoField;
}
@Override
public String toString() {
return "PackHead [id=" + id + ", bodyPropsField=" + bodyPropsField + ", bodyLength=" + bodyLength
+ ", encryptionType=" + encryptionType + ", hasSubPackage=" + hasSubPackage + ", reservedBit="
+ reservedBit + ", terminalPhone=" + terminalPhone + ", flowId=" + flowId + ", infoField="
+ infoField + ", subPackage=" + subPackage + ", subPackageSequeue=" + subPackageSequeue + "]";
}
}
}

View File

@@ -0,0 +1,124 @@
package com.hua.transport.jt808.entity;
import java.net.SocketAddress;
import io.netty.channel.Channel;
public class Session {
private String id;
private String terminalPhone;
private Channel channel = null;
private boolean isAuthenticated = false;
// 消息流水号 word(16) 按发送顺序从 0 开始循环累加
private int currentFlowId = 0;
// private ChannelGroup channelGroup = new
// DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
// 客户端上次的连接时间,该值改变的情况:
// 1. terminal --> server 心跳包
// 2. terminal --> server 数据包
private long lastCommunicateTimeStamp = 0l;
public Session() {
}
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public String getTerminalPhone() {
return terminalPhone;
}
public void setTerminalPhone(String terminalPhone) {
this.terminalPhone = terminalPhone;
}
public void setId(String id) {
this.id = id;
}
public String getId() {
return id;
}
public static String buildId(Channel channel) {
return channel.id().asLongText();
}
public static Session buildSession(Channel channel) {
return buildSession(channel, null);
}
public static Session buildSession(Channel channel, String phone) {
Session session = new Session();
session.setChannel(channel);
session.setId(buildId(channel));
session.setTerminalPhone(phone);
session.setLastCommunicateTimeStamp(System.currentTimeMillis());
return session;
}
public long getLastCommunicateTimeStamp() {
return lastCommunicateTimeStamp;
}
public void setLastCommunicateTimeStamp(long lastCommunicateTimeStamp) {
this.lastCommunicateTimeStamp = lastCommunicateTimeStamp;
}
public SocketAddress getRemoteAddr() {
System.out.println(this.channel.remoteAddress().getClass());
return this.channel.remoteAddress();
}
public boolean isAuthenticated() {
return isAuthenticated;
}
public void setAuthenticated(boolean isAuthenticated) {
this.isAuthenticated = isAuthenticated;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((id == null) ? 0 : id.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Session other = (Session) obj;
if (id == null) {
if (other.id != null)
return false;
} else if (!id.equals(other.id))
return false;
return true;
}
@Override
public String toString() {
return "Session [id=" + id + ", terminalPhone=" + terminalPhone + ", channel=" + channel + "]";
}
public synchronized int currentFlowId() {
if (currentFlowId >= 0xffff)
currentFlowId = 0;
return currentFlowId++;
}
}

View File

@@ -0,0 +1,14 @@
package com.hua.transport.jt808.entity.dto;
import lombok.Data;
import java.util.Date;
@Data
public class LocationDto {
private String imei;
private double lat;
private double lon;
private float speed;
private Date time;
}

View File

@@ -0,0 +1,44 @@
package com.hua.transport.jt808.entity.request;
import java.util.Arrays;
import com.hua.transport.jt808.common.Consts;
import com.hua.transport.jt808.entity.DataPack;
/**
* 终端鉴权消息
*
* @author huaxl
*
*/
public class AuthenticationPack extends DataPack {
private String authCode;
public AuthenticationPack() {
}
public AuthenticationPack(DataPack packageData) {
this();
this.channel = packageData.getChannel();
this.checkSum = packageData.getCheckSum();
this.bodyBytes = packageData.getBodyBytes();
this.packHead = packageData.getPackHead();
this.authCode = new String(packageData.getBodyBytes(), Consts.DEFAULT_CHARSET);
}
public void setAuthCode(String authCode) {
this.authCode = authCode;
}
public String getAuthCode() {
return authCode;
}
@Override
public String toString() {
return "TerminalAuthenticationMsg [authCode=" + authCode + ", msgHeader=" + packHead + ", msgBodyBytes="
+ Arrays.toString(bodyBytes) + ", checkSum=" + checkSum + ", channel=" + channel + "]";
}
}

View File

@@ -0,0 +1,118 @@
package com.hua.transport.jt808.entity.request;
import java.util.Date;
import com.hua.transport.jt808.entity.DataPack;
/**
* 位置信息汇报消息
*
* @author huaxl
*
*/
public class LocationPack extends DataPack {
// 告警信息
// byte[0-3]
private int warningFlagField;
// byte[4-7] 状态(DWORD(32))
private int statusField;
// byte[8-11] 纬度(DWORD(32))
private float latitude;
// byte[12-15] 经度(DWORD(32))
private float longitude;
// byte[16-17] 高程(WORD(16)) 海拔高度,单位为米( m
// TODO ==>int?海拔
private int elevation;
// byte[18-19] 速度(WORD) 1/10km/h
// TODO ==>float?速度
private float speed;
// byte[20-21] 方向(WORD) 0-359正北为 0顺时针
private int direction;
// byte[22-x] 时间(BCD[6]) YY-MM-DD-hh-mm-ss
// GMT+8 时间,本标准中之后涉及的时间均采用此时区
private Date time;
public LocationPack() {
}
public LocationPack(DataPack packageData) {
this();
this.channel = packageData.getChannel();
this.checkSum = packageData.getCheckSum();
this.bodyBytes = packageData.getBodyBytes();
this.packHead = packageData.getPackHead();
}
public float getLatitude() {
return latitude;
}
public void setLatitude(float latitude) {
this.latitude = latitude;
}
public float getLongitude() {
return longitude;
}
public void setLongitude(float longitude) {
this.longitude = longitude;
}
public int getElevation() {
return elevation;
}
public void setElevation(int elevation) {
this.elevation = elevation;
}
public float getSpeed() {
return speed;
}
public void setSpeed(float speed) {
this.speed = speed;
}
public int getDirection() {
return direction;
}
public void setDirection(int direction) {
this.direction = direction;
}
public Date getTime() {
return time;
}
public void setTime(Date time) {
this.time = time;
}
public int getWarningFlagField() {
return warningFlagField;
}
public void setWarningFlagField(int warningFlagField) {
this.warningFlagField = warningFlagField;
}
public int getStatusField() {
return statusField;
}
public void setStatusField(int statusField) {
this.statusField = statusField;
}
@Override
public String toString() {
return "LocationInfoUploadMsg [warningFlagField=" + warningFlagField + ", statusField=" + statusField
+ ", latitude=" + latitude + ", longitude=" + longitude + ", elevation=" + elevation + ", speed="
+ speed + ", direction=" + direction + ", time=" + time + "]";
}
}

View File

@@ -0,0 +1,137 @@
package com.hua.transport.jt808.entity.request;
import java.util.Arrays;
import com.hua.transport.jt808.entity.DataPack;
/**
* 终端注册消息
*
* @author huaxl
*
*/
public class RegisterPack extends DataPack {
private TerminalRegInfo terminalRegInfo;
public RegisterPack() {
}
public RegisterPack(DataPack packageData) {
this();
this.channel = packageData.getChannel();
this.checkSum = packageData.getCheckSum();
this.bodyBytes = packageData.getBodyBytes();
this.packHead = packageData.getPackHead();
}
public TerminalRegInfo getTerminalRegInfo() {
return terminalRegInfo;
}
public void setTerminalRegInfo(TerminalRegInfo msgBody) {
this.terminalRegInfo = msgBody;
}
@Override
public String toString() {
return "TerminalRegisterMsg [terminalRegInfo=" + terminalRegInfo + ", msgHeader=" + packHead
+ ", msgBodyBytes=" + Arrays.toString(bodyBytes) + ", checkSum=" + checkSum + ", channel=" + channel
+ "]";
}
public static class TerminalRegInfo {
// 省域ID(WORD),设备安装车辆所在的省域省域ID采用GB/T2260中规定的行政区划代码6位中前两位
// 0保留由平台取默认值
private int provinceId;
// 市县域ID(WORD) 设备安装车辆所在的市域或县域,市县域ID采用GB/T2260中规定的行 政区划代码6位中后四位
// 0保留由平台取默认值
private int cityId;
// 制造商ID(BYTE[5]) 5 个字节,终端制造商编码
private String manufacturerId;
// 终端型号(BYTE[8]) 八个字节, 此终端型号 由制造商自行定义 位数不足八位的,补空格。
private String terminalType;
// 终端ID(BYTE[7]) 七个字节, 由大写字母 和数字组成, 此终端 ID由制造 商自行定义
private String terminalId;
/**
*
* 车牌颜色(BYTE) 车牌颜色,按照 JT/T415-2006 的 5.4.12 未上牌时取值为0<br>
* 0===未上车牌<br>
* 1===蓝色<br>
* 2===黄色<br>
* 3===黑色<br>
* 4===白色<br>
* 9===其他
*/
private int licensePlateColor;
// 车牌(STRING) 公安交 通管理部门颁 发的机动车号牌
private String licensePlate;
public TerminalRegInfo() {
}
public int getProvinceId() {
return provinceId;
}
public void setProvinceId(int provinceId) {
this.provinceId = provinceId;
}
public int getCityId() {
return cityId;
}
public void setCityId(int cityId) {
this.cityId = cityId;
}
public String getManufacturerId() {
return manufacturerId;
}
public void setManufacturerId(String manufacturerId) {
this.manufacturerId = manufacturerId;
}
public String getTerminalType() {
return terminalType;
}
public void setTerminalType(String terminalType) {
this.terminalType = terminalType;
}
public String getTerminalId() {
return terminalId;
}
public void setTerminalId(String terminalId) {
this.terminalId = terminalId;
}
public int getLicensePlateColor() {
return licensePlateColor;
}
public void setLicensePlateColor(int licensePlate) {
this.licensePlateColor = licensePlate;
}
public String getLicensePlate() {
return licensePlate;
}
public void setLicensePlate(String licensePlate) {
this.licensePlate = licensePlate;
}
@Override
public String toString() {
return "TerminalRegInfo [provinceId=" + provinceId + ", cityId=" + cityId + ", manufacturerId="
+ manufacturerId + ", terminalType=" + terminalType + ", terminalId=" + terminalId
+ ", licensePlateColor=" + licensePlateColor + ", licensePlate=" + licensePlate + "]";
}
}
}

View File

@@ -0,0 +1,55 @@
package com.hua.transport.jt808.entity.response;
public class RegisterBodyPack {
public static final byte success = 0;
public static final byte car_already_registered = 1;
public static final byte car_not_found = 2;
public static final byte terminal_already_registered = 3;
public static final byte terminal_not_found = 4;
// byte[0-1] 应答流水号(WORD) 对应的终端注册消息的流水号
private int replyFlowId;
/***
* byte[2] 结果(BYTE) <br>
* 0成功<br>
* 1车辆已被注册<br>
* 2数据库中无该车辆<br>
**/
private byte replyCode;
// byte[3-x] 鉴权码(STRING) 只有在成功后才有该字段
private String replyToken;
public RegisterBodyPack() {
}
public int getReplyFlowId() {
return replyFlowId;
}
public void setReplyFlowId(int flowId) {
this.replyFlowId = flowId;
}
public byte getReplyCode() {
return replyCode;
}
public void setReplyCode(byte code) {
this.replyCode = code;
}
public String getReplyToken() {
return replyToken;
}
public void setReplyToken(String token) {
this.replyToken = token;
}
@Override
public String toString() {
return "TerminalRegisterMsgResp [replyFlowId=" + replyFlowId + ", replyCode=" + replyCode + ", replyToken="
+ replyToken + "]";
}
}

View File

@@ -0,0 +1,64 @@
package com.hua.transport.jt808.entity.response;
public class ServerBodyPack {
public static final byte success = 0;
public static final byte failure = 1;
public static final byte msg_error = 2;
public static final byte unsupported = 3;
public static final byte warnning_msg_ack = 4;
// byte[0-1] 应答流水号 对应的终端消息的流水号
private int replyFlowId;
// byte[2-3] 应答ID 对应的终端消息的ID
private int replyId;
/**
* 0成功确认<br>
* 1失败<br>
* 2消息有误<br>
* 3不支持<br>
* 4报警处理确认<br>
*/
private byte replyCode;
public ServerBodyPack() {
}
public ServerBodyPack(int replyFlowId, int replyId, byte replyCode) {
super();
this.replyFlowId = replyFlowId;
this.replyId = replyId;
this.replyCode = replyCode;
}
public int getReplyFlowId() {
return replyFlowId;
}
public void setReplyFlowId(int flowId) {
this.replyFlowId = flowId;
}
public int getReplyId() {
return replyId;
}
public void setReplyId(int msgId) {
this.replyId = msgId;
}
public byte getReplyCode() {
return replyCode;
}
public void setReplyCode(byte code) {
this.replyCode = code;
}
@Override
public String toString() {
return "ServerCommonRespMsg [replyFlowId=" + replyFlowId + ", replyId=" + replyId + ", replyCode=" + replyCode
+ "]";
}
}

View File

@@ -0,0 +1,36 @@
package com.hua.transport.jt808.server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
/**
* Netty Server Wrapper for Spring Boot
*/
@Component
@Slf4j
public class Jt808NettyServer implements CommandLineRunner {
@Value("${jt808.port:20048}")
private int port;
private TCPServer tcpServer;
@Override
public void run(String... args) throws Exception {
log.info("Initializing JT808 TCP Server on port: {}", port);
tcpServer = new TCPServer(port);
tcpServer.startServer();
}
// You might want to add a @PreDestroy method to stop the server gracefully
// @PreDestroy
// public void destroy() {
// if (tcpServer != null) {
// tcpServer.stopServer();
// }
// }
}

View File

@@ -0,0 +1,112 @@
package com.hua.transport.jt808.server;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import com.hua.transport.jt808.entity.Session;
public class SessionManager {
// netty生成的sessionID和Session的对应关系
private Map<String, Session> sessionIdMap;
// 终端手机号和netty生成的sessionID的对应关系
private Map<String, String> phoneMap;
private static volatile SessionManager instance = null;
public static SessionManager getInstance() {
if (instance == null) {
synchronized (SessionManager.class) {
if (instance == null) {
instance = new SessionManager();
}
}
}
return instance;
}
public SessionManager() {
this.sessionIdMap = new ConcurrentHashMap<>();
this.phoneMap = new ConcurrentHashMap<>();
}
public boolean containsKey(String sessionId) {
return sessionIdMap.containsKey(sessionId);
}
public boolean containsSession(Session session) {
return sessionIdMap.containsValue(session);
}
public Session findBySessionId(String id) {
return sessionIdMap.get(id);
}
public Session findByTerminalPhone(String phone) {
String sessionId = this.phoneMap.get(phone);
if (sessionId == null)
return null;
return this.findBySessionId(sessionId);
}
public synchronized Session put(String key, Session value) {
if (value.getTerminalPhone() != null && !"".equals(value.getTerminalPhone().trim())) {
this.phoneMap.put(value.getTerminalPhone(), value.getId());
}
return sessionIdMap.put(key, value);
}
public synchronized Session removeBySessionId(String sessionId) {
if (sessionId == null)
return null;
Session session = sessionIdMap.remove(sessionId);
if (session == null)
return null;
if (session.getTerminalPhone() != null)
this.phoneMap.remove(session.getTerminalPhone());
return session;
}
// public synchronized void remove(String sessionId) {
// if (sessionId == null)
// return;
// Session session = sessionIdMap.remove(sessionId);
// if (session == null)
// return;
// if (session.getTerminalPhone() != null)
// this.phoneMap.remove(session.getTerminalPhone());
// try {
// if (session.getChannel() != null) {
// if (session.getChannel().isActive() || session.getChannel().isOpen()) {
// session.getChannel().close();
// }
// session = null;
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
public Set<String> keySet() {
return sessionIdMap.keySet();
}
public void forEach(BiConsumer<? super String, ? super Session> action) {
sessionIdMap.forEach(action);
}
public Set<Entry<String, Session>> entrySet() {
return sessionIdMap.entrySet();
}
public List<Session> toList() {
return this.sessionIdMap.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,114 @@
package com.hua.transport.jt808.server;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
import com.hua.transport.jt808.common.Consts;
import com.hua.transport.jt808.service.codec.LogDecoder;
import com.hua.transport.jt808.service.handler.TCPServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.Future;
public class TCPServer {
private Logger log = LoggerFactory.getLogger(getClass());
private int port;
private EventLoopGroup bossGroup = null;
private EventLoopGroup workerGroup = null;
private volatile boolean isRunning = false;
public TCPServer() {
}
public TCPServer(int port) {
this();
this.port = port;
}
private void bind() throws Exception {
this.bossGroup = new NioEventLoopGroup();
this.workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)//
.channel(NioServerSocketChannel.class) //
.childHandler(new ChannelInitializer<SocketChannel>() { //
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast("idleStateHandler",
new IdleStateHandler(Consts.TCP_CLIENT_IDLE, 0, 0, TimeUnit.MINUTES));
ch.pipeline().addLast(new LogDecoder());
// 1024表示单条消息的最大长度解码器在查找分隔符的时候达到该长度还没找到的话会抛异常
ch.pipeline().addLast(
new DelimiterBasedFrameDecoder(1024, Unpooled.copiedBuffer(new byte[] { 0x7e }),
Unpooled.copiedBuffer(new byte[] { 0x7e, 0x7e })));
//ch.pipeline().addLast(new PackageDataDecoder());
ch.pipeline().addLast(new TCPServerHandler());
}
}).option(ChannelOption.SO_BACKLOG, 128) //
.childOption(ChannelOption.SO_KEEPALIVE, true);
log.info("TCP服务启动完毕,port={}", this.port);
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channelFuture.channel().closeFuture().sync();
}
public synchronized void startServer() {
if (this.isRunning) {
throw new IllegalStateException(this.getName() + " is already started .");
}
this.isRunning = true;
new Thread(() -> {
try {
this.bind();
} catch (Exception e) {
this.log.info("TCP服务启动出错:{}", e.getMessage());
e.printStackTrace();
}
}, this.getName()).start();
}
public synchronized void stopServer() {
if (!this.isRunning) {
throw new IllegalStateException(this.getName() + " is not yet started .");
}
this.isRunning = false;
try {
Future<?> future = this.workerGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
log.error("workerGroup 无法正常停止:{}", future.cause());
}
future = this.bossGroup.shutdownGracefully().await();
if (!future.isSuccess()) {
log.error("bossGroup 无法正常停止:{}", future.cause());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
this.log.info("TCP服务已经停止...");
}
private String getName() {
return "TCP-Server";
}
}

View File

@@ -0,0 +1,42 @@
package com.hua.transport.jt808.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
@Service
@Slf4j
public class ApiLogService {
private final CopyOnWriteArrayList<SseEmitter> emitters = new CopyOnWriteArrayList<>();
public SseEmitter createEmitter() {
SseEmitter emitter = new SseEmitter(30 * 60 * 1000L); // 30 min timeout
emitter.onCompletion(() -> emitters.remove(emitter));
emitter.onTimeout(() -> emitters.remove(emitter));
emitter.onError((e) -> emitters.remove(emitter));
emitters.add(emitter);
return emitter;
}
public void broadcastLog(Map<String, Object> payload) {
// Log to server file
log.info("【API Data Received】: {}", payload);
// Broadcast to Web UI
for (SseEmitter emitter : emitters) {
try {
emitter.send(SseEmitter.event().name("api-log").data(payload));
} catch (IOException e) {
emitters.remove(emitter);
}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.hua.transport.jt808.service;
public interface DeviceService {
/**
* Process location data from device
*/
void processLocation(String terminalPhone, double latitude, double longitude, float speed, java.util.Date time);
}

View File

@@ -0,0 +1,252 @@
package com.hua.transport.jt808.service.codec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.common.Consts;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.request.LocationPack;
import com.hua.transport.jt808.entity.request.RegisterPack;
import com.hua.transport.jt808.entity.request.RegisterPack.TerminalRegInfo;
import com.hua.transport.jt808.util.BCDUtil;
import com.hua.transport.jt808.util.BitUtil;
/**
* 数据包解码器
* @author huaxl
*
*/
public class DataDecoder {
private static final Logger log = LoggerFactory.getLogger(DataDecoder.class);
private BitUtil bitUtil;
private BCDUtil bcdUtil;
public DataDecoder() {
this.bitUtil = new BitUtil();
this.bcdUtil = new BCDUtil();
}
public DataPack bytes2PackageData(byte[] data) {
DataPack ret = new DataPack();
// 0. 终端套接字地址信息
// ret.setChannel(msg.getChannel());
// 1. 16byte 或 12byte 消息头
PackHead msgHeader = this.parseMsgHeaderFromBytes(data);
ret.setPackHead(msgHeader);
int msgBodyByteStartIndex = 12;
// 2. 消息体
// 有子包信息,消息体起始字节后移四个字节:消息包总数(word(16))+包序号(word(16))
if (msgHeader.isHasSubPackage()) {
msgBodyByteStartIndex = 16;
}
byte[] tmp = new byte[msgHeader.getMsgBodyLength()];
System.arraycopy(data, msgBodyByteStartIndex, tmp, 0, tmp.length);
ret.setBodyBytes(tmp);
// 3. 去掉分隔符之后,最后一位就是校验码
// int checkSumInPkg =
// this.bitOperator.oneByteToInteger(data[data.length - 1]);
int checkSumInPkg = data[data.length - 1];
int calculatedCheckSum = this.bitUtil.getCheckSum4JT808(data, 0, data.length - 1);
ret.setCheckSum(checkSumInPkg);
if (checkSumInPkg != calculatedCheckSum) {
log.warn("检验码不一致,msgid:{},pkg:{},calculated:{}", msgHeader.getId(), checkSumInPkg, calculatedCheckSum);
}
return ret;
}
private PackHead parseMsgHeaderFromBytes(byte[] data) {
PackHead msgHeader = new PackHead();
// 1. 消息ID word(16)
// byte[] tmp = new byte[2];
// System.arraycopy(data, 0, tmp, 0, 2);
// msgHeader.setMsgId(this.bitOperator.twoBytesToInteger(tmp));
msgHeader.setId(this.parseIntFromBytes(data, 0, 2));
// 2. 消息体属性 word(16)=================>
// System.arraycopy(data, 2, tmp, 0, 2);
// int msgBodyProps = this.bitOperator.twoBytesToInteger(tmp);
int msgBodyProps = this.parseIntFromBytes(data, 2, 2);
msgHeader.setBodyPropsField(msgBodyProps);
// [ 0-9 ] 0000,0011,1111,1111(3FF)(消息体长度)
msgHeader.setMsgBodyLength(msgBodyProps & 0x3ff);
// [10-12] 0001,1100,0000,0000(1C00)(加密类型)
msgHeader.setEncryptionType((msgBodyProps & 0x1c00) >> 10);
// [ 13_ ] 0010,0000,0000,0000(2000)(是否有子包)
msgHeader.setHasSubPackage(((msgBodyProps & 0x2000) >> 13) == 1);
// [14-15] 1100,0000,0000,0000(C000)(保留位)
msgHeader.setReservedBit(((msgBodyProps & 0xc000) >> 14) + "");
// 消息体属性 word(16)<=================
// 3. 终端手机号 bcd[6]
// tmp = new byte[6];
// System.arraycopy(data, 4, tmp, 0, 6);
// msgHeader.setTerminalPhone(this.bcd8421Operater.bcd2String(tmp));
msgHeader.setTerminalPhone(this.parseBcdStringFromBytes(data, 4, 6));
// 4. 消息流水号 word(16) 按发送顺序从 0 开始循环累加
// tmp = new byte[2];
// System.arraycopy(data, 10, tmp, 0, 2);
// msgHeader.setFlowId(this.bitOperator.twoBytesToInteger(tmp));
msgHeader.setFlowId(this.parseIntFromBytes(data, 10, 2));
// 5. 消息包封装项
// 有子包信息
if (msgHeader.isHasSubPackage()) {
// 消息包封装项字段
msgHeader.setInfoField(this.parseIntFromBytes(data, 12, 4));
// byte[0-1] 消息包总数(word(16))
// tmp = new byte[2];
// System.arraycopy(data, 12, tmp, 0, 2);
// msgHeader.setTotalSubPackage(this.bitOperator.twoBytesToInteger(tmp));
msgHeader.setSubPackage(this.parseIntFromBytes(data, 12, 2));
// byte[2-3] 包序号(word(16)) 从 1 开始
// tmp = new byte[2];
// System.arraycopy(data, 14, tmp, 0, 2);
// msgHeader.setSubPackageSeq(this.bitOperator.twoBytesToInteger(tmp));
msgHeader.setSubPackageSequeue(this.parseIntFromBytes(data, 12, 2));
}
return msgHeader;
}
protected String parseStringFromBytes(byte[] data, int startIndex, int lenth) {
return this.parseStringFromBytes(data, startIndex, lenth, null);
}
private String parseStringFromBytes(byte[] data, int startIndex, int lenth, String defaultVal) {
try {
byte[] tmp = new byte[lenth];
System.arraycopy(data, startIndex, tmp, 0, lenth);
return new String(tmp, Consts.DEFAULT_CHARSET);
} catch (Exception e) {
log.error("解析字符串出错:{}", e.getMessage());
e.printStackTrace();
return defaultVal;
}
}
private String parseBcdStringFromBytes(byte[] data, int startIndex, int lenth) {
return this.parseBcdStringFromBytes(data, startIndex, lenth, null);
}
private String parseBcdStringFromBytes(byte[] data, int startIndex, int lenth, String defaultVal) {
try {
byte[] tmp = new byte[lenth];
System.arraycopy(data, startIndex, tmp, 0, lenth);
return this.bcdUtil.bcd2String(tmp);
} catch (Exception e) {
log.error("解析BCD(8421码)出错:{}", e.getMessage());
e.printStackTrace();
return defaultVal;
}
}
private int parseIntFromBytes(byte[] data, int startIndex, int length) {
return this.parseIntFromBytes(data, startIndex, length, 0);
}
private int parseIntFromBytes(byte[] data, int startIndex, int length, int defaultVal) {
try {
// 字节数大于4,从起始索引开始向后处理4个字节,其余超出部分丢弃
final int len = length > 4 ? 4 : length;
byte[] tmp = new byte[len];
System.arraycopy(data, startIndex, tmp, 0, len);
return bitUtil.byteToInteger(tmp);
} catch (Exception e) {
log.error("解析整数出错:{}", e.getMessage());
e.printStackTrace();
return defaultVal;
}
}
public RegisterPack toTerminalRegisterMsg(DataPack packageData) {
RegisterPack ret = new RegisterPack(packageData);
byte[] data = ret.getBodyBytes();
TerminalRegInfo body = new TerminalRegInfo();
// 1. byte[0-1] 省域ID(WORD)
// 设备安装车辆所在的省域省域ID采用GB/T2260中规定的行政区划代码6位中前两位
// 0保留由平台取默认值
body.setProvinceId(this.parseIntFromBytes(data, 0, 2));
// 2. byte[2-3] 设备安装车辆所在的市域或县域,市县域ID采用GB/T2260中规定的行 政区划代码6位中后四位
// 0保留由平台取默认值
body.setCityId(this.parseIntFromBytes(data, 2, 2));
// 3. byte[4-8] 制造商ID(BYTE[5]) 5 个字节,终端制造商编码
// byte[] tmp = new byte[5];
body.setManufacturerId(this.parseStringFromBytes(data, 4, 5));
// 4. byte[9-16] 终端型号(BYTE[8]) 八个字节, 此终端型号 由制造商自行定义 位数不足八位的,补空格。
body.setTerminalType(this.parseStringFromBytes(data, 9, 8));
// 5. byte[17-23] 终端ID(BYTE[7]) 七个字节, 由大写字母 和数字组成, 此终端 ID由制造 商自行定义
body.setTerminalId(this.parseStringFromBytes(data, 17, 7));
// 6. byte[24] 车牌颜色(BYTE) 车牌颜 色按照JT/T415-2006 中5.4.12 的规定
body.setLicensePlateColor(this.parseIntFromBytes(data, 24, 1));
// 7. byte[25-x] 车牌(STRING) 公安交 通管理部门颁 发的机动车号牌
body.setLicensePlate(this.parseStringFromBytes(data, 25, data.length - 25));
ret.setTerminalRegInfo(body);
return ret;
}
public LocationPack toLocationInfoUploadMsg(DataPack packageData) {
LocationPack ret = new LocationPack(packageData);
final byte[] data = ret.getBodyBytes();
// 1. byte[0-3] 报警标志(DWORD(32))
ret.setWarningFlagField(this.parseIntFromBytes(data, 0, 3));
// 2. byte[4-7] 状态(DWORD(32))
ret.setStatusField(this.parseIntFromBytes(data, 4, 4));
// 3. byte[8-11] 纬度(DWORD(32)) 以度为单位的纬度值乘以10^6精确到百万分之一度
ret.setLatitude(this.parseFloatFromBytes(data, 8, 4));
// 4. byte[12-15] 经度(DWORD(32)) 以度为单位的经度值乘以10^6精确到百万分之一度
ret.setLongitude(this.parseFloatFromBytes(data, 12, 4));
// 5. byte[16-17] 高程(WORD(16)) 海拔高度,单位为米( m
ret.setElevation(this.parseIntFromBytes(data, 16, 2));
// byte[18-19] 速度(WORD) 1/10km/h
ret.setSpeed(this.parseFloatFromBytes(data, 18, 2));
// byte[20-21] 方向(WORD) 0-359正北为 0顺时针
ret.setDirection(this.parseIntFromBytes(data, 20, 2));
// byte[22-x] 时间(BCD[6]) YY-MM-DD-hh-mm-ss
// GMT+8 时间,本标准中之后涉及的时间均采用此时区
// ret.setTime(this.par);
byte[] tmp = new byte[6];
System.arraycopy(data, 22, tmp, 0, 6);
String time = this.parseBcdStringFromBytes(data, 22, 6);
return ret;
}
private float parseFloatFromBytes(byte[] data, int startIndex, int length) {
return this.parseFloatFromBytes(data, startIndex, length, 0f);
}
private float parseFloatFromBytes(byte[] data, int startIndex, int length, float defaultVal) {
try {
// 字节数大于4,从起始索引开始向后处理4个字节,其余超出部分丢弃
final int len = length > 4 ? 4 : length;
byte[] tmp = new byte[len];
System.arraycopy(data, startIndex, tmp, 0, len);
return bitUtil.byte2Float(tmp);
} catch (Exception e) {
log.error("解析浮点数出错:{}", e.getMessage());
e.printStackTrace();
return defaultVal;
}
}
}

View File

@@ -0,0 +1,102 @@
package com.hua.transport.jt808.service.codec;
import java.util.Arrays;
import com.hua.transport.jt808.common.Consts;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.Session;
import com.hua.transport.jt808.entity.request.RegisterPack;
import com.hua.transport.jt808.entity.response.ServerBodyPack;
import com.hua.transport.jt808.entity.response.RegisterBodyPack;
import com.hua.transport.jt808.util.BitUtil;
import com.hua.transport.jt808.util.JT808Util;
/**
* 数据包编码器
* @author huaxl
*
*/
public class DataEncoder {
private BitUtil bitUtil;
private JT808Util jt808Util;
public DataEncoder() {
this.bitUtil = new BitUtil();
this.jt808Util = new JT808Util();
}
public byte[] encode4TerminalRegisterResp(RegisterPack req, RegisterBodyPack respMsgBody,
int flowId) throws Exception {
// 消息体字节数组
byte[] msgBody = null;
// 鉴权码(STRING) 只有在成功后才有该字段
if (respMsgBody.getReplyCode() == RegisterBodyPack.success) {
msgBody = this.bitUtil.concatAll(Arrays.asList(//
bitUtil.integerTo2Bytes(respMsgBody.getReplyFlowId()), // 流水号(2)
new byte[] { respMsgBody.getReplyCode() }, // 结果
respMsgBody.getReplyToken().getBytes(Consts.DEFAULT_CHARSET)// 鉴权码(STRING)
));
} else {
msgBody = this.bitUtil.concatAll(Arrays.asList(//
bitUtil.integerTo2Bytes(respMsgBody.getReplyFlowId()), // 流水号(2)
new byte[] { respMsgBody.getReplyCode() }// 错误代码
));
}
// 消息头
int msgBodyProps = this.jt808Util.generateMsgBodyProps(msgBody.length, 0b000, false, 0);
byte[] msgHeader = this.jt808Util.generateMsgHeader(req.getPackHead().getTerminalPhone(),
Consts.CMD_REGISTER_RESP, msgBody, msgBodyProps, flowId);
byte[] headerAndBody = this.bitUtil.concatAll(msgHeader, msgBody);
// 校验码
int checkSum = this.bitUtil.getCheckSum4JT808(headerAndBody, 0, headerAndBody.length - 1);
// 连接并且转义
return this.doEncode(headerAndBody, checkSum);
}
// public byte[] encode4ServerCommonRespMsg(TerminalAuthenticationMsg req,
// ServerCommonRespMsgBody respMsgBody, int flowId) throws Exception {
public byte[] encode4ServerCommonRespMsg(DataPack req, ServerBodyPack respMsgBody, int flowId)
throws Exception {
byte[] msgBody = this.bitUtil.concatAll(Arrays.asList(//
bitUtil.integerTo2Bytes(respMsgBody.getReplyFlowId()), // 应答流水号
bitUtil.integerTo2Bytes(respMsgBody.getReplyId()), // 应答ID,对应的终端消息的ID
new byte[] { respMsgBody.getReplyCode() }// 结果
));
// 消息头
int msgBodyProps = this.jt808Util.generateMsgBodyProps(msgBody.length, 0b000, false, 0);
byte[] msgHeader = this.jt808Util.generateMsgHeader(req.getPackHead().getTerminalPhone(),
Consts.CMD_COMMON_RESP, msgBody, msgBodyProps, flowId);
byte[] headerAndBody = this.bitUtil.concatAll(msgHeader, msgBody);
// 校验码
int checkSum = this.bitUtil.getCheckSum4JT808(headerAndBody, 0, headerAndBody.length - 1);
// 连接并且转义
return this.doEncode(headerAndBody, checkSum);
}
public byte[] encode4ParamSetting(byte[] msgBodyBytes, Session session) throws Exception {
// 消息头
int msgBodyProps = this.jt808Util.generateMsgBodyProps(msgBodyBytes.length, 0b000, false, 0);
byte[] msgHeader = this.jt808Util.generateMsgHeader(session.getTerminalPhone(),
Consts.CMD_PARAM_SETTINGS, msgBodyBytes, msgBodyProps, session.currentFlowId());
// 连接消息头和消息体
byte[] headerAndBody = this.bitUtil.concatAll(msgHeader, msgBodyBytes);
// 校验码
int checkSum = this.bitUtil.getCheckSum4JT808(headerAndBody, 0, headerAndBody.length - 1);
// 连接并且转义
return this.doEncode(headerAndBody, checkSum);
}
private byte[] doEncode(byte[] headerAndBody, int checkSum) throws Exception {
byte[] noEscapedBytes = this.bitUtil.concatAll(Arrays.asList(//
new byte[] { Consts.PKG_DELIMITER }, // 0x7e
headerAndBody, // 消息头+ 消息体
bitUtil.integerTo1Bytes(checkSum), // 校验码
new byte[] { Consts.PKG_DELIMITER }// 0x7e
));
// 转义
return jt808Util.doEscape4Send(noEscapedBytes, 1, noEscapedBytes.length - 2);
}
}

View File

@@ -0,0 +1,40 @@
package com.hua.transport.jt808.service.codec;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.util.HexUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* 该解码器只是为了自己日志所用,没其他作用.<br>
* 最终删除
*
* @author huaxl
*
*/
public class LogDecoder extends ByteToMessageDecoder {
private final Logger log = LoggerFactory.getLogger(getClass());
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
String hex = buf2Str(in);
log.info("ip={},hex = {}", ctx.channel().remoteAddress(), hex);
ByteBuf buf = Unpooled.buffer();
while (in.isReadable()) {
buf.writeByte(in.readByte());
}
out.add(buf);
}
private String buf2Str(ByteBuf in) {
byte[] dst = new byte[in.readableBytes()];
in.getBytes(0, dst);
return HexUtil.toHexString(dst);
}
}

View File

@@ -0,0 +1,60 @@
package com.hua.transport.jt808.service.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.Session;
import com.hua.transport.jt808.server.SessionManager;
import com.hua.transport.jt808.service.codec.DataDecoder;
import com.hua.transport.jt808.service.codec.DataEncoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
public abstract class MessageHandler {
protected final Logger log = LoggerFactory.getLogger(getClass());
protected DataEncoder msgEncoder;
protected DataDecoder decoder;
protected SessionManager sessionManager;
public MessageHandler() {
this.msgEncoder = new DataEncoder();
this.decoder = new DataDecoder();
this.sessionManager = SessionManager.getInstance();
}
protected ByteBuf getByteBuf(byte[] arr) {
ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.directBuffer(arr.length);
byteBuf.writeBytes(arr);
return byteBuf;
}
public void send2Client(Channel channel, byte[] arr) throws InterruptedException {
ChannelFuture future = channel.writeAndFlush(Unpooled.copiedBuffer(arr)).sync();
if (!future.isSuccess()) {
log.error("发送数据出错:{}", future.cause());
}
}
protected int getFlowId(Channel channel, int defaultValue) {
Session session = this.sessionManager.findBySessionId(Session.buildId(channel));
if (session == null) {
return defaultValue;
}
return session.currentFlowId();
}
protected int getFlowId(Channel channel) {
return this.getFlowId(channel, 0);
}
public abstract void process(DataPack req);
}

View File

@@ -0,0 +1,36 @@
package com.hua.transport.jt808.service.handler;
import java.util.HashMap;
import java.util.Map;
import com.hua.transport.jt808.common.Consts;
import com.hua.transport.jt808.service.handler.terminal.AuthenticationHandler;
import com.hua.transport.jt808.service.handler.terminal.HeartbeatHandler;
import com.hua.transport.jt808.service.handler.terminal.LocationUploadHandler;
import com.hua.transport.jt808.service.handler.terminal.LoginOutHandler;
import com.hua.transport.jt808.service.handler.terminal.RegisterHandler;
public class MessageHandlerFactory {
/**
* 消息和处理类映射表
*/
public static Map<Integer, Class<?>> handlerMap = new HashMap<Integer, Class<?>>();
static{
handlerMap.put(Consts.MSGID_HEART_BEAT, HeartbeatHandler.class); // 终端心跳
handlerMap.put(Consts.MSGID_REGISTER, RegisterHandler.class); // 终端注册
handlerMap.put(Consts.MSGID_LOG_OUT, LoginOutHandler.class); // 终端注销
handlerMap.put(Consts.MSGID_AUTHENTICATION, AuthenticationHandler.class); // 终端鉴权
handlerMap.put(Consts.MSGID_LOCATION_UPLOAD, LocationUploadHandler.class); // 位置信息汇报
}
public static MessageHandler getInstance(Integer msgId) throws InstantiationException, IllegalAccessException{
Class clazz = handlerMap.get(msgId);
if(clazz == null){
return null;
}
return (MessageHandler)clazz.newInstance();
}
}

View File

@@ -0,0 +1,125 @@
package com.hua.transport.jt808.service.handler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.Session;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.server.SessionManager;
import com.hua.transport.jt808.service.codec.DataDecoder;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.ReferenceCountUtil;
public class TCPServerHandler extends ChannelInboundHandlerAdapter { // (1)
private final Logger logger = LoggerFactory.getLogger(getClass());
private final DataDecoder decoder;
private final SessionManager sessionManager;
public TCPServerHandler() {
this.decoder = new DataDecoder();
this.sessionManager = SessionManager.getInstance();
}
/**
*
* 处理业务逻辑
*
* @param packageData
* @throws IllegalAccessException
* @throws InstantiationException
*
*/
private void processPackageData(DataPack packageData) throws InstantiationException, IllegalAccessException {
PackHead header = packageData.getPackHead();
Integer msgId = header.getId();
logger.info("消息头部msgid={}, phone={}, flowid={}", msgId, header.getTerminalPhone(), header.getFlowId());
MessageHandler handler = MessageHandlerFactory.getInstance(msgId);
if(handler != null){
handler.process(packageData);
}else { // 其他情况
logger.error("[未知消息类型],msgId={},phone={},package={}", header.getId(), header.getTerminalPhone(), packageData);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws InterruptedException { // (2)
try {
ByteBuf buf = (ByteBuf) msg;
if (buf.readableBytes() <= 0) {
// ReferenceCountUtil.safeRelease(msg);
return;
}
byte[] bs = new byte[buf.readableBytes()];
buf.readBytes(bs);
// 字节数据转换为针对于808消息结构的实体类
DataPack pkg = this.decoder.bytes2PackageData(bs);
// 引用channel,以便回送数据给硬件
pkg.setChannel(ctx.channel());
processPackageData(pkg);
}catch (Exception e) {
// TODO: handle exception
logger.error("消息处理异常", e);
} finally {
release(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { // (4)
logger.error("发生异常:{}", cause);
//cause.printStackTrace();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Session session = Session.buildSession(ctx.channel());
sessionManager.put(session.getId(), session);
logger.debug("终端连接:{}", session);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
final String sessionId = ctx.channel().id().asLongText();
Session session = sessionManager.findBySessionId(sessionId);
this.sessionManager.removeBySessionId(sessionId);
logger.debug("终端断开连接:{}", session);
ctx.channel().close();
// ctx.close();
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (IdleStateEvent.class.isAssignableFrom(evt.getClass())) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
Session session = this.sessionManager.removeBySessionId(Session.buildId(ctx.channel()));
logger.error("服务器主动断开连接:{}", session);
ctx.close();
}
}
}
private void release(Object msg) {
try {
ReferenceCountUtil.release(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,60 @@
package com.hua.transport.jt808.service.handler.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.Session;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.request.AuthenticationPack;
import com.hua.transport.jt808.entity.response.ServerBodyPack;
import com.hua.transport.jt808.service.handler.MessageHandler;
/**
* 终端鉴权 ==> 平台通用应答
* @author huaxl
*/
public class AuthenticationHandler extends MessageHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
public AuthenticationHandler() {
super();
}
@Override
public void process(DataPack packageData) {
//
PackHead header = packageData.getPackHead();
logger.info("[终端鉴权],msgid={}, phone={},flowid={}", header.getId(), header.getTerminalPhone(), header.getFlowId());
try {
AuthenticationPack msg = new AuthenticationPack(packageData);
//this.msgProcessService.processAuthMsg(authenticationMsg);
log.debug("终端鉴权:{}", msg);
final String sessionId = Session.buildId(msg.getChannel());
Session session = sessionManager.findBySessionId(sessionId);
if (session == null) {
session = Session.buildSession(msg.getChannel(), msg.getPackHead().getTerminalPhone());
}
session.setAuthenticated(true);
session.setTerminalPhone(msg.getPackHead().getTerminalPhone());
sessionManager.put(session.getId(), session);
ServerBodyPack respMsgBody = new ServerBodyPack();
respMsgBody.setReplyCode(ServerBodyPack.success);
respMsgBody.setReplyFlowId(msg.getPackHead().getFlowId());
respMsgBody.setReplyId(msg.getPackHead().getId());
int flowId = super.getFlowId(msg.getChannel());
byte[] bs = this.msgEncoder.encode4ServerCommonRespMsg(msg, respMsgBody, flowId);
super.send2Client(msg.getChannel(), bs);
} catch (Exception e) {
logger.error("[终端鉴权]错误,err={}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,41 @@
package com.hua.transport.jt808.service.handler.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.response.ServerBodyPack;
import com.hua.transport.jt808.service.handler.MessageHandler;
/**
* 终端心跳-消息体为空 ==> 平台通用应答
* @author huaxl
*/
public class HeartbeatHandler extends MessageHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
public HeartbeatHandler() {
super();
}
@Override
public void process(DataPack packageData) {
PackHead header = packageData.getPackHead();
logger.info("[终端心跳],msgid={}, phone={},flowid={}", header.getId(), header.getTerminalPhone(), header.getFlowId());
try {
logger.debug("心跳信息:{}", packageData);
ServerBodyPack respMsgBody = new ServerBodyPack(header.getFlowId(), header.getId(), ServerBodyPack.success);
int flowId = super.getFlowId(packageData.getChannel());
byte[] bs = this.msgEncoder.encode4ServerCommonRespMsg(packageData, respMsgBody, flowId);
super.send2Client(packageData.getChannel(), bs);
} catch (Exception e) {
logger.error("[终端心跳]错误,err={}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,45 @@
package com.hua.transport.jt808.service.handler.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.request.LocationPack;
import com.hua.transport.jt808.entity.response.ServerBodyPack;
import com.hua.transport.jt808.service.handler.MessageHandler;
/**
* 处理模板
*
* @author huaxl
*/
public class LocationUploadHandler extends MessageHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
public LocationUploadHandler() {
super();
}
@Override
public void process(DataPack packageData) {
//
PackHead header = packageData.getPackHead();
logger.info("[位置信息],msgid={}, phone={},flowid={}", header.getId(), header.getTerminalPhone(), header.getFlowId());
try {
LocationPack msg = this.decoder.toLocationInfoUploadMsg(packageData);
log.debug("位置 信息:{}", msg);
ServerBodyPack respMsgBody = new ServerBodyPack(header.getFlowId(), header.getId(), ServerBodyPack.success);
int flowId = super.getFlowId(msg.getChannel());
byte[] bs = this.msgEncoder.encode4ServerCommonRespMsg(msg, respMsgBody, flowId);
super.send2Client(msg.getChannel(), bs);
} catch (Exception e) {
logger.error("[位置信息]错误,err={}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
package com.hua.transport.jt808.service.handler.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.response.ServerBodyPack;
import com.hua.transport.jt808.service.handler.MessageHandler;
/**
* 终端注销(终端注销数据消息体为空) ==> 平台通用应答
* @author huaxl
*/
public class LoginOutHandler extends MessageHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
public LoginOutHandler() {
super();
}
@Override
public void process(DataPack packageData) {
PackHead header = packageData.getPackHead();
logger.info("[终端注销],msgid={}, phone={},flowid={}", header.getId(), header.getTerminalPhone(), header.getFlowId());
try {
log.info("终端注销:{}", packageData);
final PackHead reqHeader = packageData.getPackHead();
int flowId = super.getFlowId(packageData.getChannel());
ServerBodyPack respMsgBody = new ServerBodyPack(reqHeader.getFlowId(), reqHeader.getId(), ServerBodyPack.success);
byte[] bs = this.msgEncoder.encode4ServerCommonRespMsg(packageData, respMsgBody, flowId);
super.send2Client(packageData.getChannel(), bs);
} catch (Exception e) {
logger.error("[终端注销]错误, err={}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,60 @@
package com.hua.transport.jt808.service.handler.terminal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.hua.transport.jt808.entity.DataPack;
import com.hua.transport.jt808.entity.Session;
import com.hua.transport.jt808.entity.DataPack.PackHead;
import com.hua.transport.jt808.entity.request.RegisterPack;
import com.hua.transport.jt808.entity.response.RegisterBodyPack;
import com.hua.transport.jt808.service.handler.MessageHandler;
/**
* 终端注册 ==> 终端注册应答
* @author huaxl
*/
public class RegisterHandler extends MessageHandler {
private final Logger logger = LoggerFactory.getLogger(getClass());
public RegisterHandler() {
super();
}
@Override
public void process(DataPack packageData) {
PackHead header = packageData.getPackHead();
logger.info("[终端注册],msgid={}, phone={},flowid={}", header.getId(), header.getTerminalPhone(), header.getFlowId());
try {
RegisterPack msg = this.decoder.toTerminalRegisterMsg(packageData);
log.debug("终端注册:{}", msg);
final String sessionId = Session.buildId(msg.getChannel());
Session session = sessionManager.findBySessionId(sessionId);
if (session == null) {
session = Session.buildSession(msg.getChannel(), msg.getPackHead().getTerminalPhone());
}
session.setAuthenticated(true);
session.setTerminalPhone(msg.getPackHead().getTerminalPhone());
sessionManager.put(session.getId(), session);
RegisterBodyPack respMsgBody = new RegisterBodyPack();
respMsgBody.setReplyCode(RegisterBodyPack.success);
respMsgBody.setReplyFlowId(msg.getPackHead().getFlowId());
// TODO 鉴权码暂时写死
respMsgBody.setReplyToken("123");
int flowId = super.getFlowId(msg.getChannel());
byte[] bs = this.msgEncoder.encode4TerminalRegisterResp(msg, respMsgBody, flowId);
super.send2Client(msg.getChannel(), bs);
} catch (Exception e) {
logger.error("<<<<<err={}", e.getMessage());
}
}
}

View File

@@ -0,0 +1,19 @@
package com.hua.transport.jt808.service.impl;
import com.hua.transport.jt808.service.DeviceService;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
@Service
@Slf4j
public class DeviceServiceImpl implements DeviceService {
@Override
public void processLocation(String terminalPhone, double latitude, double longitude, float speed, Date time) {
// In a real app, save to DB here
log.info(">> SERVICE: Device {} location: lat={}, lon={}, speed={}, time={}",
terminalPhone, latitude, longitude, speed, time);
}
}

View File

@@ -0,0 +1,57 @@
package com.hua.transport.jt808.util;
public class BCDUtil {
/**
* BCD字节数组===>String
*
* @param bytes
* @return 十进制字符串
*/
public String bcd2String(byte[] bytes) {
StringBuilder temp = new StringBuilder(bytes.length * 2);
for (int i = 0; i < bytes.length; i++) {
// 高四位
temp.append((bytes[i] & 0xf0) >>> 4);
// 低四位
temp.append(bytes[i] & 0x0f);
}
return temp.toString().substring(0, 1).equalsIgnoreCase("0") ? temp.toString().substring(1) : temp.toString();
}
/**
* 字符串==>BCD字节数组
*
* @param str
* @return BCD字节数组
*/
public byte[] string2Bcd(String str) {
// 奇数,前补零
if ((str.length() & 0x1) == 1) {
str = "0" + str;
}
byte ret[] = new byte[str.length() / 2];
byte bs[] = str.getBytes();
for (int i = 0; i < ret.length; i++) {
byte high = ascII2Bcd(bs[2 * i]);
byte low = ascII2Bcd(bs[2 * i + 1]);
// TODO 只遮罩BCD低四位?
ret[i] = (byte) ((high << 4) | low);
}
return ret;
}
private byte ascII2Bcd(byte asc) {
if ((asc >= '0') && (asc <= '9'))
return (byte) (asc - '0');
else if ((asc >= 'A') && (asc <= 'F'))
return (byte) (asc - 'A' + 10);
else if ((asc >= 'a') && (asc <= 'f'))
return (byte) (asc - 'a' + 10);
else
return (byte) (asc - 48);
}
}

View File

@@ -0,0 +1,394 @@
package com.hua.transport.jt808.util;
import java.util.Arrays;
import java.util.List;
public class BitUtil {
/**
* 把一个整形该为byte
*
* @param value
* @return
* @throws Exception
*/
public byte integerTo1Byte(int value) {
return (byte) (value & 0xFF);
}
/**
* 把一个整形该为1位的byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] integerTo1Bytes(int value) {
byte[] result = new byte[1];
result[0] = (byte) (value & 0xFF);
return result;
}
/**
* 把一个整形改为2位的byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] integerTo2Bytes(int value) {
byte[] result = new byte[2];
result[0] = (byte) ((value >>> 8) & 0xFF);
result[1] = (byte) (value & 0xFF);
return result;
}
/**
* 把一个整形改为3位的byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] integerTo3Bytes(int value) {
byte[] result = new byte[3];
result[0] = (byte) ((value >>> 16) & 0xFF);
result[1] = (byte) ((value >>> 8) & 0xFF);
result[2] = (byte) (value & 0xFF);
return result;
}
/**
* 把一个整形改为4位的byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] integerTo4Bytes(int value){
byte[] result = new byte[4];
result[0] = (byte) ((value >>> 24) & 0xFF);
result[1] = (byte) ((value >>> 16) & 0xFF);
result[2] = (byte) ((value >>> 8) & 0xFF);
result[3] = (byte) (value & 0xFF);
return result;
}
/**
* 把byte[]转化位整形,通常为指令用
*
* @param value
* @return
* @throws Exception
*/
public int byteToInteger(byte[] value) {
int result;
if (value.length == 1) {
result = oneByteToInteger(value[0]);
} else if (value.length == 2) {
result = twoBytesToInteger(value);
} else if (value.length == 3) {
result = threeBytesToInteger(value);
} else if (value.length == 4) {
result = fourBytesToInteger(value);
} else {
result = fourBytesToInteger(value);
}
return result;
}
/**
* 把一个byte转化位整形,通常为指令用
*
* @param value
* @return
* @throws Exception
*/
public int oneByteToInteger(byte value) {
return (int) value & 0xFF;
}
/**
* 把一个2位的数组转化位整形
*
* @param value
* @return
* @throws Exception
*/
public int twoBytesToInteger(byte[] value) {
// if (value.length < 2) {
// throw new Exception("Byte array too short!");
// }
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
return ((temp0 << 8) + temp1);
}
/**
* 把一个3位的数组转化位整形
*
* @param value
* @return
* @throws Exception
*/
public int threeBytesToInteger(byte[] value) {
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
int temp2 = value[2] & 0xFF;
return ((temp0 << 16) + (temp1 << 8) + temp2);
}
/**
* 把一个4位的数组转化位整形,通常为指令用
*
* @param value
* @return
* @throws Exception
*/
public int fourBytesToInteger(byte[] value) {
// if (value.length < 4) {
// throw new Exception("Byte array too short!");
// }
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
int temp2 = value[2] & 0xFF;
int temp3 = value[3] & 0xFF;
return ((temp0 << 24) + (temp1 << 16) + (temp2 << 8) + temp3);
}
/**
* 把一个4位的数组转化位整形
*
* @param value
* @return
* @throws Exception
*/
public long fourBytesToLong(byte[] value) throws Exception {
// if (value.length < 4) {
// throw new Exception("Byte array too short!");
// }
int temp0 = value[0] & 0xFF;
int temp1 = value[1] & 0xFF;
int temp2 = value[2] & 0xFF;
int temp3 = value[3] & 0xFF;
return (((long) temp0 << 24) + (temp1 << 16) + (temp2 << 8) + temp3);
}
/**
* 把一个数组转化长整形
*
* @param value
* @return
* @throws Exception
*/
public long bytes2Long(byte[] value) {
long result = 0;
int len = value.length;
int temp;
for (int i = 0; i < len; i++) {
temp = (len - 1 - i) * 8;
if (temp == 0) {
result += (value[i] & 0x0ff);
} else {
result += (value[i] & 0x0ff) << temp;
}
}
return result;
}
/**
* 把一个长整形改为byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] longToBytes(long value){
return longToBytes(value, 8);
}
/**
* 把一个长整形改为byte数组
*
* @param value
* @return
* @throws Exception
*/
public byte[] longToBytes(long value, int len) {
byte[] result = new byte[len];
int temp;
for (int i = 0; i < len; i++) {
temp = (len - 1 - i) * 8;
if (temp == 0) {
result[i] += (value & 0x0ff);
} else {
result[i] += (value >>> temp) & 0x0ff;
}
}
return result;
}
/**
* 得到一个消息ID
*
* @return
* @throws Exception
*/
public byte[] generateTransactionID() throws Exception {
byte[] id = new byte[16];
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 0, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 2, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 4, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 6, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 8, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 10, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 12, 2);
System.arraycopy(integerTo2Bytes((int) (Math.random() * 65536)), 0, id, 14, 2);
return id;
}
/**
* 把IP拆分位int数组
*
* @param ip
* @return
* @throws Exception
*/
public int[] getIntIPValue(String ip) throws Exception {
String[] sip = ip.split("[.]");
// if (sip.length != 4) {
// throw new Exception("error IPAddress");
// }
int[] intIP = { Integer.parseInt(sip[0]), Integer.parseInt(sip[1]), Integer.parseInt(sip[2]),
Integer.parseInt(sip[3]) };
return intIP;
}
/**
* 把byte类型IP地址转化位字符串
*
* @param address
* @return
* @throws Exception
*/
public String getStringIPValue(byte[] address) throws Exception {
int first = this.oneByteToInteger(address[0]);
int second = this.oneByteToInteger(address[1]);
int third = this.oneByteToInteger(address[2]);
int fourth = this.oneByteToInteger(address[3]);
return first + "." + second + "." + third + "." + fourth;
}
/**
* 合并字节数组
*
* @param first
* @param rest
* @return
*/
public byte[] concatAll(byte[] first, byte[]... rest) {
int totalLength = first.length;
for (byte[] array : rest) {
if (array != null) {
totalLength += array.length;
}
}
byte[] result = Arrays.copyOf(first, totalLength);
int offset = first.length;
for (byte[] array : rest) {
if (array != null) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
}
return result;
}
/**
* 合并字节数组
*
* @param rest
* @return
*/
public byte[] concatAll(List<byte[]> rest) {
int totalLength = 0;
for (byte[] array : rest) {
if (array != null) {
totalLength += array.length;
}
}
byte[] result = new byte[totalLength];
int offset = 0;
for (byte[] array : rest) {
if (array != null) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
}
return result;
}
public float byte2Float(byte[] bs) {
return Float.intBitsToFloat(
(((bs[3] & 0xFF) << 24) + ((bs[2] & 0xFF) << 16) + ((bs[1] & 0xFF) << 8) + (bs[0] & 0xFF)));
}
public float byteBE2Float(byte[] bytes) {
int l;
l = bytes[0];
l &= 0xff;
l |= ((long) bytes[1] << 8);
l &= 0xffff;
l |= ((long) bytes[2] << 16);
l &= 0xffffff;
l |= ((long) bytes[3] << 24);
return Float.intBitsToFloat(l);
}
public int getCheckSum4JT808(byte[] bs, int start, int end) {
if (start < 0 || end > bs.length)
throw new ArrayIndexOutOfBoundsException("getCheckSum4JT808 error : index out of bounds(start=" + start
+ ",end=" + end + ",bytes length=" + bs.length + ")");
int cs = 0;
for (int i = start; i < end; i++) {
cs ^= bs[i];
}
return cs;
}
public int getBitRange(int number, int start, int end) {
if (start < 0)
throw new IndexOutOfBoundsException("min index is 0,but start = " + start);
if (end >= Integer.SIZE)
throw new IndexOutOfBoundsException("max index is " + (Integer.SIZE - 1) + ",but end = " + end);
return (number << Integer.SIZE - (end + 1)) >>> Integer.SIZE - (end - start + 1);
}
public int getBitAt(int number, int index) {
if (index < 0)
throw new IndexOutOfBoundsException("min index is 0,but " + index);
if (index >= Integer.SIZE)
throw new IndexOutOfBoundsException("max index is " + (Integer.SIZE - 1) + ",but " + index);
return ((1 << index) & number) >> index;
}
public int getBitAtS(int number, int index) {
String s = Integer.toBinaryString(number);
return Integer.parseInt(s.charAt(index) + "");
}
@Deprecated
public int getBitRangeS(int number, int start, int end) {
String s = Integer.toBinaryString(number);
StringBuilder sb = new StringBuilder(s);
while (sb.length() < Integer.SIZE) {
sb.insert(0, "0");
}
String tmp = sb.reverse().substring(start, end + 1);
sb = new StringBuilder(tmp);
return Integer.parseInt(sb.reverse().toString(), 2);
}
}

View File

@@ -0,0 +1,61 @@
package com.hua.transport.jt808.util;
public class HexUtil {
private static final char[] DIGITS_HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
protected static char[] encodeHex(byte[] data) {
int l = data.length;
char[] out = new char[l << 1];
for (int i = 0, j = 0; i < l; i++) {
out[j++] = DIGITS_HEX[(0xF0 & data[i]) >>> 4];
out[j++] = DIGITS_HEX[0x0F & data[i]];
}
return out;
}
protected static byte[] decodeHex(char[] data) {
int len = data.length;
if ((len & 0x01) != 0) {
throw new RuntimeException("字符个数应该为偶数");
}
byte[] out = new byte[len >> 1];
for (int i = 0, j = 0; j < len; i++) {
int f = toDigit(data[j], j) << 4;
j++;
f |= toDigit(data[j], j);
j++;
out[i] = (byte) (f & 0xFF);
}
return out;
}
protected static int toDigit(char ch, int index) {
int digit = Character.digit(ch, 16);
if (digit == -1) {
throw new RuntimeException("Illegal hexadecimal character " + ch + " at index " + index);
}
return digit;
}
public static String toHexString(byte[] bs) {
return new String(encodeHex(bs));
}
public static String hexString2Bytes(String hex) {
return new String(decodeHex(hex.toCharArray()));
}
public static byte[] chars2Bytes(char[] bs) {
return decodeHex(bs);
}
public static void main(String[] args) {
String s = "abc你好";
String hex = toHexString(s.getBytes());
String decode = hexString2Bytes(hex);
System.out.println("原字符串:" + s);
System.out.println("十六进制字符串:" + hex);
System.out.println("还原:" + decode);
}
}

View File

@@ -0,0 +1,166 @@
package com.hua.transport.jt808.util;
import java.io.ByteArrayOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* JT808协议转义工具类
*
* <pre>
* 0x7d01 <====> 0x7d
* 0x7d02 <====> 0x7e
* </pre>
*
* @author huaxl
*
*/
public class JT808Util {
private final Logger log = LoggerFactory.getLogger(getClass());
private BitUtil bitOperator;
private BCDUtil bcd8421Operater;
public JT808Util() {
this.bitOperator = new BitUtil();
this.bcd8421Operater = new BCDUtil();
}
/**
* 接收消息时转义<br>
*
* <pre>
* 0x7d01 <====> 0x7d
* 0x7d02 <====> 0x7e
* </pre>
*
* @param bs
* 要转义的字节数组
* @param start
* 起始索引
* @param end
* 结束索引
* @return 转义后的字节数组
* @throws Exception
*/
public byte[] doEscape4Receive(byte[] bs, int start, int end) throws Exception {
if (start < 0 || end > bs.length)
throw new ArrayIndexOutOfBoundsException("doEscape4Receive error : index out of bounds(start=" + start
+ ",end=" + end + ",bytes length=" + bs.length + ")");
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
for (int i = 0; i < start; i++) {
baos.write(bs[i]);
}
for (int i = start; i < end - 1; i++) {
if (bs[i] == 0x7d && bs[i + 1] == 0x01) {
baos.write(0x7d);
i++;
} else if (bs[i] == 0x7d && bs[i + 1] == 0x02) {
baos.write(0x7e);
i++;
} else {
baos.write(bs[i]);
}
}
for (int i = end - 1; i < bs.length; i++) {
baos.write(bs[i]);
}
return baos.toByteArray();
} catch (Exception e) {
throw e;
} finally {
if (baos != null) {
baos.close();
baos = null;
}
}
}
/**
*
* 发送消息时转义<br>
*
* <pre>
* 0x7e <====> 0x7d02
* </pre>
*
* @param bs
* 要转义的字节数组
* @param start
* 起始索引
* @param end
* 结束索引
* @return 转义后的字节数组
* @throws Exception
*/
public byte[] doEscape4Send(byte[] bs, int start, int end) throws Exception {
if (start < 0 || end > bs.length)
throw new ArrayIndexOutOfBoundsException("doEscape4Send error : index out of bounds(start=" + start
+ ",end=" + end + ",bytes length=" + bs.length + ")");
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
for (int i = 0; i < start; i++) {
baos.write(bs[i]);
}
for (int i = start; i < end; i++) {
if (bs[i] == 0x7e) {
baos.write(0x7d);
baos.write(0x02);
} else {
baos.write(bs[i]);
}
}
for (int i = end; i < bs.length; i++) {
baos.write(bs[i]);
}
return baos.toByteArray();
} catch (Exception e) {
throw e;
} finally {
if (baos != null) {
baos.close();
baos = null;
}
}
}
public int generateMsgBodyProps(int msgLen, int enctyptionType, boolean isSubPackage, int reversed_14_15) {
// [ 0-9 ] 0000,0011,1111,1111(3FF)(消息体长度)
// [10-12] 0001,1100,0000,0000(1C00)(加密类型)
// [ 13_ ] 0010,0000,0000,0000(2000)(是否有子包)
// [14-15] 1100,0000,0000,0000(C000)(保留位)
if (msgLen >= 1024)
log.warn("The max value of msgLen is 1023, but {} .", msgLen);
int subPkg = isSubPackage ? 1 : 0;
int ret = (msgLen & 0x3FF) | ((enctyptionType << 10) & 0x1C00) | ((subPkg << 13) & 0x2000)
| ((reversed_14_15 << 14) & 0xC000);
return ret & 0xffff;
}
public byte[] generateMsgHeader(String phone, int msgType, byte[] body, int msgBodyProps, int flowId)
throws Exception {
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
// 1. 消息ID word(16)
baos.write(bitOperator.integerTo2Bytes(msgType));
// 2. 消息体属性 word(16)
baos.write(bitOperator.integerTo2Bytes(msgBodyProps));
// 3. 终端手机号 bcd[6]
baos.write(bcd8421Operater.string2Bcd(phone));
// 4. 消息流水号 word(16),按发送顺序从 0 开始循环累加
baos.write(bitOperator.integerTo2Bytes(flowId));
// 消息包封装项 此处不予考虑
return baos.toByteArray();
} finally {
if (baos != null) {
baos.close();
}
}
}
}

View File

@@ -0,0 +1,11 @@
server:
port: 8080
jt808:
port: 20048
logging:
level:
root: INFO
com.hua.transport.jt808: DEBUG

View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html>
<head>
<title>JT808 Server Dashboard</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { background-color: #f8f9fa; }
.dashboard-card { transition: all 0.3s; }
.dashboard-card:hover { transform: translateY(-5px); shadow: 0 4px 8px rgba(0,0,0,0.1); }
.log-console {
background-color: #1e1e1e;
color: #00ff00;
font-family: 'Courier New', Courier, monospace;
height: 300px;
overflow-y: auto;
padding: 10px;
border-radius: 5px;
font-size: 0.9rem;
}
.log-entry { margin-bottom: 5px; border-bottom: 1px solid #333; padding-bottom: 2px; }
.log-time { color: #888; margin-right: 10px; }
</style>
</head>
<body>
<div id="app">
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">JT808 Transport Server</span>
<span class="text-light">
Status:
<span class="badge" :class="connected ? 'bg-success' : 'bg-danger'">
{{ connected ? 'Connected' : 'Disconnected' }}
</span>
</span>
</div>
</nav>
<div class="container mt-4">
<div class="row">
<!-- Device Simulation Card -->
<div class="col-md-5">
<div class="card dashboard-card mb-4">
<div class="card-header bg-primary text-white">
Device Simulation (Typed API)
</div>
<div class="card-body">
<form @submit.prevent="sendReport">
<div class="mb-3">
<label class="form-label">IMEI</label>
<input v-model="form.imei" class="form-control" placeholder="123456789012">
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">Lat</label>
<input v-model.number="form.lat" type="number" step="0.000001" class="form-control">
</div>
<div class="col-6 mb-3">
<label class="form-label">Lon</label>
<input v-model.number="form.lon" type="number" step="0.000001" class="form-control">
</div>
</div>
<button type="submit" class="btn btn-primary w-100">Send Location</button>
</form>
</div>
</div>
<!-- Generic Upload Test -->
<div class="card dashboard-card mb-4">
<div class="card-header bg-info text-white">
Universal Upload Test (/upload)
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Arbitrary JSON Payload</label>
<textarea v-model="customJson" class="form-control" rows="3"></textarea>
</div>
<button @click="sendCustomJson" class="btn btn-info text-white w-100">Send to /upload</button>
</div>
</div>
</div>
<!-- Live Log Console -->
<div class="col-md-7">
<div class="card dashboard-card h-100">
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
<span>Live Data Stream (/api/v1/device/upload)</span>
<button @click="logs = []" class="btn btn-sm btn-outline-secondary">Clear</button>
</div>
<div class="card-body bg-dark p-0">
<div class="log-console" ref="console">
<div v-if="logs.length === 0" class="text-muted text-center mt-5">Waiting for data...</div>
<div v-for="(log, index) in logs" :key="index" class="log-entry">
<span class="log-time">[{{ log.time }}]</span>
<span>{{ log.data }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp } = Vue
createApp({
data() {
return {
form: {
imei: '13800138000',
lat: 34.215432,
lon: 108.924231,
speed: 60.5
},
customJson: '{\n "sensor": "temp-01",\n "value": 25.5,\n "unit": "C"\n}',
logs: [],
connected: false,
eventSource: null
}
},
mounted() {
this.connectSSE();
},
methods: {
connectSSE() {
this.eventSource = new EventSource('/api/v1/device/logs/stream');
this.eventSource.onopen = () => {
this.connected = true;
this.addLog('System connected. Listening for /upload events...');
};
this.eventSource.onerror = () => {
this.connected = false;
this.eventSource.close();
// Reconnect after 3s
setTimeout(() => this.connectSSE(), 3000);
};
this.eventSource.addEventListener('api-log', (event) => {
const data = JSON.parse(event.data);
this.addLog(data);
});
},
addLog(data) {
const now = new Date().toLocaleTimeString();
this.logs.unshift({
time: now,
data: typeof data === 'string' ? data : JSON.stringify(data)
});
// Keep last 50 logs
if (this.logs.length > 50) this.logs.pop();
},
async sendReport() {
try {
await fetch('/api/v1/device/location', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(this.form)
});
// Note: /location endpoint doesn't broadcast to SSE in this demo, only /upload does
// But we could add it if needed.
} catch (e) {
alert('Error: ' + e.message);
}
},
async sendCustomJson() {
try {
const res = await fetch('/api/v1/device/upload', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: this.customJson
});
const result = await res.json();
if (result.code !== 200) alert('Error: ' + result.message);
} catch (e) {
alert('Error: ' + e.message);
}
}
}
}).mount('#app')
</script>
</body>
</html>