This commit is contained in:
2026-04-17 10:32:57 +08:00
parent 572ad1205f
commit 17c52c978c
691 changed files with 72342 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.maibu</groupId>
<artifactId>MiddlePlatform</artifactId>
<version>4.0.0</version>
</parent>
<artifactId>maibu-web-middleware</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version> <!-- 使用最新版 -->
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.56.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.17.0</version>
</dependency>
<dependency>
<groupId>com.maibu</groupId>
<artifactId>maibu-iot-service</artifactId>
<version>${maibu.version}</version>
</dependency>
<dependency>
<groupId>com.maibu</groupId>
<artifactId>maibu-common</artifactId>
<version>${maibu.version}</version>
</dependency>
<dependency>
<groupId>com.maibu</groupId>
<artifactId>maibu-netty-server</artifactId>
<version>${maibu.version}</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -0,0 +1,16 @@
package com.maibu.common;
public class MiddleCommandConstant {
public static byte remoteControl = (byte)0x00;
public static byte heartbeat = (byte)0xff;
public static byte path = (byte)0x01;
public static byte status = (byte)0x02;
public static byte connect = (byte)0x03;
public static byte interaction = (byte)0x12; //主要用于推送状态消息等
}

View File

@@ -0,0 +1,19 @@
package com.maibu.common;
import io.netty.util.AttributeKey;
import lombok.Data;
@Data
public class MiddleConstant {
public static final AttributeKey<String> ATT_MASTER_KEY = AttributeKey.valueOf("ATT_MASTER_KEY");
// public static String nettyApiUrl = "http://127.0.0.1:8081";
public static String nettyApiUrl = "https://serviceri.satabot.com";
public static String vertxIp = "1.95.137.212";
// public static String nettyApiUrl = "http://127.0.0.1:8081";
// public static String vertxIp = "127.0.0.1";
}

View File

@@ -0,0 +1,25 @@
package com.maibu.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(5000); // 连接超时 5 秒
factory.setReadTimeout(5000); // 读取超时 5 秒
RestTemplate restTemplate = new RestTemplate(factory);
// // 只保留JSON转换器,确保只处理JSON格式的数据
// List<HttpMessageConverter<?>> converters = new ArrayList<>();
// converters.add(new MappingJackson2HttpMessageConverter());
// restTemplate.setMessageConverters(converters);
return restTemplate;
}
}

View File

@@ -0,0 +1,92 @@
package com.maibu.control;
public class DiffSteer {
public static final int Polarity_High = 0; //0
public static final int Polarity_Low = 0; //1
private float turnSpeedScale;
private float forwardSpeedScale;
private int speedAmplLimit;
private int leftWheelSpeedDir;
private int rightWheelSpeedDir;
private int xDir;
private int yDir;
private int leftWheelSpeed;
private int rightWheelSpeed;
public int getLeftWheelSpeed() {
return leftWheelSpeed;
}
public void setLeftWheelSpeed(int leftWheelSpeed) {
this.leftWheelSpeed = leftWheelSpeed;
}
public int getRightWheelSpeed() {
return rightWheelSpeed;
}
public void setRightWheelSpeed(int rightWheelSpeed) {
this.rightWheelSpeed = rightWheelSpeed;
}
// 初始化函数
public void init(int turnSpeedMax, int forwardSpeedMax, int xMax, int yMax, int speedAmplLimit) {
this.turnSpeedScale = (float) turnSpeedMax / (float) xMax;
this.forwardSpeedScale = (float) forwardSpeedMax / (float) yMax;
this.speedAmplLimit = speedAmplLimit;
this.leftWheelSpeedDir = Polarity_High;
this.rightWheelSpeedDir = Polarity_High;
this.xDir = Polarity_High;
this.yDir = Polarity_High;
}
// 设置方向极性
public void setDirPolarity(int leftWheelSpeedDir, int rightWheelSpeedDir, int xDir, int yDir) {
this.leftWheelSpeedDir = leftWheelSpeedDir;
this.rightWheelSpeedDir = rightWheelSpeedDir;
this.xDir = xDir;
this.yDir = yDir;
}
// 计算左右轮速度
public void calcWheelSpeed(int x, int y) {
int turnSpeed = signOperator((int) (x * turnSpeedScale), xDir);
int forwardSpeed = signOperator((int) (y * forwardSpeedScale), yDir);
turnSpeed = (forwardSpeed >= 0 ? turnSpeed : -turnSpeed);
leftWheelSpeed = signOperator(forwardSpeed + turnSpeed, leftWheelSpeedDir);
rightWheelSpeed = signOperator(forwardSpeed - turnSpeed, rightWheelSpeedDir);
leftWheelSpeed = clamp(leftWheelSpeed, -speedAmplLimit, speedAmplLimit);
rightWheelSpeed = clamp(rightWheelSpeed, -speedAmplLimit, speedAmplLimit);
}
// 设置前向速比
public void setTurnSpeedScale(int turnSpeedMax, int xMax) {
this.turnSpeedScale = (float) turnSpeedMax / (float) xMax;
}
// 设置转向速比
public void setForwardSpeedScale(int forwardSpeedMax, int yMax) {
this.forwardSpeedScale = (float) forwardSpeedMax / (float) yMax;
}
// 设置电机转速的最大速度限制
public void setSpeedAmplLimit(int speedAmplLimit) {
this.speedAmplLimit = speedAmplLimit;
}
// 辅助函数:符号操作
private int signOperator(int num, int polarity) {
return polarity != 0 ? -num : num;
}
// 辅助函数:限制值在范围内
private int clamp(int val, int min, int max) {
return (val < min) ? min : ((val > max) ? max : val);
}
}

View File

@@ -0,0 +1,72 @@
package com.maibu.controller;
import com.fastbee.common.core.controller.BaseController;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.manager.DeviceSessionManager;
import com.fastbee.memory.MiddleGlobalMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/websocket")
public class WebMiddlewareController extends BaseController {
@Autowired
private DeviceSessionManager deviceSessionManager;
@GetMapping("/judgeConnection")
public AjaxResult JudgeConnection(@RequestParam String userName, @RequestParam Integer sourceType, @RequestParam String token) {
String source = "web";
if (sourceType == 1) {
source = "web";
} else if (sourceType == 2) {
source = "app";
} else if (sourceType == 3) {
source = "pc";
}
String key = userName + ":" + source + ":" + token;
if (MiddleGlobalMemory.onlineSockets.containsKey(key)) {
return AjaxResult.success(true);
} else {
String keyContains = userName + ":" + source;
boolean res = true;
for (String s : MiddleGlobalMemory.onlineSockets.keySet()) {
if (s.contains(keyContains)) {
res = false;
break;
}
}
return AjaxResult.success(res);
}
}
@GetMapping("/allMemory")
public AjaxResult memory() {
Map<String, Object> res = new HashMap<>();
res.put("deviceMap", deviceSessionManager.getDeviceMap());
res.put("channelToDeviceIdMap", deviceSessionManager.getChannelToDeviceIdMap());
res.put("channelMap", deviceSessionManager.getChannelMap());
res.put("onlineSockets", MiddleGlobalMemory.onlineSockets.size());
res.put("controlMap", MiddleGlobalMemory.controlMap);
List<String> nettyClients = new ArrayList<>();
MiddleGlobalMemory.nettyClientMap.values().forEach(x -> {
if (x != null) {
nettyClients.add(x.userName +":"+ x.token);
}
});
res.put("nettyClientMap", nettyClients);
return AjaxResult.success(res);
}
}

View File

