diff --git a/maibu-netty-server/pom.xml b/maibu-netty-server/pom.xml
index 88868c0..b72657a 100644
--- a/maibu-netty-server/pom.xml
+++ b/maibu-netty-server/pom.xml
@@ -11,6 +11,41 @@
maibu-netty-server
+
+
+ io.netty
+ netty-all
+ 4.1.56.Final
+ compile
+
+
+ com.fastbee
+ fastbee-iot-service
+
+
+ com.fastbee
+ fastbee-common
+ 3.8.5
+
+
+
+ org.apache.logging.log4j
+ log4j-core
+ 2.20.0
+
+
+ org.apache.logging.log4j
+ log4j-api
+ 2.20.0
+
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.5.3
+
+
+
8
8
diff --git a/maibu-netty-server/src/main/java/com/maibu/Main.java b/maibu-netty-server/src/main/java/com/maibu/Main.java
deleted file mode 100644
index 1f18d67..0000000
--- a/maibu-netty-server/src/main/java/com/maibu/Main.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.maibu;
-
-//TIP To Run code, press or
-// click the icon in the gutter.
-public class Main {
- public static void main(String[] args) {
- //TIP Press with your caret at the highlighted text
- // to see how IntelliJ IDEA suggests fixing it.
- System.out.printf("Hello and welcome!");
-
- for (int i = 1; i <= 5; i++) {
- //TIP Press to start debugging your code. We have set one breakpoint
- // for you, but you can always add more by pressing .
- System.out.println("i = " + i);
- }
- }
-}
\ No newline at end of file
diff --git a/maibu-netty-server/src/main/java/com/maibu/common/CommandConstant.java b/maibu-netty-server/src/main/java/com/maibu/common/CommandConstant.java
new file mode 100644
index 0000000..620f77a
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/common/CommandConstant.java
@@ -0,0 +1,16 @@
+package com.fastbee.common;
+
+public class CommandConstant {
+
+ 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; //主要用于推送状态消息等
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/common/Constant.java b/maibu-netty-server/src/main/java/com/maibu/common/Constant.java
new file mode 100644
index 0000000..23c9724
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/common/Constant.java
@@ -0,0 +1,11 @@
+package com.fastbee.common;
+
+import io.netty.util.AttributeKey;
+import lombok.Data;
+
+@Data
+public class Constant {
+
+ public static final AttributeKey ATT_DEVICE_ID = AttributeKey.valueOf("deviceId");
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/common/NettyCacheKey.java b/maibu-netty-server/src/main/java/com/maibu/common/NettyCacheKey.java
new file mode 100644
index 0000000..5d17849
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/common/NettyCacheKey.java
@@ -0,0 +1,9 @@
+package com.fastbee.common;
+
+import lombok.Data;
+
+public class NettyCacheKey {
+
+ public static final String deviceRunningStatusKey = "running_status:";
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/controller/DeviceExternalApiController.java b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceExternalApiController.java
new file mode 100644
index 0000000..f579907
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceExternalApiController.java
@@ -0,0 +1,61 @@
+package com.fastbee.controller;
+
+import com.fastbee.common.CommandConstant;
+import com.fastbee.common.core.controller.BaseController;
+import com.fastbee.common.core.domain.AjaxResult;
+import com.fastbee.common.core.domain.model.LoginUser;
+import com.fastbee.common.utils.SecurityUtils;
+import com.fastbee.common.utils.json.JsonUtils;
+import com.fastbee.dto.DeviceTaskQueryDTO;
+import com.fastbee.dto.NettyDevice;
+import com.fastbee.dto.ObstacleData;
+import com.fastbee.iot.domain.DevicePlan;
+import com.fastbee.manager.DeviceSessionManager;
+import com.fastbee.service.DeviceTaskService;
+import com.fastbee.utils.CommandUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.util.CharsetUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.java_websocket.WebSocket;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/iot/api")
+public class DeviceExternalApiController extends BaseController {
+
+ @Autowired
+ private DeviceSessionManager deviceSessionManager;
+
+ @PostMapping("/obstacleData")
+ public AjaxResult obstacleData(@RequestBody ObstacleData obstacleData) {
+ Map data = obstacleData.getData();
+ String deviceId = (String) data.get("deviceId");
+ if (StringUtils.isEmpty(deviceId)) {
+ return AjaxResult.error("deviceId为空!");
+ }
+// Map obstacle = (Map) data.get("obstacle");
+// Boolean isSecure = (Boolean) data.get("isSecure");
+
+ List devices = deviceSessionManager.getSlaveControl(deviceId);
+ if (!CollectionUtils.isEmpty(devices)) {
+ devices.forEach(x -> {
+ ByteBuf buf = Unpooled.buffer();
+ CommandUtils.buildCommand(buf, obstacleData, CommandConstant.interaction);
+ String content = buf.toString(CharsetUtil.UTF_8);
+ logger.info("推送设备:{}障碍物消息:{}到:{}", deviceId, content, x.getConnectorId());
+ if (x.getChannel() != null && x.getChannel().isActive()) {
+ x.getChannel().writeAndFlush(buf);
+ }
+ });
+ }
+ return AjaxResult.success();
+ }
+
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/controller/DeviceMemoryController.java b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceMemoryController.java
new file mode 100644
index 0000000..6ab57d5
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceMemoryController.java
@@ -0,0 +1,43 @@
+package com.fastbee.controller;
+
+import com.fastbee.manager.DeviceSessionManager;
+import com.fastbee.memory.GlobalMemory;
+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.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+@RestController
+@RequestMapping("/memory/device")
+public class DeviceMemoryController {
+
+
+ @Autowired
+ private DeviceSessionManager deviceSessionManager;
+
+ @Autowired
+ private GlobalMemory globalMemory;
+
+ @GetMapping("getDeviceMemory")
+ public Map getDeviceMemory() {
+ Map res = new HashMap<>();
+ res.put("deviceMap", deviceSessionManager.getDeviceMap());
+ res.put("channelToDeviceIdMap", deviceSessionManager.getChannelToDeviceIdMap());
+ res.put("channelMap", deviceSessionManager.getChannelMap());
+ return res;
+ }
+//
+// @GetMapping("getGlobalMemory")
+// public Map getGlobalMemory() {
+// Map res = new HashMap<>();
+// res.put("sysLoginMap", globalMemory.getSysLoginMap());
+// res.put("sysLoginDataMap", globalMemory.getSysLoginDataMap());
+// return res;
+// }
+
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/controller/DeviceTaskController.java b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceTaskController.java
new file mode 100644
index 0000000..0a678e2
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/controller/DeviceTaskController.java
@@ -0,0 +1,76 @@
+package com.fastbee.controller;
+
+import com.fastbee.common.core.controller.BaseController;
+import com.fastbee.common.core.domain.AjaxResult;
+import com.fastbee.common.core.domain.model.LoginUser;
+import com.fastbee.common.utils.SecurityUtils;
+import com.fastbee.dto.DeviceTaskQueryDTO;
+import com.fastbee.iot.domain.DevicePlan;
+import com.fastbee.service.DeviceTaskService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/iot/deviceTask")
+public class DeviceTaskController extends BaseController {
+
+ @Autowired
+ private DeviceTaskService deviceTaskService;
+
+ @PostMapping("/save")
+ public AjaxResult insertOrUpdate(@RequestBody DevicePlan devicePlan) {
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ return AjaxResult.success(deviceTaskService.insertOrUpdate(devicePlan, loginUser));
+ }
+
+ @GetMapping("/getAllDeviceTaskByUser")
+ public AjaxResult getAllDeviceTaskByUser(@RequestParam Long userId) {
+ return AjaxResult.success(deviceTaskService.getDeviceTaskByUser(userId));
+ }
+
+ @PostMapping("/getDevicePlan")
+ public AjaxResult getDevicePlan(@RequestBody DeviceTaskQueryDTO dto) {
+ return AjaxResult.success(deviceTaskService.getDeviceTask(dto));
+ }
+
+ @GetMapping("/delete")
+ public AjaxResult delete(@RequestParam Long id) {
+ return AjaxResult.success(deviceTaskService.delete(id));
+ }
+
+ @GetMapping("/start")
+ public AjaxResult start(@RequestParam Long id) {
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ deviceTaskService.start(id, loginUser);
+ return AjaxResult.success();
+ }
+
+ @GetMapping("/devicePlanStatistics")
+ public AjaxResult devicePlanStatistics(@RequestParam Long userId) {
+ return AjaxResult.success(deviceTaskService.getDevicePlanStatistics(userId));
+ }
+
+ @GetMapping("/devicePlanTaskStatistics")
+ public AjaxResult devicePlanTaskStatistics(@RequestParam Long userId) {
+
+ return AjaxResult.success(deviceTaskService.getDevicePlanTaskStatistics(userId));
+ }
+
+ @GetMapping("/concurrentDeviceRoute")
+ public AjaxResult concurrentDeviceRoute(@RequestParam String deviceId) {
+ return AjaxResult.success(deviceTaskService.concurrentDevicePlanTask(deviceId));
+ }
+
+
+ @GetMapping("/workStatistics")
+ public AjaxResult workStatistics(@RequestParam Long userId) {
+ return AjaxResult.success(deviceTaskService.workStatistics(userId));
+ }
+
+ @PostMapping("/deviceTaskPool")
+ public AjaxResult deviceTaskPool(@RequestBody DeviceTaskQueryDTO dto) {
+ return AjaxResult.success(deviceTaskService.deviceTaskPool(dto));
+ }
+
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/controller/NettyDeviceController.java b/maibu-netty-server/src/main/java/com/maibu/controller/NettyDeviceController.java
new file mode 100644
index 0000000..0fffe94
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/controller/NettyDeviceController.java
@@ -0,0 +1,44 @@
+package com.fastbee.controller;
+
+import com.fastbee.common.core.controller.BaseController;
+import com.fastbee.common.core.domain.AjaxResult;
+import com.fastbee.common.core.page.TableDataInfo;
+import com.fastbee.iot.domain.DeviceRunningStatusHistory;
+import com.fastbee.entity.ErrorIdentificationStandard;
+import com.fastbee.service.NettyDeviceService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/netty/device")
+public class NettyDeviceController extends BaseController {
+
+ @Autowired
+ private NettyDeviceService nettyDeviceService;
+
+ @GetMapping("/statusHistory")
+ public TableDataInfo statusHistory(DeviceRunningStatusHistory history)
+ {
+ startPage();
+ List list = nettyDeviceService.statusHistory(history);
+ return getDataTable(list);
+ }
+
+ @GetMapping("/getErrorIdentification")
+ public TableDataInfo getErrorIdentification()
+ {
+ startPage();
+ List list = nettyDeviceService.allErrorIdentificationStandard();
+ return getDataTable(list);
+ }
+
+ @PostMapping("/updateErrorIdentification")
+ public AjaxResult updateErrorIdentification(@RequestBody ErrorIdentificationStandard standard)
+ {
+ int i = nettyDeviceService.updateErrorIdentification(standard);
+ return AjaxResult.success(i);
+ }
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/controller/TransferDeviceController.java b/maibu-netty-server/src/main/java/com/maibu/controller/TransferDeviceController.java
new file mode 100644
index 0000000..2c3c9e9
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/controller/TransferDeviceController.java
@@ -0,0 +1,130 @@
+package com.fastbee.controller;
+
+import com.fastbee.common.annotation.Log;
+import com.fastbee.common.core.controller.BaseController;
+import com.fastbee.common.core.domain.AjaxResult;
+import com.fastbee.common.core.domain.model.LoginUser;
+import com.fastbee.common.enums.BusinessType;
+import com.fastbee.common.utils.MessageUtils;
+import com.fastbee.dto.DeviceBindDTO;
+import com.fastbee.service.TransferDeviceService;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/forward/device")
+public class TransferDeviceController extends BaseController {
+
+ @Autowired
+ private TransferDeviceService transferDeviceService;
+
+ @PostMapping("bind")
+ @ApiOperation("绑定设备")
+ @Log(title = "绑定设备", businessType = BusinessType.OTHER)
+ public AjaxResult bind(@RequestBody DeviceBindDTO bindDTO) {
+
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ LoginUser user = getLoginUser();
+ return transferDeviceService.bind(bindDTO, user);
+ }
+
+ @PostMapping("unbind")
+ @ApiOperation("解绑设备")
+ @Log(title = "解绑设备", businessType = BusinessType.OTHER)
+ public AjaxResult unbind(@RequestBody DeviceBindDTO bindDTO) {
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ LoginUser user = getLoginUser();
+ return transferDeviceService.unbind(bindDTO, user);
+ }
+
+ @PostMapping("updateDeviceAlias")
+ @ApiOperation("更新设备别名")
+ @Log(title = "更新设备别名", businessType = BusinessType.OTHER)
+ public AjaxResult updateDeviceAlias(@RequestBody DeviceBindDTO bindDTO) {
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ if (StringUtils.isEmpty(bindDTO.getDeviceAlias())) {
+ return AjaxResult.error("设备别名为空!");
+ }
+ LoginUser user = getLoginUser();
+ return transferDeviceService.updateDeviceAlias(bindDTO, user);
+ }
+
+ @PostMapping("switchDevice")
+ @ApiOperation("切换设备")
+ @Log(title = "切换设备", businessType = BusinessType.OTHER)
+ public AjaxResult switchDevice(@RequestBody DeviceBindDTO bindDTO) {
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ if (StringUtils.isEmpty(bindDTO.getPlatform())) {
+ return AjaxResult.error("平台为空!");
+ }
+ LoginUser user = getLoginUser();
+ if (user == null) {
+ return AjaxResult.error("当前token用户不存在!");
+ }
+ return AjaxResult.success(transferDeviceService.switchDevice(bindDTO, user));
+ }
+
+ /**
+ * 获取当前的控制权在哪 能否控制
+ */
+ @PostMapping("remoteControl")
+ @ApiOperation("获取当前设备控制权")
+ @Log(title = "获取当前设备控制权", businessType = BusinessType.OTHER)
+ public AjaxResult remoteControl(@RequestBody DeviceBindDTO bindDTO) {
+ try {
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ if (StringUtils.isEmpty(bindDTO.getPlatform())) {
+ return AjaxResult.error("平台为空!");
+ }
+ LoginUser user = getLoginUser();
+ return AjaxResult.success(transferDeviceService.remoteControl(bindDTO, user));
+ } catch (Exception e) {
+ logger.error("获取设备控制权出错", e);
+ return AjaxResult.error();
+ }
+ }
+
+ /**
+ * 切换控制请求
+ */
+ @PostMapping("switchControl")
+ @ApiOperation("切换控制请求")
+ @Log(title = "切换控制请求", businessType = BusinessType.OTHER)
+ public AjaxResult switchControl(@RequestBody DeviceBindDTO bindDTO) {
+ if (StringUtils.isEmpty(bindDTO.getDeviceId())) {
+ return AjaxResult.error("deviceId为空!");
+ }
+ if (StringUtils.isEmpty(bindDTO.getPlatform())) {
+ return AjaxResult.error("平台为空!");
+ }
+ LoginUser user = getLoginUser();
+ return AjaxResult.success(transferDeviceService.switchControl(bindDTO, user));
+ }
+
+ /**
+ * 释放控制权
+ */
+ @PostMapping("releaseControl")
+ @ApiOperation("释放控制权")
+ @Log(title = "释放控制权", businessType = BusinessType.OTHER)
+ public AjaxResult releaseControl(@RequestBody DeviceBindDTO bindDTO) {
+ if (StringUtils.isEmpty(bindDTO.getPlatform())) {
+ return AjaxResult.error("平台为空!");
+ }
+ LoginUser user = getLoginUser();
+ transferDeviceService.releaseControl(bindDTO, user);
+ return AjaxResult.success();
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/Connector.java b/maibu-netty-server/src/main/java/com/maibu/dto/Connector.java
new file mode 100644
index 0000000..ff7136b
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/Connector.java
@@ -0,0 +1,69 @@
+package com.fastbee.dto;
+
+import com.fastbee.enums.ConnectorStatus;
+import io.netty.channel.Channel;
+import lombok.Data;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+@Data
+public class Connector {
+ // 获取设备ID
+ private final String connectorId;
+ // 设备ID
+ private ConnectorStatus connectorStatus; // 设备状态(可用、禁言、不可用)
+ // 获取连接的设备列表
+ private Set connectedConnectors; // 连接的设备列表 主机绑定从机 从机无绑定
+
+ private Channel channel;
+
+ // 无参构造函数,解决Lombok需要默认构造函数的问题
+ public Connector() {
+ this.connectorId = "";
+ this.connectorStatus = ConnectorStatus.ACTIVE;
+ this.connectedConnectors = new HashSet<>();
+ }
+
+ // 构造器
+ public Connector(String connectorId) {
+ this.connectorId = connectorId;
+ this.connectorStatus = ConnectorStatus.ACTIVE; // 默认状态为完全可用
+ this.connectedConnectors = new HashSet<>();
+ }
+
+ //
+ public void addConnectedDevice(Connector connector) {
+ connectedConnectors.add(connector);
+ }
+
+ // 移除连接的设备,确保双向移除
+ public void removeConnectedDevice(Connector connector) {
+ // 从当前设备的连接列表中移除
+ connectedConnectors.remove(connector);
+
+ }
+
+ public boolean isOnline() {
+ return channel != null && channel.isActive();
+ }
+
+ public boolean canSend() {
+ return connectorStatus == ConnectorStatus.ACTIVE;
+ }
+
+ public boolean canReceive() {
+ return connectorStatus != ConnectorStatus.DISABLED;
+ }
+
+ // 打印设备信息
+ public void printConnectorInfo() {
+ System.out.println("Device ID: " + connectorId);
+ System.out.println("Device Status: " + connectorStatus);
+ System.out.println("Connected Devices: ");
+ for (Connector connectedDevice : connectedConnectors) {
+ System.out.println("- " + connectedDevice.getConnectorId());
+ }
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceBindDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceBindDTO.java
new file mode 100644
index 0000000..9520930
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceBindDTO.java
@@ -0,0 +1,25 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceBindDTO {
+ /**
+ * 主机名
+ */
+ private String platform;
+ /**
+ * 设备id
+ */
+ private String deviceId;
+ /**
+ * 设备别名
+ */
+ private String deviceAlias;
+
+ /**
+ * 用户名
+ */
+ private String userId;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceControlDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceControlDTO.java
new file mode 100644
index 0000000..4cccae2
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceControlDTO.java
@@ -0,0 +1,15 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceControlDTO {
+ private boolean remoteControl;
+
+ private String deviceId;
+
+ private String reason;
+
+ private String owner;// app/pc 具体被占用方
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDTO.java
new file mode 100644
index 0000000..4054775
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDTO.java
@@ -0,0 +1,18 @@
+package com.fastbee.dto;
+
+import com.fastbee.enums.RespondCode;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class DeviceErrorPushDTO {
+
+ private List details;
+
+ private long time;
+
+ private String deviceId;
+
+ private RespondCode event;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDetail.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDetail.java
new file mode 100644
index 0000000..3d4b835
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceErrorPushDetail.java
@@ -0,0 +1,15 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceErrorPushDetail {
+
+ private String errorName;
+
+ private String description;
+
+ private String range;
+
+ private String value;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceLoginResponseDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceLoginResponseDTO.java
new file mode 100644
index 0000000..43ac747
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceLoginResponseDTO.java
@@ -0,0 +1,9 @@
+package com.fastbee.dto;
+
+import com.fastbee.enums.RespondCode;
+import lombok.Data;
+
+@Data
+public class DeviceLoginResponseDTO {
+ private RespondCode respond;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanStatisticsDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanStatisticsDTO.java
new file mode 100644
index 0000000..2b56432
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanStatisticsDTO.java
@@ -0,0 +1,12 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DevicePlanStatisticsDTO {
+
+ private String name;
+ private Integer planned;
+ private int completed;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanTaskStatisticsDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanTaskStatisticsDTO.java
new file mode 100644
index 0000000..96217be
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DevicePlanTaskStatisticsDTO.java
@@ -0,0 +1,13 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DevicePlanTaskStatisticsDTO {
+
+ private String date;
+ private Integer success;
+ private Integer paused;
+ private Integer failed;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRequestDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRequestDTO.java
new file mode 100644
index 0000000..c8545e1
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRequestDTO.java
@@ -0,0 +1,14 @@
+package com.fastbee.dto;
+
+import com.fastbee.enums.CommandRequestType;
+import lombok.Data;
+
+@Data
+public class DeviceRequestDTO {
+ private CommandRequestType request;
+ private String deviceId;
+ private String userId;
+ private String platform;
+ private String token;
+ private DeviceRespondDTO respond;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRespondDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRespondDTO.java
new file mode 100644
index 0000000..c9263f5
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRespondDTO.java
@@ -0,0 +1,11 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceRespondDTO {
+ private Boolean switchResult;
+ private String deviceId;
+ private String holder;
+ private String reason;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRunningStatus.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRunningStatus.java
new file mode 100644
index 0000000..1c126ec
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceRunningStatus.java
@@ -0,0 +1,46 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceRunningStatus {
+
+ private static final long serialVersionUID = 1L;
+
+ private Long id;
+ /** 产品分类ID */
+ 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 Q0;
+
+ private String Q1;
+
+ private String Q2;
+
+ private String Q3;
+
+ private String latitude;
+
+ private String longitude;
+}
+
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceStatusChangeDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceStatusChangeDTO.java
new file mode 100644
index 0000000..20f9ff1
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceStatusChangeDTO.java
@@ -0,0 +1,15 @@
+package com.fastbee.dto;
+
+import com.fastbee.enums.RespondCode;
+import lombok.Data;
+
+@Data
+public class DeviceStatusChangeDTO {
+
+ private RespondCode event;
+
+ private String deviceId;
+
+ private String status;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceSwitchControlDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceSwitchControlDTO.java
new file mode 100644
index 0000000..c95a7d2
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceSwitchControlDTO.java
@@ -0,0 +1,19 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+public class DeviceSwitchControlDTO {
+ private boolean switchControl;
+
+ private String reason;
+
+ private String holder;
+
+
+ public DeviceSwitchControlDTO(boolean switchControl) {
+ this.switchControl = switchControl;
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceTaskQueryDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceTaskQueryDTO.java
new file mode 100644
index 0000000..351e9a2
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceTaskQueryDTO.java
@@ -0,0 +1,19 @@
+package com.fastbee.dto;
+
+import com.fastbee.iot.enums.DeviceTaskStaus;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+public class DeviceTaskQueryDTO {
+
+ private Long userId;
+ private LocalDateTime startTime;
+ private LocalDateTime endTime;
+ private DeviceTaskStaus taskStaus;
+ private String planName;
+ private Long planId;
+ private String deviceId;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/DeviceWorkStatisticsDTO.java b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceWorkStatisticsDTO.java
new file mode 100644
index 0000000..b33844e
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/DeviceWorkStatisticsDTO.java
@@ -0,0 +1,12 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceWorkStatisticsDTO {
+
+ private String label;
+ private Integer value;
+ private Integer unit;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/NettyDevice.java b/maibu-netty-server/src/main/java/com/maibu/dto/NettyDevice.java
new file mode 100644
index 0000000..6346def
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/NettyDevice.java
@@ -0,0 +1,42 @@
+package com.fastbee.dto;
+
+
+import com.fastbee.entity.path.LatAndLngEntity;
+import com.fastbee.enums.ConnectorType;
+import com.fastbee.iot.domain.DeviceRunStatistics;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class NettyDevice extends Connector {
+
+ private ConnectorType deviceType; // 设备角色类型:MASTER、SLAVE、GENERAL
+
+ private String currentSlaveId;// 当前对应的下位机 master
+
+ private Long currentTaskId; //当前执行的任务;
+
+ private Integer onlineStatus; //在线状态 0 离线 1在线
+
+ private Queue locationQueue = new LinkedList<>();
+
+ private DeviceRunStatistics latestStatus;
+
+ private Long latestLoginTime;
+
+ public NettyDevice(String connectorId) {
+ super(connectorId);
+ this.deviceType = ConnectorType.MASTER; // 默认上位机角色
+ }
+
+ public NettyDevice(String connectorId, ConnectorType deviceType) {
+ super(connectorId);
+ this.deviceType = deviceType;
+ }
+
+}
+
diff --git a/maibu-netty-server/src/main/java/com/maibu/dto/ObstacleData.java b/maibu-netty-server/src/main/java/com/maibu/dto/ObstacleData.java
new file mode 100644
index 0000000..ce6dded
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/dto/ObstacleData.java
@@ -0,0 +1,12 @@
+package com.fastbee.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class ObstacleData {
+
+ private String type;
+ private Map data;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/ErrorIdentificationStandard.java b/maibu-netty-server/src/main/java/com/maibu/entity/ErrorIdentificationStandard.java
new file mode 100644
index 0000000..7fc331d
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/ErrorIdentificationStandard.java
@@ -0,0 +1,23 @@
+package com.fastbee.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fastbee.enums.CompareEnum;
+import lombok.Data;
+import org.springframework.data.annotation.Id;
+
+import java.util.List;
+
+
+@Data
+@TableName("error_identification")
+public class ErrorIdentificationStandard {
+ @TableId(type = IdType.AUTO)
+ private Long id;
+ private String field;
+ private String compareValues;
+ private CompareEnum compareType;
+ private String errorDescription;
+ private String errorName;
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/path/LatAndLngEntity.java b/maibu-netty-server/src/main/java/com/maibu/entity/path/LatAndLngEntity.java
new file mode 100644
index 0000000..3f1db85
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/path/LatAndLngEntity.java
@@ -0,0 +1,14 @@
+package com.fastbee.entity.path;
+
+import lombok.Data;
+
+@Data
+public class LatAndLngEntity {
+ private double lat;
+ private double lng;
+
+ @Override
+ public String toString() {
+ return "{" + lat + "," + lng + '}';
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/path/MowingPathGenerator.java b/maibu-netty-server/src/main/java/com/maibu/entity/path/MowingPathGenerator.java
new file mode 100644
index 0000000..079c791
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/path/MowingPathGenerator.java
@@ -0,0 +1,553 @@
+package com.fastbee.entity.path;
+
+
+import com.fastbee.common.utils.json.JsonUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class MowingPathGenerator {
+ public static final double M_PI = 3.14159265358979323846;
+ public static final double Earth_Radius = 6378137.0;
+ public static final double EPSILON = 1e-6;
+ public static final double REL_EPSILON = 1e-8;
+ public static final double ABS_EPSILON = 1e-12;
+
+ public static class Point {
+ public double x, y;
+
+ public Point(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ public static class PathPoint {
+ public double lat, lon;
+
+ public PathPoint(double lat, double lon) {
+ this.lat = lat;
+ this.lon = lon;
+ }
+ }
+
+ public static class Polygon {
+ public Point[] vertices;
+ public int num_vertices;
+
+ public Polygon(Point[] vertices, int num_vertices) {
+ this.vertices = vertices;
+ this.num_vertices = num_vertices;
+ }
+ }
+
+ public static class PolygonWithHoles {
+ public Polygon outer;
+ public Polygon[] holes;
+ public int num_holes;
+
+ public PolygonWithHoles(Polygon outer, Polygon[] holes, int num_holes) {
+ this.outer = outer;
+ this.holes = holes;
+ this.num_holes = num_holes;
+ }
+ }
+
+ public static class ProjectedPoint {
+ public Point pt;
+ public double proj;
+
+ public ProjectedPoint(Point pt, double proj) {
+ this.pt = pt;
+ this.proj = proj;
+ }
+ }
+
+ public static boolean isEqual(double a, double b) {
+ double diff = Math.abs(a - b);
+ double max_ab = Math.max(Math.abs(a), Math.abs(b));
+ double tolerance = REL_EPSILON * max_ab + ABS_EPSILON;
+ return diff <= tolerance;
+ }
+
+ public static double cross(Point a, Point b) {
+ return a.x * b.y - a.y * b.x;
+ }
+
+ public static boolean getLineIntersection(Point p1, Point p2, Point q1, Point q2, Point intersection) {
+ Point r = new Point(p2.x - p1.x, p2.y - p1.y);
+ Point s = new Point(q2.x - q1.x, q2.y - q1.y);
+ double rxs = cross(r, s);
+ Point qp = new Point(q1.x - p1.x, q1.y - p1.y);
+ double q_p_cross_r = cross(qp, r);
+
+ if (Math.abs(rxs) < EPSILON) {
+ return false;
+ }
+
+ double t = cross(qp, s) / rxs;
+ double u = q_p_cross_r / rxs;
+
+ if (t >= 0.0 - EPSILON && t <= 1.0 + EPSILON && u >= 0.0 - EPSILON && u <= 1.0 + EPSILON) {
+ intersection.x = p1.x + t * r.x;
+ intersection.y = p1.y + t * r.y;
+ return true;
+ }
+
+ return false;
+ }
+
+ public static boolean isPointOnSegment(Point pt, Point p1, Point p2) {
+ if (pt.x < Math.min(p1.x, p2.x) - EPSILON || pt.x > Math.max(p1.x, p2.x) + EPSILON) {
+ return false;
+ }
+ if (pt.y < Math.min(p1.y, p2.y) - EPSILON || pt.y > Math.max(p1.y, p2.y) + EPSILON) {
+ return false;
+ }
+
+ double cross = (p2.x - p1.x) * (pt.y - p1.y) - (p2.y - p1.y) * (pt.x - p1.x);
+ if (Math.abs(cross) > EPSILON) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public static void makeScanline(PolygonWithHoles field, Point lineStart, Point lineEnd, Point normal, double min_proj, Point dir) {
+ double min_x = Double.POSITIVE_INFINITY, max_x = Double.NEGATIVE_INFINITY;
+ double min_y = Double.POSITIVE_INFINITY, max_y = Double.NEGATIVE_INFINITY;
+
+ for (int i = 0; i < field.outer.num_vertices; i++) {
+ Point p = field.outer.vertices[i];
+ if (p.x < min_x) min_x = p.x;
+ if (p.x > max_x) max_x = p.x;
+ if (p.y < min_y) min_y = p.y;
+ if (p.y > max_y) max_y = p.y;
+ }
+
+ double dx = max_x - min_x, dy = max_y - min_y;
+ double len_scan = Math.sqrt(dx * dx + dy * dy) * 1.1;
+
+ Point lineOrigin = new Point(normal.x * min_proj, normal.y * min_proj);
+
+ lineStart.x = lineOrigin.x - dir.x * len_scan;
+ lineStart.y = lineOrigin.y - dir.y * len_scan;
+
+ lineEnd.x = lineOrigin.x + dir.x * len_scan;
+ lineEnd.y = lineOrigin.y + dir.y * len_scan;
+ }
+
+ public static void collectHoleIntersections(PolygonWithHoles poly, Point q1, Point q2, List intersections, Point dir) {
+ intersections.clear();
+
+ for (int h = 0; h < poly.num_holes; h++) {
+ for (int i = 0; i < poly.holes[h].num_vertices; i++) {
+ Point p1 = poly.holes[h].vertices[i];
+ Point p2 = poly.holes[h].vertices[(i + 1) % poly.holes[h].num_vertices];
+ Point inter = new Point(0, 0);
+
+ if (getLineIntersection(p1, p2, q1, q2, inter)) {
+ double proj = inter.x * dir.x + inter.y * dir.y;
+ intersections.add(new ProjectedPoint(new Point(inter.x, inter.y), proj));
+ }
+ }
+ }
+ }
+
+ public static void collectOuterIntersections(PolygonWithHoles poly, Point q1, Point q2, List intersections, Point dir) {
+ intersections.clear();
+
+ for (int i = 0; i < poly.outer.num_vertices; i++) {
+ Point p1 = poly.outer.vertices[i];
+ Point p2 = poly.outer.vertices[(i + 1) % poly.outer.num_vertices];
+ Point inter = new Point(0, 0);
+
+ if (getLineIntersection(p1, p2, q1, q2, inter)) {
+ double proj = inter.x * dir.x + inter.y * dir.y;
+ intersections.add(new ProjectedPoint(new Point(inter.x, inter.y), proj));
+ }
+ }
+ }
+
+ public static void generateMowingPath(PolygonWithHoles field, double width, Point scanStart, Point scanEnd, List pathPoints) {
+ pathPoints.clear();
+
+ Point dir = new Point(scanEnd.x - scanStart.x, scanEnd.y - scanStart.y);
+ double len = Math.sqrt(dir.x * dir.x + dir.y * dir.y);
+ dir.x /= len;
+ dir.y /= len;
+ Point normal = new Point(-dir.y, dir.x);
+
+ double min_proj = Double.POSITIVE_INFINITY, max_proj = Double.NEGATIVE_INFINITY;
+ for (int i = 0; i < field.outer.num_vertices; i++) {
+ Point p = field.outer.vertices[i];
+ double proj = p.x * normal.x + p.y * normal.y;
+ if (proj < min_proj) min_proj = proj;
+ if (proj > max_proj) max_proj = proj;
+ }
+
+ int lines = (int) ((max_proj - min_proj) / width + 1);
+ int max_outer_points = lines * field.outer.num_vertices * 2;
+ int max_hole_points = lines * field.num_holes * 10 * 2;
+
+ System.out.printf("dir = (%.4f, %.4f)\n", dir.x, dir.y);
+ System.out.printf("normal = (%.4f, %.4f)\n", normal.x, normal.y);
+ System.out.printf("Projection range: %.4f ~ %.4f (%.4f)\n", min_proj, max_proj, max_proj - min_proj);
+ System.out.printf("lines = %d\n", lines);
+
+ Point lineStart = new Point(0, 0);
+ Point lineEnd = new Point(0, 0);
+ makeScanline(field, lineStart, lineEnd, normal, min_proj, dir);
+ int direction = 1;
+
+ List> outerIntersections = new ArrayList<>();
+ List> holeIntersections = new ArrayList<>();
+ List numHole = new ArrayList<>();
+ List numOuter = new ArrayList<>();
+
+ for (int l = 0; l < lines; l++) {
+ List outerInts = new ArrayList<>();
+ collectOuterIntersections(field, lineStart, lineEnd, outerInts, dir);
+
+ List holeInts = new ArrayList<>();
+ collectHoleIntersections(field, lineStart, lineEnd, holeInts, dir);
+
+ holeInts.sort(Comparator.comparingDouble(p -> p.proj));
+
+ if (direction == -1) {
+ Collections.reverse(holeInts);
+ }
+
+ outerIntersections.add(outerInts);
+ holeIntersections.add(holeInts);
+ numHole.add(holeInts.size());
+ numOuter.add(outerInts.size());
+
+ lineStart.x += normal.x * width;
+ lineStart.y += normal.y * width;
+ lineEnd.x += normal.x * width;
+ lineEnd.y += normal.y * width;
+ direction *= -1;
+ }
+
+ direction = 1;
+
+ for (int l = 0; l < lines; l++) {
+ List intersections = new ArrayList<>();
+ intersections.addAll(outerIntersections.get(l));
+ intersections.addAll(holeIntersections.get(l));
+
+ intersections.sort(Comparator.comparingDouble(p -> p.proj));
+
+ if (direction == -1) {
+ Collections.reverse(intersections);
+ }
+
+ boolean isHoleStart = false;
+ int layerHoleNum = numHole.get(l);
+
+ if (layerHoleNum > 0) {
+ ProjectedPoint holeStart = holeIntersections.get(l).get(0);
+ ProjectedPoint holeEnd = holeIntersections.get(l).get(layerHoleNum - 1);
+
+ for (int i = 0; i < intersections.size(); i++) {
+ ProjectedPoint testPoint = intersections.get(i);
+
+ if (isEqual(testPoint.proj, holeStart.proj)) {
+ isHoleStart = true;
+ }
+
+ if (isHoleStart) {
+ pathPoints.add(holeStart.pt);
+
+ int holeId = -1;
+ int startIndex = -1, endIndex = -1;
+ boolean isHoleVertices = false;
+
+ for (int h = 0; h < field.num_holes; h++) {
+ Polygon hole = field.holes[h];
+
+ for (int j = 0; j < hole.num_vertices; j++) {
+ Point p1 = hole.vertices[j];
+ Point p2 = hole.vertices[(j + 1) % hole.num_vertices];
+
+ if (isEqual(holeStart.pt.x, p1.x) && isEqual(holeStart.pt.y, p1.y)) {
+ isHoleVertices = true;
+ break;
+ }
+
+ if (isPointOnSegment(holeStart.pt, p1, p2)) {
+ holeId = h;
+ startIndex = (j + 1) % hole.num_vertices;
+ }
+
+ if (isPointOnSegment(holeEnd.pt, p1, p2)) {
+ endIndex = j;
+ }
+ }
+ }
+
+ if (!isHoleVertices) {
+ if (holeId >= 0 && startIndex >= 0 && endIndex >= 0) {
+ Polygon hole = field.holes[holeId];
+ int index = startIndex;
+ int indexHoleIntersections = 1;
+ boolean forward = true;
+
+ for (int step = 0; step < hole.num_vertices; step++) {
+ if (indexHoleIntersections < layerHoleNum - 1) {
+ Point current = hole.vertices[index];
+ Point next = hole.vertices[(index + 1) % hole.num_vertices];
+ Point intersectionPt = holeIntersections.get(l).get(indexHoleIntersections).pt;
+
+ if (forward && !isPointOnSegment(intersectionPt, current, next)) {
+ forward = false;
+ index = (index - 1 + hole.num_vertices) % hole.num_vertices;
+ endIndex = (endIndex + 1) % hole.num_vertices;
+ System.out.println("is_point_on_segment");
+ }
+
+ if (indexHoleIntersections % 2 != 0) {
+ pathPoints.add(hole.vertices[index]);
+ }
+
+ pathPoints.add(holeIntersections.get(l).get(indexHoleIntersections).pt);
+ indexHoleIntersections++;
+ i++;
+ } else {
+ pathPoints.add(hole.vertices[index]);
+ }
+
+ if (index == endIndex) break;
+
+ if (forward) {
+ index = (index + 1) % hole.num_vertices;
+ } else {
+ index = (index - 1 + hole.num_vertices) % hole.num_vertices;
+ }
+ }
+ }
+ }
+
+ pathPoints.add(holeEnd.pt);
+ i++;
+ isHoleStart = false;
+ continue;
+ }
+
+ pathPoints.add(intersections.get(i).pt);
+ }
+ } else {
+ for (ProjectedPoint pp : intersections) {
+ pathPoints.add(pp.pt);
+ }
+ }
+
+ direction *= -1;
+ }
+ }
+
+ public static void latlonToXY(double lat0, double lon0, double lat, double lon, Point result) {
+ double d_lat = Math.toRadians(lat - lat0);
+ double d_lon = Math.toRadians(lon - lon0);
+ double lat0_rad = Math.toRadians(lat0);
+
+ result.x = Earth_Radius * d_lon * Math.cos(lat0_rad);
+ result.y = Earth_Radius * d_lat;
+ }
+
+ public static void xyToLatlon(double lat0, double lon0, double x, double y, PathPoint result) {
+ double lat0_rad = Math.toRadians(lat0);
+
+ result.lat = lat0 + (y / Earth_Radius) * (180.0 / M_PI);
+ result.lon = lon0 + (x / (Earth_Radius * Math.cos(lat0_rad))) * (180.0 / M_PI);
+ }
+
+ public static void getCoord(PathPoint[] gps, Point[] points, PathPoint start, int cnt) {
+ for (int i = 0; i < cnt; i++) {
+ latlonToXY(start.lat, start.lon, gps[i].lat, gps[i].lon, points[i]);
+ }
+ }
+
+ public static void getLatlon(PathPoint[] gps, Point[] points, PathPoint start, int cnt) {
+ for (int i = 0; i < cnt; i++) {
+ xyToLatlon(start.lat, start.lon, points[i].x, points[i].y, gps[i]);
+ }
+ }
+
+
+ public static void parseJson(String json) {
+ RPPathData data = JsonUtils.parseObject(json,RPPathData.class);
+
+ PathPoint refPoint = data.refPoint;
+ PathPoint[] outer = data.Polygon.outer.toArray(new PathPoint[0]);
+ PathPoint start = data.scanLine.start;
+ PathPoint end = data.scanLine.end;
+ PathPoint[] holes = data.Polygon.holes.toArray(new PathPoint[0]);
+ // 打印验证
+ System.out.println("Ref Point: " + refPoint);
+ System.out.println("Start: " + start);
+ System.out.println("End: " + end);
+ System.out.println("Outer:");
+ for (PathPoint p : outer) {
+ System.out.println(" " + p);
+ }
+ System.out.println("Holes:");
+ for (PathPoint p : holes) {
+ System.out.println(" " + p);
+ }
+
+ }
+
+
+ public static List generateMowingPath(String json) {
+ RPPathData data = JsonUtils.parseObject(json,RPPathData.class);
+
+ PathPoint refPoint = data.refPoint;
+ PathPoint[] outer = data.Polygon.outer.toArray(new PathPoint[0]);
+ PathPoint start = data.scanLine.start;
+ PathPoint end = data.scanLine.end;
+ List allHolePoints = new ArrayList<>();
+ for (List hole : data.Polygon.holes) {
+ allHolePoints.addAll(hole);
+ }
+ PathPoint[] holes = allHolePoints.toArray(new PathPoint[0]);
+
+
+ Point[] outerPoint = new Point[outer.length];
+ Point startPoint = new Point(0, 0);
+ Point endPoint = new Point(0, 0);
+ Point[] holesPoint = new Point[holes.length];
+
+ latlonToXY(refPoint.lat, refPoint.lon, start.lat, start.lon, startPoint);
+ latlonToXY(refPoint.lat, refPoint.lon, end.lat, end.lon, endPoint);
+
+ for (int i = 0; i < outer.length; i++) {
+ outerPoint[i] = new Point(0, 0);
+ latlonToXY(refPoint.lat, refPoint.lon, outer[i].lat, outer[i].lon, outerPoint[i]);
+ System.out.printf("(%.14f, %.14f)\n", outerPoint[i].x, outerPoint[i].y);
+// Log.d("MowingPathGenerator", "Outer Point: " + "(" + outerPoint[i].x + ", " + outerPoint[i].y + " )");
+ }
+
+ if (holes.length > 0) {
+ for (int i = 0; i < holes.length; i++) {
+ holesPoint[i] = new Point(0, 0);
+ latlonToXY(refPoint.lat, refPoint.lon, holes[i].lat, holes[i].lon, holesPoint[i]);
+// Log.d("MowingPathGenerator", "holes Point: " + "(" + holesPoint[i].x + ", " + holesPoint[i].y + " )");
+ System.out.printf("(%.14f, %.14f)\n", holesPoint[i].x, holesPoint[i].y);
+ }
+ }
+
+
+ Polygon outerPolygon = new Polygon(outerPoint, outer.length);
+
+// Polygon holesPolygon[] = {new Polygon(holesPoint, holesPoint.length)};
+// PolygonWithHoles field = new PolygonWithHoles(outerPolygon, holesPolygon,holesPoint.length);
+
+ Polygon[] holesPolygons = new Polygon[data.Polygon.holes.size()];
+ for (int i = 0; i < data.Polygon.holes.size(); i++) {
+ List hole = data.Polygon.holes.get(i);
+ Point[] holePoints = new Point[hole.size()];
+ for (int j = 0; j < hole.size(); j++) {
+ holePoints[j] = new Point(0, 0);
+ latlonToXY(refPoint.lat, refPoint.lon, hole.get(j).lat, hole.get(j).lon, holePoints[j]);
+ }
+ holesPolygons[i] = new Polygon(holePoints, holePoints.length);
+ }
+ PolygonWithHoles field = new PolygonWithHoles(outerPolygon, holesPolygons, holesPolygons.length);
+
+
+// PolygonWithHoles field = new PolygonWithHoles(outerPolygon, null,0);
+
+ double width = 0.8;
+ List path = new ArrayList<>();
+
+ generateMowingPath(field, width, startPoint, endPoint, path);
+
+ PathPoint[] gpsout = new PathPoint[path.size()];
+
+ for (int i = 0; i < gpsout.length; i++) {
+ gpsout[i] = new PathPoint(0, 0);
+ }
+
+ for (int i = 0; i < path.size(); i++) {
+ if (i >= gpsout.length) break;
+ xyToLatlon(start.lat, start.lon, path.get(i).x, path.get(i).y, gpsout[i]);
+ }
+ List Points = new ArrayList<>();
+
+ System.out.printf("Generated %d path points:\n", path.size());
+ for (int i = 0; i < path.size(); i++) {
+ if (i >= gpsout.length) break;
+ System.out.printf("[%.14f, %.14f]\n", gpsout[i].lon, gpsout[i].lat);
+// Log.d("MowingPathGenerator", "gpsout Point: " + "(" + gpsout[i].lon + ", " + gpsout[i].lat + " )");
+ LatAndLngEntity latAndLngEntity = new LatAndLngEntity();
+ latAndLngEntity.setLat(gpsout[i].lat);
+ latAndLngEntity.setLng(gpsout[i].lon);
+ Points.add(latAndLngEntity);
+ }
+
+ return Points;
+ }
+}
+
+
+// public static void main(String[] args) {
+//
+// PathPoint start = new PathPoint(32.08599985199932, 120.77586450935124);
+// PathPoint[] gpsout = new PathPoint[1024];
+// PathPoint[] gpsin = {
+// new PathPoint(32.08599985199932, 120.77586450935124),
+// new PathPoint(32.08624992154597, 120.77670803771835),
+// new PathPoint(32.08568874374706, 120.77710241458807),
+// new PathPoint(32.0854398968189, 120.77615838649545)
+// };
+//
+// PathPoint scanStart = new PathPoint(32.08599985199932, 120.77586450935124);
+// PathPoint scanEnd = new PathPoint(32.08624992154597, 120.77670803771835);
+// Point startPoint = new Point(0,0);
+// Point endPoint = new Point(0,0);
+//
+// latlonToXY(start.lat, start.lon, scanStart.lat, scanStart.lon, startPoint);
+// latlonToXY(start.lat, start.lon, scanEnd.lat, scanEnd.lon, endPoint);
+//
+// Point[] outerVertices = new Point[4];
+// for (int i = 0; i < 4; i++) {
+// outerVertices[i] = new Point(0, 0);
+// latlonToXY(start.lat, start.lon, gpsin[i].lat, gpsin[i].lon, outerVertices[i]);
+// System.out.printf("(%.14f, %.14f)\n", outerVertices[i].x, outerVertices[i].y);
+// }
+//
+// Polygon outer = new Polygon(outerVertices, 4);
+// Point[] hole1Vertices = {
+// new Point(40, -10), new Point(80, -10), new Point(80, -40),
+// new Point(60, -28), new Point(40, -40)
+// };
+// Polygon[] holes = {
+// new Polygon(hole1Vertices, 5)
+// };
+// PolygonWithHoles field = new PolygonWithHoles(outer, holes, 1);
+//
+// double width = 0.8;
+// List path = new ArrayList<>();
+//
+// generateMowingPath(field, width, startPoint, endPoint, path);
+//
+// for (int i = 0; i < gpsout.length; i++) {
+// gpsout[i] = new PathPoint(0, 0);
+// }
+//
+// for (int i = 0; i < path.size(); i++) {
+// if (i >= gpsout.length) break;
+// xyToLatlon(start.lat, start.lon, path.get(i).x, path.get(i).y, gpsout[i]);
+// }
+//
+// System.out.printf("Generated %d path points:\n", path.size());
+// for (int i = 0; i < path.size(); i++) {
+// if (i >= gpsout.length) break;
+// System.out.printf("{%.14f, %.14f}\n", gpsout[i].lon, gpsout[i].lat);
+// }
+// }
+//}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/path/RPPathData.java b/maibu-netty-server/src/main/java/com/maibu/entity/path/RPPathData.java
new file mode 100644
index 0000000..f346b9c
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/path/RPPathData.java
@@ -0,0 +1,20 @@
+package com.fastbee.entity.path;
+import com.fastbee.entity.path.MowingPathGenerator.*;
+
+import java.util.List;
+
+public class RPPathData {
+ public ScanLine scanLine;
+ public PathPoint refPoint;
+ public PolygonData Polygon;
+
+ public static class ScanLine {
+ public PathPoint start;
+ public PathPoint end;
+ }
+
+ public static class PolygonData {
+ public List outer;
+ public List> holes;
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanRecEntity.java b/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanRecEntity.java
new file mode 100644
index 0000000..06e41c1
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanRecEntity.java
@@ -0,0 +1,30 @@
+package com.fastbee.entity.path;
+
+import lombok.Data;
+
+@Data
+public class RoutePlanRecEntity {
+ private byte header1;
+ private byte header2;
+ private byte commandType;
+ private short pointNum;
+ private byte status;
+ private short crc16;
+ private byte endFlag1;
+ private byte endFlag2;
+
+ @Override
+ public String toString() {
+ return "RoutePlanRecEntity{" +
+ "header1=" + header1 +
+ ", header2=" + header2 +
+ ", commandType=" + commandType +
+ ", pointNum=" + pointNum +
+ ", status=" + status +
+ ", crc16=" + crc16 +
+ ", endFlag1=" + endFlag1 +
+ ", endFlag2=" + endFlag2 +
+ '}';
+ }
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanSendEntity.java b/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanSendEntity.java
new file mode 100644
index 0000000..3761cd9
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/entity/path/RoutePlanSendEntity.java
@@ -0,0 +1,78 @@
+package com.fastbee.entity.path;
+
+import lombok.Data;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+@Data
+public class RoutePlanSendEntity {
+
+ public RoutePlanSendEntity() {
+ header1 = (byte) 0xAB;
+ header2 = (byte) 0xAA;
+ commandType = 0x01;
+ pointCounts = 1;
+ targetLatitude = 0;
+ targetLongitude = 0;
+ speed = 0x3E8;
+ crc16 = 0;
+ endFlag1 = (byte) 0xAA;
+ endFlag2 = (byte) 0xAB;
+ }
+
+ @Override
+ public String toString() {
+ return "RoutePlanSendEntity{" +
+ "header1=" + header1 +
+ ", header2=" + header2 +
+ ", commandType=" + commandType +
+ ", pointCounts=" + pointCounts +
+ ", targetLatitude=" + targetLatitude +
+ ", targetLongitude=" + targetLongitude +
+ ", speed=" + speed +
+ ", crc16=" + crc16 +
+ ", endFlag1=" + endFlag1 +
+ ", endFlag2=" + endFlag2 +
+ '}';
+ }
+
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(27); // 修改为27字节
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+
+ buffer.put((byte) 0xAB);
+ buffer.put((byte) 0xAA);
+
+ buffer.put((byte) 0x01);
+
+ buffer.putShort(pointCounts);
+
+
+ long latitudeBits = Double.doubleToLongBits(targetLatitude);
+ buffer.putLong(latitudeBits);
+
+ long longitudeBits = Double.doubleToLongBits(targetLongitude);
+ buffer.putLong(longitudeBits);
+
+ buffer.putShort(speed);
+ buffer.putShort(crc16);
+
+ buffer.put((byte) 0xAA);
+ buffer.put((byte) 0xAB);
+
+ return buffer.array();
+ }
+
+ private byte header1;
+ private byte header2;
+ private byte commandType;
+ private short pointCounts;
+ private double targetLatitude; // 改为 double
+ private double targetLongitude; // 改为 double
+ private short speed;
+ private short crc16;
+ private byte endFlag1;
+ private byte endFlag2;
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/CommandRequestType.java b/maibu-netty-server/src/main/java/com/maibu/enums/CommandRequestType.java
new file mode 100644
index 0000000..fe1eb0b
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/CommandRequestType.java
@@ -0,0 +1,5 @@
+package com.fastbee.enums;
+
+public enum CommandRequestType {
+ switch_control
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/CompareEnum.java b/maibu-netty-server/src/main/java/com/maibu/enums/CompareEnum.java
new file mode 100644
index 0000000..d46b675
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/CompareEnum.java
@@ -0,0 +1,94 @@
+package com.fastbee.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 比较操作符枚举类
+ * 用于标准化管理比较类型,如>、<、between等
+ * 基于项目现有的使用模式实现
+ */
+@Getter
+@AllArgsConstructor
+public enum CompareEnum {
+
+ /** 等于 */
+ EQ("=", "等于", "=", true),
+
+ /** 不等于 */
+ NEQ("!=", "不等于", "!=", true),
+
+ /** 大于 */
+ GT(">", "大于", ">", true),
+
+ /** 小于 */
+ LT("<", "小于", "<", true),
+
+ /** 大于等于 */
+ GTE(">=", "大于等于", ">=", true),
+
+ /** 小于等于 */
+ LTE("<=", "小于等于", "<=", true),
+
+ /** 在之间 */
+ BETWEEN("between", "在之间", "between", false),
+
+ /** 不在之间 */
+ NOT_BETWEEN("notBetween", "不在之间", "notBetween", false),
+
+ /** 包含 */
+ CONTAIN("contain", "包含", "contain", true),
+
+ /** 不包含 */
+ NOT_CONTAIN("notContain", "不包含", "notContain", true);
+
+ /** 操作符值(存储在数据库中) */
+ private final String value;
+
+ /** 操作符名称(用于前端显示) */
+ private final String name;
+
+ /** 表达式符号(用于计算表达式) */
+ private final String symbol;
+
+ /** 是否为单值比较(true:单值, false:范围值) */
+ private final boolean singleValue;
+
+ /**
+ * 根据值获取比较操作符枚举
+ * @param value 操作符值
+ * @return 比较操作符枚举
+ */
+ public static CompareEnum getByValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (CompareEnum operator : CompareEnum.values()) {
+ if (operator.getValue().equals(value)) {
+ return operator;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取所有比较操作符列表
+ * @return 比较操作符列表
+ */
+ public static List getAll() {
+ return Arrays.asList(CompareEnum.values());
+ }
+
+ /**
+ * 判断是否为范围比较操作符
+ * @param value 操作符值
+ * @return 是否为范围比较
+ */
+ public static boolean isRangeOperator(String value) {
+ CompareEnum operator = getByValue(value);
+ return operator != null && !operator.isSingleValue();
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/ConnectionCloseReason.java b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectionCloseReason.java
new file mode 100644
index 0000000..bfba5c4
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectionCloseReason.java
@@ -0,0 +1,7 @@
+package com.fastbee.enums;
+
+public enum ConnectionCloseReason {
+ NROMAL,
+ TIMEOUT,
+ REPEAT
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorStatus.java b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorStatus.java
new file mode 100644
index 0000000..405b0ae
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorStatus.java
@@ -0,0 +1,7 @@
+package com.fastbee.enums;
+
+public enum ConnectorStatus {
+ ACTIVE, // 完全可用
+ RECEIVER, // 禁言(不可发送但可以接收)
+ DISABLED // 完全不可用
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorType.java b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorType.java
new file mode 100644
index 0000000..b190eea
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/ConnectorType.java
@@ -0,0 +1,7 @@
+package com.fastbee.enums;
+
+public enum ConnectorType {
+ MASTER, // 上位机(主动绑定别人)
+ SLAVE, // 下位机(只能被动绑定)
+ GENERAL // 不区分角色(将来扩展用)
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/DeviceStatus.java b/maibu-netty-server/src/main/java/com/maibu/enums/DeviceStatus.java
new file mode 100644
index 0000000..72da658
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/DeviceStatus.java
@@ -0,0 +1,35 @@
+package com.fastbee.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum DeviceStatus {
+
+
+ online(1, "online", "在线"),
+ offline(0, "offline", "离线");
+
+ private int type;
+ private String code;
+ private String description;
+
+ public static DeviceStatus convert(int type) {
+ for (DeviceStatus value : DeviceStatus.values()) {
+ if (value.type == type) {
+ return value;
+ }
+ }
+ return null;
+ }
+
+ public static DeviceStatus convert(String code) {
+ for (DeviceStatus value : DeviceStatus.values()) {
+ if (value.code.equals(code)) {
+ return value;
+ }
+ }
+ return null;
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/enums/RespondCode.java b/maibu-netty-server/src/main/java/com/maibu/enums/RespondCode.java
new file mode 100644
index 0000000..a56c2a0
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/enums/RespondCode.java
@@ -0,0 +1,17 @@
+package com.fastbee.enums;
+
+public enum RespondCode {
+ /**
+ * 账号已登录
+ */
+ have_logged_in,
+
+ /**
+ * 设备状态改变
+ */
+ device_status_changed,
+ /**
+ * 设备错误推送
+ */
+ device_error_push
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/init/InitThread.java b/maibu-netty-server/src/main/java/com/maibu/init/InitThread.java
new file mode 100644
index 0000000..7e11fda
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/init/InitThread.java
@@ -0,0 +1,103 @@
+package com.fastbee.init;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fastbee.common.constant.FastBeeConstant;
+import com.fastbee.common.core.domain.entity.UserClient;
+import com.fastbee.framework.mybatis.LambdaQueryWrapperX;
+import com.fastbee.iot.domain.DevicePlan;
+import com.fastbee.iot.domain.DevicePlanTask;
+import com.fastbee.iot.enums.DeviceTaskStaus;
+import com.fastbee.iot.mapper.DevicePlanMapper;
+import com.fastbee.iot.mapper.DevicePlanTaskMapper;
+import com.fastbee.memory.GlobalMemory;
+import com.fastbee.service.DevicePlanTaskMonitorService;
+import com.fastbee.service.DeviceThreadService;
+import com.fastbee.system.mapper.SysUserClientMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+
+@Slf4j
+@Component
+@Order(20)
+public class InitThread implements ApplicationRunner {
+
+
+ @Resource(name = FastBeeConstant.TASK.DEVICE_ERROR_MONITOR)
+ private Executor deviceErrorMonitorExecutor;
+
+ @Resource(name = FastBeeConstant.TASK.DEVICE_TASK_HANDLER)
+ private Executor deviceTaskHandlerExecutor;
+
+ @Autowired
+ private DeviceThreadService deviceThreadService;
+
+ @Autowired
+ private DevicePlanTaskMonitorService devicePlanTaskMonitorService;
+
+ @Autowired
+ private DevicePlanMapper devicePlanMapper;
+
+ @Autowired
+ private SysUserClientMapper sysUserClientMapper;
+
+ @Override
+ public void run(ApplicationArguments args) {
+ recoverMemory();
+ init();
+ }
+
+
+ private void init() {
+ //设备错误监听线程
+ deviceErrorMonitorExecutor.execute(() -> {
+ try {
+ deviceThreadService.deviceErrorMonitor();
+ } catch (InterruptedException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ //设备任务处理线程
+ deviceTaskHandlerExecutor.execute(() -> {
+ //任务下发
+ try {
+ devicePlanTaskMonitorService.generatePlanTask();
+ //执行准备队列
+ devicePlanTaskMonitorService.doLoop();
+ //执行任务
+ devicePlanTaskMonitorService.doExecute();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ /**
+ * 恢复内存
+ */
+ public void recoverMemory(){
+ //todo 启动时清除当前登录记录
+// System.out.println("开始清除登录信息");
+// sysUserClientMapper.clearDeviceName();
+ // 未执行完的重复计划
+ LambdaQueryWrapperX query = new LambdaQueryWrapperX<>();
+ List list = Arrays.asList(DeviceTaskStaus.NEW.getCode(),DeviceTaskStaus.EXECUTING.getCode(),DeviceTaskStaus.PAUSE.getCode());
+ query.in(DevicePlan::getTaskStaus, list);
+ List devicePlanList = devicePlanMapper.selectList(query);
+ if(!CollectionUtils.isEmpty(devicePlanList)){
+ devicePlanList.forEach(GlobalMemory::addDevicePlan);
+ }
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/manager/DeviceSessionManager.java b/maibu-netty-server/src/main/java/com/maibu/manager/DeviceSessionManager.java
new file mode 100644
index 0000000..c83fc0b
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/manager/DeviceSessionManager.java
@@ -0,0 +1,228 @@
+package com.fastbee.manager;
+
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fastbee.dto.Connector;
+import com.fastbee.dto.NettyDevice;
+import com.fastbee.enums.ConnectorStatus;
+import com.fastbee.enums.ConnectorType;
+import com.fastbee.netty.handler.CommandForwardHandler;
+import io.netty.channel.Channel;
+import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 设备会话管理器
+ * 负责管理设备信息、连接通道、双向绑定关系、状态管理和消息转发
+ */
+@Component
+@Data
+public class DeviceSessionManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(DeviceSessionManager.class);
+ // 设备ID -> 设备对象(业务实体)
+ private final ConcurrentHashMap deviceMap = new ConcurrentHashMap<>();
+
+ // 设备ID -> Channel(通信通道)
+ private final ConcurrentHashMap channelMap = new ConcurrentHashMap<>();
+
+ // Channel -> 设备ID(反查)
+ private final ConcurrentHashMap channelToDeviceIdMap = new ConcurrentHashMap<>();
+
+
+ public void clearChannelToDeviceIdMap(String deviceId) {
+ if (StringUtils.isEmpty(deviceId) || CollectionUtils.isEmpty(channelToDeviceIdMap)) {
+ return;
+ }
+ // 获取 entrySet 迭代器,安全遍历+删除
+ Iterator> iterator = channelToDeviceIdMap.entrySet().iterator();
+ while (iterator.hasNext()) {
+ Map.Entry entry = iterator.next();
+ Channel channel = entry.getKey();
+ String value = entry.getValue();
+ // 匹配目标 deviceId
+ if (deviceId.equals(value)) {
+ // 关闭 Channel:捕获 IO 异常,避免单个 Channel 关闭失败影响整体遍历
+ if (channel != null && channel.isActive()) {
+ try {
+ channel.close().syncUninterruptibly(); // 优雅关闭,等待关闭完成
+ } catch (Exception e) {
+ // 记录日志,不中断后续操作
+ logger.error("关闭 Channel 失败", e);
+ }
+ }
+ // 关键:使用迭代器的 remove() 方法,安全移除键值对,不会触发并发修改异常
+ iterator.remove();
+ }
+ }
+ }
+
+
+ /**
+ * 注册设备连接(设备上线)
+ *
+ * @param deviceId 设备ID
+ * @param channel 通信通道
+ * @param deviceType 设备类型(MASTER/SLAVE)
+ */
+ public void registerDevice(String deviceId, Channel channel, ConnectorType deviceType, ConnectorStatus status, String slaveId) {
+ NettyDevice nettyDevice = deviceMap.computeIfAbsent(deviceId, id -> new NettyDevice(id, deviceType));
+ nettyDevice.setChannel(channel);
+ nettyDevice.setConnectorStatus(status); // 默认上线即ACTIVE
+ nettyDevice.setCurrentSlaveId(slaveId);
+ nettyDevice.setLatestLoginTime(System.currentTimeMillis());
+ deviceMap.put(deviceId, nettyDevice);
+ channelMap.put(deviceId, channel);
+ channelToDeviceIdMap.put(channel, deviceId);
+ System.out.println("Registered device: " + deviceId + " type: " + deviceType);
+ }
+
+ /**
+ * 上位机绑定下位机,建立双向绑定关系
+ *
+ * @param masterId 上位机设备ID
+ * @param slaveId 下位机设备ID
+ */
+ public void bindConnector(String masterId, String slaveId) {
+ NettyDevice master = deviceMap.get(masterId);
+ if (master == null) return;
+ NettyDevice slave = deviceMap.computeIfAbsent(slaveId, id -> new NettyDevice(id, ConnectorType.SLAVE));
+ master.addConnectedDevice(slave);
+ System.out.println("Bind connectors for master " + masterId + " and slave " + slaveId);
+ }
+
+ /**
+ * 根据设备ID获取设备对象
+ */
+ public NettyDevice getDevice(String deviceId) {
+ return deviceMap.get(deviceId);
+ }
+
+ // 模糊匹配
+ public List getDevicesLike(String query) {
+ return deviceMap.values().stream()
+ .filter(device -> device.getConnectorId().contains(query))
+ .collect(Collectors.toList());
+ }
+
+
+ public List getSlaveControl(String slaveId) {
+ // 获取所有连接的主机 且是active状态的
+ return deviceMap.values().stream()
+ .filter(device -> ObjectUtil.equals(device.getCurrentSlaveId(), slaveId)
+ && ConnectorStatus.ACTIVE.equals(device.getConnectorStatus()))
+ .collect(Collectors.toList());
+ }
+
+
+ public List getAllSlaveControl(String slaveId) {
+ // 获取所有连接的主机 不区分状态
+ return deviceMap.values().stream()
+ .filter(device -> ObjectUtil.equals(device.getCurrentSlaveId(), slaveId))
+ .collect(Collectors.toList());
+ }
+
+
+ /// 模糊匹配
+ public NettyDevice getUserControl(String userName) {
+ return deviceMap.values().stream()
+ .filter(device -> device.getConnectorId().contains(userName)
+ && ConnectorStatus.ACTIVE.equals(device.getConnectorStatus()))
+ .findFirst().orElse(null);
+ }
+
+
+ /**
+ * 根据设备ID获取Channel
+ */
+ public Channel getChannel(String deviceId) {
+ return channelMap.get(deviceId);
+ }
+
+ /**
+ * 根据Channel反查设备ID
+ */
+ public String getDeviceIdByChannel(Channel channel) {
+ return channelToDeviceIdMap.get(channel);
+ }
+
+
+ /**
+ * 根据从机查控制的主机
+ */
+ public NettyDevice getMasterBySlaveId(String slaveId) {
+ return deviceMap.values().stream().filter(x -> Objects.equals(x.getCurrentSlaveId(), slaveId)
+ && ConnectorStatus.ACTIVE.equals(x.getConnectorStatus())).findFirst().orElse(null);
+ }
+
+ /**
+ * 根据从机查所有主机
+ */
+ public List getMastersBySlaveId(String slaveId) {
+ return deviceMap.values().stream().filter(x -> Objects.equals(x.getCurrentSlaveId(), slaveId)).collect(Collectors.toList());
+ }
+
+ /**
+ * 移除设备连接(设备下线)
+ */
+ public void removeDevice(String deviceId) {
+ NettyDevice nettyDevice = deviceMap.remove(deviceId);
+// Channel channel = channelMap.remove(deviceId);
+ clearChannelToDeviceIdMap(deviceId);
+// if (channel != null) {
+// channelToDeviceIdMap.remove(channel);
+// channel.close();
+// }
+ if (nettyDevice != null) {
+ // 仅当设备是上位机时,才解绑绑定关系
+ if (nettyDevice.getDeviceType() == ConnectorType.MASTER) {
+ for (Connector connected : new HashSet<>(nettyDevice.getConnectedConnectors())) {
+ nettyDevice.removeConnectedDevice(connected);
+ }
+ } else {
+ // 如果是下位机,则保留绑定关系,只清理channel和状态
+ nettyDevice.setChannel(null);
+ nettyDevice.setConnectorStatus(ConnectorStatus.DISABLED); // 或者设置为离线状态
+ // 注意:此时device仍留在deviceMap中
+ nettyDevice.setLatestStatus(null);
+ deviceMap.put(deviceId, nettyDevice);
+ }
+ System.out.println("Removed device: " + deviceId);
+ }
+ }
+
+
+ /**
+ * 判断设备是否在线
+ */
+ public boolean isOnline(String deviceId) {
+ Channel ch = channelMap.get(deviceId);
+ return ch != null && ch.isActive();
+ }
+
+ /**
+ * 判断设备是否允许发送消息(ACTIVE状态才允许)
+ */
+ public boolean canSend(String deviceId) {
+ NettyDevice nettyDevice = deviceMap.get(deviceId);
+ return nettyDevice != null && nettyDevice.canSend();
+ }
+
+ /**
+ * 判断设备是否允许接收消息(非DISABLED状态才允许)
+ */
+ public boolean canReceive(String deviceId) {
+ NettyDevice nettyDevice = deviceMap.get(deviceId);
+ return nettyDevice != null && nettyDevice.canReceive();
+ }
+
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/mapper/ErrorIdentificationStandardMapper.java b/maibu-netty-server/src/main/java/com/maibu/mapper/ErrorIdentificationStandardMapper.java
new file mode 100644
index 0000000..9f3d8f2
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/mapper/ErrorIdentificationStandardMapper.java
@@ -0,0 +1,13 @@
+package com.fastbee.mapper;
+
+import com.fastbee.entity.ErrorIdentificationStandard;
+import com.fastbee.framework.mybatis.mapper.BaseMapperX;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 设备历史状态
+ */
+@Repository
+public interface ErrorIdentificationStandardMapper extends BaseMapperX {
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/memory/GlobalMemory.java b/maibu-netty-server/src/main/java/com/maibu/memory/GlobalMemory.java
new file mode 100644
index 0000000..ba1ee5c
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/memory/GlobalMemory.java
@@ -0,0 +1,97 @@
+package com.fastbee.memory;
+
+import com.fastbee.entity.ErrorIdentificationStandard;
+import com.fastbee.iot.domain.DevicePlan;
+import com.fastbee.iot.domain.DevicePlanTask;
+import lombok.Data;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.java_websocket.WebSocket;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@Data
+public class GlobalMemory {
+
+ public List standard = null;
+
+ //key deviceId
+ public static ConcurrentHashMap> devicePlanMap = new ConcurrentHashMap<>();
+ //key deviceId prepare
+ public static ConcurrentHashMap> devicePlanTaskPrepareMap = new ConcurrentHashMap<>();
+ //key deviceId execute
+ public static ConcurrentHashMap devicePlanTaskExecuteMap = new ConcurrentHashMap<>();
+
+ // 设备当前控制权在哪里key->deviceId value:userName:app/web/pc:token
+// public static final ConcurrentHashMap controlLockMap = new ConcurrentHashMap<>();
+
+
+ public static void addDevicePlan(DevicePlan devicePlan) {
+ if (devicePlan != null && !StringUtils.isEmpty(devicePlan.getDeviceId())) {
+ String deviceId = devicePlan.getDeviceId();
+ List list = devicePlanMap.get(deviceId);
+ if (CollectionUtils.isEmpty(list)) {
+ list = new ArrayList<>();
+ }
+ boolean exists = list.stream()
+ .anyMatch(l -> Objects.equals(l.getId(), devicePlan.getId()));
+ if (!exists) {
+ list.add(devicePlan);
+ }
+ devicePlanMap.put(deviceId, list);
+ }
+ }
+
+
+ public static void removeDevicePlan(DevicePlan devicePlan) {
+ if (devicePlan != null && !StringUtils.isEmpty(devicePlan.getDeviceId())) {
+ String deviceId = devicePlan.getDeviceId();
+ List list = devicePlanMap.get(deviceId);
+ if (!CollectionUtils.isEmpty(list)) {
+ list.removeIf(x -> x.getId().equals(devicePlan.getId()));
+ }
+ }
+ }
+
+ public static void addDevicePlanPrepare(DevicePlanTask devicePlanTask) {
+ if (devicePlanTask != null && !StringUtils.isEmpty(devicePlanTask.getDeviceId())) {
+ String deviceId = devicePlanTask.getDeviceId();
+ List list = devicePlanTaskPrepareMap.get(deviceId);
+ if (CollectionUtils.isEmpty(list)) {
+ list = new ArrayList<>();
+ }
+ list.add(devicePlanTask);
+ devicePlanTaskPrepareMap.put(deviceId, list);
+ }
+ }
+
+ public static void removeDevicePlanPrepare(DevicePlanTask devicePlanTask) {
+ if (devicePlanTask != null && !StringUtils.isEmpty(devicePlanTask.getDeviceId())) {
+ String deviceId = devicePlanTask.getDeviceId();
+ List list = devicePlanTaskPrepareMap.get(deviceId);
+ if (!CollectionUtils.isEmpty(list)) {
+ list.removeIf(x -> x.getId().equals(devicePlanTask.getId()));
+ }
+ }
+ }
+
+
+ public static void addDevicePlanExecute(DevicePlanTask devicePlanTask) {
+ if (devicePlanTask != null && !StringUtils.isEmpty(devicePlanTask.getDeviceId())) {
+ String deviceId = devicePlanTask.getDeviceId();
+ devicePlanTaskExecuteMap.put(deviceId, devicePlanTask);
+ }
+ }
+
+ public static void removeDevicePlanExecute(String deviceId) {
+ if (!StringUtils.isEmpty(deviceId)) {
+ devicePlanTaskExecuteMap.remove(deviceId);
+ }
+ }
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/NettyServer.java b/maibu-netty-server/src/main/java/com/maibu/netty/NettyServer.java
new file mode 100644
index 0000000..0e6c70d
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/netty/NettyServer.java
@@ -0,0 +1,63 @@
+package com.fastbee.netty;
+
+import com.fastbee.netty.handler.*;
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.*;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.timeout.IdleStateHandler;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class NettyServer {
+
+ private EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+ private EventLoopGroup workerGroup = new NioEventLoopGroup();
+
+ @Autowired
+ private CommandForwardHandler commandForwardHandler;
+
+ @Autowired
+ private HeartbeatHandler heartbeatHandler;
+
+ @Autowired
+ private DeviceConnectHandler deviceConnectHandler;
+
+ @Autowired
+ private DataToDataBaseHandler dataToDataBaseHandler;
+
+ public void start(){
+ try {
+ ServerBootstrap serverBootstrap = new ServerBootstrap();
+ serverBootstrap.group(bossGroup, workerGroup)
+ .channel(NioServerSocketChannel.class)
+ .option(ChannelOption.SO_BACKLOG, 1024)
+ .childHandler(new ChannelInitializer() {
+ @Override
+ protected void initChannel(SocketChannel ch) throws Exception {
+ ChannelPipeline pipeline = ch.pipeline();
+ pipeline.addLast(new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS));
+ pipeline.addLast(new HeaderFooterDecoder());
+ pipeline.addLast(new HexDecoder());
+ pipeline.addLast(new HexEncoder());
+ pipeline.addLast(deviceConnectHandler);
+ pipeline.addLast(heartbeatHandler);
+ pipeline.addLast(dataToDataBaseHandler);
+ pipeline.addLast(commandForwardHandler);
+ }
+ });
+ System.out.println("聊天室服务器启动了...");
+ ChannelFuture channelFuture = serverBootstrap.bind(9001).sync();
+ channelFuture.channel().closeFuture().sync();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ } finally {
+ bossGroup.shutdownGracefully();
+ workerGroup.shutdownGracefully();
+ }
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/CommandForwardHandler.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/CommandForwardHandler.java
new file mode 100644
index 0000000..6fea558
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/CommandForwardHandler.java
@@ -0,0 +1,111 @@
+package com.fastbee.netty.handler;
+
+import com.fastbee.common.CommandConstant;
+import com.fastbee.common.Constant;
+import com.fastbee.dto.Connector;
+import com.fastbee.dto.NettyDevice;
+import com.fastbee.enums.ConnectorType;
+import com.fastbee.manager.DeviceSessionManager;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.collections.CollectionUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Set;
+
+@Component
+@ChannelHandler.Sharable
+public class CommandForwardHandler extends SimpleChannelInboundHandler {
+
+
+ private static final Logger logger = LoggerFactory.getLogger(CommandForwardHandler.class);
+
+ @Autowired
+ private DeviceSessionManager deviceSessionManager;
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
+ Channel currentChannel = ctx.channel();
+ byte[] bytes = Hex.decodeHex(msg.toCharArray());
+ if (bytes.length > 3 && (bytes[2] == CommandConstant.remoteControl || bytes[2] == CommandConstant.path || bytes[2] == CommandConstant.status)) {
+ String senderDeviceId = deviceSessionManager.getDeviceIdByChannel(currentChannel);
+ NettyDevice senderNettyDevice =
+ senderDeviceId != null ? deviceSessionManager.getDevice(senderDeviceId) : null;
+
+ if (senderNettyDevice == null) {
+ logger.debug("发送拒绝,设备:{}未发送上线指令", senderDeviceId);
+ ByteBuf buf = ctx.alloc().buffer();
+ buf.writeShort(0xABAA);
+ buf.writeByte(0x03);
+ buf.writeShort(0x00);
+ buf.writeShort(0x00);
+ buf.writeShort(0xAAAB);
+ currentChannel.writeAndFlush(buf);
+ logger.debug("已重新请求设备连接");
+ return;
+ }
+
+ if (!senderNettyDevice.canSend()) {
+ logger.debug("发送拒绝:设备:{}已处于禁止发送状态", senderDeviceId);
+ return;
+ }
+
+ if (ConnectorType.MASTER.equals(senderNettyDevice.getDeviceType())) {
+ Set connectedDevices = senderNettyDevice.getConnectedConnectors();
+ if (connectedDevices == null || connectedDevices.isEmpty()) {
+ logger.debug("发送拒绝master:{},该设备没有关联的设备", senderDeviceId);
+ return;
+ }
+ for (Connector target : connectedDevices) {
+ if (!target.isOnline()) {
+ logger.debug("发送拒绝master:{},消息到达的salve设备:{},已离线", senderDeviceId, target.getConnectorId());
+ continue;
+ }
+ if (!target.canReceive()) {
+ logger.debug("发送拒绝master:{},消息到达的slave设备:{},处于禁止接收状态", senderDeviceId, target.getConnectorId());
+ continue;
+ }
+ Channel targetChannel = target.getChannel();
+ if (targetChannel != null && targetChannel.isActive()) {
+ targetChannel.writeAndFlush(msg);
+ logger.debug("从master:{},向slave:{}, 转发消息:{}", senderDeviceId, target.getConnectorId(), msg);
+ }
+ }
+ } else if (ConnectorType.SLAVE.equals(senderNettyDevice.getDeviceType())) {
+ List masters = deviceSessionManager.getMastersBySlaveId(senderDeviceId);
+ if (CollectionUtils.isEmpty(masters)) {
+ logger.debug("发送拒绝slave:{},该设备没有关联的master设备", senderDeviceId);
+ return;
+ }
+ for (NettyDevice master : masters) {
+ if (!master.isOnline()) {
+ logger.debug("发送拒绝slave:{},消息到达的master{},设备已离线 ", senderDeviceId, master.getConnectorId());
+ continue;
+ }
+ if (!master.canReceive()) {
+ logger.debug("发送拒绝slave:{},消息到达的master{},处于禁止接收状态 ", senderDeviceId, master.getConnectorId());
+ continue;
+ }
+ Channel targetChannel = master.getChannel();
+ if (targetChannel != null && targetChannel.isActive()) {
+ targetChannel.writeAndFlush(msg);
+ logger.debug("从slave:{},向master:{}, 转发消息:{}", senderDeviceId, master.getConnectorId(), msg);
+ }
+ }
+ }
+ } else {
+ ctx.fireChannelRead(msg); // 不是控制指令,继续传递给其他Handler
+ }
+ }
+
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/DataToDataBaseHandler.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/DataToDataBaseHandler.java
new file mode 100644
index 0000000..b7d0f69
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/DataToDataBaseHandler.java
@@ -0,0 +1,135 @@
+package com.fastbee.netty.handler;
+
+import com.fastbee.common.CommandConstant;
+import com.fastbee.common.NettyCacheKey;
+import com.fastbee.common.core.redis.RedisCache;
+import com.fastbee.dto.NettyDevice;
+import com.fastbee.iot.domain.DeviceRunStatistics;
+import com.fastbee.iot.domain.DeviceRunningStatusHistory;
+import com.fastbee.iot.domain.DeviceStatusRecordDTO;
+import com.fastbee.manager.DeviceSessionManager;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Component
+@ChannelHandler.Sharable
+public class DataToDataBaseHandler extends SimpleChannelInboundHandler {
+
+ @Autowired
+ private DeviceSessionManager deviceSessionManager;
+
+ @Autowired
+ private RedisCache redisCache;
+
+ private static final Long interval = 5000L;
+
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
+ Channel channel = ctx.channel();
+ String deviceID = deviceSessionManager.getDeviceIdByChannel(channel);
+ if (StringUtils.isEmpty(deviceID)) return;
+
+ byte[] bytes = Hex.decodeHex(msg.toCharArray());
+ if (bytes[2] == CommandConstant.status) {
+ String key = NettyCacheKey.deviceRunningStatusKey + deviceID;
+ DeviceStatusRecordDTO recordDTO = redisCache.getCacheObject(key);
+ boolean record = false;
+ List histories;
+ if (recordDTO == null) {
+ recordDTO = new DeviceStatusRecordDTO();
+ histories = new ArrayList<>();
+ record = true;
+ } else {
+ long lastRecordTime = recordDTO.getLastRecordTime();
+ histories = recordDTO.getHistories();
+ if (System.currentTimeMillis() - lastRecordTime >= interval) {
+ record = true;
+ }
+ }
+ if (record) {
+ String deviceStatus = new String(bytes, 2, bytes.length - 4, StandardCharsets.UTF_8).trim();
+ String[] split = deviceStatus.split(",");
+
+ DeviceRunningStatusHistory deviceRunningStatus = getDeviceRunningStatusHistory(deviceID, split);
+ if (histories == null) {
+ histories = new ArrayList<>();
+ }
+ histories.add(deviceRunningStatus);
+ recordDTO.setHistories(histories);
+ recordDTO.setLastRecordTime(System.currentTimeMillis());
+
+ redisCache.setCacheObject(key, recordDTO);
+ // 判断整点 记录到数据库
+// deviceStatusHistoryMapper.insert(deviceRunningStatus);
+ NettyDevice nettyDevice = deviceSessionManager.getDevice(deviceID);
+ String workArea = deviceRunningStatus.getWorkingArea();
+ DeviceRunStatistics latestStatus;
+ if(nettyDevice.getLatestStatus() == null){
+ latestStatus = new DeviceRunStatistics();
+ latestStatus.setStartTime(LocalDateTime.now());
+ latestStatus.setDeviceId(deviceID);
+ }else {
+ latestStatus = nettyDevice.getLatestStatus();
+ // 使用Duration.between计算两个时间点之间的持续时间
+ Duration duration = Duration.between(latestStatus.getStartTime(), LocalDateTime.now());
+ long minutes = duration.toMinutes();
+ latestStatus.setTime(minutes);
+ }
+ latestStatus.setWorkArea(Double.valueOf(workArea));
+ nettyDevice.setLatestStatus(latestStatus);
+ }
+// else{
+// System.out.println(false);
+// }
+ }
+ ctx.fireChannelRead(msg);
+ }
+
+ private DeviceRunningStatusHistory getDeviceRunningStatusHistory(String deviceID, String[] split) {
+ DeviceRunningStatusHistory deviceRunningStatus = new DeviceRunningStatusHistory();
+ deviceRunningStatus.setDeviceId(deviceID);
+ deviceRunningStatus.setVoltage(split[0]);
+ deviceRunningStatus.setLeftTargetSpeed(split[1]);
+ deviceRunningStatus.setRightTargetSpeed(split[2]);
+ deviceRunningStatus.setLeftMeasureSpeed(split[3]);
+ deviceRunningStatus.setRightMeasureSpeed(split[4]);
+ deviceRunningStatus.setLeftCurrent(split[5]);
+ deviceRunningStatus.setRightCurrent(split[6]);
+ deviceRunningStatus.setLeftMotorTemp(split[7]);
+ deviceRunningStatus.setRightMotorTemp(split[8]);
+ deviceRunningStatus.setChipTemp(split[9]);
+ deviceRunningStatus.setYaw(split[10]);
+ deviceRunningStatus.setPitch(split[11]);
+ deviceRunningStatus.setRoll(split[12]);
+ deviceRunningStatus.setSatelliteCnt(split[13]);
+ deviceRunningStatus.setQual(split[14]);
+ deviceRunningStatus.setHeadingStatus(split[15]);
+ deviceRunningStatus.setLatitude(split[16]);
+ deviceRunningStatus.setLongitude(split[17]);
+
+ //18 时间暂未给值
+
+ deviceRunningStatus.setCuttingSpeed(split[19]);
+ deviceRunningStatus.setControlMode(split[20]);
+ deviceRunningStatus.setBattery(split[21]);
+ deviceRunningStatus.setWorkingArea(split[22]);
+ deviceRunningStatus.setObstacleSign(split[23]);
+
+ deviceRunningStatus.setCreateTime(new Date());
+ return deviceRunningStatus;
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/DeviceConnectHandler.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/DeviceConnectHandler.java
new file mode 100644
index 0000000..213c5aa
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/DeviceConnectHandler.java
@@ -0,0 +1,466 @@
+package com.fastbee.netty.handler;
+
+import com.fastbee.common.CommandConstant;
+import com.fastbee.common.core.domain.entity.SysUser;
+import com.fastbee.common.core.domain.entity.UserClient;
+import com.fastbee.common.utils.json.JsonUtils;
+import com.fastbee.dto.*;
+import com.fastbee.entity.path.LatAndLngEntity;
+import com.fastbee.enums.*;
+import com.fastbee.iot.domain.Device;
+import com.fastbee.iot.mapper.DeviceRunStatisticsMapper;
+import com.fastbee.iot.service.IDeviceService;
+import com.fastbee.manager.DeviceSessionManager;
+import com.fastbee.memory.GlobalMemory;
+import com.fastbee.system.service.ISysUserClientService;
+import com.fastbee.system.service.ISysUserService;
+import com.fastbee.utils.CommandUtils;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.util.AttributeKey;
+import io.netty.util.CharsetUtil;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDateTime;
+import java.util.Date;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@ChannelHandler.Sharable
+public class DeviceConnectHandler extends SimpleChannelInboundHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(DeviceConnectHandler.class);
+
+ public static final AttributeKey ATT_DEVICE_ID = AttributeKey.valueOf("deviceId");
+ public static final AttributeKey ATT_CLOSE_REASON = AttributeKey.valueOf("closeReason");
+
+ //记录切换控制权请求来源 key 是请求的masterId,value是控制当前设备的主机
+ private final ConcurrentHashMap controlRequestMap = new ConcurrentHashMap<>();
+
+ @Autowired
+ private final DeviceSessionManager sessionManager;
+
+ @Autowired
+ private ISysUserClientService sysUserClientService;
+
+ @Autowired
+ private IDeviceService deviceService;
+
+ @Autowired
+ private ISysUserService userService;
+
+ @Autowired
+ private DeviceRunStatisticsMapper deviceRunStatisticsMapper;
+
+ public DeviceConnectHandler(DeviceSessionManager sessionManager) {
+ this.sessionManager = sessionManager;
+ }
+
+ @Override
+ public void channelActive(ChannelHandlerContext ctx) {
+ logger.info("Channel active: {}", ctx.channel().remoteAddress());
+ ctx.fireChannelActive();
+
+ //todo 更新单次统计消息
+
+ }
+
+ @Override
+ public void channelInactive(ChannelHandlerContext ctx) {
+ Channel channel = ctx.channel();
+ ConnectionCloseReason reason = channel.attr(ATT_CLOSE_REASON).get();
+ String deviceId = channel.attr(ATT_DEVICE_ID).get();
+
+ if (reason == ConnectionCloseReason.REPEAT) {
+ logger.warn("重复连接断开: {}", channel.remoteAddress());
+ return;
+ }
+
+ if (deviceId != null) {
+ NettyDevice nettyDevice = sessionManager.getDevice(deviceId);
+ if (nettyDevice != null && nettyDevice.getDeviceType() == ConnectorType.MASTER) {
+ nettyDevice.setConnectorStatus(ConnectorStatus.DISABLED);
+ nettyDevice.setCurrentSlaveId(null);
+ sysUserClientService.updateAliveStatus(deviceId, "", 0);
+ logger.info("{}: 设备断开 status: {}", deviceId, ConnectorStatus.DISABLED);
+// updateLogoutSingle(deviceId);
+ //todo 自动切换到其他?
+ }
+ if (nettyDevice != null && nettyDevice.getDeviceType() == ConnectorType.SLAVE) {
+ nettyDevice.setOnlineStatus(0);
+ //推送json 找到设备绑定的主机进行推送
+ List controlMasters = sessionManager.getAllSlaveControl(deviceId);
+ if (!CollectionUtils.isEmpty(controlMasters)) {
+ controlMasters.forEach(x -> {
+ DeviceStatusChangeDTO changeDTO = new DeviceStatusChangeDTO();
+ changeDTO.setDeviceId(deviceId);
+ changeDTO.setStatus(DeviceStatus.offline.getCode());
+ changeDTO.setEvent(RespondCode.device_status_changed);
+ ByteBuf buf = ctx.alloc().buffer();
+ CommandUtils.buildCommand(buf, changeDTO, CommandConstant.interaction);
+ String content = buf.toString(CharsetUtil.UTF_8);
+ logger.info("推送下位机离线状态消息:{}", content);
+ if (x.getChannel() != null && x.getChannel().isActive()) {
+ x.getChannel().writeAndFlush(buf);
+ }
+ });
+ }
+ Device device = deviceService.selectDeviceBySerialNumber(deviceId);
+ if (device != null) {
+ device.setOnlineStatus(DeviceStatus.offline.getType());
+ device.setUpdateTime(new Date());
+ deviceService.updateDeviceBySelf(device);
+ //todo 更新历史作业消息
+ }
+ if (nettyDevice.getLatestStatus() != null) {
+ nettyDevice.getLatestStatus().setEndTime(LocalDateTime.now());
+ deviceRunStatisticsMapper.insert(nettyDevice.getLatestStatus());
+ }
+ }
+ sessionManager.removeDevice(deviceId);
+ logger.info("{}: 设备断开连接,地址: {}", deviceId, channel.remoteAddress());
+ } else {
+ logger.warn("未知设备断开连接: {}", channel.remoteAddress());
+ }
+ }
+
+ @Override
+ protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
+ try {
+ byte[] bytes = Hex.decodeHex(msg.toCharArray());
+
+ if (bytes.length < 5) {
+ logger.warn("收到无效数据包,长度过短");
+ return;
+ }
+ byte cmdType = bytes[2];
+ if (cmdType == CommandConstant.connect) {
+ // 下位机连接
+ String data = new String(bytes, 2, bytes.length - 4, StandardCharsets.UTF_8).trim();
+ logger.info("{}: 03指令", data);
+ if (!StringUtils.isEmpty(data)) {
+ if (data.contains(":")) {
+ logger.info("上位机连接 data:{}", data);
+ //判断 是否更新连接了 更新连接就返回失败
+ String[] parts = data.split(":");
+ if (parts.length == 3) {
+ String deviceId = parts[0] + ":" + parts[1];
+ String sessionId = parts[2];
+ String userName = parts[0];
+ logger.info("上位机连接 deviceId: {}", deviceId);
+ logger.info("上位机连接 sessionId: {}", sessionId);
+ logger.info("上位机连接 userName: {}", userName);
+ logger.info("上位机连接 type: {}", parts[1]);
+ SysUser sysUser = userService.selectUserByUserName(userName);
+ if (sysUser != null) {
+ sysUserClientService.insertUserClient(sysUser.getUserId(), userName, deviceId, "", 0);
+ }
+ //如果 当前角色存在sessionId同时和当前的穿过来不同则下发剔除指令
+ boolean t = false;
+ if (StringUtils.isEmpty(sessionId) || sysUser == null) {
+ t = true;
+ }
+ if (sysUser != null && !StringUtils.isEmpty(sessionId) && !StringUtils.isEmpty(sysUser.getSessionId())) {
+ if (!sysUser.getSessionId().equals(sessionId)) {
+ t = true;
+ }
+ }
+ if (t) {
+ //发送已登录状态到客户端
+ if ("web".equals(parts[1])) {
+ NettyDevice device = sessionManager.getDevice(deviceId);
+ logger.info("web 上位机连接重连: {}", JsonUtils.toJsonString(device));
+ if (device != null && !StringUtils.isEmpty(device.getCurrentSlaveId())) {
+ logger.info("上位机连接重连: {}", deviceId);
+ } else {
+ //判断是否账号在控制空 如果在控制设置账号为非active
+ List userClients = sysUserClientService.selectUserClientByUserName(userName);
+ ConnectorStatus status = ConnectorStatus.ACTIVE;
+ logger.info("web 上位机连接重连2: {}", JsonUtils.toJsonString(userClients));
+ UserClient nowClient = null;
+ if (!CollectionUtils.isEmpty(userClients)) {
+ long count = userClients.stream().filter(x ->
+ x.getIsAlive() == 1 && !deviceId.equals(x.getClientName())).count();
+ if (count > 0) {
+ status = ConnectorStatus.RECEIVER;
+ }
+ nowClient = userClients.stream().filter(x ->
+ x.getIsAlive() == 1 && deviceId.equals(x.getClientName())).findFirst().orElse(null);
+ }
+ logger.info("web 上位机连接重连3: {}", JsonUtils.toJsonString(status));
+ if (!ConnectorStatus.ACTIVE.equals(status)) {
+ handleMasterConnect(ctx, deviceId, null, status);
+ } else {
+ logger.info("web 上位机连接重连4: {}", JsonUtils.toJsonString(nowClient));
+ if (nowClient != null) {
+ handleMasterConnect(ctx, deviceId, nowClient.getDeviceName(), status);
+ } else {
+ handleMasterConnect(ctx, deviceId, null, status);
+ }
+
+ }
+ }
+ } else {
+ DeviceLoginResponseDTO responseDTO = new DeviceLoginResponseDTO();
+ responseDTO.setRespond(RespondCode.have_logged_in);
+ ByteBuf buf = ctx.alloc().buffer();
+ CommandUtils.buildCommand(buf, responseDTO, CommandConstant.interaction);
+ ctx.channel().writeAndFlush(buf);
+ }
+ } else {
+ //判断是否账号在控制空 如果在控制设置账号为非active
+ List userClients = sysUserClientService.selectUserClientByUserName(userName);
+ ConnectorStatus status = ConnectorStatus.ACTIVE;
+ if (!CollectionUtils.isEmpty(userClients)) {
+ long count = userClients.stream().filter(x ->
+ x.getIsAlive() == 1 && !deviceId.equals(x.getClientName())).count();
+ if (count > 0) {
+ status = ConnectorStatus.RECEIVER;
+ }
+ }
+ logger.info("上位机上线:{}", status);
+// if (ConnectorStatus.ACTIVE.equals(status)) {
+// //todo 设置当前控制的token
+// sysUser.setSessionId(sessionId);
+// userService.updateUserProfile(sysUser);
+// }
+ handleMasterConnect(ctx, deviceId, null, status);
+ }
+ }
+ } else {
+ handleSlaveConnect(ctx, data);
+ logger.info("{}: 下位机连接", data);
+ Device device = deviceService.selectDeviceBySerialNumber(data);
+ if (device == null) {
+ device = new Device();
+ device.setStatus(3);
+ device.setOnlineStatus(DeviceStatus.online.getType());
+ device.setSerialNumber(data);
+ device.setDeviceName(data);
+ device.setProductId(137L);
+ device.setTenantId(-1L);
+ device.setFirmwareVersion(BigDecimal.valueOf(1.0));
+ device.setProductName("割草机产品MC700");
+ device.setCreateTime(new Date());
+ device.setUpdateTime(new Date());
+ deviceService.insertDeviceBySelf(device);
+ } else {
+ device.setStatus(3);
+ device.setOnlineStatus(DeviceStatus.online.getType());
+ device.setUpdateTime(new Date());
+ deviceService.updateDeviceBySelf(device);
+ }
+ NettyDevice slaveDevice = sessionManager.getDevice(data);
+ if (slaveDevice != null) {
+ slaveDevice.setOnlineStatus(1);
+ }
+ //推送json 找到设备绑定的主机进行推送
+ List controlMasters = sessionManager.getAllSlaveControl(device.getSerialNumber());
+ if (!CollectionUtils.isEmpty(controlMasters)) {
+ controlMasters.forEach(x -> {
+ DeviceStatusChangeDTO changeDTO = new DeviceStatusChangeDTO();
+ changeDTO.setDeviceId(data);
+ changeDTO.setStatus(DeviceStatus.online.getCode());
+ changeDTO.setEvent(RespondCode.device_status_changed);
+ ByteBuf buf = ctx.alloc().buffer();
+ CommandUtils.buildCommand(buf, changeDTO, CommandConstant.interaction);
+ String content = buf.toString(CharsetUtil.UTF_8);
+ logger.info("推送下位机登录状态消息:{}", content);
+ if (x.getChannel() != null && x.getChannel().isActive()) {
+ x.getChannel().writeAndFlush(buf);
+ }
+ });
+ }
+ }
+ }
+ } else if (cmdType == CommandConstant.interaction) {
+ String data = new String(bytes, 2, bytes.length - 4, StandardCharsets.UTF_8).trim();
+ logger.info("12指令 :{}", data);
+ DeviceRequestDTO requestDTO = JsonUtils.parseObject(data, DeviceRequestDTO.class);
+ logger.info("12指令解析结果 :{}", JsonUtils.toJsonString(requestDTO));
+ if (requestDTO != null) {
+ if (requestDTO.getRequest() != null) {
+ logger.info("12指令解析,请求转发");
+ requestDispatch(requestDTO, bytes, ctx);
+ } else {
+ if (requestDTO.getRespond() != null) {
+ logger.info("12指令解析,切换权限");
+ responseDispatch(requestDTO.getRespond(), bytes);
+ } else {
+ logger.info("权限切换参数错误");
+ }
+ }
+ }
+ }
+// else if (cmdType == CommandConstant.path) {
+// Channel channel = ctx.channel();
+// String deviceId = channel.attr(ATT_DEVICE_ID).get();
+// NettyDevice device = sessionManager.getDevice(deviceId);
+// if(CollectionUtils.isEmpty(device.getLocationQueue())) return;
+// Queue locationQueue = device.getLocationQueue();
+//
+// }
+ ctx.fireChannelRead(msg); // 如有其他 handler 需要处理
+ } catch (Exception e) {
+ logger.error("deviceConnect channelRead:{}", e.getMessage());
+ }
+ }
+
+ /**
+ * 处理反馈 待扩展
+ */
+ private void responseDispatch(DeviceRespondDTO respond, byte[] bytes) {
+ Boolean switchResult = respond.getSwitchResult();
+ String slaveId = respond.getDeviceId();
+ //找到请求的 设备 他这个是先记录切换请求做个绑定关系,在同意或者反对时能直接找到对应 请求的设备 直接发送报文
+ String masterId = getRequestMaster(slaveId);
+ if (switchResult) {
+ List controlList = sessionManager.getSlaveControl(slaveId);
+ NettyDevice masterDevice = sessionManager.getDevice(masterId);
+ NettyDevice slaveDevice = sessionManager.getDevice(slaveId);
+ if (!CollectionUtils.isEmpty(controlList)) {
+ //如果当前设备有控制
+ controlList.forEach(c -> {
+ logger.info("12指令切换权限,c:{}", c.getConnectorId());
+ if (!c.getConnectorId().equals(masterId)) {
+// c.setCurrentSlaveId(null);
+ c.removeConnectedDevice(slaveDevice);
+ c.setConnectorStatus(ConnectorStatus.RECEIVER);
+ sysUserClientService.updateAliveStatus(c.getConnectorId(), slaveId, 2);
+ }
+ });
+ }
+ logger.info("12指令解析,切换权限,slaveId:{},masterId :{}", slaveId, masterId);
+ masterDevice.setCurrentSlaveId(slaveId);
+ masterDevice.setConnectorStatus(ConnectorStatus.ACTIVE);
+ masterDevice.addConnectedDevice(slaveDevice);
+ sysUserClientService.updateAliveStatus(masterDevice.getConnectorId(), slaveId, 1);
+ } else {
+ logger.info("权限切换请求被拒绝");
+ }
+ NettyDevice masterDevice = sessionManager.getDevice(masterId);
+ controlRequestMap.remove(masterId);
+ Channel channel = masterDevice.getChannel();
+ channel.writeAndFlush(Unpooled.wrappedBuffer(bytes));
+ }
+
+ /**
+ * 获取当前请求目标设备的主机
+ */
+ private String getRequestMaster(String slaveId) {
+ return controlRequestMap.keySet().stream().filter(x -> controlRequestMap.get(x).equals(slaveId)).findFirst().orElse(null);
+ }
+
+ /**
+ * 处理转发0x12请求命令
+ */
+ private void requestDispatch(DeviceRequestDTO requestDTO, byte[] bytes, ChannelHandlerContext ctx) {
+ CommandRequestType request = requestDTO.getRequest();
+ switch (request) {
+ case switch_control:
+ // 权限切换
+ handlePermissionRequest(requestDTO, bytes, ctx);
+ break;
+ }
+ }
+
+ /**
+ * 找到当前设备控制的主机 转发控制请求
+ */
+ private void handlePermissionRequest(DeviceRequestDTO requestDTO, byte[] bytes, ChannelHandlerContext ctx) {
+ logger.info("转发控制请求 :{}", JsonUtils.toJsonString(requestDTO));
+ String slaveId = requestDTO.getDeviceId();
+ List controlList = sessionManager.getSlaveControl(slaveId);
+ String masterId = requestDTO.getUserId() + ":" + requestDTO.getPlatform();
+ logger.info("转发控制请求 controlList :{}", JsonUtils.toJsonString(controlList));
+ if (!CollectionUtils.isEmpty(controlList)) {
+ //如果当前设备有控制
+ logger.info("转发控制请求 发送报文");
+ NettyDevice controlDevice = controlList.get(0);
+ if (controlDevice != null && controlDevice.getChannel() != null && controlDevice.getChannel().isActive()) {
+ controlDevice.getChannel().writeAndFlush(Unpooled.wrappedBuffer(bytes));
+ logger.info("转发控制请求 发送报文结束 deviceId :{}", controlDevice.getConnectorId());
+ controlRequestMap.put(masterId, slaveId);
+ logger.info("转发控制请求 controlRequestMap:{}", JsonUtils.toJsonString(controlRequestMap));
+ }
+
+ } else {
+ //判断直接给权限 还是 判断是否有该账号其他端的的控制有权限
+ NettyDevice activeDevice = sessionManager.getUserControl(requestDTO.getUserId());
+ boolean sendMes = true;
+ if (activeDevice == null) {
+ sendMes = false;
+ } else {
+ if (activeDevice.getConnectorId().equals(masterId)) {
+ sendMes = false;
+ }
+ }
+ if (sendMes) {
+ if (activeDevice.getChannel() != null && activeDevice.getChannel().isActive()) {
+ activeDevice.getChannel().writeAndFlush(Unpooled.wrappedBuffer(bytes));
+ logger.info("转发控制请求2 发送报文结束 deviceId :{}", activeDevice.getConnectorId());
+ controlRequestMap.put(masterId, slaveId);
+ logger.info("转发控制请求2 controlRequestMap:{}", JsonUtils.toJsonString(controlRequestMap));
+ }
+ } else {
+ NettyDevice masterDevice = sessionManager.getDevice(masterId);
+ if (masterDevice != null) {
+ NettyDevice slaveDevice = sessionManager.getDevice(slaveId);
+ masterDevice.setCurrentSlaveId(slaveId);
+ masterDevice.setConnectorStatus(ConnectorStatus.ACTIVE);
+ masterDevice.addConnectedDevice(slaveDevice);
+
+ sysUserClientService.updateAliveStatus(masterDevice.getConnectorId(), slaveId, 1);
+ if (masterDevice.getChannel() != null && masterDevice.getChannel().isActive()) {
+ DeviceRequestDTO reDto = new DeviceRequestDTO();
+ DeviceRespondDTO respond = new DeviceRespondDTO();
+ respond.setSwitchResult(true);
+ respond.setHolder("you");
+ respond.setDeviceId(slaveId);
+ reDto.setRespond(respond);
+ ByteBuf buf = ctx.alloc().buffer();
+ CommandUtils.buildCommand(buf, reDto, CommandConstant.interaction);
+ String content = buf.toString(CharsetUtil.UTF_8);
+ masterDevice.getChannel().writeAndFlush(buf);
+ logger.info("转发控制请求设置当前控制2 发送报文:{} deviceId :{}", content, masterId);
+ }
+ } else {
+
+ }
+ }
+ }
+ }
+
+ private void handleSlaveConnect(ChannelHandlerContext ctx, String slaveId) {
+ ctx.channel().attr(ATT_DEVICE_ID).set(slaveId);
+ sessionManager.registerDevice(slaveId, ctx.channel(), ConnectorType.SLAVE, ConnectorStatus.ACTIVE, null);
+ }
+
+ private void handleMasterConnect(ChannelHandlerContext ctx, String masterId, String slaveId, ConnectorStatus status) {
+ ctx.channel().attr(ATT_DEVICE_ID).set(masterId);
+ sessionManager.registerDevice(masterId, ctx.channel(), ConnectorType.MASTER, status, slaveId);
+// sessionManager.bindConnector(masterId, slaveId);
+ }
+
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ String deviceId = ctx.channel().attr(ATT_DEVICE_ID).get();
+ logger.error("异常设备 {} 出现错误: {}", deviceId, cause.getMessage(), cause);
+ ctx.close();
+ }
+}
diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeaderFooterDecoder.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeaderFooterDecoder.java
new file mode 100644
index 0000000..ffeafb0
--- /dev/null
+++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeaderFooterDecoder.java
@@ -0,0 +1,72 @@
+package com.fastbee.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.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+public class HeaderFooterDecoder 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 = 4096;
+
+ @Override
+ protected void decode(ChannelHandlerContext ctx, ByteBuf in, List