commit dcd409e5d0a9f96c0f0c4f232dadb83dcb617f2d Author: lzh Date: Fri Dec 12 11:45:17 2025 +0800 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1c2a23 --- /dev/null +++ b/.gitignore @@ -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* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -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 diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..2803497 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..990dc9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..67e1e61 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e159a9a --- /dev/null +++ b/Dockerfile @@ -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"] + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..989e2c5 --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e0515f --- /dev/null +++ b/README.md @@ -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 +} +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..841a306 --- /dev/null +++ b/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + com.hua + hua-transport-jt808 + 0.0.1-SNAPSHOT + hua-transport-jt808 + JT808 Transport Server with Spring Boot + + 17 + UTF-8 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + io.netty + netty-all + + + + + org.apache.commons + commons-lang3 + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/hua/transport/jt808/Jt808Application.java b/src/main/java/com/hua/transport/jt808/Jt808Application.java new file mode 100644 index 0000000..cb2f6fb --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/Jt808Application.java @@ -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); + } + +} + diff --git a/src/main/java/com/hua/transport/jt808/common/CommonResult.java b/src/main/java/com/hua/transport/jt808/common/CommonResult.java new file mode 100644 index 0000000..3ba6c10 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/common/CommonResult.java @@ -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 { + private long code; + private String message; + private T data; + + public static CommonResult success(T data) { + return new CommonResult<>(200, "Success", data); + } + + public static CommonResult success(T data, String message) { + return new CommonResult<>(200, message, data); + } + + public static CommonResult failed(String message) { + return new CommonResult<>(500, message, null); + } +} + diff --git a/src/main/java/com/hua/transport/jt808/common/Consts.java b/src/main/java/com/hua/transport/jt808/common/Consts.java new file mode 100644 index 0000000..6b59e4e --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/common/Consts.java @@ -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; + +} diff --git a/src/main/java/com/hua/transport/jt808/controller/DeviceController.java b/src/main/java/com/hua/transport/jt808/controller/DeviceController.java new file mode 100644 index 0000000..48f369c --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/controller/DeviceController.java @@ -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 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 receiveDeviceData(@RequestBody Map payload) { + // 广播日志(同时会记录到服务器日志文件) + apiLogService.broadcastLog(payload); + + return CommonResult.success("设备数据接收成功"); + } + + /** + * 前端 SSE 连接接口,用于实时接收日志 + */ + @GetMapping("/logs/stream") + public SseEmitter streamLogs() { + return apiLogService.createEmitter(); + } + + @GetMapping("/health") + public String health() { + return "OK"; + } +} diff --git a/src/main/java/com/hua/transport/jt808/entity/DataPack.java b/src/main/java/com/hua/transport/jt808/entity/DataPack.java new file mode 100644 index 0000000..fa86877 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/DataPack.java @@ -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 + "]"; + } + + } + +} diff --git a/src/main/java/com/hua/transport/jt808/entity/Session.java b/src/main/java/com/hua/transport/jt808/entity/Session.java new file mode 100644 index 0000000..86fcc13 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/Session.java @@ -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++; + } + +} \ No newline at end of file diff --git a/src/main/java/com/hua/transport/jt808/entity/dto/LocationDto.java b/src/main/java/com/hua/transport/jt808/entity/dto/LocationDto.java new file mode 100644 index 0000000..34a9ee0 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/dto/LocationDto.java @@ -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; +} + diff --git a/src/main/java/com/hua/transport/jt808/entity/request/AuthenticationPack.java b/src/main/java/com/hua/transport/jt808/entity/request/AuthenticationPack.java new file mode 100644 index 0000000..aa08e2e --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/request/AuthenticationPack.java @@ -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 + "]"; + } + +} diff --git a/src/main/java/com/hua/transport/jt808/entity/request/LocationPack.java b/src/main/java/com/hua/transport/jt808/entity/request/LocationPack.java new file mode 100644 index 0000000..2e6ed4c --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/request/LocationPack.java @@ -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 + "]"; + } + +} diff --git a/src/main/java/com/hua/transport/jt808/entity/request/RegisterPack.java b/src/main/java/com/hua/transport/jt808/entity/request/RegisterPack.java new file mode 100644 index 0000000..4367b37 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/request/RegisterPack.java @@ -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
+ * 0===未上车牌
+ * 1===蓝色
+ * 2===黄色
+ * 3===黑色
+ * 4===白色
+ * 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 + "]"; + } + + } +} diff --git a/src/main/java/com/hua/transport/jt808/entity/response/RegisterBodyPack.java b/src/main/java/com/hua/transport/jt808/entity/response/RegisterBodyPack.java new file mode 100644 index 0000000..45e8bbc --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/response/RegisterBodyPack.java @@ -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)
+ * 0:成功
+ * 1:车辆已被注册
+ * 2:数据库中无该车辆
+ **/ + 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 + "]"; + } + +} diff --git a/src/main/java/com/hua/transport/jt808/entity/response/ServerBodyPack.java b/src/main/java/com/hua/transport/jt808/entity/response/ServerBodyPack.java new file mode 100644 index 0000000..1cad385 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/entity/response/ServerBodyPack.java @@ -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:成功∕确认
+ * 1:失败
+ * 2:消息有误
+ * 3:不支持
+ * 4:报警处理确认
+ */ + 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 + + "]"; + } + +} diff --git a/src/main/java/com/hua/transport/jt808/server/Jt808NettyServer.java b/src/main/java/com/hua/transport/jt808/server/Jt808NettyServer.java new file mode 100644 index 0000000..24fb288 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/server/Jt808NettyServer.java @@ -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(); + // } + // } +} + diff --git a/src/main/java/com/hua/transport/jt808/server/SessionManager.java b/src/main/java/com/hua/transport/jt808/server/SessionManager.java new file mode 100644 index 0000000..fec3495 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/server/SessionManager.java @@ -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 sessionIdMap; + // 终端手机号和netty生成的sessionID的对应关系 + private Map 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 keySet() { + return sessionIdMap.keySet(); + } + + public void forEach(BiConsumer action) { + sessionIdMap.forEach(action); + } + + public Set> entrySet() { + return sessionIdMap.entrySet(); + } + + public List toList() { + return this.sessionIdMap.entrySet().stream().map(e -> e.getValue()).collect(Collectors.toList()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/hua/transport/jt808/server/TCPServer.java b/src/main/java/com/hua/transport/jt808/server/TCPServer.java new file mode 100644 index 0000000..2f9f197 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/server/TCPServer.java @@ -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() { // + @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"; + } +} \ No newline at end of file diff --git a/src/main/java/com/hua/transport/jt808/service/ApiLogService.java b/src/main/java/com/hua/transport/jt808/service/ApiLogService.java new file mode 100644 index 0000000..29c5598 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/ApiLogService.java @@ -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 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 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); + } + } + } +} + diff --git a/src/main/java/com/hua/transport/jt808/service/DeviceService.java b/src/main/java/com/hua/transport/jt808/service/DeviceService.java new file mode 100644 index 0000000..2ac2ae4 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/DeviceService.java @@ -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); +} + diff --git a/src/main/java/com/hua/transport/jt808/service/codec/DataDecoder.java b/src/main/java/com/hua/transport/jt808/service/codec/DataDecoder.java new file mode 100644 index 0000000..18f9292 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/codec/DataDecoder.java @@ -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; + } + } +} diff --git a/src/main/java/com/hua/transport/jt808/service/codec/DataEncoder.java b/src/main/java/com/hua/transport/jt808/service/codec/DataEncoder.java new file mode 100644 index 0000000..511e4c0 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/codec/DataEncoder.java @@ -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); + } +} diff --git a/src/main/java/com/hua/transport/jt808/service/codec/LogDecoder.java b/src/main/java/com/hua/transport/jt808/service/codec/LogDecoder.java new file mode 100644 index 0000000..7e56bfd --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/codec/LogDecoder.java @@ -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; + +/** + * 该解码器只是为了自己日志所用,没其他作用.
+ * 最终删除 + * + * @author huaxl + * + */ +public class LogDecoder extends ByteToMessageDecoder { + + private final Logger log = LoggerFactory.getLogger(getClass()); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List 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); + } +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/MessageHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/MessageHandler.java new file mode 100644 index 0000000..abc67d5 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/MessageHandler.java @@ -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); +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/MessageHandlerFactory.java b/src/main/java/com/hua/transport/jt808/service/handler/MessageHandlerFactory.java new file mode 100644 index 0000000..65b20b6 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/MessageHandlerFactory.java @@ -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> handlerMap = new HashMap>(); + 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(); + } + +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/TCPServerHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/TCPServerHandler.java new file mode 100644 index 0000000..25960ac --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/TCPServerHandler.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/hua/transport/jt808/service/handler/terminal/AuthenticationHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/terminal/AuthenticationHandler.java new file mode 100644 index 0000000..1f5f3ff --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/terminal/AuthenticationHandler.java @@ -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()); + } + + } + +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/terminal/HeartbeatHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/terminal/HeartbeatHandler.java new file mode 100644 index 0000000..bf993e8 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/terminal/HeartbeatHandler.java @@ -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()); + } + + } + +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/terminal/LocationUploadHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/terminal/LocationUploadHandler.java new file mode 100644 index 0000000..4f00604 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/terminal/LocationUploadHandler.java @@ -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()); + } + + } + +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/terminal/LoginOutHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/terminal/LoginOutHandler.java new file mode 100644 index 0000000..8130e86 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/terminal/LoginOutHandler.java @@ -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()); + } + + } + +} diff --git a/src/main/java/com/hua/transport/jt808/service/handler/terminal/RegisterHandler.java b/src/main/java/com/hua/transport/jt808/service/handler/terminal/RegisterHandler.java new file mode 100644 index 0000000..dfb899a --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/service/handler/terminal/RegisterHandler.java @@ -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("<<<<> SERVICE: Device {} location: lat={}, lon={}, speed={}, time={}", + terminalPhone, latitude, longitude, speed, time); + } +} + diff --git a/src/main/java/com/hua/transport/jt808/util/BCDUtil.java b/src/main/java/com/hua/transport/jt808/util/BCDUtil.java new file mode 100644 index 0000000..521728a --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/util/BCDUtil.java @@ -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); + } +} diff --git a/src/main/java/com/hua/transport/jt808/util/BitUtil.java b/src/main/java/com/hua/transport/jt808/util/BitUtil.java new file mode 100644 index 0000000..4742ebe --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/util/BitUtil.java @@ -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 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); + } +} diff --git a/src/main/java/com/hua/transport/jt808/util/HexUtil.java b/src/main/java/com/hua/transport/jt808/util/HexUtil.java new file mode 100644 index 0000000..8d1ae97 --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/util/HexUtil.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/hua/transport/jt808/util/JT808Util.java b/src/main/java/com/hua/transport/jt808/util/JT808Util.java new file mode 100644 index 0000000..5852fdb --- /dev/null +++ b/src/main/java/com/hua/transport/jt808/util/JT808Util.java @@ -0,0 +1,166 @@ +package com.hua.transport.jt808.util; + +import java.io.ByteArrayOutputStream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * JT808协议转义工具类 + * + *
+ * 0x7d01 <====> 0x7d
+ * 0x7d02 <====> 0x7e
+ * 
+ * + * @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(); + } + + /** + * 接收消息时转义
+ * + *
+	 * 0x7d01 <====> 0x7d
+	 * 0x7d02 <====> 0x7e
+	 * 
+ * + * @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; + } + } + } + + /** + * + * 发送消息时转义
+ * + *
+	 *  0x7e <====> 0x7d02
+	 * 
+ * + * @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(); + } + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..74eaa76 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,11 @@ +server: + port: 8080 + +jt808: + port: 20048 + +logging: + level: + root: INFO + com.hua.transport.jt808: DEBUG + diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html new file mode 100644 index 0000000..1d5fe0b --- /dev/null +++ b/src/main/resources/static/index.html @@ -0,0 +1,187 @@ + + + + JT808 Server Dashboard + + + + + + + +
+ + +
+
+ +
+
+
+ Device Simulation (Typed API) +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ Universal Upload Test (/upload) +
+
+
+ + +
+ +
+
+
+ + +
+
+
+ Live Data Stream (/api/v1/device/upload) + +
+
+
+
Waiting for data...
+
+ [{{ log.time }}] + {{ log.data }} +
+
+
+
+
+
+
+
+ + + + diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..74eaa76 --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,11 @@ +server: + port: 8080 + +jt808: + port: 20048 + +logging: + level: + root: INFO + com.hua.transport.jt808: DEBUG + diff --git a/target/classes/static/index.html b/target/classes/static/index.html new file mode 100644 index 0000000..1d5fe0b --- /dev/null +++ b/target/classes/static/index.html @@ -0,0 +1,187 @@ + + + + JT808 Server Dashboard + + + + + + + +
+ + +
+
+ +
+
+
+ Device Simulation (Typed API) +
+
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+ Universal Upload Test (/upload) +
+
+
+ + +
+ +
+
+
+ + +
+
+
+ Live Data Stream (/api/v1/device/upload) + +
+
+
+
Waiting for data...
+
+ [{{ log.time }}] + {{ log.data }} +
+
+
+
+
+
+
+
+ + + +