@@ -0,0 +1,62 @@
package com.maibu.dto;
import lombok.Data;
import java.util.Date;
@Data
public class DeviceRunningStatus {
private String deviceId;
private String voltage; //电压
private String leftTargetSpeed; // 左轮目标速度
private String rightTargetSpeed;// 右轮目标速度
private String leftMeasureSpeed;// 左轮测量速度
private String rightMeasureSpeed;// 右轮测量速度
private String leftCurrent;// 左轮电流
private String rightCurrent;// 右轮电流
private String leftMotorTemp;// 左轮温度
private String rightMotorTemp;// 右轮温度
private String chipTemp;//芯片温度
private String yaw;//偏航角
private String pitch;//仰俯角
private String roll;//翻滚角
private String satelliteCnt;//跟踪的卫星数
private String qual;//定位质量
private String headingStatus;//航向角状态
private String latitude;//纬度
private String longitude;//经度
private Date time;
private String cuttingSpeed;//割刀速度
private String controlMode;//控制模式
private String battery;//电池电量
private String workingArea;//作业面积
private String obstacleSign;//障碍物标志位
private Date createTime;
}

View File

@@ -0,0 +1,10 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class DeviceStatusDetail {
private String name;
private String value;
private String unit;
}

View File

@@ -0,0 +1,97 @@
package com.maibu.dto;
import lombok.Data;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@Data
public class LawnMoverProtocolEntity {
public LawnMoverProtocolEntity() {
Header1 = (byte) 0xAB;
Header2 = (byte) 0xAA;
CommandType = 0x00;
LeftWheelSpeed = 0x0000;
RightWheelSpeed = 0x0000;
ChassisLiftFlag = 0x00;
MowerSpeedFlag = 0x00;
IgnitionFlag = 0x00;
EmergencyStopFlag = 0x00;
Crc16 = 0x0000;
EndFlag1 = (byte) 0xAA;
EndFlag2 = (byte) 0xAB;
}
@Override
public String toString() {
return "LawnMoverProtocolEntity{" +
"Header1=" + Header1 +
", Header2=" + Header2 +
", CommandType=" + CommandType +
", LeftWheelSpeed=" + LeftWheelSpeed +
", RightWheelSpeed=" + RightWheelSpeed +
", ChassisLiftFlag=" + ChassisLiftFlag +
", MowerSpeedFlag=" + MowerSpeedFlag +
", IgnitionFlag=" + IgnitionFlag +
", EmergencyStopFlag=" + EmergencyStopFlag +
", Crc16=" + Crc16 +
", EndFlag1=" + EndFlag1 +
", EndFlag2=" + EndFlag2 +
'}';
}
public byte[] toBytes() {
ByteBuffer buffer = ByteBuffer.allocate(15);
buffer.order(ByteOrder.LITTLE_ENDIAN); // 根据协议决定字节序
buffer.put(Header1);
buffer.put(Header2);
buffer.put(CommandType);
buffer.putShort(LeftWheelSpeed);
buffer.putShort(RightWheelSpeed);
buffer.put(ChassisLiftFlag);
buffer.put(MowerSpeedFlag);
buffer.put(IgnitionFlag);
buffer.put(EmergencyStopFlag);
buffer.putShort(Crc16); // 如果要计算真实 CRC16 可在此插入计算逻辑
buffer.put(EndFlag1);
buffer.put(EndFlag2);
return buffer.array();
}
public static String printBytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 3);
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
// 包头起始字节1
private byte Header1;
// 包头起始字节2
private byte Header2;
// 指令类型
private byte CommandType;
// 左轮速度:16位有符号
private short LeftWheelSpeed;
// 右轮速度:16位有符号
private short RightWheelSpeed;
// 底盘升降标志位
private byte ChassisLiftFlag;
// 割刀速度标志位
private byte MowerSpeedFlag;
// 点火标志位
private byte IgnitionFlag;
// 保留位
private byte EmergencyStopFlag;
// Crc校验
private short Crc16;
// 包尾字节1
private byte EndFlag1;
// 包尾字节2
private byte EndFlag2;
}

View File

@@ -0,0 +1,15 @@
package com.maibu.dto;
public enum MesType {
AUTH, //登录连接权限web->be
deviceInfo, //设备状态 be->web
path,//路径下发 web->be
have_logged_in,//消息
device_status_changed,//上下线状态变化
device_error_push, //错误推送
heartbeat,
remoteControl,
detectionReport,//障碍物消息
switchPermission,//切换控制申请
switchResult;//切换结果
}

View File

@@ -0,0 +1,26 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class NettySwitchDeviceDTO {
/**
* 主机名
*/
private String platform;
/**
* 设备id
*/
private String deviceId;
/**
* 设备别名
*/
private String deviceAlias;
/**
* 用户名
*/
private String userId;
}

View File

@@ -0,0 +1,9 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class ObstacleMessageDTO {
private String data;
private MesType type;
}

View File

@@ -0,0 +1,14 @@
package com.maibu.dto;
import lombok.Data;
import java.util.List;
@Data
public class RemoteData {
private List<Double> axes;
private List<Boolean> buttons;
private Boolean connected;
}

View File

@@ -0,0 +1,10 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class ResultDTO {
private Object code;
private String msg;
private Object data;
}

View File

@@ -0,0 +1,12 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class WebAuthResponseDTO {
private String userName;
private String deviceId;
private String platform;
private Boolean isAuth;
private MesType type;
}

View File

@@ -0,0 +1,10 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class WebHasLoginDTO {
private MesType type;
private String userName;
private String token;
}

View File

@@ -0,0 +1,14 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class WebMessageBaseDTO {
private MesType type;
private String userName;
private String deviceId;
private String token;
private String message;
private RemoteData remoteData;
private Boolean switchResult;
}

View File

@@ -0,0 +1,13 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class WebOlineStatusMessageDTO {
private String deviceId;
private Integer onlineStatus;
private Integer hasError;
private String errorCode;
private String description;
private MesType type;
}

View File

@@ -0,0 +1,11 @@
package com.maibu.dto;
import lombok.Data;
import java.util.List;
@Data
public class WebStatusMessageDTO {
private List<DeviceStatusDetail> data;
private MesType type;
}

View File

@@ -0,0 +1,9 @@
package com.maibu.dto;
import lombok.Data;
@Data
public class WebSwitchControlResponseDTO {
private DeviceRespondDTO respond;
private MesType type;
}

View File

