first commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal 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
8
.idea/.gitignore
generated
vendored
Normal 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
20
.idea/compiler.xml
generated
Normal 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
7
.idea/encodings.xml
generated
Normal 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
25
.idea/jarRepositories.xml
generated
Normal 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
12
.idea/misc.xml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
24
Dockerfile
Normal 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
201
LICENSE
Normal 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
56
README.md
Normal 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
70
pom.xml
Normal 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>
|
||||
14
src/main/java/com/hua/transport/jt808/Jt808Application.java
Normal file
14
src/main/java/com/hua/transport/jt808/Jt808Application.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
46
src/main/java/com/hua/transport/jt808/common/Consts.java
Normal file
46
src/main/java/com/hua/transport/jt808/common/Consts.java
Normal 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;
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
205
src/main/java/com/hua/transport/jt808/entity/DataPack.java
Normal file
205
src/main/java/com/hua/transport/jt808/entity/DataPack.java
Normal 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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
124
src/main/java/com/hua/transport/jt808/entity/Session.java
Normal file
124
src/main/java/com/hua/transport/jt808/entity/Session.java
Normal 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++;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
112
src/main/java/com/hua/transport/jt808/server/SessionManager.java
Normal file
112
src/main/java/com/hua/transport/jt808/server/SessionManager.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
||||
114
src/main/java/com/hua/transport/jt808/server/TCPServer.java
Normal file
114
src/main/java/com/hua/transport/jt808/server/TCPServer.java
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
57
src/main/java/com/hua/transport/jt808/util/BCDUtil.java
Normal file
57
src/main/java/com/hua/transport/jt808/util/BCDUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
394
src/main/java/com/hua/transport/jt808/util/BitUtil.java
Normal file
394
src/main/java/com/hua/transport/jt808/util/BitUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
61
src/main/java/com/hua/transport/jt808/util/HexUtil.java
Normal file
61
src/main/java/com/hua/transport/jt808/util/HexUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
166
src/main/java/com/hua/transport/jt808/util/JT808Util.java
Normal file
166
src/main/java/com/hua/transport/jt808/util/JT808Util.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/main/resources/application.yml
Normal file
11
src/main/resources/application.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
jt808:
|
||||
port: 20048
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.hua.transport.jt808: DEBUG
|
||||
|
||||
187
src/main/resources/static/index.html
Normal file
187
src/main/resources/static/index.html
Normal 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>
|
||||
11
target/classes/application.yml
Normal file
11
target/classes/application.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
jt808:
|
||||
port: 20048
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.hua.transport.jt808: DEBUG
|
||||
|
||||
187
target/classes/static/index.html
Normal file
187
target/classes/static/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user