feat:测试平台2.0更新
All checks were successful
iot-test-platform CI/CD / build-and-deploy (push) Successful in 15s
All checks were successful
iot-test-platform CI/CD / build-and-deploy (push) Successful in 15s
This commit is contained in:
@@ -1,129 +0,0 @@
|
||||
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;
|
||||
|
||||
// Bluetooth Extension Data
|
||||
private java.util.List<BluetoothInfo> bluetoothInfos;
|
||||
|
||||
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 java.util.List<BluetoothInfo> getBluetoothInfos() {
|
||||
return bluetoothInfos;
|
||||
}
|
||||
|
||||
public void setBluetoothInfos(java.util.List<BluetoothInfo> bluetoothInfos) {
|
||||
this.bluetoothInfos = bluetoothInfos;
|
||||
}
|
||||
|
||||
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 + ", bluetoothInfos=" + bluetoothInfos + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package com.hua.transport.jt808.entity.request;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.hua.transport.jt808.entity.DataPack;
|
||||
|
||||
/**
|
||||
* 终端注册消息
|
||||
*
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
public class RegisterPack extends DataPack {
|
||||
|
||||
private TerminalRegInfo terminalRegInfo;
|
||||
|
||||
public RegisterPack() {
|
||||
}
|
||||
|
||||
public RegisterPack(DataPack packageData) {
|
||||
this();
|
||||
this.channel = packageData.getChannel();
|
||||
this.checkSum = packageData.getCheckSum();
|
||||
this.bodyBytes = packageData.getBodyBytes();
|
||||
this.packHead = packageData.getPackHead();
|
||||
}
|
||||
|
||||
public TerminalRegInfo getTerminalRegInfo() {
|
||||
return terminalRegInfo;
|
||||
}
|
||||
|
||||
public void setTerminalRegInfo(TerminalRegInfo msgBody) {
|
||||
this.terminalRegInfo = msgBody;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TerminalRegisterMsg [terminalRegInfo=" + terminalRegInfo + ", msgHeader=" + packHead
|
||||
+ ", msgBodyBytes=" + Arrays.toString(bodyBytes) + ", checkSum=" + checkSum + ", channel=" + channel
|
||||
+ "]";
|
||||
}
|
||||
|
||||
public static class TerminalRegInfo {
|
||||
// 省域ID(WORD),设备安装车辆所在的省域,省域ID采用GB/T2260中规定的行政区划代码6位中前两位
|
||||
// 0保留,由平台取默认值
|
||||
private int provinceId;
|
||||
// 市县域ID(WORD) 设备安装车辆所在的市域或县域,市县域ID采用GB/T2260中规定的行 政区划代码6位中后四位
|
||||
// 0保留,由平台取默认值
|
||||
private int cityId;
|
||||
// 制造商ID(BYTE[5]) 5 个字节,终端制造商编码
|
||||
private String manufacturerId;
|
||||
// 终端型号(BYTE[8]) 八个字节, 此终端型号 由制造商自行定义 位数不足八位的,补空格。
|
||||
private String terminalType;
|
||||
// 终端ID(BYTE[7]) 七个字节, 由大写字母 和数字组成, 此终端 ID由制造 商自行定义
|
||||
private String terminalId;
|
||||
/**
|
||||
*
|
||||
* 车牌颜色(BYTE) 车牌颜色,按照 JT/T415-2006 的 5.4.12 未上牌时,取值为0<br>
|
||||
* 0===未上车牌<br>
|
||||
* 1===蓝色<br>
|
||||
* 2===黄色<br>
|
||||
* 3===黑色<br>
|
||||
* 4===白色<br>
|
||||
* 9===其他
|
||||
*/
|
||||
private int licensePlateColor;
|
||||
// 车牌(STRING) 公安交 通管理部门颁 发的机动车号牌
|
||||
private String licensePlate;
|
||||
|
||||
public TerminalRegInfo() {
|
||||
}
|
||||
|
||||
public int getProvinceId() {
|
||||
return provinceId;
|
||||
}
|
||||
|
||||
public void setProvinceId(int provinceId) {
|
||||
this.provinceId = provinceId;
|
||||
}
|
||||
|
||||
public int getCityId() {
|
||||
return cityId;
|
||||
}
|
||||
|
||||
public void setCityId(int cityId) {
|
||||
this.cityId = cityId;
|
||||
}
|
||||
|
||||
public String getManufacturerId() {
|
||||
return manufacturerId;
|
||||
}
|
||||
|
||||
public void setManufacturerId(String manufacturerId) {
|
||||
this.manufacturerId = manufacturerId;
|
||||
}
|
||||
|
||||
public String getTerminalType() {
|
||||
return terminalType;
|
||||
}
|
||||
|
||||
public void setTerminalType(String terminalType) {
|
||||
this.terminalType = terminalType;
|
||||
}
|
||||
|
||||
public String getTerminalId() {
|
||||
return terminalId;
|
||||
}
|
||||
|
||||
public void setTerminalId(String terminalId) {
|
||||
this.terminalId = terminalId;
|
||||
}
|
||||
|
||||
public int getLicensePlateColor() {
|
||||
return licensePlateColor;
|
||||
}
|
||||
|
||||
public void setLicensePlateColor(int licensePlate) {
|
||||
this.licensePlateColor = licensePlate;
|
||||
}
|
||||
|
||||
public String getLicensePlate() {
|
||||
return licensePlate;
|
||||
}
|
||||
|
||||
public void setLicensePlate(String licensePlate) {
|
||||
this.licensePlate = licensePlate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TerminalRegInfo [provinceId=" + provinceId + ", cityId=" + cityId + ", manufacturerId="
|
||||
+ manufacturerId + ", terminalType=" + terminalType + ", terminalId=" + terminalId
|
||||
+ ", licensePlateColor=" + licensePlateColor + ", licensePlate=" + licensePlate + "]";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.hua.transport.jt808.entity.response;
|
||||
|
||||
public class RegisterBodyPack {
|
||||
|
||||
public static final byte success = 0;
|
||||
public static final byte car_already_registered = 1;
|
||||
public static final byte car_not_found = 2;
|
||||
public static final byte terminal_already_registered = 3;
|
||||
public static final byte terminal_not_found = 4;
|
||||
// byte[0-1] 应答流水号(WORD) 对应的终端注册消息的流水号
|
||||
private int replyFlowId;
|
||||
/***
|
||||
* byte[2] 结果(BYTE) <br>
|
||||
* 0:成功<br>
|
||||
* 1:车辆已被注册<br>
|
||||
* 2:数据库中无该车辆<br>
|
||||
**/
|
||||
private byte replyCode;
|
||||
// byte[3-x] 鉴权码(STRING) 只有在成功后才有该字段
|
||||
private String replyToken;
|
||||
|
||||
public RegisterBodyPack() {
|
||||
}
|
||||
|
||||
public int getReplyFlowId() {
|
||||
return replyFlowId;
|
||||
}
|
||||
|
||||
public void setReplyFlowId(int flowId) {
|
||||
this.replyFlowId = flowId;
|
||||
}
|
||||
|
||||
public byte getReplyCode() {
|
||||
return replyCode;
|
||||
}
|
||||
|
||||
public void setReplyCode(byte code) {
|
||||
this.replyCode = code;
|
||||
}
|
||||
|
||||
public String getReplyToken() {
|
||||
return replyToken;
|
||||
}
|
||||
|
||||
public void setReplyToken(String token) {
|
||||
this.replyToken = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TerminalRegisterMsgResp [replyFlowId=" + replyFlowId + ", replyCode=" + replyCode + ", replyToken="
|
||||
+ replyToken + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.hua.transport.jt808.entity.response;
|
||||
|
||||
public class ServerBodyPack {
|
||||
|
||||
public static final byte success = 0;
|
||||
public static final byte failure = 1;
|
||||
public static final byte msg_error = 2;
|
||||
public static final byte unsupported = 3;
|
||||
public static final byte warnning_msg_ack = 4;
|
||||
|
||||
// byte[0-1] 应答流水号 对应的终端消息的流水号
|
||||
private int replyFlowId;
|
||||
// byte[2-3] 应答ID 对应的终端消息的ID
|
||||
private int replyId;
|
||||
/**
|
||||
* 0:成功∕确认<br>
|
||||
* 1:失败<br>
|
||||
* 2:消息有误<br>
|
||||
* 3:不支持<br>
|
||||
* 4:报警处理确认<br>
|
||||
*/
|
||||
private byte replyCode;
|
||||
|
||||
public ServerBodyPack() {
|
||||
}
|
||||
|
||||
public ServerBodyPack(int replyFlowId, int replyId, byte replyCode) {
|
||||
super();
|
||||
this.replyFlowId = replyFlowId;
|
||||
this.replyId = replyId;
|
||||
this.replyCode = replyCode;
|
||||
}
|
||||
|
||||
public int getReplyFlowId() {
|
||||
return replyFlowId;
|
||||
}
|
||||
|
||||
public void setReplyFlowId(int flowId) {
|
||||
this.replyFlowId = flowId;
|
||||
}
|
||||
|
||||
public int getReplyId() {
|
||||
return replyId;
|
||||
}
|
||||
|
||||
public void setReplyId(int msgId) {
|
||||
this.replyId = msgId;
|
||||
}
|
||||
|
||||
public byte getReplyCode() {
|
||||
return replyCode;
|
||||
}
|
||||
|
||||
public void setReplyCode(byte code) {
|
||||
this.replyCode = code;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ServerCommonRespMsg [replyFlowId=" + replyFlowId + ", replyId=" + replyId + ", replyCode=" + replyCode
|
||||
+ "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808;
|
||||
package com.iot.transport.jt808;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.common;
|
||||
package com.iot.transport.jt808.common;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.common;
|
||||
package com.iot.transport.jt808.common;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.hua.transport.jt808.controller;
|
||||
package com.iot.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 com.iot.transport.jt808.common.CommonResult;
|
||||
import com.iot.transport.jt808.entity.dto.LocationDto;
|
||||
import com.iot.transport.jt808.service.ApiLogService;
|
||||
import com.iot.transport.jt808.service.DeviceService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.entity;
|
||||
package com.iot.transport.jt808.entity;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.entity;
|
||||
package com.iot.transport.jt808.entity;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.entity.dto;
|
||||
package com.iot.transport.jt808.entity.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import java.util.Date;
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.hua.transport.jt808.entity.request;
|
||||
package com.iot.transport.jt808.entity.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.hua.transport.jt808.common.Consts;
|
||||
import com.hua.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
|
||||
/**
|
||||
* 终端鉴权消息
|
||||
@@ -11,6 +15,9 @@ import com.hua.transport.jt808.entity.DataPack;
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class AuthenticationPack extends DataPack {
|
||||
|
||||
private String authCode;
|
||||
@@ -26,19 +33,4 @@ public class AuthenticationPack extends DataPack {
|
||||
this.packHead = packageData.getPackHead();
|
||||
this.authCode = new String(packageData.getBodyBytes(), Consts.DEFAULT_CHARSET);
|
||||
}
|
||||
|
||||
public void setAuthCode(String authCode) {
|
||||
this.authCode = authCode;
|
||||
}
|
||||
|
||||
public String getAuthCode() {
|
||||
return authCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TerminalAuthenticationMsg [authCode=" + authCode + ", msgHeader=" + packHead + ", msgBodyBytes="
|
||||
+ Arrays.toString(bodyBytes) + ", checkSum=" + checkSum + ", channel=" + channel + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.iot.transport.jt808.entity.request;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 0xFE 扩展信息:电池、版本、ICCID等
|
||||
*/
|
||||
public class BatteryVersionInfo {
|
||||
// 0x02: 备用电池电量百分比
|
||||
private int batteryLevel;
|
||||
|
||||
// 0x07: 版本信息
|
||||
private String version;
|
||||
|
||||
// 0x20: ICCID
|
||||
private String iccid;
|
||||
|
||||
// 其他可能的扩展字段
|
||||
private Map<Integer, String> extraData = new HashMap<>();
|
||||
|
||||
public int getBatteryLevel() {
|
||||
return batteryLevel;
|
||||
}
|
||||
|
||||
public void setBatteryLevel(int batteryLevel) {
|
||||
this.batteryLevel = batteryLevel;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getIccid() {
|
||||
return iccid;
|
||||
}
|
||||
|
||||
public void setIccid(String iccid) {
|
||||
this.iccid = iccid;
|
||||
}
|
||||
|
||||
public void addExtra(int id, String data) {
|
||||
this.extraData.put(id, data);
|
||||
}
|
||||
|
||||
public Map<Integer, String> getExtraData() {
|
||||
return extraData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BatteryVersionInfo{" +
|
||||
"batteryLevel=" + batteryLevel +
|
||||
", version='" + version + '\'' +
|
||||
", iccid='" + iccid + '\'' +
|
||||
", extraData=" + extraData +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.entity.request;
|
||||
package com.iot.transport.jt808.entity.request;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.iot.transport.jt808.entity.request;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
|
||||
/**
|
||||
* 位置信息汇报消息
|
||||
*
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
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;
|
||||
|
||||
// Generic Extensions Map
|
||||
// Key: Extension ID (e.g., 0xF3, 0x01)
|
||||
// Value: Parsed extension object or raw data
|
||||
private Map<Integer, Object> extensions;
|
||||
|
||||
public LocationPack() {
|
||||
this.extensions = new HashMap<>();
|
||||
}
|
||||
|
||||
public LocationPack(DataPack packageData) {
|
||||
this();
|
||||
this.channel = packageData.getChannel();
|
||||
this.checkSum = packageData.getCheckSum();
|
||||
this.bodyBytes = packageData.getBodyBytes();
|
||||
this.packHead = packageData.getPackHead();
|
||||
}
|
||||
|
||||
public void addExtension(Integer id, Object data) {
|
||||
if (this.extensions == null) {
|
||||
this.extensions = new HashMap<>();
|
||||
}
|
||||
this.extensions.put(id, data);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<BluetoothInfo> getBluetoothInfos() {
|
||||
if (extensions != null && extensions.containsKey(0xF3)) {
|
||||
return (List<BluetoothInfo>) extensions.get(0xF3);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBluetoothInfos(List<BluetoothInfo> bluetoothInfos) {
|
||||
addExtension(0xF3, bluetoothInfos);
|
||||
}
|
||||
|
||||
public BatteryVersionInfo getBatteryVersionInfo() {
|
||||
if (extensions != null && extensions.containsKey(0xFE)) {
|
||||
return (BatteryVersionInfo) extensions.get(0xFE);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setBatteryVersionInfo(BatteryVersionInfo info) {
|
||||
addExtension(0xFE, info);
|
||||
}
|
||||
|
||||
public Float getMileage() {
|
||||
if (extensions != null && extensions.containsKey(0x01)) return (Float) extensions.get(0x01);
|
||||
return null;
|
||||
}
|
||||
public void setMileage(Float mileage) { addExtension(0x01, mileage); }
|
||||
|
||||
public Integer getSignalStrength() {
|
||||
if (extensions != null && extensions.containsKey(0x30)) return (Integer) extensions.get(0x30);
|
||||
return null;
|
||||
}
|
||||
public void setSignalStrength(Integer signalStrength) { addExtension(0x30, signalStrength); }
|
||||
|
||||
public void setVersionInfo(String versionInfo) {
|
||||
BatteryVersionInfo info = getBatteryVersionInfo();
|
||||
if (info == null) {
|
||||
info = new BatteryVersionInfo();
|
||||
setBatteryVersionInfo(info);
|
||||
}
|
||||
info.setVersion(versionInfo);
|
||||
}
|
||||
|
||||
public void setBatteryLevel(int batteryLevel) {
|
||||
BatteryVersionInfo info = getBatteryVersionInfo();
|
||||
if (info == null) {
|
||||
info = new BatteryVersionInfo();
|
||||
setBatteryVersionInfo(info);
|
||||
}
|
||||
info.setBatteryLevel(batteryLevel);
|
||||
}
|
||||
|
||||
public void setIccid(String iccid) {
|
||||
BatteryVersionInfo info = getBatteryVersionInfo();
|
||||
if (info == null) {
|
||||
info = new BatteryVersionInfo();
|
||||
setBatteryVersionInfo(info);
|
||||
}
|
||||
info.setIccid(iccid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.iot.transport.jt808.entity.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
|
||||
/**
|
||||
* 终端注册消息
|
||||
*
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
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();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class TerminalRegInfo {
|
||||
// 省域ID(WORD),设备安装车辆所在的省域,省域ID采用GB/T2260中规定的行政区划代码6位中前两位
|
||||
// 0保留,由平台取默认值
|
||||
private int provinceId;
|
||||
// 市县域ID(WORD) 设备安装车辆所在的市域或县域,市县域ID采用GB/T2260中规定的行 政区划代码6位中后四位
|
||||
// 0保留,由平台取默认值
|
||||
private int cityId;
|
||||
// 制造商ID(BYTE[5]) 5 个字节,终端制造商编码
|
||||
private String manufacturerId;
|
||||
// 终端型号(BYTE[8]) 八个字节, 此终端型号 由制造商自行定义 位数不足八位的,补空格。
|
||||
private String terminalType;
|
||||
// 终端ID(BYTE[7]) 七个字节, 由大写字母 和数字组成, 此终端 ID由制造 商自行定义
|
||||
private String terminalId;
|
||||
/**
|
||||
*
|
||||
* 车牌颜色(BYTE) 车牌颜色,按照 JT/T415-2006 的 5.4.12 未上牌时,取值为0<br>
|
||||
* 0===未上车牌<br>
|
||||
* 1===蓝色<br>
|
||||
* 2===黄色<br>
|
||||
* 3===黑色<br>
|
||||
* 4===白色<br>
|
||||
* 9===其他
|
||||
*/
|
||||
private int licensePlateColor;
|
||||
// 车牌(STRING) 公安交 通管理部门颁 发的机动车号牌
|
||||
private String licensePlate;
|
||||
|
||||
public TerminalRegInfo() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.iot.transport.jt808.entity.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 终端注册应答
|
||||
*
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RegisterBodyPack {
|
||||
|
||||
public static final byte success = 0;
|
||||
public static final byte car_already_registered = 1;
|
||||
public static final byte car_not_found = 2;
|
||||
public static final byte terminal_already_registered = 3;
|
||||
public static final byte terminal_not_found = 4;
|
||||
// byte[0-1] 应答流水号(WORD) 对应的终端注册消息的流水号
|
||||
private int replyFlowId;
|
||||
/***
|
||||
* byte[2] 结果(BYTE) <br>
|
||||
* 0:成功<br>
|
||||
* 1:车辆已被注册<br>
|
||||
* 2:数据库中无该车辆<br>
|
||||
**/
|
||||
private byte replyCode;
|
||||
// byte[3-x] 鉴权码(STRING) 只有在成功后才有该字段
|
||||
private String replyToken;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.iot.transport.jt808.entity.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 平台通用应答
|
||||
*
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ServerBodyPack {
|
||||
|
||||
public static final byte success = 0;
|
||||
public static final byte failure = 1;
|
||||
public static final byte msg_error = 2;
|
||||
public static final byte unsupported = 3;
|
||||
public static final byte warnning_msg_ack = 4;
|
||||
|
||||
// byte[0-1] 应答流水号 对应的终端消息的流水号
|
||||
private int replyFlowId;
|
||||
// byte[2-3] 应答ID 对应的终端消息的ID
|
||||
private int replyId;
|
||||
/**
|
||||
* 0:成功∕确认<br>
|
||||
* 1:失败<br>
|
||||
* 2:消息有误<br>
|
||||
* 3:不支持<br>
|
||||
* 4:报警处理确认<br>
|
||||
*/
|
||||
private byte replyCode;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.hua.transport.jt808.server;
|
||||
package com.iot.transport.jt808.server;
|
||||
|
||||
import com.iot.transport.jt808.service.ApiLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -16,8 +18,8 @@ public class Jt808NettyServer implements CommandLineRunner {
|
||||
@Value("${jt808.port:20048}")
|
||||
private int port;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private com.hua.transport.jt808.service.ApiLogService apiLogService;
|
||||
@Autowired
|
||||
private ApiLogService apiLogService;
|
||||
|
||||
private TCPServer tcpServer;
|
||||
|
||||
@@ -27,7 +29,7 @@ public class Jt808NettyServer implements CommandLineRunner {
|
||||
tcpServer = new TCPServer(port, apiLogService);
|
||||
tcpServer.startServer();
|
||||
}
|
||||
|
||||
|
||||
// You might want to add a @PreDestroy method to stop the server gracefully
|
||||
// @PreDestroy
|
||||
// public void destroy() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.server;
|
||||
package com.iot.transport.jt808.server;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -8,7 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.hua.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
|
||||
public class SessionManager {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.hua.transport.jt808.server;
|
||||
package com.iot.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 com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.service.codec.LogDecoder;
|
||||
import com.iot.transport.jt808.service.handler.TCPServerHandler;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
@@ -20,7 +20,7 @@ import io.netty.handler.codec.DelimiterBasedFrameDecoder;
|
||||
import io.netty.handler.timeout.IdleStateHandler;
|
||||
import io.netty.util.concurrent.Future;
|
||||
|
||||
import com.hua.transport.jt808.service.ApiLogService;
|
||||
import com.iot.transport.jt808.service.ApiLogService;
|
||||
|
||||
public class TCPServer {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.service;
|
||||
package com.iot.transport.jt808.service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.service;
|
||||
package com.iot.transport.jt808.service;
|
||||
|
||||
public interface DeviceService {
|
||||
/**
|
||||
@@ -1,19 +1,19 @@
|
||||
package com.hua.transport.jt808.service.codec;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
import com.iot.transport.jt808.entity.request.RegisterPack;
|
||||
import com.iot.transport.jt808.entity.request.RegisterPack.TerminalRegInfo;
|
||||
import com.iot.transport.jt808.util.BCDUtil;
|
||||
import com.iot.transport.jt808.util.BitUtil;
|
||||
|
||||
import com.hua.transport.jt808.entity.request.BluetoothInfo;
|
||||
import com.hua.transport.jt808.util.HexUtil;
|
||||
import com.iot.transport.jt808.entity.request.BluetoothInfo;
|
||||
import com.iot.transport.jt808.util.HexUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -22,6 +22,9 @@ import java.util.List;
|
||||
* @author huaxl
|
||||
*
|
||||
*/
|
||||
import com.iot.transport.jt808.service.codec.parser.ExtensionParser;
|
||||
import com.iot.transport.jt808.service.codec.parser.ExtensionParserFactory;
|
||||
|
||||
public class DataDecoder {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DataDecoder.class);
|
||||
@@ -237,7 +240,6 @@ public class DataDecoder {
|
||||
|
||||
// 扩展协议解析 (Starting from index 28)
|
||||
int index = 28;
|
||||
List<BluetoothInfo> bluetoothInfos = new ArrayList<>();
|
||||
|
||||
while (index < data.length) {
|
||||
int extId = data[index] & 0xFF;
|
||||
@@ -245,83 +247,31 @@ public class DataDecoder {
|
||||
if (index + 1 >= data.length) break;
|
||||
int extLen = data[index + 1] & 0xFF;
|
||||
|
||||
if (extId == 0xF3) {
|
||||
// F3 扩展 (蓝牙信标扫描附加信息)
|
||||
// index+2: 特定标识 0xE6
|
||||
// index+3: 特定标识 0x0C
|
||||
// index+4: 扫描到的蓝牙数据组数
|
||||
if (index + 4 < data.length) {
|
||||
int groupCount = data[index + 4] & 0xFF;
|
||||
int currentOffset = index + 5;
|
||||
|
||||
for (int i = 0; i < groupCount; i++) {
|
||||
if (currentOffset + 7 > data.length) break;
|
||||
|
||||
// MAC (6 bytes)
|
||||
byte[] macBytes = new byte[6];
|
||||
System.arraycopy(data, currentOffset, macBytes, 0, 6);
|
||||
String mac = HexUtil.toHexString(macBytes);
|
||||
// Add colons to MAC
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int k=0; k<mac.length(); k+=2) {
|
||||
if(k>0) sb.append(":");
|
||||
sb.append(mac.substring(k, k+2));
|
||||
}
|
||||
String macStr = sb.toString().toUpperCase();
|
||||
|
||||
// RSSI (1 byte)
|
||||
int rssi = data[currentOffset + 6]; // Signed byte
|
||||
|
||||
BluetoothInfo info = new BluetoothInfo(macStr, rssi);
|
||||
info.setType(0xF3);
|
||||
bluetoothInfos.add(info);
|
||||
|
||||
currentOffset += 7;
|
||||
}
|
||||
|
||||
// Try to capture custom data if present at the end of the block
|
||||
int expectedEnd = index + 2 + extLen;
|
||||
if (currentOffset < expectedEnd && expectedEnd <= data.length) {
|
||||
int customLen = expectedEnd - currentOffset;
|
||||
byte[] customBytes = new byte[customLen];
|
||||
System.arraycopy(data, currentOffset, customBytes, 0, customLen);
|
||||
String customHex = HexUtil.toHexString(customBytes);
|
||||
if (!bluetoothInfos.isEmpty()) {
|
||||
bluetoothInfos.get(bluetoothInfos.size() - 1).setCustomData(customHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if (extId == 0xF4) {
|
||||
// F4 扩展 (蓝牙BLE扫描周围同类产品)
|
||||
// index+2: 扫描到的蓝牙数据组数
|
||||
if (index + 2 < data.length) {
|
||||
int groupCount = data[index + 2] & 0xFF;
|
||||
int currentOffset = index + 3;
|
||||
|
||||
for (int i = 0; i < groupCount; i++) {
|
||||
// Each group: 6 bytes MAC (from IMEI) + 1 byte RSSI = 7 bytes
|
||||
if (currentOffset + 7 > data.length) break;
|
||||
|
||||
byte[] macBytes = new byte[6];
|
||||
System.arraycopy(data, currentOffset, macBytes, 0, 6);
|
||||
String mac = HexUtil.toHexString(macBytes); // This is partial IMEI
|
||||
// RSSI (1 byte)
|
||||
int rssi = data[currentOffset + 6];
|
||||
|
||||
BluetoothInfo info = new BluetoothInfo(mac, rssi); // raw hex as mac for now
|
||||
info.setType(0xF4);
|
||||
bluetoothInfos.add(info);
|
||||
|
||||
currentOffset += 7;
|
||||
}
|
||||
// Validate remaining length
|
||||
if (index + 2 + extLen > data.length) {
|
||||
log.warn("Extension length out of bounds: ID={}, Len={}, Remaining={}",
|
||||
String.format("0x%02X", extId), extLen, data.length - index - 2);
|
||||
break;
|
||||
}
|
||||
|
||||
int contentStart = index + 2;
|
||||
|
||||
ExtensionParser parser = ExtensionParserFactory.getParser(extId);
|
||||
if (parser != null) {
|
||||
try {
|
||||
parser.parse(ret, data, contentStart, extLen);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse extension 0x{}: {}", String.format("%02X", extId), e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// Store unknown extensions as raw hex string
|
||||
byte[] raw = new byte[extLen];
|
||||
System.arraycopy(data, contentStart, raw, 0, extLen);
|
||||
ret.addExtension(extId, HexUtil.toHexString(raw));
|
||||
}
|
||||
|
||||
index += (2 + extLen);
|
||||
}
|
||||
|
||||
ret.setBluetoothInfos(bluetoothInfos);
|
||||
|
||||
return ret;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
package com.hua.transport.jt808.service.codec;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.entity.request.RegisterPack;
|
||||
import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
||||
import com.iot.transport.jt808.entity.response.RegisterBodyPack;
|
||||
import com.iot.transport.jt808.util.BitUtil;
|
||||
import com.iot.transport.jt808.util.JT808Util;
|
||||
|
||||
/**
|
||||
* 数据包编码器
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.hua.transport.jt808.service.codec;
|
||||
package com.iot.transport.jt808.service.codec;
|
||||
|
||||
import java.util.List;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.hua.transport.jt808.util.HexUtil;
|
||||
import com.iot.transport.jt808.util.HexUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.BluetoothInfo;
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
import com.iot.transport.jt808.util.HexUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BluetoothBeaconParser implements ExtensionParser {
|
||||
@Override
|
||||
public int getExtensionId() {
|
||||
return 0xF3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(LocationPack locationPack, byte[] data, int offset, int length) {
|
||||
// Offset points to content after ID and Length
|
||||
// Header: E6 (1 byte) + 0C (1 byte) + Count (1 byte) = 3 bytes
|
||||
if (length < 3) return;
|
||||
|
||||
int currentOffset = offset + 2; // Skip E6 0C
|
||||
int count = data[currentOffset] & 0xFF;
|
||||
currentOffset++;
|
||||
|
||||
List<BluetoothInfo> list = locationPack.getBluetoothInfos();
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
locationPack.setBluetoothInfos(list);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (currentOffset + 7 > offset + length) break;
|
||||
|
||||
byte[] macBytes = new byte[6];
|
||||
System.arraycopy(data, currentOffset, macBytes, 0, 6);
|
||||
String mac = formatMac(HexUtil.toHexString(macBytes));
|
||||
|
||||
int rssi = data[currentOffset + 6]; // Signed
|
||||
|
||||
BluetoothInfo info = new BluetoothInfo(mac, rssi);
|
||||
info.setType(0xF3);
|
||||
list.add(info);
|
||||
|
||||
currentOffset += 7;
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMac(String hex) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for(int k=0; k<hex.length(); k+=2) {
|
||||
if(k>0) sb.append(":");
|
||||
sb.append(hex.substring(k, k+2));
|
||||
}
|
||||
return sb.toString().toUpperCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
|
||||
public class ExtendedInfoParser implements ExtensionParser {
|
||||
@Override
|
||||
public int getExtensionId() {
|
||||
return 0xFE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(LocationPack locationPack, byte[] data, int offset, int length) {
|
||||
// 0xFE extension parsing based on 4.1.1 example
|
||||
// Structure seems flexible inside 0xFE container
|
||||
// E6 (Identifier) + ID + Len + Content ...
|
||||
|
||||
int currentOffset = offset;
|
||||
if (length > 0 && (data[currentOffset] & 0xFF) == 0xE6) {
|
||||
currentOffset++; // Skip E6
|
||||
|
||||
while (currentOffset < offset + length) {
|
||||
if (currentOffset + 2 > offset + length) break;
|
||||
|
||||
int subId = data[currentOffset] & 0xFF;
|
||||
int subLen = 0;
|
||||
// It seems sub-length is 2 bytes or 1 byte? Example says: "0001(长度2字节)" for ID 02
|
||||
// Let's assume 2 bytes length based on example "0001"
|
||||
// But wait, "38:消息长度" for the whole FE block.
|
||||
// Inside FE: "02(ID) 0001(长度2字节) 64" -> ID(1) + Len(2) + Val(1)
|
||||
|
||||
int lenHigh = data[currentOffset + 1] & 0xFF;
|
||||
int lenLow = data[currentOffset + 2] & 0xFF;
|
||||
subLen = (lenHigh << 8) | lenLow;
|
||||
|
||||
currentOffset += 3;
|
||||
|
||||
if (currentOffset + subLen > offset + length) break;
|
||||
|
||||
if (subId == 0x02) {
|
||||
// Battery
|
||||
int battery = data[currentOffset] & 0xFF;
|
||||
locationPack.setBatteryLevel(battery);
|
||||
} else if (subId == 0x07) {
|
||||
// Version Info (ASCII)
|
||||
String version = new String(data, currentOffset, subLen);
|
||||
locationPack.setVersionInfo(version);
|
||||
} else if (subId == 0x20) {
|
||||
// ICCID
|
||||
// Example shows BCD format? "8986..."
|
||||
// Spec says "10BYTES".
|
||||
// BCD to String
|
||||
// We need a helper, but for now lets assume simple hex string or we can use existing BCDUtil if we can access it.
|
||||
// Since we don't have BCDUtil here easily without injection, let's use Hex for now or just skip
|
||||
// locationPack.setIccid(...);
|
||||
}
|
||||
|
||||
currentOffset += subLen;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
|
||||
public interface ExtensionParser {
|
||||
int getExtensionId();
|
||||
void parse(LocationPack locationPack, byte[] data, int offset, int length);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExtensionParserFactory {
|
||||
private static final Map<Integer, ExtensionParser> parsers = new HashMap<>();
|
||||
|
||||
static {
|
||||
register(new MileageParser());
|
||||
register(new SignalStrengthParser());
|
||||
register(new BluetoothBeaconParser());
|
||||
register(new NearbyBleParser());
|
||||
register(new ExtendedInfoParser());
|
||||
}
|
||||
|
||||
public static void register(ExtensionParser parser) {
|
||||
parsers.put(parser.getExtensionId(), parser);
|
||||
}
|
||||
|
||||
public static ExtensionParser getParser(int extensionId) {
|
||||
return parsers.get(extensionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
|
||||
public class MileageParser implements ExtensionParser {
|
||||
@Override
|
||||
public int getExtensionId() {
|
||||
return 0x01;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(LocationPack locationPack, byte[] data, int offset, int length) {
|
||||
if (length == 4) {
|
||||
long mileage = 0;
|
||||
mileage |= (data[offset] & 0xFFL) << 24;
|
||||
mileage |= (data[offset + 1] & 0xFFL) << 16;
|
||||
mileage |= (data[offset + 2] & 0xFFL) << 8;
|
||||
mileage |= (data[offset + 3] & 0xFFL);
|
||||
// 1/10 km -> km
|
||||
locationPack.setMileage(mileage / 10.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.BluetoothInfo;
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
import com.iot.transport.jt808.util.HexUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class NearbyBleParser implements ExtensionParser {
|
||||
@Override
|
||||
public int getExtensionId() {
|
||||
return 0xF4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(LocationPack locationPack, byte[] data, int offset, int length) {
|
||||
// Content: Count (1 byte) + N * (MAC 6 bytes + RSSI 1 byte)
|
||||
if (length < 1) return;
|
||||
|
||||
int count = data[offset] & 0xFF;
|
||||
int currentOffset = offset + 1;
|
||||
|
||||
List<BluetoothInfo> list = locationPack.getBluetoothInfos();
|
||||
if (list == null) {
|
||||
list = new ArrayList<>();
|
||||
locationPack.setBluetoothInfos(list);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (currentOffset + 7 > offset + length) break;
|
||||
|
||||
byte[] macBytes = new byte[6];
|
||||
System.arraycopy(data, currentOffset, macBytes, 0, 6);
|
||||
String mac = HexUtil.toHexString(macBytes); // Partial IMEI usually
|
||||
|
||||
int rssi = data[currentOffset + 6];
|
||||
|
||||
BluetoothInfo info = new BluetoothInfo(mac, rssi);
|
||||
info.setType(0xF4);
|
||||
list.add(info);
|
||||
|
||||
currentOffset += 7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.iot.transport.jt808.service.codec.parser;
|
||||
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
|
||||
public class SignalStrengthParser implements ExtensionParser {
|
||||
@Override
|
||||
public int getExtensionId() {
|
||||
return 0x30;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parse(LocationPack locationPack, byte[] data, int offset, int length) {
|
||||
if (length == 1) {
|
||||
int signal = data[offset] & 0xFF;
|
||||
locationPack.setSignalStrength(signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.hua.transport.jt808.service.handler;
|
||||
package com.iot.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 com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.server.SessionManager;
|
||||
import com.iot.transport.jt808.service.codec.DataDecoder;
|
||||
import com.iot.transport.jt808.service.codec.DataEncoder;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.hua.transport.jt808.service.handler;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.service.handler.terminal.AuthenticationHandler;
|
||||
import com.iot.transport.jt808.service.handler.terminal.HeartbeatHandler;
|
||||
import com.iot.transport.jt808.service.handler.terminal.LocationUploadHandler;
|
||||
import com.iot.transport.jt808.service.handler.terminal.LoginOutHandler;
|
||||
import com.iot.transport.jt808.service.handler.terminal.RegisterHandler;
|
||||
|
||||
public class MessageHandlerFactory {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.hua.transport.jt808.service.handler;
|
||||
package com.iot.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 com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.server.SessionManager;
|
||||
import com.iot.transport.jt808.service.codec.DataDecoder;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
@@ -16,12 +16,15 @@ import io.netty.handler.timeout.IdleState;
|
||||
import io.netty.handler.timeout.IdleStateEvent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
|
||||
import com.hua.transport.jt808.service.ApiLogService;
|
||||
import com.iot.transport.jt808.service.ApiLogService;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.hua.transport.jt808.common.Consts;
|
||||
import com.hua.transport.jt808.entity.request.LocationPack;
|
||||
import com.iot.transport.jt808.common.Consts;
|
||||
import com.iot.transport.jt808.entity.request.BatteryVersionInfo;
|
||||
import com.iot.transport.jt808.entity.request.BluetoothInfo;
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
import java.util.List;
|
||||
|
||||
public class TCPServerHandler extends ChannelInboundHandlerAdapter { // (1)
|
||||
|
||||
@@ -67,6 +70,36 @@ public class TCPServerHandler extends ChannelInboundHandlerAdapter { // (1)
|
||||
try {
|
||||
LocationPack locPack = this.decoder.toLocationInfoUploadMsg(packageData);
|
||||
logMap.put("details", locPack.toString());
|
||||
|
||||
// Enhanced fields for Frontend Dashboard
|
||||
logMap.put("type", "badge");
|
||||
logMap.put("id", header.getTerminalPhone());
|
||||
|
||||
// Battery
|
||||
BatteryVersionInfo batInfo = locPack.getBatteryVersionInfo();
|
||||
if (batInfo != null) {
|
||||
logMap.put("battery", batInfo.getBatteryLevel());
|
||||
}
|
||||
|
||||
// Status (Simple logic: SOS if bit 0 of warning flag is set)
|
||||
if ((locPack.getWarningFlagField() & 0x01) != 0) {
|
||||
logMap.put("status", "sos");
|
||||
} else {
|
||||
logMap.put("status", "active");
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
List<BluetoothInfo> bles = locPack.getBluetoothInfos();
|
||||
if (bles != null) {
|
||||
logMap.put("bluetooth", bles);
|
||||
}
|
||||
|
||||
// Location
|
||||
Map<String, Object> locMap = new HashMap<>();
|
||||
locMap.put("lat", locPack.getLatitude());
|
||||
locMap.put("lon", locPack.getLongitude());
|
||||
logMap.put("location", locMap);
|
||||
|
||||
} catch (Exception e) {
|
||||
logMap.put("details", packageData.toString() + " (Parse Error: " + e.getMessage() + ")");
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.hua.transport.jt808.service.handler.terminal;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.request.AuthenticationPack;
|
||||
import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
||||
import com.iot.transport.jt808.service.handler.MessageHandler;
|
||||
|
||||
/**
|
||||
* 终端鉴权 ==> 平台通用应答
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.hua.transport.jt808.service.handler.terminal;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
||||
import com.iot.transport.jt808.service.handler.MessageHandler;
|
||||
|
||||
/**
|
||||
* 终端心跳-消息体为空 ==> 平台通用应答
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.hua.transport.jt808.service.handler.terminal;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.request.LocationPack;
|
||||
import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
||||
import com.iot.transport.jt808.service.handler.MessageHandler;
|
||||
|
||||
/**
|
||||
* 处理模板
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.hua.transport.jt808.service.handler.terminal;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.response.ServerBodyPack;
|
||||
import com.iot.transport.jt808.service.handler.MessageHandler;
|
||||
|
||||
/**
|
||||
* 终端注销(终端注销数据消息体为空) ==> 平台通用应答
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.hua.transport.jt808.service.handler.terminal;
|
||||
package com.iot.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;
|
||||
import com.iot.transport.jt808.entity.DataPack;
|
||||
import com.iot.transport.jt808.entity.Session;
|
||||
import com.iot.transport.jt808.entity.DataPack.PackHead;
|
||||
import com.iot.transport.jt808.entity.request.RegisterPack;
|
||||
import com.iot.transport.jt808.entity.response.RegisterBodyPack;
|
||||
import com.iot.transport.jt808.service.handler.MessageHandler;
|
||||
|
||||
/**
|
||||
* 终端注册 ==> 终端注册应答
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.hua.transport.jt808.service.impl;
|
||||
package com.iot.transport.jt808.service.impl;
|
||||
|
||||
import com.hua.transport.jt808.service.DeviceService;
|
||||
import com.iot.transport.jt808.service.DeviceService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import java.util.Date;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.util;
|
||||
package com.iot.transport.jt808.util;
|
||||
|
||||
public class BCDUtil {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.util;
|
||||
package com.iot.transport.jt808.util;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.util;
|
||||
package com.iot.transport.jt808.util;
|
||||
|
||||
public class HexUtil {
|
||||
|
||||
@@ -58,4 +58,16 @@ public class HexUtil {
|
||||
System.out.println("十六进制字符串:" + hex);
|
||||
System.out.println("还原:" + decode);
|
||||
}
|
||||
|
||||
public static String toHexString(byte[] data, int offset, int subLen) {
|
||||
if (data == null || offset < 0 || subLen < 0 || offset + subLen > data.length) {
|
||||
return "";
|
||||
}
|
||||
char[] out = new char[subLen << 1];
|
||||
for (int i = 0, j = 0; i < subLen; i++) {
|
||||
out[j++] = DIGITS_HEX[(0xF0 & data[offset + i]) >>> 4];
|
||||
out[j++] = DIGITS_HEX[0x0F & data[offset + i]];
|
||||
}
|
||||
return new String(out);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hua.transport.jt808.util;
|
||||
package com.iot.transport.jt808.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
@@ -7,5 +7,5 @@ jt808:
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.hua.transport.jt808: DEBUG
|
||||
com.iot.transport.jt808: DEBUG
|
||||
|
||||
|
||||
@@ -1,101 +1,354 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<title>JT808 Server Dashboard</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IoT 设备测试平台</title>
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
<!-- Bootstrap 5 -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background-color: #f8f9fa; }
|
||||
.dashboard-card { transition: all 0.3s; }
|
||||
.dashboard-card:hover { transform: translateY(-5px); shadow: 0 4px 8px rgba(0,0,0,0.1); }
|
||||
:root {
|
||||
--primary-bg: #f4f6f9;
|
||||
--card-bg: #ffffff;
|
||||
--sidebar-bg: #343a40;
|
||||
--accent-color: #3b82f6;
|
||||
}
|
||||
body {
|
||||
background-color: var(--primary-bg);
|
||||
font-family: 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
.navbar {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.dashboard-card {
|
||||
background: var(--card-bg);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.dashboard-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card-header {
|
||||
background-color: transparent;
|
||||
border-bottom: 1px solid rgba(0,0,0,0.05);
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.card-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
/* 特定板块样式 */
|
||||
.badge-card .card-icon { background-color: rgba(13, 110, 253, 0.1); color: #0d6efd; }
|
||||
.counter-card .card-icon { background-color: rgba(25, 135, 84, 0.1); color: #198754; }
|
||||
|
||||
.stat-value { font-size: 1.8rem; font-weight: bold; margin-bottom: 0; }
|
||||
.stat-label { color: #6c757d; font-size: 0.9rem; }
|
||||
|
||||
/* 日志控制台 */
|
||||
.log-console {
|
||||
background-color: #1e1e1e;
|
||||
color: #00ff00;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
height: 300px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9rem;
|
||||
padding: 15px;
|
||||
border-radius: 0 0 12px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.log-entry { margin-bottom: 5px; border-bottom: 1px solid #333; padding-bottom: 2px; }
|
||||
.log-time { color: #888; margin-right: 10px; }
|
||||
.log-entry {
|
||||
margin-bottom: 6px;
|
||||
border-bottom: 1px solid #333;
|
||||
padding-bottom: 4px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.log-time { color: #888; margin-right: 10px; min-width: 80px; }
|
||||
.log-source { margin-right: 10px; font-weight: bold; }
|
||||
.log-data { color: #ce9178; word-break: break-all; }
|
||||
|
||||
/* 动画 */
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.5s; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
|
||||
/* 设备列表项 */
|
||||
.device-item {
|
||||
border-left: 4px solid transparent;
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.device-item:hover { transform: translateX(2px); box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
||||
.device-item.active { border-left-color: #28a745; }
|
||||
.device-item.inactive { border-left-color: #6c757d; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<nav class="navbar navbar-dark bg-dark">
|
||||
<div id="app" class="d-flex flex-column vh-100">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark flex-shrink-0">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">JT808 Transport Server</span>
|
||||
<span class="text-light">
|
||||
Status:
|
||||
<span class="badge" :class="connected ? 'bg-success' : 'bg-danger'">
|
||||
{{ connected ? 'Connected' : 'Disconnected' }}
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="fas fa-network-wired me-2"></i>IoT 数据监控平台
|
||||
</a>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-light me-3">
|
||||
<i class="fas fa-circle me-1" :class="connected ? 'text-success' : 'text-danger'"></i>
|
||||
{{ connected ? '已连接实时流' : '断开连接' }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<!-- Device Simulation Card -->
|
||||
<div class="col-md-5">
|
||||
<div class="card dashboard-card mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Device Simulation (Typed API)
|
||||
<div class="container-fluid flex-grow-1 overflow-hidden">
|
||||
<div class="row h-100">
|
||||
<!-- 左侧:模拟与控制 (固定宽度 350px) -->
|
||||
<div class="col-auto bg-white border-end py-3 overflow-auto" style="width: 350px;">
|
||||
<div class="card dashboard-card mb-4 border-0 shadow-sm">
|
||||
<div class="card-header bg-white text-primary border-bottom-0">
|
||||
<span><i class="fas fa-vial me-2"></i>数据模拟器</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form @submit.prevent="sendReport">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">IMEI</label>
|
||||
<input v-model="form.imei" class="form-control" placeholder="123456789012">
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Lat</label>
|
||||
<input v-model.number="form.lat" type="number" step="0.000001" class="form-control">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label">Lon</label>
|
||||
<input v-model.number="form.lon" type="number" step="0.000001" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Send Location</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<ul class="nav nav-tabs nav-fill mb-3" id="pills-tab" role="tablist">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link py-2 small" @click="mode='badge'" :class="{active: mode==='badge'}">工牌</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link py-2 small" @click="mode='counter'" :class="{active: mode==='counter'}">计数器</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Generic Upload Test -->
|
||||
<div class="card dashboard-card mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
Universal Upload Test (/upload)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Arbitrary JSON Payload</label>
|
||||
<textarea v-model="customJson" class="form-control" rows="3"></textarea>
|
||||
<div v-if="mode === 'badge'">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">工牌 ID</label>
|
||||
<input v-model="badgeForm.id" class="form-control form-control-sm" placeholder="BADGE-001">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">电量 (%)</label>
|
||||
<input type="range" v-model.number="badgeForm.battery" class="form-range" min="0" max="100">
|
||||
<div class="text-end text-muted small">{{ badgeForm.battery }}%</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">状态</label>
|
||||
<select v-model="badgeForm.status" class="form-select form-select-sm">
|
||||
<option value="active">活跃</option>
|
||||
<option value="idle">静止</option>
|
||||
<option value="sos">SOS报警</option>
|
||||
</select>
|
||||
</div>
|
||||
<button @click="sendBadgeData" class="btn btn-primary btn-sm w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i>发送工牌数据
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'counter'">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">设备 ID</label>
|
||||
<input v-model="counterForm.id" class="form-control form-control-sm" placeholder="CNT-GATE-01">
|
||||
</div>
|
||||
<div class="row g-2">
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label small text-muted">进入人数</label>
|
||||
<input type="number" v-model.number="counterForm.inCount" class="form-control form-control-sm">
|
||||
</div>
|
||||
<div class="col-6 mb-3">
|
||||
<label class="form-label small text-muted">离开人数</label>
|
||||
<input type="number" v-model.number="counterForm.outCount" class="form-control form-control-sm">
|
||||
</div>
|
||||
</div>
|
||||
<button @click="sendCounterData" class="btn btn-success btn-sm w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i>发送计数数据
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'custom'">
|
||||
<div class="mb-3">
|
||||
<label class="form-label small text-muted">JSON 数据</label>
|
||||
<textarea v-model="customJson" class="form-control font-monospace form-control-sm" rows="5"></textarea>
|
||||
</div>
|
||||
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
|
||||
</div>
|
||||
<button @click="sendCustomJson" class="btn btn-info text-white w-100">Send to /upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Live Log Console -->
|
||||
<div class="col-md-7">
|
||||
<div class="card dashboard-card h-100">
|
||||
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center">
|
||||
<span>Live Data Stream (/api/v1/device/upload)</span>
|
||||
<button @click="logs = []" class="btn btn-sm btn-outline-secondary">Clear</button>
|
||||
<!-- 中间:实时监控与日志 (自适应宽度) -->
|
||||
<div class="col d-flex flex-column h-100 p-0 overflow-hidden bg-light">
|
||||
<!-- 顶部:状态卡片 (占比 60%) -->
|
||||
<div class="flex-grow-1 p-4 overflow-auto" style="flex: 3;">
|
||||
<div class="row g-4 h-100">
|
||||
<!-- 工牌监控区域 -->
|
||||
<div class="col-md-7 h-100">
|
||||
<div class="card dashboard-card badge-card h-100 d-flex flex-column">
|
||||
<div class="card-header flex-shrink-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="card-icon"><i class="fas fa-id-card"></i></div>
|
||||
<div>
|
||||
<h5 class="mb-0">智能工牌监控</h5>
|
||||
<small class="text-muted">实时人员定位与状态</small>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-primary rounded-pill">{{ Object.keys(badges).length }} 在线</span>
|
||||
</div>
|
||||
<div class="card-body overflow-auto flex-grow-1 bg-light p-3">
|
||||
<div v-if="Object.keys(badges).length === 0" class="text-center text-muted py-5">
|
||||
<div class="mb-3"><i class="fas fa-satellite-dish fa-3x opacity-25"></i></div>
|
||||
<p>暂无工牌数据接入</p>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div v-for="(badge, id) in badges" :key="id" class="col-12 col-xl-6">
|
||||
<div class="device-item h-100 d-flex flex-column"
|
||||
:class="badge.status === 'active' ? 'active' : (badge.status === 'sos' ? 'border-danger' : 'inactive')">
|
||||
|
||||
<!-- 头部:ID与状态 -->
|
||||
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||
<div>
|
||||
<div class="d-flex align-items-center">
|
||||
<h6 class="mb-0 fw-bold me-2">{{ badge.id }}</h6>
|
||||
<span v-if="badge.status === 'sos'" class="badge bg-danger">SOS</span>
|
||||
<span v-else class="badge bg-success" style="font-size: 0.7rem;">在线</span>
|
||||
</div>
|
||||
<small class="text-muted" style="font-size: 0.75rem;">
|
||||
<i class="far fa-clock me-1"></i>{{ formatTime(badge.lastUpdate) }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div class="fw-bold" :class="getBatteryColor(badge.battery)">
|
||||
<i class="fas fa-battery-half me-1"></i>{{ badge.battery }}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核心指标:蓝牙与位置 -->
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="row g-0 align-items-center text-center small">
|
||||
<div class="col-6 border-end">
|
||||
<div class="text-primary fw-bold mb-1">
|
||||
<i class="fab fa-bluetooth-b me-1"></i>{{ (badge.bluetooth || []).length }}
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.7rem;">蓝牙信标</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div v-if="badge.location" class="text-dark fw-bold mb-1" title="点击查看详情">
|
||||
{{ badge.location.lat.toFixed(4) }}, {{ badge.location.lon.toFixed(4) }}
|
||||
</div>
|
||||
<div v-else class="text-muted mb-1">-</div>
|
||||
<div class="text-muted" style="font-size: 0.7rem;">GPS坐标</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 详细蓝牙列表 (折叠展示) -->
|
||||
<div v-if="badge.bluetooth && badge.bluetooth.length > 0" class="mt-2 bg-light rounded p-2" style="font-size: 0.75rem;">
|
||||
<div v-for="(ble, idx) in badge.bluetooth.slice(0, 2)" :key="idx" class="d-flex justify-content-between text-muted">
|
||||
<span>MAC: {{ ble.mac }}</span>
|
||||
<span>RSSI: {{ ble.rssi }}</span>
|
||||
</div>
|
||||
<div v-if="badge.bluetooth.length > 2" class="text-center text-primary" style="font-size: 0.7rem;">
|
||||
+{{ badge.bluetooth.length - 2 }} 更多...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 客流计数器区域 -->
|
||||
<div class="col-md-5 h-100">
|
||||
<div class="card dashboard-card counter-card h-100 d-flex flex-column">
|
||||
<div class="card-header flex-shrink-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="card-icon"><i class="fas fa-users"></i></div>
|
||||
<div>
|
||||
<h5 class="mb-0">客流计数器</h5>
|
||||
<small class="text-muted">进出人流实时统计</small>
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge bg-success rounded-pill">{{ Object.keys(counters).length }} 设备</span>
|
||||
</div>
|
||||
<div class="card-body overflow-auto flex-grow-1 bg-light p-3">
|
||||
<div v-if="Object.keys(counters).length === 0" class="text-center text-muted py-5">
|
||||
<div class="mb-3"><i class="fas fa-chart-bar fa-3x opacity-25"></i></div>
|
||||
<p>暂无计数器在线</p>
|
||||
</div>
|
||||
<div v-for="(counter, id) in counters" :key="id" class="card mb-3 border-0 shadow-sm">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex justify-content-between mb-3 border-bottom pb-2">
|
||||
<h6 class="fw-bold mb-0 text-success"><i class="fas fa-door-open me-2"></i>{{ counter.id }}</h6>
|
||||
<small class="text-muted">{{ formatTime(counter.lastUpdate) }}</small>
|
||||
</div>
|
||||
<div class="row text-center">
|
||||
<div class="col-4 border-end">
|
||||
<div class="h3 mb-0 text-primary">{{ counter.inCount }}</div>
|
||||
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">In</small>
|
||||
</div>
|
||||
<div class="col-4 border-end">
|
||||
<div class="h3 mb-0 text-warning">{{ counter.outCount }}</div>
|
||||
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">Out</small>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="h3 mb-0 text-dark">{{ counter.inCount - counter.outCount }}</div>
|
||||
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">Stay</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body bg-dark p-0">
|
||||
<div class="log-console" ref="console">
|
||||
<div v-if="logs.length === 0" class="text-muted text-center mt-5">Waiting for data...</div>
|
||||
<div v-for="(log, index) in logs" :key="index" class="log-entry">
|
||||
</div>
|
||||
|
||||
<!-- 底部:实时日志 (占比 40%) -->
|
||||
<div class="flex-grow-1 p-4 pt-0" style="flex: 2; min-height: 0;">
|
||||
<div class="card dashboard-card h-100 d-flex flex-column">
|
||||
<div class="card-header bg-dark text-white rounded-top flex-shrink-0 py-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-terminal me-2"></i>
|
||||
<span class="small">实时数据日志流</span>
|
||||
</div>
|
||||
<div>
|
||||
<button @click="logs = []" class="btn btn-sm btn-outline-secondary text-white border-0 py-0">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<span class="badge bg-secondary ms-2" style="font-size: 0.7rem;">{{ logs.length }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="log-console flex-grow-1" ref="console" style="height: auto; border-radius: 0 0 12px 12px;">
|
||||
<div v-if="logs.length === 0" class="text-muted text-center mt-4 small">等待数据接入...</div>
|
||||
<div v-for="(log, index) in logs" :key="index" class="log-entry small">
|
||||
<span class="log-time">[{{ log.time }}]</span>
|
||||
<span class="badge me-2" :class="log.source === 'TCP' ? 'bg-warning text-dark' : 'bg-success'">{{ log.source }}</span>
|
||||
<span>{{ log.data }}</span>
|
||||
<span class="badge me-2" :class="getSourceClass(log.source)">{{ log.source }}</span>
|
||||
<span class="log-data">{{ log.data }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,19 +360,30 @@
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
imei: '13800138000',
|
||||
lat: 34.215432,
|
||||
lon: 108.924231,
|
||||
speed: 60.5
|
||||
},
|
||||
customJson: '{\n "sensor": "temp-01",\n "value": 25.5,\n "unit": "C"\n}',
|
||||
logs: [],
|
||||
mode: 'badge',
|
||||
connected: false,
|
||||
eventSource: null
|
||||
eventSource: null,
|
||||
// 模拟表单数据
|
||||
badgeForm: {
|
||||
id: 'BADGE-001',
|
||||
battery: 85,
|
||||
status: 'active'
|
||||
},
|
||||
counterForm: {
|
||||
id: 'GATE-01',
|
||||
inCount: 10,
|
||||
outCount: 5
|
||||
},
|
||||
customJson: '{\n "deviceType": "sensor",\n "temp": 24.5\n}',
|
||||
|
||||
// 实时数据存储
|
||||
badges: {}, // Map: id -> badge data
|
||||
counters: {}, // Map: id -> counter data
|
||||
logs: []
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -131,62 +395,119 @@
|
||||
|
||||
this.eventSource.onopen = () => {
|
||||
this.connected = true;
|
||||
this.addLog('System connected. Listening for /upload events...');
|
||||
this.addLog({ source: 'SYSTEM', message: 'SSE 连接成功,监听数据流...' });
|
||||
};
|
||||
|
||||
this.eventSource.onerror = () => {
|
||||
this.connected = false;
|
||||
this.eventSource.close();
|
||||
// Reconnect after 3s
|
||||
setTimeout(() => this.connectSSE(), 3000);
|
||||
setTimeout(() => this.connectSSE(), 3000); // 重连
|
||||
};
|
||||
|
||||
this.eventSource.addEventListener('log-event', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
this.addLog(data);
|
||||
try {
|
||||
const rawData = JSON.parse(event.data);
|
||||
this.handleIncomingData(rawData);
|
||||
} catch (e) {
|
||||
console.error('Parse error', e);
|
||||
}
|
||||
});
|
||||
},
|
||||
addLog(data) {
|
||||
const now = new Date().toLocaleTimeString();
|
||||
const source = data.source || 'INFO';
|
||||
// Remove source/timestamp from display data to avoid clutter if desired, or just show all
|
||||
handleIncomingData(data) {
|
||||
const now = new Date();
|
||||
|
||||
// 1. 处理工牌数据 (识别 type=badge)
|
||||
if (data.type === 'badge' && data.id) {
|
||||
this.badges[data.id] = {
|
||||
...data,
|
||||
lastUpdate: now
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 处理计数器数据 (识别 type=counter)
|
||||
else if (data.type === 'counter' && data.id) {
|
||||
this.counters[data.id] = {
|
||||
...data,
|
||||
lastUpdate: now
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 添加到日志
|
||||
const source = data.source || 'API';
|
||||
// 移除展示不需要的元数据
|
||||
const displayData = { ...data };
|
||||
delete displayData.source;
|
||||
delete displayData.timestamp;
|
||||
|
||||
|
||||
this.logs.unshift({
|
||||
time: now,
|
||||
time: now.toLocaleTimeString(),
|
||||
source: source,
|
||||
data: JSON.stringify(displayData)
|
||||
});
|
||||
// Keep last 50 logs
|
||||
if (this.logs.length > 50) this.logs.pop();
|
||||
|
||||
if (this.logs.length > 100) this.logs.pop();
|
||||
},
|
||||
async sendReport() {
|
||||
try {
|
||||
await fetch('/api/v1/device/location', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(this.form)
|
||||
});
|
||||
// Note: /location endpoint doesn't broadcast to SSE in this demo, only /upload does
|
||||
// But we could add it if needed.
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
}
|
||||
},
|
||||
async sendCustomJson() {
|
||||
|
||||
// 发送模拟请求
|
||||
async postData(payload) {
|
||||
try {
|
||||
const res = await fetch('/api/v1/device/upload', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: this.customJson
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.code !== 200) alert('Error: ' + result.message);
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
alert('发送失败: ' + e.message);
|
||||
}
|
||||
},
|
||||
|
||||
sendBadgeData() {
|
||||
const payload = {
|
||||
type: 'badge',
|
||||
id: this.badgeForm.id,
|
||||
battery: this.badgeForm.battery,
|
||||
status: this.badgeForm.status,
|
||||
ts: Date.now()
|
||||
};
|
||||
this.postData(payload);
|
||||
},
|
||||
|
||||
sendCounterData() {
|
||||
const payload = {
|
||||
type: 'counter',
|
||||
id: this.counterForm.id,
|
||||
inCount: this.counterForm.inCount,
|
||||
outCount: this.counterForm.outCount,
|
||||
ts: Date.now()
|
||||
};
|
||||
this.postData(payload);
|
||||
},
|
||||
|
||||
sendCustomJson() {
|
||||
try {
|
||||
const payload = JSON.parse(this.customJson);
|
||||
this.postData(payload);
|
||||
} catch (e) {
|
||||
alert('JSON 格式错误');
|
||||
}
|
||||
},
|
||||
|
||||
// 辅助函数
|
||||
formatTime(date) {
|
||||
if (!date) return '';
|
||||
return new Date(date).toLocaleTimeString();
|
||||
},
|
||||
getBatteryColor(level) {
|
||||
if (level > 60) return 'text-success';
|
||||
if (level > 20) return 'text-warning';
|
||||
return 'text-danger';
|
||||
},
|
||||
getSourceClass(source) {
|
||||
if (source === 'SYSTEM') return 'bg-info text-dark';
|
||||
if (source === 'TCP') return 'bg-warning text-dark';
|
||||
return 'bg-success';
|
||||
}
|
||||
}
|
||||
}).mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user