@@ -0,0 +1,95 @@
package com.maibu.memory;
import com.fastbee.control.DiffSteer;
import com.fastbee.netty.NettyClient;
import io.vertx.core.Vertx;
import lombok.Data;
import org.java_websocket.WebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Data
public class MiddleGlobalMemory {
private static final Logger logger = LoggerFactory.getLogger(MiddleGlobalMemory.class);
/**
* -- GETTER --
* 获取Vert.x实例
*/
public static Vertx vertx;
// 存储所有在线WebSocket连接
// key: test:web:token
public static final ConcurrentHashMap<String, WebSocket> onlineSockets = new ConcurrentHashMap<>();
// 当前账号控制中的设备 value: test:web:token key deviceId
public static final ConcurrentHashMap<String, String> controlMap = new ConcurrentHashMap<>();
public static final ConcurrentHashMap<String, NettyClient> nettyClientMap = new ConcurrentHashMap<>();
public static DiffSteer diffSteer = null;
public static void initDiff() {
if (diffSteer == null) {
diffSteer = new DiffSteer();
diffSteer.init(3000, 3000, 1000, 1000, 3000);
diffSteer.setDirPolarity(
DiffSteer.Polarity_Low,
DiffSteer.Polarity_High,
DiffSteer.Polarity_Low,
DiffSteer.Polarity_High
);
}
}
// web端操作的权限只能有一个web 端操作设备 key deviceId,value userName:web:token
// // todo 发送请求 请求成功下发,一端下线后才能解锁
// public static final ConcurrentHashMap<String, String> controlLockMap = new ConcurrentHashMap<>();
//删除web端操作的控制锁
// public static void removeLockByMaster(String value) {
// if(!CollectionUtils.isEmpty(controlLockMap)){
// controlLockMap.keySet().removeIf(controlLock -> controlLockMap.get(controlLock).equals(value));
// }
// }
/**
* 关闭资源
*/
public void vertxClose() {
if (vertx != null) {
vertx.close(res -> {
if (res.succeeded()) {
logger.info("Vert.x 关闭成功");
} else {
logger.error("Vert.x 关闭失败: {}", res.cause().getMessage());
}
});
}
}
public static String findBySocket(WebSocket socket) {
return onlineSockets.entrySet().stream()
.filter(entry -> Objects.equals(entry.getValue(), socket))
.map(Map.Entry::getKey)
.findFirst().orElse(null);
}
public static String findNettyClient(NettyClient nettyClient) {
return nettyClientMap.entrySet().stream()
.filter(entry -> Objects.equals(entry.getValue(), nettyClient))
.map(Map.Entry::getKey)
.findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,117 @@
package com.maibu.memory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
// 系统监控类
@Component
public class SystemMonitor {
private final Logger logger = LoggerFactory.getLogger(SystemMonitor.class);
// 启动监控任务
@PostConstruct
public void startMonitoring() {
// JVM 内存监控
startMemoryMonitor();
// CPU 使用率监控
startCpuMonitor();
// 线程状态监控
startThreadMonitor();
// 连接数监控
startConnectionMonitor();
}
// 内存监控
private void startMemoryMonitor() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
Runtime runtime = Runtime.getRuntime();
long totalMemory = runtime.totalMemory() / (1024 * 1024);
long freeMemory = runtime.freeMemory() / (1024 * 1024);
long usedMemory = totalMemory - freeMemory;
long maxMemory = runtime.maxMemory() / (1024 * 1024);
double memoryUsage = (double) usedMemory / maxMemory * 100;
logger.info("内存使用情况 - 总内存: {}MB, 已用内存: {}MB, 空闲内存: {}MB, 使用率: {}",
totalMemory, usedMemory, freeMemory, String.format("%.2f", memoryUsage));
if (memoryUsage > 80) {
logger.warn("内存使用率过高: {}", String.format("%.2f", memoryUsage));
// 可以添加告警逻辑
}
}, 1, 5, TimeUnit.MINUTES);
}
// CPU 监控
private void startCpuMonitor() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
try {
// 使用 OperatingSystemMXBean 获取 CPU 使用率
OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
com.sun.management.OperatingSystemMXBean sunOsBean =
(com.sun.management.OperatingSystemMXBean) osBean;
double cpuUsage = sunOsBean.getSystemCpuLoad() * 100;
logger.info("CPU 使用率: {}", String.format("%.2f", cpuUsage));
if (cpuUsage > 80) {
logger.warn("CPU 使用率过高: {}", String.format("%.2f", cpuUsage));
// 可以添加告警逻辑
}
}
} catch (Exception e) {
logger.error("CPU 监控异常: {}", e.getMessage());
}
}, 1, 5, TimeUnit.MINUTES);
}
// 线程监控
private void startThreadMonitor() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
int threadCount = threadBean.getThreadCount();
int peakThreadCount = threadBean.getPeakThreadCount();
long daemonThreadCount = threadBean.getDaemonThreadCount();
logger.info("线程状态 - 当前线程数: {}, 峰值线程数: {}, 守护线程数: {}",
threadCount, peakThreadCount, daemonThreadCount);
// 检查死锁
long[] deadlockedThreads = threadBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
logger.error("发现死锁线程: {}", Arrays.toString(deadlockedThreads));
// 可以添加告警逻辑
}
}, 1, 10, TimeUnit.MINUTES);
}
// 连接数监控
private void startConnectionMonitor() {
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleAtFixedRate(() -> {
int webSocketCount = MiddleGlobalMemory.onlineSockets.size();
int nettyClientCount = MiddleGlobalMemory.nettyClientMap.size();
logger.info("连接数统计 - WebSocket连接数: {}, Netty客户端数: {}",
webSocketCount, nettyClientCount);
}, 1, 5, TimeUnit.MINUTES);
}
}

View File

@@ -0,0 +1,48 @@
package com.maibu.netty;
import com.fastbee.memory.MiddleGlobalMemory;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
/**
* 系统初始化资源类
* 实现CommandLineRunner接口,在Spring Boot启动时自动执行
*/
@Getter
@Component
public class InitResource implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(InitResource.class);
@Override
public void run(String... args) throws Exception {
logger.info("InitResource 开始初始化系统资源...");
// 初始化Vert.x
initVertx();
logger.info("InitResource 系统资源初始化完成");
}
/**
* 初始化Vert.x
*/
private void initVertx() {
try {
VertxOptions options = new VertxOptions()
.setWorkerPoolSize(20)
.setEventLoopPoolSize(10)
.setWarningExceptionTime(5000000000L);
MiddleGlobalMemory.vertx = Vertx.vertx(options);
logger.info("Vert.x 初始化成功");
} catch (Exception e) {
logger.error("Vert.x 初始化失败: {}", e.getMessage(), e);
}
}
}

View File

@@ -0,0 +1,223 @@
package com.maibu.netty;
import com.fastbee.common.MiddleConstant;
import com.fastbee.netty.handler.*;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
/**
* Netty客户端实现类
* 提供与TCP服务器的连接管理、消息发送、重连机制和心跳检测功能
*/
public class NettyClient {
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
// private static volatile NettyClient instance;
public String token;
public String userName;
public String requestDeviceId; //前端需要连接的设备
public String currentDeviceId; //当前连接的设备
public Long heartBeatTimer;
public String key; //userName + web + token
public String host;
public int port;
public int reconnectDelay = 5;
public int heartbeatInterval = 5;
public EventLoopGroup group;
public Bootstrap bootstrap;
public Channel channel;
public Long lastSwitchTime = -1L;
public Long Interval = 5000L;
public static final byte[] HEARTBEAT_PACKET = new byte[]{(byte) 0xAB, (byte) 0xAA, (byte) 0xFF, (byte) 0xAA, (byte) 0xAB};
// /**
// * 获取单例实例(无回调)
// */
// public static NettyClient getInstance() {
// if (instance == null) {
// synchronized (NettyClient.class) {
// if (instance == null) {
// instance = new NettyClient();
// }
// }
// }
// return instance;
// }
/**
* 初始化并连接到服务器
*/
public void initConnect(String host, int port) {
if (channel != null && channel.isActive()) {
logger.info("Netty客户端已连接,不重复初始化");
return;
}
this.host = host;
this.port = port;
connect();
}
/**
* 初始化Netty客户端
*/
public void initClient() {
group = new NioEventLoopGroup();
bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
// 设置共享属性
ch.attr(MiddleConstant.ATT_MASTER_KEY).set(key);
// 空闲检测处理器(心跳)
pipeline.addLast(new IdleStateHandler(0, heartbeatInterval, 0, TimeUnit.SECONDS));
pipeline.addLast(new MiddleHeaderFooterDecoder());
// 编解码器
pipeline.addLast(new MiddleHexDecoder());
pipeline.addLast(new MiddleHexEncoder());
// 自定义处理器
pipeline.addLast(new ClientHandler());
}
});
}
/**
* 连接到服务器
*/
public void connect() {
if (bootstrap == null || host == null || port <= 0) {
logger.error("Netty客户端未正确初始化");
return;
}
logger.info("正在连接到Netty服务器: {}:{}", host, port);
bootstrap.connect(host, port).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
channel = future.channel();
logger.info("Netty客户端连接成功: {}:{}", host, port);
// notifyConnectionStatus(true);
} else {
logger.error("Netty客户端连接失败,{}秒后重试: {}", reconnectDelay, future.cause().getMessage());
// notifyConnectionStatus(false);
// 重连
group.schedule(this::connect, reconnectDelay, TimeUnit.SECONDS);
}
});
}
/**
* 断开连接
*/
public void disconnect() {
if (channel != null) {
channel.close();
channel = null;
}
// notifyConnectionStatus(false);
logger.info("Netty客户端已断开连接");
}
/**
* 发送数据
*/
public void sendData(byte[] data) {
if (channel != null && channel.isActive()) {
channel.writeAndFlush(data).addListener((ChannelFutureListener) future -> {
if (future.isSuccess()) {
logger.debug("Netty客户端发送数据成功: {}", bytesToHex(data));
} else {
logger.error("Netty客户端发送数据失败: {}", future.cause().getMessage());
}
});
} else {
logger.error("Netty客户端未连接,发送数据失败");
}
}
/**
* 发送十六进制字符串数据
*/
public void sendHexData(String hexData) {
try {
byte[] data = hexStringToBytes(hexData);
sendData(data);
} catch (IllegalArgumentException e) {
logger.error("无效的十六进制字符串: {}", hexData);
}
}
/**
* 十六进制字符串转字节数组
*/
private static byte[] hexStringToBytes(String hexString) {
hexString = hexString.replaceAll("\\s+", "");
if (hexString.length() % 2 != 0) {
throw new IllegalArgumentException("十六进制字符串长度必须为偶数");
}
byte[] result = new byte[hexString.length() / 2];
for (int i = 0; i < hexString.length(); i += 2) {
result[i / 2] = (byte) Integer.parseInt(hexString.substring(i, i + 2), 16);
}
return result;
}
/**
* 字节数组转十六进制字符串
*/
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
/**
* 检查客户端是否已连接
*/
public boolean isConnected() {
return channel != null && channel.isActive();
}
/**
* 设置重连延迟时间(秒)
*/
public void setReconnectDelay(int reconnectDelay) {
if (reconnectDelay > 0) {
this.reconnectDelay = reconnectDelay;
}
}
/**
* 设置心跳间隔时间(秒)
*/
public void setHeartbeatInterval(int heartbeatInterval) {
if (heartbeatInterval > 0) {
this.heartbeatInterval = heartbeatInterval;
}
}
}

