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 out) { + while (true) { + int readerIndex = in.readerIndex(); + int writerIndex = in.writerIndex(); + + int headerPos = findPattern(in, HEADER, readerIndex); + if (headerPos == -1) { + // 没有包头,丢弃所有数据(防止残留字节误判) + in.clear(); + return; + } + + int searchStart = headerPos + HEADER.length; + int footerPos = findPattern(in, FOOTER, searchStart); + if (footerPos == -1) { + // 没有完整帧,保留到下次 + in.readerIndex(headerPos); + return; + } + + int frameLength = footerPos + FOOTER.length - headerPos; + if (frameLength > MAX_FRAME_LENGTH) { + in.readerIndex(footerPos + FOOTER.length); + continue; + } + + // 读取完整帧 + in.readerIndex(headerPos); + ByteBuf frame = in.readRetainedSlice(frameLength); + + byte[] frameBytes = new byte[frame.readableBytes()]; + frame.getBytes(frame.readerIndex(), frameBytes); + + System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")) + + " 收到原始数据:" + Hex.encodeHexString(frameBytes).toUpperCase()); + + out.add(frame); + } + } + + + private int findPattern(ByteBuf buf, byte[] pattern, int fromIndex) { + for (int i = fromIndex; i <= buf.writerIndex() - pattern.length; i++) { + boolean match = true; + for (int j = 0; j < pattern.length; j++) { + if (buf.getByte(i + j) != pattern[j]) { + match = false; + break; + } + } + if (match) return i; + } + return -1; + } +} diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeartbeatHandler.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeartbeatHandler.java new file mode 100644 index 0000000..bfcb3a0 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HeartbeatHandler.java @@ -0,0 +1,94 @@ +package com.fastbee.netty.handler; + + +import com.fastbee.common.Constant; +import com.fastbee.manager.DeviceSessionManager; +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.ChannelInboundHandlerAdapter; +import io.netty.handler.timeout.IdleState; +import io.netty.handler.timeout.IdleStateEvent; +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 java.util.Arrays; +import java.util.concurrent.TimeUnit; + +@Component +@ChannelHandler.Sharable +public class HeartbeatHandler extends ChannelInboundHandlerAdapter { + + @Autowired + private DeviceSessionManager sessionManager; + + private static final byte[] HEARTBEAT_PACKET = {(byte) 0xAB, (byte) 0xAA, (byte) 0xFF, (byte) 0xAA, (byte) 0xAB}; + private static final int HEARTBEAT_INTERVAL = 2; // 5秒发一次心跳 + + private static final Logger logger = LoggerFactory.getLogger(HeartbeatHandler.class); + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + // 连接建立后启动定时任务,主动发心跳 + scheduleHeartbeat(ctx); + super.channelActive(ctx); + } + + private void scheduleHeartbeat(ChannelHandlerContext ctx) { + ctx.executor().scheduleAtFixedRate(() -> { + try { + if (ctx.channel().isActive()) { + String deviceId = ctx.channel().attr(Constant.ATT_DEVICE_ID).get(); + if (!StringUtils.isEmpty(deviceId)) { + ByteBuf heartbeat = Unpooled.wrappedBuffer(HEARTBEAT_PACKET); + ctx.writeAndFlush(heartbeat); + logger.debug("发送心跳包给客户端:{},deviceId:{}", ctx.channel().remoteAddress(), deviceId); + } + } + } catch (Exception e) { + logger.error("heartbeat error", e); + } + }, 0, HEARTBEAT_INTERVAL, TimeUnit.SECONDS); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof ByteBuf) { + ByteBuf buf = (ByteBuf) msg; + if (buf.readableBytes() == HEARTBEAT_PACKET.length) { + byte[] data = new byte[HEARTBEAT_PACKET.length]; + buf.getBytes(0, data); + if (Arrays.equals(data, HEARTBEAT_PACKET)) { + String deviceId = ctx.channel().attr(Constant.ATT_DEVICE_ID).get(); + logger.debug("收到客户端心跳回复 deviceId:{}", deviceId); + return; // 过滤心跳包 + } + } + } + super.channelRead(ctx, msg); + } + + // 处理 IdleState 事件 + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof IdleStateEvent) { + IdleStateEvent idleEvt = (IdleStateEvent) evt; + if (idleEvt.state() == IdleState.READER_IDLE) { + String deviceId = ctx.channel().attr(Constant.ATT_DEVICE_ID).get(); + logger.debug("超时未收到客户端心跳回复,断开连接,deviceId:{}", deviceId); + String currentDeviceId = sessionManager.getDeviceIdByChannel(ctx.channel()); + if (currentDeviceId != null) { + sessionManager.removeDevice(currentDeviceId); + } + ctx.close(); + } + } else { + super.userEventTriggered(ctx, evt); + } + } +} diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexDecoder.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexDecoder.java new file mode 100644 index 0000000..7134f16 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexDecoder.java @@ -0,0 +1,60 @@ +package com.fastbee.netty.handler; + +import com.fastbee.common.CommandConstant; +import com.fastbee.common.Constant; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.binary.Hex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +// 解码器:ByteBuf → 16进制字符串 +@Slf4j +public class HexDecoder extends ByteToMessageDecoder { + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); + + private static final Logger logger = LoggerFactory.getLogger(HexDecoder.class); + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + + byte[] bytes = new byte[in.readableBytes()]; + in.readBytes(bytes); + logger.debug("收到报文:{}", toHexString(bytes)); + out.add(Hex.encodeHexString(bytes)); // 转为16进制字符串 +// String now = LocalDateTime.now().format(FORMATTER); + + Channel channel = ctx.channel(); + String deviceId = channel.attr(Constant.ATT_DEVICE_ID).get(); + if (bytes[2] == CommandConstant.connect) { + logger.debug("收到连接请求:{}", Hex.encodeHexString(bytes)); + } else if (bytes[2] == CommandConstant.remoteControl) { + logger.debug("收到:{}控制请求:{}", deviceId, Hex.encodeHexString(bytes)); + } else if (bytes[2] == CommandConstant.heartbeat) { + logger.debug("收到:{}心跳包", deviceId); + } else if (bytes[2] == CommandConstant.status) { + logger.debug("收到:{}状态信息:{}", deviceId, Hex.encodeHexString(bytes)); + } else if (bytes[2] == CommandConstant.interaction) { + logger.debug("收到:{}0x12状态消息推送:{}", deviceId, Hex.encodeHexString(bytes)); + } else if (bytes[2] == CommandConstant.path) { + logger.debug("收到:{}路径规划消息推送:{}", deviceId, Hex.encodeHexString(bytes)); + } + } + + private static String toHexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString().trim(); + } + +} + diff --git a/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexEncoder.java b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexEncoder.java new file mode 100644 index 0000000..b32e175 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/netty/handler/HexEncoder.java @@ -0,0 +1,18 @@ +package com.fastbee.netty.handler; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import org.apache.commons.codec.binary.Hex; + +import java.util.Arrays; + +// 编码器:16进制字符串 → ByteBuf +public class HexEncoder extends MessageToByteEncoder { + @Override + protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception { + byte[] bytes = Hex.decodeHex(msg.toCharArray()); // 16进制字符串转字节数组 + out.writeBytes(bytes); + } + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/scheduled/ScheduledTask.java b/maibu-netty-server/src/main/java/com/maibu/scheduled/ScheduledTask.java new file mode 100644 index 0000000..36cebba --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/scheduled/ScheduledTask.java @@ -0,0 +1,78 @@ +package com.fastbee.scheduled; + +import com.fastbee.common.NettyCacheKey; +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.iot.domain.DevicePlan; +import com.fastbee.iot.domain.DeviceStatusRecordDTO; +import com.fastbee.iot.mapper.DevicePlanMapper; +import com.fastbee.iot.mapper.DeviceStatusHistoryMapper; +import com.fastbee.memory.GlobalMemory; +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Component +public class ScheduledTask { + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + @Autowired + private RedisCache redisCache; + + @Autowired + private DeviceStatusHistoryMapper deviceStatusHistoryMapper; + + @Autowired + private GlobalMemory globalMemory; + + @Autowired + private DevicePlanMapper devicePlanMapper; + + + //整点更新设备状态 + @Scheduled(cron = "0 0 * * * ?") +// @Scheduled(cron = "0 * * * * ?") + public void executeHourlyTask() { + System.out.println("整点定时任务执行时间: " + dateFormat.format(new Date())); + String key = NettyCacheKey.deviceRunningStatusKey; +// Collection keys = redisCache.keys(key); + Collection keys = redisCache.getListKeyByPrefix(key); + if (!CollectionUtils.isEmpty(keys)) { + keys.forEach(k -> { + DeviceStatusRecordDTO recordDTO = redisCache.getCacheObject(k); + if (recordDTO != null && !CollectionUtils.isEmpty(recordDTO.getHistories())) { +// recordDTO.getHistories().forEach(x -> { +// deviceStatusHistoryMapper.insert(x); +// }); + deviceStatusHistoryMapper.insertBatch(recordDTO.getHistories()); + } + redisCache.deleteObject(k); + }); + } + System.out.println("任务执行完成"); + } + + + //0点刷新任务创建状态 + @Scheduled(cron = "0 0 0 * * ?") + public void refreshDevicePlanTask() { + System.out.println("0点定时刷新状态"); + Map> map = GlobalMemory.devicePlanMap; + if (!CollectionUtils.isEmpty(map.keySet())) { + map.values().forEach(x -> { + if (!CollectionUtils.isEmpty(x)) { + x.forEach(y -> { + y.setDayTriggerFlag(false); + }); + } + }); + } + System.out.println("0点定时刷新状态执行完成"); + } +} \ No newline at end of file diff --git a/maibu-netty-server/src/main/java/com/maibu/service/DevicePathService.java b/maibu-netty-server/src/main/java/com/maibu/service/DevicePathService.java new file mode 100644 index 0000000..12cf24a --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/DevicePathService.java @@ -0,0 +1,16 @@ +package com.fastbee.service; + +import org.springframework.stereotype.Service; + +import java.util.LinkedList; +import java.util.Queue; + +@Service +public class DevicePathService { + + + + + + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/service/DevicePlanTaskMonitorService.java b/maibu-netty-server/src/main/java/com/maibu/service/DevicePlanTaskMonitorService.java new file mode 100644 index 0000000..e2ac183 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/DevicePlanTaskMonitorService.java @@ -0,0 +1,275 @@ +package com.fastbee.service; + + +import com.alibaba.fastjson2.JSON; +import com.fastbee.common.NettyCacheKey; +import com.fastbee.dto.DeviceErrorPushDTO; +import com.fastbee.dto.DeviceErrorPushDetail; +import com.fastbee.dto.NettyDevice; +import com.fastbee.enums.RespondCode; +import com.fastbee.iot.domain.*; +import com.fastbee.iot.enums.DeviceTaskPlanRepeatType; +import com.fastbee.iot.enums.DeviceTaskStaus; +import com.fastbee.iot.mapper.DevicePlanMapper; +import com.fastbee.iot.mapper.DevicePlanTaskMapper; +import com.fastbee.iot.mapper.WorkRecordMapper; +import com.fastbee.iot.model.WorkRecord; +import com.fastbee.manager.DeviceSessionManager; +import com.fastbee.memory.GlobalMemory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +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.Service; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class DevicePlanTaskMonitorService { + + + @Autowired + private GlobalMemory globalMemory; + + @Autowired + private DevicePlanTaskMapper devicePlanTaskMapper; + + @Autowired + private DevicePlanMapper devicePlanMapper; + + @Autowired + private DeviceSessionManager deviceSessionManager; + + @Autowired + private WorkRecordMapper workRecordMapper; + + + /** + * 计划迟的任务生成机器执行任务 + */ + public void generatePlanTask() throws InterruptedException { + + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + try { + Map> planMap = GlobalMemory.devicePlanMap; + if (!CollectionUtils.isEmpty(planMap.values())) { + Iterator iterator = planMap.keySet().iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + List list = planMap.get(key); + if (!CollectionUtils.isEmpty(list)) { + list.forEach(this::createPlanTask); + } + } + } + } catch (Exception e) { + log.error("执行任务生成线程错误", e); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + } + + private void createPlanTask(DevicePlan devicePlan) { + if (devicePlan.isDayTriggerFlag()) return; //当天应该创建的全部创建完 + DeviceTaskPlanRule rules = devicePlan.getDeviceTaskPlanRules(); + if (rules != null) { + DeviceTaskPlanRepeatType type = rules.getType(); + switch (type) { + case DAY: + dayPlan(devicePlan); + break; + case WEEK: + weekPlan(devicePlan); + break; + case MONTH: + monthPlan(devicePlan); + break; +// case ONCE: +// oncePlan(devicePlan); +// break; + } + } + } + + public void dayPlan(DevicePlan devicePlan) { + DeviceTaskPlanRule rules = devicePlan.getDeviceTaskPlanRules(); + LocalTime nowTime = LocalTime.now(); + LocalTime dayTime = LocalTime.parse(rules.getDayTime()); + if (nowTime.isAfter(dayTime)) { + DevicePlanTask devicePlanTask = new DevicePlanTask(); + devicePlanTask.setPlanId(devicePlan.getId()); + devicePlanTask.setDeviceId(devicePlan.getDeviceId()); + devicePlanTask.setTaskStaus(DeviceTaskStaus.NEW); + devicePlanTask.setRouteId(devicePlan.getRouteId()); + devicePlanTask.setCreateTime(LocalDateTime.now()); + devicePlanTask.setTaskType("");//待拓展任务类型 + devicePlanTaskMapper.insert(devicePlanTask); + globalMemory.addDevicePlanPrepare(devicePlanTask); + //啥时候重置?每日0点? + devicePlan.setDayTriggerFlag(true); + devicePlan.setUpdateTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.EXECUTING); + //todo 怎么判断任务全部排完了?排完了就删除 + LocalDate today = LocalDate.now(); // 获取当前日期(系统时区) + LocalDate endTime = rules.getEndTime(); + if (endTime != null && endTime.isEqual(today)) { + devicePlan.setFinishTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.FINISH); + GlobalMemory.removeDevicePlan(devicePlan); + } + devicePlanMapper.updateById(devicePlan); + } + } + + public void weekPlan(DevicePlan devicePlan) { + DeviceTaskPlanRule rules = devicePlan.getDeviceTaskPlanRules(); + LocalTime nowTime = LocalTime.now(); + List days = devicePlan.getDeviceTaskPlanRules().getDays(); + // 1. 获取当前日期 + LocalDate today = LocalDate.now(); + DayOfWeek dayOfWeek = today.getDayOfWeek(); + LocalTime dayTime = LocalTime.parse(rules.getDayTime()); + if (!CollectionUtils.isEmpty(days) && days.contains(dayOfWeek) && nowTime.isAfter(dayTime)) { + DevicePlanTask devicePlanTask = new DevicePlanTask(); + devicePlanTask.setPlanId(devicePlan.getId()); + devicePlanTask.setDeviceId(devicePlan.getDeviceId()); + devicePlanTask.setTaskStaus(DeviceTaskStaus.NEW); + devicePlanTask.setRouteId(devicePlan.getRouteId()); + devicePlanTask.setCreateTime(LocalDateTime.now()); + devicePlanTask.setTaskType("");//待拓展任务类型 + devicePlanTaskMapper.insert(devicePlanTask); + globalMemory.addDevicePlanPrepare(devicePlanTask); + //啥时候重置?每日0点? + devicePlan.setDayTriggerFlag(true); + devicePlan.setUpdateTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.EXECUTING); + //todo 怎么判断任务全部排完了?排完了就删除 + LocalDate endTime = rules.getEndTime(); + if (endTime != null && endTime.isEqual(today)) { + devicePlan.setFinishTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.FINISH); + GlobalMemory.removeDevicePlan(devicePlan); + } + devicePlanMapper.updateById(devicePlan); + } + } + + public void monthPlan(DevicePlan devicePlan) { + DeviceTaskPlanRule rules = devicePlan.getDeviceTaskPlanRules(); + List dayOfMonthList = rules.getDayOfMonth(); + LocalDate today = LocalDate.now(); + LocalTime nowTime = LocalTime.now(); + int dayOfMonth = today.getDayOfMonth(); + LocalTime dayTime = LocalTime.parse(rules.getDayTime()); + if (!CollectionUtils.isEmpty(dayOfMonthList) && dayOfMonthList.contains(dayOfMonth) && nowTime.isAfter(dayTime)) { + DevicePlanTask devicePlanTask = new DevicePlanTask(); + devicePlanTask.setPlanId(devicePlan.getId()); + devicePlanTask.setDeviceId(devicePlan.getDeviceId()); + devicePlanTask.setTaskStaus(DeviceTaskStaus.NEW); + devicePlanTask.setRouteId(devicePlan.getRouteId()); + devicePlanTask.setCreateTime(LocalDateTime.now()); + devicePlanTask.setTaskType("");//待拓展任务类型 + devicePlanTaskMapper.insert(devicePlanTask); + globalMemory.addDevicePlanPrepare(devicePlanTask); + //啥时候重置?每日0点? + devicePlan.setDayTriggerFlag(true); + devicePlan.setUpdateTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.EXECUTING); + //todo 怎么判断任务全部排完了?排完了就删除 + LocalDate endTime = rules.getEndTime(); + if (endTime != null && endTime.isEqual(today)) { + devicePlan.setFinishTime(LocalDateTime.now()); + devicePlan.setTaskStaus(DeviceTaskStaus.FINISH); + GlobalMemory.removeDevicePlan(devicePlan); + } + devicePlanMapper.updateById(devicePlan); + } + } + + public void oncePlan(DevicePlan devicePlan) { +// DeviceTaskPlanRule rules = devicePlan.getDeviceTaskPlanRules(); + + + } + + public void doLoop() throws InterruptedException { + //prepare 进执行execute的逻辑 + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + try { + Map> planMap = GlobalMemory.devicePlanTaskPrepareMap; + if (!CollectionUtils.isEmpty(planMap.keySet())) { + planMap.keySet().forEach(key -> { + NettyDevice device = deviceSessionManager.getDevice(key); + //判断是否能进execute + if (device != null && device.getCurrentTaskId() == null && device.getOnlineStatus() != null && device.getOnlineStatus() == 1) { + List list = planMap.get(key); + if (!CollectionUtils.isEmpty(list)) { + list.removeIf(Objects::isNull); + list.sort( + Comparator.comparing( + DevicePlanTask::getStartTime, + Comparator.nullsLast(Comparator.naturalOrder()) + ) + ); + if (!CollectionUtils.isEmpty(list)) { + DevicePlanTask devicePlanTask = list.get(0); + String deviceId = devicePlanTask.getDeviceId(); + if (!StringUtils.isEmpty(deviceId) && deviceId.equals(key)) { + GlobalMemory.addDevicePlanExecute(devicePlanTask); + device.setCurrentTaskId(devicePlanTask.getId()); + GlobalMemory.removeDevicePlanPrepare(devicePlanTask); + } + } + } + } + }); + } + } catch (Exception e) { + log.error("执行任务prepare线程错误", e); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + } + + public void doExecute() { + //prepare 进执行execute的逻辑 + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + try { + Map planMap = GlobalMemory.devicePlanTaskExecuteMap; + if (!CollectionUtils.isEmpty(planMap.keySet())) { + planMap.keySet().forEach(key -> { + NettyDevice device = deviceSessionManager.getDevice(key); + if (device != null && device.getCurrentTaskId() == null && device.getOnlineStatus() != null && device.getOnlineStatus() == 1) { + DevicePlanTask devicePlanTask = planMap.get(key); + if (devicePlanTask != null && DeviceTaskStaus.NEW.equals(devicePlanTask.getTaskStaus())) { + devicePlanTask.setTaskStaus(DeviceTaskStaus.EXECUTING); + //todo 任务结束的逻辑,执行完路线规划更新 任务完成 + Long routId = devicePlanTask.getRouteId(); + WorkRecord workRecord = workRecordMapper.selectWorkRecordById(routId); + if (workRecord != null) { + //todo 执行转化 并更新到nettyDevice + //todo 更新到device + + } + } + } + }); + } + } catch (Exception e) { + log.error("执行任务execute线程错误", e); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + } + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/service/DeviceTaskService.java b/maibu-netty-server/src/main/java/com/maibu/service/DeviceTaskService.java new file mode 100644 index 0000000..40e278b --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/DeviceTaskService.java @@ -0,0 +1,347 @@ +package com.fastbee.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fastbee.common.core.domain.model.LoginUser; +import com.fastbee.dto.DevicePlanStatisticsDTO; +import com.fastbee.dto.DevicePlanTaskStatisticsDTO; +import com.fastbee.dto.DeviceTaskQueryDTO; +import com.fastbee.dto.DeviceWorkStatisticsDTO; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.domain.DevicePlan; +import com.fastbee.iot.domain.DevicePlanTask; +import com.fastbee.iot.domain.DeviceTaskPlanRule; +import com.fastbee.iot.enums.DeviceTaskStaus; +import com.fastbee.iot.mapper.DeviceMapper; +import com.fastbee.iot.mapper.DevicePlanMapper; +import com.fastbee.iot.mapper.DevicePlanTaskMapper; +import com.fastbee.iot.mapper.WorkRecordMapper; +import com.fastbee.iot.model.WorkRecord; +import com.fastbee.memory.GlobalMemory; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.RequestParam; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; + +@Service +@Slf4j +public class DeviceTaskService { + + @Autowired + private DevicePlanMapper devicePlanMapper; + + @Autowired + private DevicePlanTaskMapper devicePlanTaskMapper; + + @Autowired + private DeviceMapper deviceMapper; + + @Autowired + private WorkRecordMapper workRecordMapper; + + + public int insertOrUpdate(DevicePlan devicePlan, LoginUser loginUser) { + Long id = devicePlan.getId(); + if (id != null) { + devicePlan.setUpdateBy(loginUser.getUsername()); + devicePlan.setUpdateTime(LocalDateTime.now()); + transferDays(devicePlan); + int i = devicePlanMapper.updateById(devicePlan); + GlobalMemory.addDevicePlan(devicePlan); + return i; + } else { + devicePlan.setTaskStaus(DeviceTaskStaus.NEW); + devicePlan.setCreateTime(LocalDateTime.now()); + devicePlan.setCreateBy(loginUser.getUsername()); + transferDays(devicePlan); + int i = devicePlanMapper.insert(devicePlan); + GlobalMemory.addDevicePlan(devicePlan); + return i; + } + } + + public void transferDays(DevicePlan devicePlan) { + DeviceTaskPlanRule deviceTaskPlanRules = devicePlan.getDeviceTaskPlanRules(); + if (deviceTaskPlanRules != null && !CollectionUtils.isEmpty(deviceTaskPlanRules.getDays())) { + List days = new ArrayList<>(); + deviceTaskPlanRules.getDays().forEach(x -> { + String day = null; + switch (x) { + case MONDAY: + day = "周一"; + break; + case TUESDAY: + day = "周二"; + break; + case WEDNESDAY: + day = "周三"; + break; + case THURSDAY: + day = "周四"; + break; + case FRIDAY: + day = "周五"; + break; + case SATURDAY: + day = "周六"; + break; + case SUNDAY: + day = "周日"; + break; + } + days.add(day); + }); + devicePlan.getDeviceTaskPlanRules().setDaysTranslate(days); + } + } + + public List getDeviceTaskByUser(Long userId) { + if (userId != null) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DevicePlan::getUserId, userId); + return transferDevicePlanData(devicePlanMapper.selectList(queryWrapper)); + } else { + return new ArrayList<>(); + } + } + + public List getDeviceTask(DeviceTaskQueryDTO dto) { + if (dto != null) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (dto.getUserId() != null) { + queryWrapper.eq(DevicePlan::getUserId, dto.getUserId()); + } + if (dto.getDeviceId() != null) { + queryWrapper.eq(DevicePlan::getDeviceId, dto.getDeviceId()); + } + if (dto.getTaskStaus() != null) { + queryWrapper.eq(DevicePlan::getTaskStaus, dto.getTaskStaus()); + } + if (dto.getPlanName() != null) { + queryWrapper.like(DevicePlan::getPlanName, dto.getPlanName()); + } + queryWrapper.eq(DevicePlan::getDelFlag, 0); + List list = devicePlanMapper.selectList(queryWrapper); + if (!CollectionUtils.isEmpty(list)) { + list.forEach(x -> { + Device device = deviceMapper.selectDeviceBySerialNumber(x.getDeviceId()); + if (device != null) { + x.setDeviceAlias(device.getDeviceAlias()); + } + }); + } + return transferDevicePlanData(list); + } else { + return new ArrayList<>(); + } + } + + public int delete(Long id) { + DevicePlan planTask = devicePlanMapper.selectById(id); + if (planTask != null) { + planTask.setDelFlag(1); + GlobalMemory.removeDevicePlan(planTask); + return devicePlanMapper.updateById(planTask); + } + return 0; + } + + public void start(Long id, LoginUser loginUser) { + if (id != null) { + DevicePlan planTask = devicePlanMapper.selectById(id); + if (planTask != null) { + planTask.setTaskStaus(DeviceTaskStaus.EXECUTING); + planTask.setUpdateTime(LocalDateTime.now()); + planTask.setUpdateBy(loginUser.getUsername()); + devicePlanMapper.updateById(planTask); + } + } + } + + public List getDevicePlanStatistics(Long userId) { + List list = new ArrayList<>(); + LocalDate today = LocalDate.now(); + LocalDateTime monthStart = LocalDateTime.of( + today.getYear(), // 当前年 + today.getMonth(), // 当前月 + 1, // 当月第一天 + 0, 0, 0, 0 // 时:分:秒:纳秒(00:00:00.000) + ); + LocalDate lastDayOfMonth = today.withDayOfMonth(today.lengthOfMonth()); // 获取当月最后一天 + LocalDateTime monthEnd = LocalDateTime.of( + lastDayOfMonth.getYear(), + lastDayOfMonth.getMonth(), + lastDayOfMonth.getDayOfMonth(), + 23, 59, 59, 999_999_999 // 时:分:秒:纳秒(23:59:59.999) + ); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.ge(DevicePlan::getCreateTime, monthStart) + .le(DevicePlan::getCreateTime, monthEnd) + .eq(DevicePlan::getUserId,userId); + List devicePlans = transferDevicePlanData(devicePlanMapper.selectList(queryWrapper)); + if (!CollectionUtils.isEmpty(devicePlans)) { + List dates = getMonthDays(); + for (LocalDate date : dates) { + List plans = devicePlans.stream().filter(x -> x.getCreateTime().toLocalDate().equals(date)).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(plans)) { + DevicePlanStatisticsDTO dto = new DevicePlanStatisticsDTO(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M.d"); + dto.setName(formatter.format(date)); + long count = plans.stream().filter(x -> DeviceTaskStaus.FINISH.equals(x.getTaskStaus())).count(); + dto.setCompleted((int) count); + dto.setPlanned(plans.size()); + list.add(dto); + } + } + } + return list; + } + + public List getMonthDays() { + LocalDate today = LocalDate.now(); + int totalDays = today.lengthOfMonth(); + LocalDate firstDayOfMonth = LocalDate.of(today.getYear(), today.getMonth(), 1); + List allDaysOfCurrentMonth = new ArrayList<>(); + for (int i = 0; i < totalDays; i++) { + // 每次累加1天,获取当月每一天 + LocalDate currentDay = firstDayOfMonth.plusDays(i); + allDaysOfCurrentMonth.add(currentDay); + } + return allDaysOfCurrentMonth; + } + + public List getDevicePlanTaskStatistics(Long userId) { + List list = new ArrayList<>(); + // 1. 获取当前日期 + LocalDate today = LocalDate.now(); + // 2. 构造当前月份的起始时间:当月第一天 00:00:00 + LocalDateTime monthStart = LocalDateTime.of( + today.getYear(), // 当前年 + today.getMonth(), // 当前月 + 1, // 当月第一天 + 0, 0, 0, 0 // 时:分:秒:纳秒(00:00:00.000) + ); + // 3. 构造当前月份的结束时间:当月最后一天 23:59:59.999 + LocalDate lastDayOfMonth = today.withDayOfMonth(today.lengthOfMonth()); // 获取当月最后一天 + LocalDateTime monthEnd = LocalDateTime.of( + lastDayOfMonth.getYear(), + lastDayOfMonth.getMonth(), + lastDayOfMonth.getDayOfMonth(), + 23, 59, 59, 999_999_999 // 时:分:秒:纳秒(23:59:59.999) + ); + // 4. 构建 LambdaQueryWrapper 进行范围查询 + LambdaQueryWrapper query = new LambdaQueryWrapper<>(); + query.eq(DevicePlan::getUserId,userId); + List devicePlans = transferDevicePlanData(devicePlanMapper.selectList(query)); + if(CollectionUtils.isEmpty(devicePlans)) return new ArrayList<>(); + List planIds = devicePlans.stream().map(DevicePlan::getId).collect(Collectors.toList()); + if(CollectionUtils.isEmpty(planIds)) return new ArrayList<>(); + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + // 核心:筛选 createTime 大于等于起始时间,且小于等于结束时间 + queryWrapper.ge(DevicePlanTask::getCreateTime, monthStart) + .le(DevicePlanTask::getCreateTime, monthEnd) + .in(DevicePlanTask::getPlanId,planIds); + List devicePlanTasks = transferDeviceTaskData(devicePlanTaskMapper.selectList(queryWrapper)); + if (!CollectionUtils.isEmpty(devicePlanTasks)) { + List dates = getMonthDays(); + for (LocalDate date : dates) { + List planTasks = devicePlanTasks.stream().filter(x -> x.getCreateTime().toLocalDate().equals(date)).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(planTasks)) { + DevicePlanTaskStatisticsDTO dto = new DevicePlanTaskStatisticsDTO(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("M.d"); + dto.setDate(formatter.format(date)); + long finishCount = planTasks.stream().filter(x -> DeviceTaskStaus.FINISH.equals(x.getTaskStaus())).count(); + dto.setSuccess((int) finishCount); + long failedCount = planTasks.stream().filter(x -> DeviceTaskStaus.FAILED.equals(x.getTaskStaus())).count(); + dto.setFailed((int) failedCount); + long pauseCount = planTasks.stream().filter(x -> DeviceTaskStaus.PAUSE.equals(x.getTaskStaus())).count(); + dto.setPaused((int) pauseCount); + list.add(dto); + } + } + } + return list; + } + + + public WorkRecord concurrentDevicePlanTask(String deviceId) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(DevicePlanTask::getDeviceId, deviceId) + .eq(DevicePlanTask::getTaskStaus, DeviceTaskStaus.EXECUTING); + List devicePlanTasks = devicePlanTaskMapper.selectList(queryWrapper); + Long routId = CollectionUtils.isEmpty(devicePlanTasks) ? null : devicePlanTasks.get(0).getRouteId(); + return routId == null ? null : workRecordMapper.selectWorkRecordById(routId); + } + + + public List workStatistics(Long userId) { + //todo update + + List result = new ArrayList<>(); + Random random = new Random(); + int times = random.nextInt(100); + int s = random.nextInt(1000); + DeviceWorkStatisticsDTO dto = new DeviceWorkStatisticsDTO(); + dto.setLabel("割草次数"); + dto.setValue(times); + DeviceWorkStatisticsDTO dto2 = new DeviceWorkStatisticsDTO(); + dto2.setLabel("割草时长(s)"); + dto2.setValue(s); + DeviceWorkStatisticsDTO dto3 = new DeviceWorkStatisticsDTO(); + dto3.setLabel("割草面积(m2)"); + dto3.setValue(s * 5); + result.add(dto); + result.add(dto2); + result.add(dto3); + return result; + } + + + public List deviceTaskPool(DeviceTaskQueryDTO dto) { + List list = new ArrayList<>(); + GlobalMemory.devicePlanTaskPrepareMap.values().forEach(list::addAll); + list.addAll(GlobalMemory.devicePlanTaskExecuteMap.values()); + if (dto != null) { + if (dto.getTaskStaus() != null) { + list = list.stream().filter(x -> x.getTaskStaus().equals(dto.getTaskStaus())).collect(Collectors.toList()); + } + if (!StringUtils.isEmpty(dto.getDeviceId())) { + list = list.stream().filter(x -> x.getDeviceId().equals(dto.getDeviceId())).collect(Collectors.toList()); + } + if (dto.getPlanId() != null) { + list = list.stream().filter(x -> x.getPlanId().equals(dto.getPlanId())).collect(Collectors.toList()); + } + } + return transferDeviceTaskData(list); + } + + + public List transferDevicePlanData(List list) { + if (CollectionUtils.isEmpty(list)) return list; + list.forEach(x -> { + DeviceTaskStaus taskStaus = x.getTaskStaus(); + x.setTaskStausTranslate(taskStaus.getDescription()); + }); + return list; + } + + public List transferDeviceTaskData(List list) { + if (CollectionUtils.isEmpty(list)) return list; + list.forEach(x -> { + DeviceTaskStaus taskStaus = x.getTaskStaus(); + x.setTaskStausTranslate(taskStaus.getDescription()); + }); + return list; + } + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/service/DeviceThreadService.java b/maibu-netty-server/src/main/java/com/maibu/service/DeviceThreadService.java new file mode 100644 index 0000000..36ee11d --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/DeviceThreadService.java @@ -0,0 +1,166 @@ +package com.fastbee.service; + + +import cn.hutool.core.io.IoUtil; +import com.alibaba.fastjson2.JSON; +import com.fastbee.common.CommandConstant; +import com.fastbee.common.NettyCacheKey; +import com.fastbee.common.core.redis.RedisCache; +import com.fastbee.dto.DeviceErrorPushDTO; +import com.fastbee.dto.DeviceErrorPushDetail; +import com.fastbee.dto.NettyDevice; +import com.fastbee.iot.domain.DeviceRunningStatusHistory; +import com.fastbee.entity.ErrorIdentificationStandard; +import com.fastbee.enums.CompareEnum; +import com.fastbee.enums.RespondCode; +import com.fastbee.iot.domain.DeviceStatusRecordDTO; +import com.fastbee.manager.DeviceSessionManager; +import com.fastbee.mapper.ErrorIdentificationStandardMapper; +import com.fastbee.memory.GlobalMemory; +import com.fastbee.netty.handler.DeviceConnectHandler; +import com.fastbee.utils.CommandUtils; +import com.fastbee.utils.CompareUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class DeviceThreadService { + + @Autowired + private ResourceLoader resourceLoader; + + @Autowired + private GlobalMemory globalMemory; + + @Autowired + private RedisCache redisCache; + + @Autowired + private DeviceSessionManager sessionManager; + + @Autowired + private ErrorIdentificationStandardMapper standardMapper; + + + public void deviceErrorMonitor() throws InterruptedException, IOException { + initStandard(); + + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + executor.scheduleWithFixedDelay(() -> { + try { + String key = NettyCacheKey.deviceRunningStatusKey; + Collection keys = redisCache.getListKeyByPrefix(key); + if (!CollectionUtils.isEmpty(keys)) { + keys.forEach(k -> { + DeviceStatusRecordDTO recordDTO = redisCache.getCacheObject(k); + if (recordDTO != null && !CollectionUtils.isEmpty(recordDTO.getHistories())) { + recordDTO.getHistories().sort(Comparator.comparing(DeviceRunningStatusHistory::getCreateTime).reversed()); + DeviceRunningStatusHistory history = recordDTO.getHistories().get(0); + List compared = compareErrorStandard(history, globalMemory.standard); + + String[] parts = k.split(":"); + String deviceId = parts[1]; + + DeviceErrorPushDTO pushDTO = new DeviceErrorPushDTO(); + pushDTO.setDeviceId(deviceId); + pushDTO.setTime(System.currentTimeMillis()); + pushDTO.setDetails(compared); + pushDTO.setEvent(RespondCode.device_error_push); + //todo push 推送到前端 + pushErrorMessage(deviceId, JSON.toJSONString(pushDTO)); + } + }); + } + } catch (Exception e) { + log.error("执行监测错误线程错误:{}", e.getMessage()); + } + }, 0, 2, TimeUnit.MINUTES); + } + + public void initStandard() throws IOException { + if (globalMemory.standard == null) { + List standards = standardMapper.selectList(); + if (CollectionUtils.isEmpty(standards)) { + // 读取resource目录下的device-config.json文件 + Resource resource = resourceLoader.getResource("classpath:errorStandard.json"); + // 使用fastjson2转换为DeviceConfig对象 + String jsonContent = IoUtil.readUtf8(resource.getInputStream()); + globalMemory.standard = JSON.parseArray(jsonContent, ErrorIdentificationStandard.class); + } else { + globalMemory.standard = standards; + } + } + } + + public void pushErrorMessage(String deviceId, String dto) { + NettyDevice device = sessionManager.getDevice(deviceId); + if (device != null) { + ByteBuf buf = Unpooled.buffer(); + CommandUtils.buildCommand(buf, dto, CommandConstant.interaction); + if (device.getChannel() != null && device.getChannel().isActive()) { + device.getChannel().writeAndFlush(buf); + } + } + } + + + public List compareErrorStandard(DeviceRunningStatusHistory history, List standards) { + if (history == null || CollectionUtils.isEmpty(standards)) return null; + List list = new ArrayList<>(); + standards.forEach(standard -> { + String fieldName = standard.getField(); + String compareValues = standard.getCompareValues(); + CompareEnum compareType = standard.getCompareType(); + String targetValue = knownClassFieldFinding(fieldName, history); + if (!StringUtils.isEmpty(compareValues) && !StringUtils.isEmpty(targetValue)) { + boolean result = CompareUtils.compare(compareType, compareValues, targetValue); + if (!result) { + DeviceErrorPushDetail detail = new DeviceErrorPushDetail(); + detail.setErrorName(standard.getErrorName()); + detail.setDescription(standard.getErrorDescription()); + detail.setValue(targetValue); + detail.setRange(compareValues); + list.add(detail); + } + } + }); + return list; + } + + + private static String knownClassFieldFinding(String fieldName, DeviceRunningStatusHistory history) { + try { + // 1. 获取类的Class对象 + Class historyClass = DeviceRunningStatusHistory.class; + Field field = historyClass.getDeclaredField(fieldName); + field.setAccessible(true); + Object value = field.get(history); + return (String) value; + } catch (NoSuchFieldException | IllegalAccessException e) { + System.out.println("获取字段失败: " + e.getMessage()); + log.error("获取属性值失败:{}", e.getMessage()); + } + return null; + } + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/service/NettyDeviceService.java b/maibu-netty-server/src/main/java/com/maibu/service/NettyDeviceService.java new file mode 100644 index 0000000..c12a2a4 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/NettyDeviceService.java @@ -0,0 +1,44 @@ +package com.fastbee.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fastbee.iot.domain.DeviceRunningStatusHistory; +import com.fastbee.entity.ErrorIdentificationStandard; +import com.fastbee.iot.mapper.DeviceStatusHistoryMapper; +import com.fastbee.mapper.ErrorIdentificationStandardMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@Slf4j +public class NettyDeviceService { + + @Autowired + private DeviceStatusHistoryMapper deviceStatusHistoryMapper; + + @Autowired + private ErrorIdentificationStandardMapper errorIdentificationStandardMapper; + + public List statusHistory(DeviceRunningStatusHistory history) { + + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + if (!StringUtils.isEmpty(history.getDeviceId())) { + queryWrapper.eq(DeviceRunningStatusHistory::getDeviceId, history.getDeviceId()); + } + return deviceStatusHistoryMapper.selectList(queryWrapper); + } + + + public List allErrorIdentificationStandard() { + return errorIdentificationStandardMapper.selectList(null); + } + + public int updateErrorIdentification(ErrorIdentificationStandard standard) { + return errorIdentificationStandardMapper.updateById(standard); + } + + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/service/TransferDeviceService.java b/maibu-netty-server/src/main/java/com/maibu/service/TransferDeviceService.java new file mode 100644 index 0000000..6f450b3 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/service/TransferDeviceService.java @@ -0,0 +1,386 @@ +package com.fastbee.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.fastbee.common.core.domain.AjaxResult; +import com.fastbee.common.core.domain.entity.UserClient; +import com.fastbee.common.core.domain.model.LoginUser; +import com.fastbee.common.exception.ServiceException; +import com.fastbee.common.utils.MessageUtils; +import com.fastbee.dto.*; +import com.fastbee.enums.ConnectorStatus; +import com.fastbee.iot.domain.Device; +import com.fastbee.iot.mapper.DeviceMapper; +import com.fastbee.iot.service.IDeviceService; +import com.fastbee.manager.DeviceSessionManager; +import com.fastbee.system.mapper.SysUserClientMapper; +import com.fastbee.system.service.ISysUserClientService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +@Service +@Slf4j +public class TransferDeviceService { + + @Autowired + private IDeviceService iDeviceService; + + @Autowired + private ISysUserClientService sysUserClientService; + + @Autowired + private DeviceSessionManager sessionManager; + + @Autowired + private SysUserClientMapper userClientMapper; + + @Autowired + private DeviceMapper deviceMapper; + + + public AjaxResult bind(DeviceBindDTO bindDTO, LoginUser user) { + try { + String deviceId = bindDTO.getDeviceId(); + String deviceAlias = bindDTO.getDeviceAlias(); + //判断当前设备是否存 + Device device = iDeviceService.selectDeviceBySerialNumber(deviceId); + if (device == null) { + return AjaxResult.error("设备不存在"); + } else { + Long tenantId = device.getTenantId(); + if (tenantId != -1) { + if (!tenantId.equals(user.getUserId())) { + return AjaxResult.error("设备有其他用户绑定"); + } + } + device.setTenantId(user.getUserId()); + device.setTenantName(user.getUsername()); + device.setUpdateTime(new Date()); + device.setDeviceAlias(deviceAlias); + device.setActiveTime(new Date()); + iDeviceService.updateDeviceBySelf(device); +// deviceMapper.updateById(device); + return AjaxResult.success(true); + } + } catch (Exception e) { + log.error("设备绑定失败: {}", e.getMessage(), e); + return AjaxResult.error("设备绑定失败:" + e.getMessage()); + } + } + + public AjaxResult unbind(DeviceBindDTO bindDTO, LoginUser user) { + try { + String deviceId = bindDTO.getDeviceId(); + //判断当前设备是否存 + Device device = iDeviceService.selectDeviceBySerialNumber(deviceId); + if (device == null) { + return AjaxResult.error("设备不存在"); + } else { + Long tenantId = device.getTenantId(); + if (tenantId == -1) { + return AjaxResult.error("设备未绑定"); + } + if (!tenantId.equals(user.getUserId())) { + return AjaxResult.error("无解绑权限"); + } + device.setTenantId(-1L); + device.setTenantName(""); + device.setUpdateTime(new Date()); + device.setDeviceAlias(""); +// deviceMapper.updateById(device); + iDeviceService.updateDeviceBySelf(device); + //判断是否正在控制 如果正在控制,删除控制状态 + LambdaQueryWrapper lambdaWrapper = new LambdaQueryWrapper<>(); + lambdaWrapper.eq(UserClient::getDeviceName, deviceId); + List users = userClientMapper.selectList(lambdaWrapper); + log.info("users:{}", users); + if (!CollectionUtils.isEmpty(users)) { + users.forEach(userClient -> { + log.info("userClient:{}", userClient); + if (userClient != null) { + String masterId = userClient.getClientName(); + NettyDevice deviceMaster = sessionManager.getDevice(masterId); + if (deviceMaster != null) { + String oldSlave = userClient.getDeviceName(); + //删除主的连接关系 + deviceMaster.getConnectedConnectors().removeIf(x -> x.getConnectorId().equals(oldSlave)); + //删除从的连接关系 + NettyDevice oldSlaveDevice = sessionManager.getDevice(oldSlave); + if (oldSlaveDevice != null && !CollectionUtils.isEmpty(oldSlaveDevice.getConnectedConnectors())) { + Set connectedDevices = oldSlaveDevice.getConnectedConnectors(); + connectedDevices.removeIf(x -> x.getConnectorId().equals(masterId)); + } + } + sysUserClientService.updateAliveStatus(masterId, "", 0); + log.info("updateById:{}", userClient.getClientName()); + } + }); + } + return AjaxResult.success(true); + } + } catch (Exception e) { + log.error("设备解绑失败: {}", e.getMessage(), e); + return AjaxResult.error("设备解绑失败:" + e.getMessage()); + } + } + + + public AjaxResult updateDeviceAlias(DeviceBindDTO bindDTO, LoginUser user) { + try { + String deviceId = bindDTO.getDeviceId(); + //判断当前设备是否存 + Device device = iDeviceService.selectDeviceBySerialNumber(deviceId); + if (device == null) { + return AjaxResult.error("设备不存在"); + } else { + Long tenantId = device.getTenantId(); + if (tenantId == -1) { + return AjaxResult.error("设备未绑定"); + } + if (!tenantId.equals(user.getUserId())) { + return AjaxResult.error("无操作权限"); + } + device.setDeviceAlias(bindDTO.getDeviceAlias()); + device.setUpdateTime(new Date()); + device.setCreateBy(user.getUsername()); + iDeviceService.updateDeviceBySelf(device); + return AjaxResult.success(true); + } + } catch (Exception e) { + log.error("设备更新失败: {}", e.getMessage(), e); + return AjaxResult.error("设备更新失败:" + e.getMessage()); + } + } + + public synchronized boolean switchDevice(DeviceBindDTO bindDTO, LoginUser user) { + try { + String slaveId = bindDTO.getDeviceId(); + String platform = bindDTO.getPlatform(); + String userName = user.getUsername(); + String masterId = userName + ":" + platform; + //todo 判断是否有权限 正在控制才能切 + //todo 离线没有控制权的怎么切 + log.info("switchDevice:{}", masterId); + NettyDevice deviceMaster = sessionManager.getDevice(masterId); + log.info("deviceMaster:{}", deviceMaster); + if (deviceMaster != null) { + log.info("ConnectorStatus:{}", deviceMaster.getConnectorStatus()); + if (ConnectorStatus.RECEIVER.equals(deviceMaster.getConnectorStatus())) { + //当前账号没有控制则可以切换为控制? + NettyDevice activeDevice = sessionManager.getUserControl(userName); + deviceMaster.getConnectedConnectors().clear(); + if (activeDevice == null) { + log.info("bindConnector ACTIVE :{}", masterId); + sessionManager.bindConnector(masterId, slaveId); + deviceMaster.setConnectorStatus(ConnectorStatus.ACTIVE); + deviceMaster.setCurrentSlaveId(slaveId); + sysUserClientService.updateAliveStatus(masterId, slaveId, 1); + } else { + log.info("bindConnector RECEIVER :{}", masterId); + sessionManager.bindConnector(masterId, slaveId); + deviceMaster.setConnectorStatus(ConnectorStatus.RECEIVER); + deviceMaster.setCurrentSlaveId(slaveId); + sysUserClientService.updateAliveStatus(masterId, slaveId, 2); + } + } else { + //从关联其他设备切换到 当前禁言 假如有控制且不是当前更新为2 + List controls = sessionManager.getSlaveControl(slaveId); + if (CollectionUtils.isEmpty(controls)) { + deviceMaster.getConnectedConnectors().clear(); + log.info("bindConnector 1:{}", masterId); + sessionManager.bindConnector(masterId, slaveId); + deviceMaster.setConnectorStatus(ConnectorStatus.ACTIVE); + deviceMaster.setCurrentSlaveId(slaveId); + sysUserClientService.updateAliveStatus(masterId, slaveId, 1); + } else { + NettyDevice controlDevice = controls.get(0); + if (!masterId.equals(controlDevice.getConnectorId())) { + //当前的设备有控制 的主机 + deviceMaster.getConnectedConnectors().clear(); + log.info("bindConnector RECEIVER 2:{}", masterId); + sessionManager.bindConnector(masterId, slaveId); + deviceMaster.setConnectorStatus(ConnectorStatus.RECEIVER); + deviceMaster.setCurrentSlaveId(slaveId); + sysUserClientService.updateAliveStatus(masterId, slaveId, 2); + } + //当前 切当前不做操作 + } + } + return true; + } + } catch (Exception e) { + log.error("切换设备失败: {}", e.getMessage(), e); + } + return false; + } + +// public DeviceControlDTO remoteControl(DeviceBindDTO bindDTO, LoginUser user) { +// try { +// String slaveId = bindDTO.getDeviceId(); +// String platform = bindDTO.getPlatform(); +// String userName = user.getUsername(); +// String masterId = userName + ":" + platform; +// DeviceControlDTO dto = new DeviceControlDTO(); +// NettyDevice deviceMaster = sessionManager.getDevice(masterId); +// log.info("remoteControl deviceMaster:{}", masterId); +// if (deviceMaster != null) { +// if (ConnectorStatus.ACTIVE.equals(deviceMaster.getConnectorStatus())) { +// if (!StringUtils.isEmpty(deviceMaster.getCurrentSlaveId())) { +// if (deviceMaster.getCurrentSlaveId().equalsIgnoreCase(slaveId)) { +// dto.setRemoteControl(true); +// dto.setOwner(platform); +// } else { +// dto.setRemoteControl(false); +// dto.setReason("occupied"); +// List list = sessionManager.getSlaveControl(slaveId); +// if (!CollectionUtils.isEmpty(list)) { +// dto.setOwner(list.get(0).getConnectorId()); +// } +// } +// } else { +// dto.setRemoteControl(false); +// dto.setReason("no control"); +// List list = sessionManager.getSlaveControl(slaveId); +// if (!CollectionUtils.isEmpty(list)) { +// dto.setOwner(list.get(0).getConnectorId()); +// } +// } +// } else { +// dto.setRemoteControl(false); +// dto.setReason("not active"); +// List list = sessionManager.getSlaveControl(slaveId); +// if (!CollectionUtils.isEmpty(list)) { +// dto.setOwner(list.get(0).getConnectorId()); +// } +// } +// } else { +// dto.setRemoteControl(false); +// dto.setReason("not exist"); +// List list = sessionManager.getSlaveControl(slaveId); +// if (!CollectionUtils.isEmpty(list)) { +// dto.setOwner(list.get(0).getConnectorId()); +// } +// } +// return dto; +// } catch (Exception e) { +// log.error(e.getMessage()); +// } +// return null; +// } + + + public DeviceControlDTO remoteControl(DeviceBindDTO bindDTO, LoginUser user) { + try { +// String slaveId = bindDTO.getDeviceId(); + String platform = bindDTO.getPlatform(); + String userName = user.getUsername(); + String masterId = userName + ":" + platform; + DeviceControlDTO dto = new DeviceControlDTO(); + NettyDevice deviceMaster = sessionManager.getDevice(masterId); + log.info("remoteControl deviceMaster:{}", masterId); + if (deviceMaster != null) { + if (ConnectorStatus.ACTIVE.equals(deviceMaster.getConnectorStatus())) { + dto.setRemoteControl(true); + dto.setOwner(platform); + } else { + dto.setRemoteControl(false); + dto.setReason("occupied"); + NettyDevice activeDevice = sessionManager.getUserControl(userName); + if (activeDevice != null) { + String[] l = activeDevice.getConnectorId().split(":"); + dto.setOwner(l[1]); + } + } + } else { + dto.setRemoteControl(false); + dto.setReason("not exist"); + NettyDevice activeDevice = sessionManager.getUserControl(userName); + if (activeDevice != null) { + String[] l = activeDevice.getConnectorId().split(":"); + dto.setOwner(l[1]); + } + } + return dto; + } catch (Exception e) { + log.error("获取设备权限失败", e); + } + return null; + } + + + public DeviceSwitchControlDTO switchControl(DeviceBindDTO bindDTO, LoginUser user) { + try { + String slaveId = bindDTO.getDeviceId(); + String platform = bindDTO.getPlatform(); + String userName = user.getUsername(); + String masterId = userName + ":" + platform; + + DeviceSwitchControlDTO dto = new DeviceSwitchControlDTO(); + //收到明确的切换设备请求了 从哪边切换到另一边 + //判断当前是否有控制的设备 + NettyDevice masterDevice = sessionManager.getDevice(masterId); + if (masterDevice == null || !masterDevice.getChannel().isActive()) { + //主机已离线 + dto.setSwitchControl(false); + dto.setReason("主机已离线"); + return dto; + } + + NettyDevice slaveDevice = sessionManager.getDevice(slaveId); + if (slaveDevice == null || !masterDevice.getChannel().isActive()) { + //从机已离线 + //主机已离线 + dto.setSwitchControl(false); + dto.setReason("从机已离线"); + return dto; + } + + List controlList = sessionManager.getSlaveControl(slaveId); + + if (!CollectionUtils.isEmpty(controlList)) { + //如果当前设备有控制 + controlList.forEach(c -> { + if (!c.getConnectorId().equals(masterId)) { + c.setCurrentSlaveId(null); + c.removeConnectedDevice(slaveDevice); + c.setConnectorStatus(ConnectorStatus.RECEIVER); + sysUserClientService.updateAliveStatus(c.getConnectorId(), slaveId, 2); + } + }); + } + masterDevice.setCurrentSlaveId(slaveId); + masterDevice.setConnectorStatus(ConnectorStatus.ACTIVE); + sysUserClientService.updateAliveStatus(masterDevice.getConnectorId(), slaveId, 1); + + dto.setSwitchControl(true); + dto.setHolder(platform); + return dto; + } catch (Exception e) { + log.error(e.getMessage()); + return new DeviceSwitchControlDTO(false); + } + } + + public void releaseControl(DeviceBindDTO bindDTO, LoginUser user) { + try { + String platform = bindDTO.getPlatform(); + String userName = user.getUsername(); + String masterId = userName + ":" + platform; + NettyDevice deviceMaster = sessionManager.getDevice(masterId); + if (deviceMaster != null) { + sysUserClientService.updateAliveStatus(masterId, "", 0); + deviceMaster.setCurrentSlaveId(null); + deviceMaster.getConnectedConnectors().clear(); + } + } catch (Exception e) { + log.error(e.getMessage()); + } + } + +} diff --git a/maibu-netty-server/src/main/java/com/maibu/utils/CommandUtils.java b/maibu-netty-server/src/main/java/com/maibu/utils/CommandUtils.java new file mode 100644 index 0000000..8a3db05 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/utils/CommandUtils.java @@ -0,0 +1,26 @@ +package com.fastbee.utils; + +import com.fastbee.common.utils.json.JsonUtils; +import io.netty.buffer.ByteBuf; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +@Component +public class CommandUtils { + + + public static void buildCommand(ByteBuf buf, Object json, byte command) { + buf.writeShort(0xABAA); + buf.writeByte(command);//推送tcp消息指令 + if (json != null) { + String jsonStr = JsonUtils.toJsonString(json); + byte[] jsonBytes = jsonStr.getBytes(StandardCharsets.UTF_8); + //写入JSON数据的字节数组 + buf.writeBytes(jsonBytes); + } + buf.writeByte(0x00); + buf.writeByte(0x00); + buf.writeShort(0xAAAB); + } +} diff --git a/maibu-netty-server/src/main/java/com/maibu/utils/CompareUtils.java b/maibu-netty-server/src/main/java/com/maibu/utils/CompareUtils.java new file mode 100644 index 0000000..e41c7f8 --- /dev/null +++ b/maibu-netty-server/src/main/java/com/maibu/utils/CompareUtils.java @@ -0,0 +1,88 @@ +package com.fastbee.utils; + +import com.fastbee.enums.CompareEnum; +import lombok.extern.slf4j.Slf4j; + +/** + * 比较工具类 + * 提供基于比较操作符的通用比较功能 + */ +@Slf4j +public class CompareUtils { + + /** + * 比较两个值是否满足指定的操作符条件 + * + * @param operator 比较操作符 + * @param targetValue 目标值(用于比较的值) + * @param compareValue 比较值(要比较的值) + * @return 是否满足条件 + */ + public static boolean compare(CompareEnum operator, String targetValue, String compareValue) { + if (operator == null || targetValue == null || compareValue == null) { + return false; + } + try { + // 根据操作符执行不同的比较逻辑 + switch (operator) { + case EQ: + return compareValue.equals(targetValue); + case NEQ: + return !compareValue.equals(targetValue); + case GT: + return isNumeric(compareValue) && isNumeric(targetValue) && + Double.parseDouble(compareValue) > Double.parseDouble(targetValue); + case LT: + return isNumeric(compareValue) && isNumeric(targetValue) && + Double.parseDouble(compareValue) < Double.parseDouble(targetValue); + case GTE: + return isNumeric(compareValue) && isNumeric(targetValue) && + Double.parseDouble(compareValue) >= Double.parseDouble(targetValue); + case LTE: + return isNumeric(compareValue) && isNumeric(targetValue) && + Double.parseDouble(compareValue) <= Double.parseDouble(targetValue); + case BETWEEN: + // 范围值用英文中划线分割 + String[] rangeValues = targetValue.split("-"); + if (rangeValues.length != 2) { + return false; + } + return isNumeric(compareValue) && isNumeric(rangeValues[0]) && isNumeric(rangeValues[1]) && + Double.parseDouble(compareValue) >= Double.parseDouble(rangeValues[0]) && + Double.parseDouble(compareValue) <= Double.parseDouble(rangeValues[1]); + case NOT_BETWEEN: + // 范围值用英文中划线分割 + String[] notRangeValues = targetValue.split("-"); + if (notRangeValues.length != 2) { + return false; + } + return isNumeric(compareValue) && isNumeric(notRangeValues[0]) && isNumeric(notRangeValues[1]) && + (Double.parseDouble(compareValue) < Double.parseDouble(notRangeValues[0]) || + Double.parseDouble(compareValue) > Double.parseDouble(notRangeValues[1])); + case CONTAIN: + return compareValue.contains(targetValue); + case NOT_CONTAIN: + return !compareValue.contains(targetValue); + default: + return false; + } + } catch (Exception e) { + log.error("比较操作失败,操作符: {}, 目标值: {}, 比较值: {}", operator.getValue(), targetValue, compareValue, e); + return false; + } + } + + /** + * 判断字符串是否为数字 + * + * @param str 要判断的字符串 + * @return 是否为数字 + */ + private static boolean isNumeric(String str) { + if (str == null || str.isEmpty()) { + return false; + } + // 使用正则表达式判断是否为数字 + return str.matches("^[+-]?\\d*(\\.\\d+)?$"); + } +} \ No newline at end of file diff --git a/maibu-netty-server/src/main/resources/errorStandard.json b/maibu-netty-server/src/main/resources/errorStandard.json new file mode 100644 index 0000000..a006d8f --- /dev/null +++ b/maibu-netty-server/src/main/resources/errorStandard.json @@ -0,0 +1,16 @@ +[ + { + "field": "voltage", + "compareValues": 58, + "compareType": "GT", + "errorName" : "电压过高", + "errorDescription" : "当前电池电压过高,超过正常范围" + }, + { + "field": "battery", + "compareValues": 20, + "compareType": "LT", + "errorName" : "电量过低", + "errorDescription" : "当前电池电量过低,请及时充电" + } +] \ No newline at end of file diff --git a/maibu-netty-server/src/main/resources/log4j2.xml b/maibu-netty-server/src/main/resources/log4j2.xml new file mode 100644 index 0000000..3bef3ff --- /dev/null +++ b/maibu-netty-server/src/main/resources/log4j2.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maibu-netty-server/src/main/resources/logback-spring.xml b/maibu-netty-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..03e28a2 --- /dev/null +++ b/maibu-netty-server/src/main/resources/logback-spring.xml @@ -0,0 +1,96 @@ + + + + + + + ${LOG_HOME}/trace.log + + ${LOG_HOME}/trace.%d{yyyy-MM-dd}.log + + + TRACE + ACCEPT + DENY + + + [%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + ${LOG_HOME}/debug.log + + ${LOG_HOME}/debug.%d{yyyy-MM-dd}.log + + + DEBUG + ACCEPT + DENY + + + [%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + ${LOG_HOME}/info.log + + ${LOG_HOME}/info.%d{yyyy-MM-dd}.log + + + INFO + ACCEPT + DENY + + + [%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + ${LOG_HOME}/warn.log + + ${LOG_HOME}/warn.%d{yyyy-MM-dd}.log + + + WARN + ACCEPT + DENY + + + [%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + ${LOG_HOME}/error.log + + ${LOG_HOME}/error.%d{yyyy-MM-dd}.log + + + ERROR + ACCEPT + DENY + + + [%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + + + [%d{HH:mm:ss}] [%-5level] %logger{36} - %msg%n + + + + + + + + + + + +