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

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

20
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="hua-transport-jt808" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="hua-transport-jt808" options="-parameters" />
<module name="iot-test-platform" options="-parameters" />
</option>
</component>
</project>

7
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

25
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="alimaven" />
<option name="name" value="aliyun maven" />
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Build Stage (Optional, if you want to build inside Docker)
# FROM maven:3.9-eclipse-temurin-17 AS build
# WORKDIR /app
# COPY pom.xml .
# COPY src ./src
# RUN mvn clean package -DskipTests
# Run Stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
VOLUME /tmp
# Copy the built jar (Assuming you built it locally with 'mvn package' or in CI)
# If using the build stage above, change to: COPY --from=build /app/target/*.jar app.jar
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
# Expose Web Port and JT808 TCP Port
EXPOSE 8080
EXPOSE 20048
# Run the application
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

201
LICENSE Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

56
README.md Normal file
View File

@@ -0,0 +1,56 @@
# JT808 Spring Boot Transport Server
This project has been modernized to a Spring Boot 3 + Netty architecture, supporting both TCP (JT808) and HTTP API channels.
## Features
- **Dual Channel**:
- **TCP (Port 20048)**: Standard JT808 protocol for device connections.
- **HTTP API (Port 8080)**: RESTful API for testing or third-party integration.
- **Web UI**: Built-in Dashboard at `http://localhost:8080/`.
- **Docker Ready**: Simple deployment via Docker.
## Structure
- `src/main/java/com/hua/transport/jt808`
- `Jt808Application.java`: Spring Boot Entry Point.
- `server/Jt808NettyServer.java`: Netty Server Wrapper.
- `controller/DeviceController.java`: HTTP API Controller.
- `service/`: Business Logic Layer.
- `tcp/`: Original JT808 Protocol Logic.
- `src/main/resources/static`: Frontend Static Files (Vue 3 Dashboard).
## How to Run
### Local Development
```bash
mvn clean spring-boot:run
```
Access Dashboard: http://localhost:8080/
### Docker
1. Build JAR:
```bash
mvn clean package -DskipTests
```
2. Build Image:
```bash
docker build -t jt808-server .
```
3. Run:
```bash
docker run -d -p 8080:8080 -p 20048:20048 --name jt808 jt808-server
```
## API Usage
**Report Location (HTTP):**
POST /api/v1/device/location
```json
{
"imei": "123456789012",
"lat": 30.123456,
"lon": 120.654321,
"speed": 60.0
}
```

70
pom.xml Normal file
View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hua</groupId>
<artifactId>hua-transport-jt808</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hua-transport-jt808</name>
<description>JT808 Transport Server with Spring Boot</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- Replace FastJSON with Jackson (already included in web starter, but explicit annotations needed) -->
<!-- Jackson is transitive dependency of starter-web, so no need to add explicitly unless specific version needed -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

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>

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>