View File

@@ -0,0 +1,494 @@
package com.maibu.netty.handler;
import cn.hutool.json.JSONObject;
import com.fastbee.common.MiddleCommandConstant;
import com.fastbee.common.MiddleConstant;
import com.fastbee.common.utils.json.JsonUtils;
import com.fastbee.common.utils.spring.SpringUtils;
import com.fastbee.dto.*;
import com.fastbee.memory.MiddleGlobalMemory;
import com.fastbee.netty.NettyClient;
import com.fastbee.utils.HttpService;
import com.fastbee.websocket.WebsocketHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.java_websocket.WebSocket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Netty客户端处理器
*/
@Slf4j
public class ClientHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ClientHandler.class);
private static final HttpService httpService = SpringUtils.getBean(HttpService.class);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
byte[] data = (byte[]) msg;
logger.debug("channelRead2: {}", Arrays.toString(data));
//假如是心跳包
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
if (isHeartbeat(data)) {
//websocket 不在了停止发 心跳
if (webSocket != null && webSocket.isOpen()) {
ctx.writeAndFlush(data);
}
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
if (nettyClient != null && StringUtils.isEmpty(nettyClient.currentDeviceId)) {
if (nettyClient.lastSwitchTime == -1 || System.currentTimeMillis() - nettyClient.lastSwitchTime > nettyClient.Interval) {
if (!StringUtils.isEmpty(nettyClient.requestDeviceId) && !StringUtils.isEmpty(key)) {
switchDevice(nettyClient, key);
nettyClient.lastSwitchTime = System.currentTimeMillis();
}
}
}
} else if (isPushMsg(data)) {
try {
int dataStart = 3; // 跳过 0:AB 1:AA 2:cmd
// 数据体结束位置(不含CRC和帧尾)
int dataEnd = data.length - 4; // 去掉 CRC_L、CRC_H、AA、AB
int dataLen = dataEnd - dataStart;
if (dataLen <= 0) {
System.out.println("数据体长度异常");
return;
}
byte[] jsonBytes = Arrays.copyOfRange(data, dataStart, dataEnd);
String json = new String(jsonBytes, StandardCharsets.UTF_8);
logger.debug("收到推送2: {}", json);
if (StringUtils.isNotBlank(json)) {
JSONObject obj = new JSONObject(json);
if (obj.get("event") != null) {
//状态变化 上下线
String eventType = obj.get("event").toString();
String deviceId = obj.get("deviceId").toString();
if ("device_status_changed".equals(eventType)) {
String status = obj.get("status").toString();
WebOlineStatusMessageDTO dto = new WebOlineStatusMessageDTO();
dto.setDeviceId(deviceId);
dto.setType(MesType.device_status_changed);
if (status.equals("offline")) {
dto.setOnlineStatus(0);
} else {
dto.setOnlineStatus(1);
}
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
} else if ("device_error_push".equals(eventType)) {
//todo 更新推送的错误
String details = obj.get("details").toString();
WebOlineStatusMessageDTO dto = new WebOlineStatusMessageDTO();
dto.setDeviceId(deviceId);
dto.setType(MesType.device_error_push);
dto.setHasError(1);
dto.setDescription("");
dto.setErrorCode("");
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
}
} else {
/* 判断 respond 字段 */
if (obj.get("request") != null) {
String deviceId = obj.get("deviceId").toString();
String userName = obj.get("userId").toString();
String platform = obj.get("platform").toString();
//直接转发 websocket 申请
WebAuthResponseDTO request = new WebAuthResponseDTO();
request.setType(MesType.switchPermission);
request.setUserName(userName);
request.setDeviceId(deviceId);
request.setPlatform(platform);
logger.debug("json: {}", JsonUtils.toJsonString(request));
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(request));
} else {
if (obj.get("respond") != null) {
if ("have_logged_in".equals(obj.get("respond").toString())) {
/* 如果回复 have_logged_in 则调用logout */
//System.out.println("检测到重复登录,执行 logout");
//NettyClient nettyClient = GlobalMemory.nettyClientMap.get(key);
//WebHasLoginDTO dto = new WebHasLoginDTO();
//dto.setToken(nettyClient.token);
//dto.setUserName(nettyClient.userName);
//dto.setType(MesType.have_logged_in);
//WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
} else {
//todo respond netty_server返回结果处理
DeviceRespondDTO respond = JsonUtils.parseObject(obj.get("respond").toString(), DeviceRespondDTO.class);
if (respond != null) {
//直接转发 websocket 申请
WebSwitchControlResponseDTO responseDTO = new WebSwitchControlResponseDTO();
responseDTO.setType(MesType.switchResult);
responseDTO.setRespond(respond);
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(responseDTO));
}
}
} else {
if (obj.get("type").toString() != null && "detectionReport".equals(obj.get("type").toString())) {
WebsocketHandler.sendMessageToClient(webSocket, json);
}
}
}
}
//error 推送
}
//TODO web 无切换控制请求
} catch (Exception e) {
logger.error("事件推送解析失败:{}", e.getMessage());
}
} else if (isStatusDate(data)) {
//状态消息推送给前端
WebStatusMessageDTO dto = createWebDeviceStatusMessage(data);
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(dto));
} else if (isPath(data)) {
//已到达
if (data[5] == (byte) 0x01) {
}
}
} catch (Exception e) {
logger.error("channelRead error:{}", e.getMessage());
}
}
public WebStatusMessageDTO createWebDeviceStatusMessage(byte[] data) {
WebStatusMessageDTO webStatusMessageDTO = new WebStatusMessageDTO();
if (data[2] == MiddleCommandConstant.status) {
String deviceStatus = new String(data, 2, data.length - 4, StandardCharsets.UTF_8).trim();
String[] split = deviceStatus.split(",");
List<DeviceStatusDetail> transferData = transferStatusData(split);
webStatusMessageDTO.setData(transferData);
webStatusMessageDTO.setType(MesType.deviceInfo);
}
return webStatusMessageDTO;
}
private List<DeviceStatusDetail> transferStatusData(String[] split) {
List<DeviceStatusDetail> transferData = new ArrayList<>();
DeviceStatusDetail detail0 = new DeviceStatusDetail();
detail0.setName("voltage");
detail0.setValue(split[0]);
detail0.setUnit("");
transferData.add(detail0);
DeviceStatusDetail detail1 = new DeviceStatusDetail();
detail1.setName("leftTargetSpeed");
detail1.setValue(split[1]);
detail1.setUnit("");
transferData.add(detail1);
DeviceStatusDetail detail2 = new DeviceStatusDetail();
detail2.setName("rightTargetSpeed");
detail2.setValue(split[2]);
detail2.setUnit("");
transferData.add(detail2);
DeviceStatusDetail detail3 = new DeviceStatusDetail();
detail3.setName("leftMeasureSpeed");
detail3.setValue(split[3]);
detail3.setUnit("");
transferData.add(detail3);
DeviceStatusDetail detail4 = new DeviceStatusDetail();
detail4.setName("rightMeasureSpeed");
detail4.setValue(split[4]);
detail4.setUnit("");
transferData.add(detail4);
DeviceStatusDetail detail5 = new DeviceStatusDetail();
detail5.setName("leftCurrent");
detail5.setValue(split[5]);
detail5.setUnit("");
transferData.add(detail5);
DeviceStatusDetail detail6 = new DeviceStatusDetail();
detail6.setName("rightCurrent");
detail6.setValue(split[6]);
detail6.setUnit("");
transferData.add(detail6);
DeviceStatusDetail detail7 = new DeviceStatusDetail();
detail7.setName("leftMotorTemp");
detail7.setValue(split[7]);
detail7.setUnit("");
transferData.add(detail7);
DeviceStatusDetail detail8 = new DeviceStatusDetail();
detail8.setName("rightMotorTemp");
detail8.setValue(split[8]);
detail8.setUnit("");
transferData.add(detail8);
DeviceStatusDetail detail9 = new DeviceStatusDetail();
detail9.setName("chipTemp");
detail9.setValue(split[9]);
detail9.setUnit("");
transferData.add(detail9);
DeviceStatusDetail detail10 = new DeviceStatusDetail();
detail10.setName("yaw");
detail10.setValue(split[10]);
detail10.setUnit("");
transferData.add(detail10);
DeviceStatusDetail detail11 = new DeviceStatusDetail();
detail11.setName("pitch");
detail11.setValue(split[11]);
detail11.setUnit("");
transferData.add(detail11);
DeviceStatusDetail detail12 = new DeviceStatusDetail();
detail12.setName("roll");
detail12.setValue(split[12]);
detail12.setUnit("");
transferData.add(detail12);
DeviceStatusDetail detail13 = new DeviceStatusDetail();
detail13.setName("satelliteCnt");
detail13.setValue(split[13]);
detail13.setUnit("");
transferData.add(detail13);
DeviceStatusDetail detail14 = new DeviceStatusDetail();
detail14.setName("headingStatus");
detail14.setValue(split[14]);
detail14.setUnit("");
transferData.add(detail14);
DeviceStatusDetail detail15 = new DeviceStatusDetail();
detail15.setName("headingStatus");
detail15.setValue(split[15]);
detail15.setUnit("");
transferData.add(detail15);
DeviceStatusDetail detail16 = new DeviceStatusDetail();
detail16.setName("latitude");
detail16.setValue(split[16]);
detail16.setUnit("");
transferData.add(detail16);
DeviceStatusDetail detail17 = new DeviceStatusDetail();
detail17.setName("longitude");
detail17.setValue(split[17]);
detail17.setUnit("");
transferData.add(detail17);
//18 时间暂未给值
DeviceStatusDetail detail19 = new DeviceStatusDetail();
detail19.setName("cuttingSpeed");
detail19.setValue(split[19]);
detail19.setUnit("");
transferData.add(detail19);
DeviceStatusDetail detail20 = new DeviceStatusDetail();
detail20.setName("controlMode");
detail20.setValue(split[20]);
detail20.setUnit("");
transferData.add(detail20);
DeviceStatusDetail detail21 = new DeviceStatusDetail();
detail21.setName("battery");
detail21.setValue(split[21]);
detail21.setUnit("");
transferData.add(detail21);
DeviceStatusDetail detail22 = new DeviceStatusDetail();
detail22.setName("workingArea");
detail22.setValue(split[22]);
detail22.setUnit("");
transferData.add(detail22);
DeviceStatusDetail detail23 = new DeviceStatusDetail();
detail23.setName("obstacleSign");
detail23.setValue(split[23]);
detail23.setUnit("");
transferData.add(detail23);
return transferData;
}
@Override
public void channelActive(ChannelHandlerContext ctx) {
logger.debug("连接开始channelActive:ChannelId:{} ", ctx.channel().id().asLongText());
//websocket连上后连接netty成功后发送登录认证包
// key username + : web + token
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
System.out.println(key);
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
if (nettyClient != null) {
// 构造并发送认证包
byte[] deviceNameBytes = key.getBytes(StandardCharsets.UTF_8);
byte[] packet = new byte[3 + deviceNameBytes.length + 4];
packet[0] = (byte) 0xAB;
packet[1] = (byte) 0xAA;
packet[2] = 0x03;
System.arraycopy(deviceNameBytes, 0, packet, 3, deviceNameBytes.length);
int tailStart = 3 + deviceNameBytes.length;
packet[tailStart] = 0x00;
packet[tailStart + 1] = 0x00;
packet[tailStart + 2] = (byte) 0xAA;
packet[tailStart + 3] = (byte) 0xAB;
ctx.writeAndFlush(packet);
// startHeartbeat(nettyClient, ctx, key);
}
}
public static void switchDevice(NettyClient nettyClient, String key) {
try {
// 2. 调用 POST 接口:传入 Token B(与 Token A 不同)
String token = nettyClient.token;
String postUrl = MiddleConstant.nettyApiUrl + "/forward/device/switchDevice";
NettySwitchDeviceDTO dto = new NettySwitchDeviceDTO();
dto.setDeviceId(nettyClient.requestDeviceId);
dto.setPlatform("web");
String result = httpService.doPost(postUrl, token, dto);
ResultDTO resultDTO = JsonUtils.parseObject(result, ResultDTO.class);
if (resultDTO != null && resultDTO.getData() != null) {
if ((boolean) resultDTO.getData()) {
logger.debug("用户:{}切换设备:{}成功", key, nettyClient.requestDeviceId);
//切换成功推送 websocket
nettyClient.currentDeviceId = nettyClient.requestDeviceId;
// nettyClient.requestDeviceId = null;
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
if (webSocket != null && webSocket.isOpen()) {
WebAuthResponseDTO responseDTO = new WebAuthResponseDTO();
responseDTO.setDeviceId(nettyClient.currentDeviceId);
responseDTO.setUserName(nettyClient.userName);
responseDTO.setType(MesType.AUTH);
responseDTO.setIsAuth(true);
WebsocketHandler.sendMessageToClient(webSocket, JsonUtils.toJsonString(responseDTO));
}
} else {
logger.debug("用户:{}切换设备:{}失败", key, nettyClient.requestDeviceId);
}
}
} catch (Exception e) {
log.error("切换设备,switchDevice发生错误", e);
}
}
/**
* 启动心跳检测
*/
private void startHeartbeat(NettyClient nettyClient, ChannelHandlerContext ctx, String key) {
stopHeartbeat(nettyClient);
MiddleGlobalMemory.vertx.setPeriodic(200, timerId -> {
try {
nettyClient.heartBeatTimer = timerId;
if (ctx.channel() != null && ctx.channel().isActive()) {
ctx.channel().writeAndFlush(NettyClient.HEARTBEAT_PACKET);
logger.debug("Netty客户端:{}发送心跳包", nettyClient.key);
}
} catch (Exception e) {
logger.error("循环任务执行异常:" + e.getMessage());
}
});
}
/**
* 关闭心跳检测
*/
private void stopHeartbeat(NettyClient nettyClient) {
if (nettyClient != null && nettyClient.heartBeatTimer != null) {
MiddleGlobalMemory.vertx.cancelTimer(nettyClient.heartBeatTimer);
nettyClient.heartBeatTimer = null;
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
// stopHeartbeat(nettyClient);
nettyClient.currentDeviceId = null;
logger.debug("连接断开channelInactive,ChannelId:{} ", ctx.channel().id().asLongText());
WebSocket webSocket = MiddleGlobalMemory.onlineSockets.get(key);
if (webSocket != null && webSocket.isOpen()) {
ctx.channel().eventLoop().schedule(nettyClient::connect, nettyClient.reconnectDelay, TimeUnit.SECONDS);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
String key = ctx.channel().attr(MiddleConstant.ATT_MASTER_KEY).get();
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
// stopHeartbeat(nettyClient);
cause.printStackTrace();
logger.error("异常断开exceptionCaught:{}", cause.getMessage());
ctx.close();
}
private boolean isHeartbeat(byte[] data) {
return data.length == 5 &&
data[0] == (byte) 0xAB &&
data[1] == (byte) 0xAA &&
data[2] == (byte) 0xFF &&
data[3] == (byte) 0xAA &&
data[4] == (byte) 0xAB;
}
/// 服务器重新请求下位机连接指令。即重新发送连接码
private boolean isReconnectCommand(byte[] data) {
return data.length == 9 &&
data[0] == (byte) 0xAB &&
data[1] == (byte) 0xAA &&
data[2] == (byte) 0x03 &&
data[3] == (byte) 0x00 &&
data[4] == (byte) 0x00 &&
data[5] == (byte) 0x00 &&
data[6] == (byte) 0x00 &&
data[7] == (byte) 0xAA &&
data[8] == (byte) 0xAB;
}
private boolean isPushMsg(byte[] data) {
return data[0] == (byte) 0xAB &&
data[1] == (byte) 0xAA &&
data[2] == (byte) 0x12 &&
data[data.length - 2] == (byte) 0xAA &&
data[data.length - 1] == (byte) 0xAB;
// // 只要是以 '{' 开头、以 '}' 结尾,就认为是事件 JSON
// if (data == null || data.length < 2) return false;
// return data[0] == '{' && data[data.length - 1] == '}';
}
private boolean isStatusDate(byte[] data) {
return data[0] == (byte) 0xAB &&
data[1] == (byte) 0xAA &&
data[2] == (byte) 0x02 &&
data[data.length - 2] == (byte) 0xAA &&
data[data.length - 1] == (byte) 0xAB;
}
private boolean isPath(byte[] data) {
return data[0] == (byte) 0xAB &&
data[1] == (byte) 0xAA &&
data[2] == (byte) 0x01 &&
data[data.length - 2] == (byte) 0xAA &&
data[data.length - 1] == (byte) 0xAB;
}
}

View File

@@ -0,0 +1,93 @@
package com.maibu.netty.handler;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import org.apache.commons.codec.binary.Hex;
import java.util.List;
public class MiddleHeaderFooterDecoder extends ByteToMessageDecoder {
private static final byte[] HEADER = {(byte) 0xAB, (byte) 0xAA};
private static final byte[] FOOTER = {(byte) 0xAA, (byte) 0xAB};
private static final int MAX_FRAME_LENGTH = 500;
private static final boolean DEBUG = true; // 可控制是否打印日志
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
if (DEBUG) {
byte[] raw = new byte[in.readableBytes()];
in.getBytes(in.readerIndex(), raw);
System.out.println("收到原始数据:" + Hex.encodeHexString(raw).toUpperCase());
}
int readerIndex = in.readerIndex();
while (readerIndex <= in.writerIndex() - HEADER.length) {
// 找帧头
int headerPos = findPattern(in, HEADER, readerIndex);
if (headerPos == -1) {
// 没找到帧头,丢弃无效数据
in.readerIndex(in.writerIndex() - (HEADER.length - 1));
return;
}
// 从 header 之后找帧尾
int searchStart = headerPos + HEADER.length;
int footerPos = -1;
while (searchStart <= in.writerIndex() - FOOTER.length) {
footerPos = findPattern(in, FOOTER, searchStart);
if (footerPos == -1) break; // 没找到尾,等待更多数据
// 检查是否有嵌套帧头
int nestedHeader = findPattern(in, HEADER, searchStart);
if (nestedHeader != -1 && nestedHeader < footerPos) {
// 有嵌套帧头,从新头重新搜索
headerPos = nestedHeader;
searchStart = headerPos + HEADER.length;
continue;
}
break;
}
if (footerPos == -1) {
// 第一帧未完整,保留数据,等待更多
in.readerIndex(headerPos);
return;
}
int frameLength = footerPos + FOOTER.length - headerPos;
if (frameLength > MAX_FRAME_LENGTH) {
// 超长帧,跳过异常数据
in.readerIndex(headerPos + 1); // 从下一个字节重新找
readerIndex = in.readerIndex();
continue;
}
// 提取完整帧
in.readerIndex(headerPos);
ByteBuf frame = in.readRetainedSlice(frameLength);
out.add(frame);
// 下次从 footer 后面继续解析
readerIndex = in.readerIndex();
}
// 更新 readerIndex
in.readerIndex(readerIndex);
}
private int findPattern(ByteBuf buf, byte[] pattern, int fromIndex) {
for (int i = fromIndex; i <= buf.writerIndex() - pattern.length; i++) {
boolean match = true;
for (int j = 0; j < pattern.length; j++) {
if (buf.getByte(i + j) != pattern[j]) {
match = false;
break;
}
}
if (match) return i;
}
return -1;
}
}

View File

@@ -0,0 +1,17 @@
package com.maibu.netty.handler;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MiddleHexDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
byte[] data = new byte[in.readableBytes()];
in.readBytes(data);
out.add(data);
}
}

View File

@@ -0,0 +1,13 @@
package com.maibu.netty.handler;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MiddleHexEncoder extends MessageToByteEncoder<byte[]> {
@Override
protected void encode(ChannelHandlerContext ctx, byte[] msg, ByteBuf out) {
out.writeBytes(msg);
}
}

View File

@@ -0,0 +1,85 @@
package com.maibu.utils;
import com.fastbee.common.MiddleCommandConstant;
import com.fastbee.control.DiffSteer;
import com.fastbee.dto.LawnMoverProtocolEntity;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.List;
public class ControlUtils {
public static LawnMoverProtocolEntity getCarControlMessage(DiffSteer diffSteer, int originX, int originY, List<Boolean> buttons) {
LawnMoverProtocolEntity state = new LawnMoverProtocolEntity();
diffSteer.calcWheelSpeed(originX, originY);
state.setCommandType(MiddleCommandConstant.remoteControl);
state.setLeftWheelSpeed((short) diffSteer.getLeftWheelSpeed());
state.setRightWheelSpeed((short) diffSteer.getRightWheelSpeed());
//对应的 button数组 L1 button【4】 R1 button【5】 A对应的button【0】 B对应button【1】 X对应button【2】 Y对应button【3】 button[7]急停
//A升B降底盘 X割刀加速Y减速 L1点火 R1熄火
Boolean A = buttons.get(0);
Boolean B = buttons.get(1);
Boolean X = buttons.get(2);
Boolean Y = buttons.get(3);
Boolean L1 = buttons.get(4);
Boolean R1 = buttons.get(5);
Boolean eStop = buttons.get(8); //急停
//底盘升降
int chassisLiftFlag = 0;
if (A & !B) {
chassisLiftFlag = 1;
} else if (!A & B) {
chassisLiftFlag = 2;
}
state.setChassisLiftFlag((byte) chassisLiftFlag);
//割刀加减速
int mowerSpeedFlag = 0;
if (X & !Y) {
mowerSpeedFlag = 1;
} else if (!X & Y) {
mowerSpeedFlag = 2;
}
state.setMowerSpeedFlag((byte) mowerSpeedFlag);
//点火熄火
int ignitionFlag = 0;
if (L1 & !R1) {
ignitionFlag = 1;
} else if (!L1 & R1) {
ignitionFlag = 2;
}
state.setIgnitionFlag((byte) ignitionFlag);
int emergencyStopFlag = eStop ? 1 : 0;
state.setEmergencyStopFlag((byte) emergencyStopFlag);
// val toBytes = state.toBytes()
// println("toBytes==" + LawnMoverProtocolEntity.printBytesToHex(toBytes))
return state;
}
public static byte[] buildControlCommand(LawnMoverProtocolEntity entity) {
ByteBuffer buffer = ByteBuffer.allocate(15);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.put(entity.getHeader1());
buffer.put(entity.getHeader2());
buffer.put(entity.getCommandType());
buffer.putShort(entity.getLeftWheelSpeed());//左轮速度
buffer.putShort(entity.getRightWheelSpeed());//右轮速度
buffer.put(entity.getChassisLiftFlag());//底盘升降
buffer.put(entity.getMowerSpeedFlag());//割刀加减速
buffer.put(entity.getIgnitionFlag());//点火状态
buffer.put(entity.getEmergencyStopFlag());//急停
buffer.put((byte) 0x00);
buffer.put((byte) 0x00);
buffer.put(entity.getEndFlag1());
buffer.put(entity.getEndFlag2());
return buffer.array();
}
}

View File

@@ -0,0 +1,15 @@
package com.maibu.utils;
public class HexUtils {
/**
* 字节数组转十六进制字符串
*/
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X ", b));
}
return sb.toString().trim();
}
}

View File

@@ -0,0 +1,109 @@
package com.maibu.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
@Service
public class HttpService {
@Autowired
private RestTemplate restTemplate;
/**
* 通用 GET 请求(手动传入 Token,每次可传不同值)
*
* @param apiUrl 目标接口地址
* @param token 自定义 Token(可为 null,不传则不携带 Authorization 头)
* @return 接口响应字符串
*/
public String doGet(String apiUrl, String token) {
// 1. 构建请求头(携带传入的 Token)
HttpHeaders headers = buildHeadersWithToken(token);
// 2. 封装请求实体(仅携带请求头,GET 无需请求体)
HttpEntity<Void> httpEntity = new HttpEntity<>(headers);
// 3. 发送 GET 请求
try {
ResponseEntity<String> response = restTemplate.exchange(
apiUrl,
HttpMethod.GET,
httpEntity,
String.class
);
// 校验响应状态并返回结果
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody() == null ? "" : response.getBody();
} else {
throw new RuntimeException("GET 请求失败,响应码:" + response.getStatusCode() + ",响应体:" + response.getBody());
}
} catch (HttpClientErrorException | HttpServerErrorException e) {
// 捕获 HTTP 4xx/5xx 异常
throw new RuntimeException("GET 请求异常,响应码:" + e.getStatusCode() + ",响应体:" + e.getResponseBodyAsString(), e);
} catch (Exception e) {
// 捕获其他未知异常
throw new RuntimeException("GET 请求未知异常:" + e.getMessage(), e);
}
}
/**
* 通用 POST 请求(手动传入 Token,支持 JSON 请求体,每次可传不同 Token)
*
* @param apiUrl 目标接口地址
* @param token 自定义 Token(可为 null,不传则不携带 Authorization 头)
* @param requestParam JSON 请求体(实体类/字符串均可,支持 null)
* @return 接口响应字符串
*/
public String doPost(String apiUrl, String token, Object requestParam) {
// 1. 构建请求头(携带传入的 Token)
HttpHeaders headers = buildHeadersWithToken(token);
// 2. 封装请求实体(携带请求头 + 请求体)
HttpEntity<Object> httpEntity = new HttpEntity<>(requestParam, headers);
// 3. 发送 POST 请求
try {
ResponseEntity<String> response = restTemplate.exchange(
apiUrl,
HttpMethod.POST,
httpEntity,
String.class
);
// 校验响应状态并返回结果
if (response.getStatusCode().is2xxSuccessful()) {
return response.getBody() == null ? "" : response.getBody();
} else {
throw new RuntimeException("POST 请求失败,响应码:" + response.getStatusCode() + ",响应体:" + response.getBody());
}
} catch (HttpClientErrorException | HttpServerErrorException e) {
// 捕获 HTTP 4xx/5xx 异常
throw new RuntimeException("POST 请求异常,响应码:" + e.getStatusCode() + ",响应体:" + e.getResponseBodyAsString(), e);
} catch (Exception e) {
// 捕获其他未知异常
throw new RuntimeException("POST 请求未知异常:" + e.getMessage(), e);
}
}
/**
* 构建携带 Token 的通用请求头(默认 JSON 格式)
*
* @param token 自定义 Token(可为 null)
* @return HttpHeaders
*/
private HttpHeaders buildHeadersWithToken(String token) {
HttpHeaders headers = new HttpHeaders();
// 设置默认 Content-Type 为 JSON 格式
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
headers.set("Accept", "application/json");
// 若 Token 不为空,添加 Authorization 头(默认 Bearer 格式,可按需修改)
if (token != null && !token.trim().isEmpty()) {
// 如需去掉 Bearer 前缀,直接改为 headers.set("Authorization", token.trim())
headers.set("Authorization", "Bearer " + token.trim());
}
// 可按需添加其他通用请求头(如 User-Agent)
// headers.set("User-Agent", "RestTemplate-Common/1.0");
return headers;
}
}

View File

@@ -0,0 +1,303 @@
package com.maibu.websocket;
import com.fastbee.common.CommandConstant;
import com.fastbee.common.MiddleConstant;
import com.fastbee.common.utils.json.JsonUtils;
import com.fastbee.dto.*;
import com.fastbee.enums.CommandRequestType;
import com.fastbee.memory.MiddleGlobalMemory;
import com.fastbee.netty.NettyClient;
import com.fastbee.utils.CommandUtils;
import com.fastbee.utils.ControlUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.apache.commons.lang3.StringUtils;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* WebSocket服务器实现类
* 使用java-websocket库实现WebSocket服务端功能
*/
public class WebsocketHandler extends WebSocketServer {
private static final Logger logger = LoggerFactory.getLogger(WebsocketHandler.class);
// 存储客户端连接:key=WebSocket连接对象,value=最后一次活跃时间(毫秒)
private final Map<WebSocket, Long> clientActiveTimeMap = new ConcurrentHashMap<>();
// 空闲超时时间(10秒):超过10秒无消息则判定为断开
private static final long IDLE_TIMEOUT = 10 * 1000;
// 定时检测线程池(每隔10秒检测一次空闲连接)
private final ScheduledExecutorService idleCheckExecutor = Executors.newSingleThreadScheduledExecutor();
public WebsocketHandler(int port) {
super(new InetSocketAddress(port));
// 启动空闲连接检测任务
// startIdleCheckTask();
}
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
logger.info("客户端连接成功: remoteAddress={}, 当前在线数: {}", conn.getRemoteSocketAddress(), MiddleGlobalMemory.onlineSockets.size());
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
String key = MiddleGlobalMemory.findBySocket(conn);
if (key != null) {
releaseResource(key);
logger.info("客户端断开连接: remoteAddress={}, code={}, reason={}, 当前在线数: {}", conn.getRemoteSocketAddress(), code, reason, MiddleGlobalMemory.onlineSockets.size());
}
}
@Override
public void onMessage(WebSocket conn, String message) {
try {
WebMessageBaseDTO webMessageBaseDTO = JsonUtils.parseObject(message, WebMessageBaseDTO.class);
logger.debug("收到websocket消息:{}", message);
if (webMessageBaseDTO != null) {
String token = webMessageBaseDTO.getToken();
String userName = webMessageBaseDTO.getUserName();
String deviceId = webMessageBaseDTO.getDeviceId();
MesType type = webMessageBaseDTO.getType();
String key = userName + ":web" + ":" + token;
if (MesType.AUTH.equals(type)) {
if (StringUtils.isEmpty(deviceId)) {
return;
}
//todo 先关闭原先的
WebSocket oldWebsocket = MiddleGlobalMemory.onlineSockets.get(key);
NettyClient oldNettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
if (oldWebsocket != null && oldWebsocket.isOpen()) {
oldWebsocket.close();
}
if (oldNettyClient != null && oldNettyClient.isConnected()) {
oldNettyClient.disconnect();
}
MiddleGlobalMemory.onlineSockets.put(key, conn);
//这里可以根据需要初始化Netty客户端
NettyClient client = new NettyClient();
client.key = key;
client.requestDeviceId = deviceId;
client.token = token;
client.userName = userName;
client.initClient();
client.initConnect(MiddleConstant.vertxIp, 9001);
MiddleGlobalMemory.nettyClientMap.put(key, client);
} else if (MesType.path.equals(type)) {
//todo下发路径 预留
pathDistribute();
} else if (MesType.remoteControl.equals(type)) {
//遥控
RemoteData remoteData = webMessageBaseDTO.getRemoteData();
if (remoteData != null) {
List<Double> axes = remoteData.getAxes();
List<Boolean> buttons = remoteData.getButtons();
Double y = axes.get(1) * 1000 * -1; //前后值反了
Double x = axes.get(2) * 1000;
int intX = (int) x.doubleValue();
int intY = (int) y.doubleValue();
MiddleGlobalMemory.initDiff();
LawnMoverProtocolEntity entity = ControlUtils.getCarControlMessage(MiddleGlobalMemory.diffSteer, intX, intY, buttons);
byte[] buf = ControlUtils.buildControlCommand(entity);
logger.debug("Netty客户端:{}发送遥控数据: {}", userName, JsonUtils.toJsonString(entity));
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
if (nettyClient != null && nettyClient.isConnected()) {
// if (!nettyClient.isConnected()) nettyClient.connect();
nettyClient.sendData(buf);
}
}
}
// else if(MesType.heartbeat.equals(type)){
// clientActiveTimeMap.put(conn,System.currentTimeMillis());
// }
else if(MesType.switchPermission.equals(type)){
DeviceRequestDTO requestDTO = new DeviceRequestDTO();
requestDTO.setRequest(CommandRequestType.switch_control);
requestDTO.setToken(token);
requestDTO.setPlatform("web");
requestDTO.setDeviceId(deviceId);
requestDTO.setUserId(userName);
logger.debug("Netty客户端:{}发送操控请求数据: {}", userName, JsonUtils.toJsonString(requestDTO));
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
ByteBuf buf = Unpooled.buffer();
CommandUtils.buildCommand(buf,requestDTO, CommandConstant.interaction);
if (nettyClient != null && nettyClient.isConnected()) {
// if (!nettyClient.isConnected()) nettyClient.connect();
nettyClient.sendData(buf.array());
}
} else if(MesType.switchResult.equals(type)){
DeviceRequestDTO requestDTO = new DeviceRequestDTO();
requestDTO.setToken(token);
requestDTO.setPlatform("web");
requestDTO.setDeviceId(deviceId);
requestDTO.setUserId(userName);
DeviceRespondDTO respond = new DeviceRespondDTO();
respond.setSwitchResult(webMessageBaseDTO.getSwitchResult());
respond.setDeviceId(deviceId);
requestDTO.setRespond(respond);
logger.debug("Netty客户端:{}发送切换操控权请求反馈: {}", userName, JsonUtils.toJsonString(requestDTO));
NettyClient nettyClient = MiddleGlobalMemory.nettyClientMap.get(key);
ByteBuf buf = Unpooled.buffer();
CommandUtils.buildCommand(buf,requestDTO, CommandConstant.interaction);
if (nettyClient != null && nettyClient.isConnected()) {
// if (!nettyClient.isConnected()) nettyClient.connect();
nettyClient.sendData(buf.array());
}
}
}
} catch (Exception e) {
logger.error("onMessage error:{}", e.getMessage());
}
}
public void releaseResource(String key) {
MiddleGlobalMemory.onlineSockets.remove(key);
if (MiddleGlobalMemory.nettyClientMap.containsKey(key)) {
NettyClient client = MiddleGlobalMemory.nettyClientMap.get(key);
if (client != null) {
if (client.isConnected()) {
client.disconnect();
}
MiddleGlobalMemory.nettyClientMap.remove(key);
}
}
}
public void pathDistribute() {
}
@Override
public void onError(WebSocket conn, Exception ex) {
String key = MiddleGlobalMemory.findBySocket(conn);
logger.error("WebSocket错误: clientId={}, remoteAddress={}, error={}", key, conn != null ? conn.getRemoteSocketAddress() : null, ex.getMessage(), ex);
// 发生错误时清理连接
if (key != null) {
releaseResource(key);
}
}
@Override
public void onStart() {
logger.info("WebSocket服务端启动成功,监听端口: {}", getPort());
}
/**
* 给指定WebSocket连接发送消息
*/
public static void sendMessageToClient(WebSocket conn, String message) {
if (conn != null && conn.isOpen()) {
conn.send(message);
String clientId = MiddleGlobalMemory.findBySocket(conn);
logger.debug("发送消息给客户端: clientId={}, message={}", clientId, message);
}
}
/**
* 广播消息给所有连接的客户端
*/
public void broadcastMessage(String message) {
Collection<WebSocket> connections = getConnections();
for (WebSocket conn : connections) {
if (conn.isOpen()) {
conn.send(message);
}
}
logger.debug("广播消息给所有客户端: message={}, 客户端数量: {}", message, connections.size());
}
// 核心:定时检测空闲连接(每隔10秒执行一次)
private void startIdleCheckTask() {
idleCheckExecutor.scheduleAtFixedRate(() -> {
long now = System.currentTimeMillis();
// 遍历所有连接,检测是否超时
for (Map.Entry<WebSocket, Long> entry : clientActiveTimeMap.entrySet()) {
WebSocket conn = entry.getKey();
Long lastActiveTime = entry.getValue();
// 超时判定:当前时间 - 最后活跃时间 > 空闲超时时间
if (now - lastActiveTime > IDLE_TIMEOUT) {
System.out.println("客户端空闲超时:" + conn.getRemoteSocketAddress() + ",强制关闭连接");
// 强制关闭连接(会触发 onClose,但 remote=true 表示服务端主动关闭)
conn.close(1001, "idle timeout");
}
}
}, 10, 10, TimeUnit.SECONDS);
}
// 核心新增:停止WebSocket服务的方法(释放端口+线程池+Netty资源)
public void stopServer() throws InterruptedException {
logger.info("开始关闭WebSocket服务,监听端口: {}", getPort());
// 1. 关闭所有客户端连接
Collection<WebSocket> connections = getConnections();
for (WebSocket conn : connections) {
if (conn.isOpen()) {
try {
conn.close(1000, "server shutdown"); // 正常关闭客户端
String key = MiddleGlobalMemory.findBySocket(conn);
if (key != null) {
releaseResource(key); // 清理对应Netty资源
}
} catch (Exception e) {
logger.warn("关闭客户端连接失败: {}", conn.getRemoteSocketAddress(), e);
}
}
}
// clientActiveTimeMap.clear(); // 清空空闲记录
//
// // 2. 关闭定时检测线程池(核心:终止非守护线程)
// if (!idleCheckExecutor.isShutdown()) {
// idleCheckExecutor.shutdownNow(); // 立即关闭线程池
// try {
// // 等待线程池终止,最多等3秒
// if (!idleCheckExecutor.awaitTermination(3, TimeUnit.SECONDS)) {
// logger.warn("空闲检测线程池未正常终止");
// }
// } catch (InterruptedException e) {
// idleCheckExecutor.shutdownNow();
// Thread.currentThread().interrupt();
// }
// }
// 3. 批量关闭所有Netty客户端连接
for (NettyClient client : MiddleGlobalMemory.nettyClientMap.values()) {
if (client != null && client.isConnected()) {
try {
client.disconnect();
} catch (Exception e) {
logger.warn("关闭Netty客户端失败", e);
}
}
}
MiddleGlobalMemory.nettyClientMap.clear();
MiddleGlobalMemory.onlineSockets.clear();
// 4. 关闭WebSocketServer本身,释放9002端口(java-websocket核心方法)
try {
this.stop(3000); // 3秒超时关闭服务端
logger.info("WebSocket服务端关闭成功,端口: {}", getPort());
} catch (InterruptedException e) {
logger.error("关闭WebSocket服务端失败", e);
this.stop(); // 强制关闭
}
}
}