This commit is contained in:
2026-04-17 10:42:16 +08:00
parent 17c52c978c
commit d26dde07d7
64 changed files with 4737 additions and 17 deletions

View File

@@ -11,6 +11,41 @@
<artifactId>maibu-netty-server</artifactId>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.56.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-iot-service</artifactId>
</dependency>
<dependency>
<groupId>com.fastbee</groupId>
<artifactId>fastbee-common</artifactId>
<version>3.8.5</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version> <!-- 使用最新版 -->
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>

View File

@@ -1,17 +0,0 @@
package com.maibu;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
public static void main(String[] args) {
//TIP Press <shortcut actionId="ShowIntentionActions"/> 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 <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
System.out.println("i = " + i);
}
}
}

View File

@@ -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; //主要用于推送状态消息等
}

View File

@@ -0,0 +1,11 @@
package com.fastbee.common;
import io.netty.util.AttributeKey;
import lombok.Data;
@Data
public class Constant {
public static final AttributeKey<String> ATT_DEVICE_ID = AttributeKey.valueOf("deviceId");
}

View File

@@ -0,0 +1,9 @@
package com.fastbee.common;
import lombok.Data;
public class NettyCacheKey {
public static final String deviceRunningStatusKey = "running_status:";
}

View File

@@ -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<String, Object> data = obstacleData.getData();
String deviceId = (String) data.get("deviceId");
if (StringUtils.isEmpty(deviceId)) {
return AjaxResult.error("deviceId为空!");
}
// Map<String, Object> obstacle = (Map<String, Object>) data.get("obstacle");
// Boolean isSecure = (Boolean) data.get("isSecure");
List<NettyDevice> 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();
}
}

View File

@@ -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<String, Object> getDeviceMemory() {
Map<String, Object> res = new HashMap<>();
res.put("deviceMap", deviceSessionManager.getDeviceMap());
res.put("channelToDeviceIdMap", deviceSessionManager.getChannelToDeviceIdMap());
res.put("channelMap", deviceSessionManager.getChannelMap());
return res;
}
//
// @GetMapping("getGlobalMemory")
// public Map<String, Object> getGlobalMemory() {
// Map<String, Object> res = new HashMap<>();
// res.put("sysLoginMap", globalMemory.getSysLoginMap());
// res.put("sysLoginDataMap", globalMemory.getSysLoginDataMap());
// return res;
// }
}

View File

@@ -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));
}
}

View File

@@ -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<DeviceRunningStatusHistory> list = nettyDeviceService.statusHistory(history);
return getDataTable(list);
}
@GetMapping("/getErrorIdentification")
public TableDataInfo getErrorIdentification()
{
startPage();
List<ErrorIdentificationStandard> list = nettyDeviceService.allErrorIdentificationStandard();
return getDataTable(list);
}
@PostMapping("/updateErrorIdentification")
public AjaxResult updateErrorIdentification(@RequestBody ErrorIdentificationStandard standard)
{
int i = nettyDeviceService.updateErrorIdentification(standard);
return AjaxResult.success(i);
}
}

View File

@@ -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();
}
}

View File

@@ -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<Connector> 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());
}
}
}

View File

@@ -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;
}

View File

@@ -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 具体被占用方
}

View File

@@ -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<DeviceErrorPushDetail> details;
private long time;
private String deviceId;
private RespondCode event;
}

View File

@@ -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;
}

View File

@@ -0,0 +1,9 @@
package com.fastbee.dto;
import com.fastbee.enums.RespondCode;
import lombok.Data;
@Data
public class DeviceLoginResponseDTO {
private RespondCode respond;
}

View File

@@ -0,0 +1,12 @@
package com.fastbee.dto;
import lombok.Data;
@Data
public class DevicePlanStatisticsDTO {
private String name;
private Integer planned;
private int completed;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,12 @@
package com.fastbee.dto;
import lombok.Data;
@Data
public class DeviceWorkStatisticsDTO {
private String label;
private Integer value;
private Integer unit;
}

View File

@@ -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<LatAndLngEntity> 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;
}
}

View File

@@ -0,0 +1,12 @@
package com.fastbee.dto;
import lombok.Data;
import java.util.Map;
@Data
public class ObstacleData {
private String type;
private Map<String,Object> data;
}

View File

@@ -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;
}

View File

@@ -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 + '}';
}
}

View File

@@ -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<ProjectedPoint> 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<ProjectedPoint> 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<Point> 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<List<ProjectedPoint>> outerIntersections = new ArrayList<>();
List<List<ProjectedPoint>> holeIntersections = new ArrayList<>();
List<Integer> numHole = new ArrayList<>();
List<Integer> numOuter = new ArrayList<>();
for (int l = 0; l < lines; l++) {
List<ProjectedPoint> outerInts = new ArrayList<>();
collectOuterIntersections(field, lineStart, lineEnd, outerInts, dir);
List<ProjectedPoint> 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<ProjectedPoint> 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<LatAndLngEntity> 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<PathPoint> allHolePoints = new ArrayList<>();
for (List<PathPoint> 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<PathPoint> 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<Point> 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<LatAndLngEntity> 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<Point> 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);
// }
// }
//}

View File

@@ -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<PathPoint> outer;
public List<List<PathPoint>> holes;
}
}

View File

@@ -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 +
'}';
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,5 @@
package com.fastbee.enums;
public enum CommandRequestType {
switch_control
}

View File

@@ -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<CompareEnum> getAll() {
return Arrays.asList(CompareEnum.values());
}
/**
* 判断是否为范围比较操作符
* @param value 操作符值
* @return 是否为范围比较
*/
public static boolean isRangeOperator(String value) {
CompareEnum operator = getByValue(value);
return operator != null && !operator.isSingleValue();
}
}

View File

@@ -0,0 +1,7 @@
package com.fastbee.enums;
public enum ConnectionCloseReason {
NROMAL,
TIMEOUT,
REPEAT
}

View File

@@ -0,0 +1,7 @@
package com.fastbee.enums;
public enum ConnectorStatus {
ACTIVE, // 完全可用
RECEIVER, // 禁言(不可发送但可以接收)
DISABLED // 完全不可用
}

View File

@@ -0,0 +1,7 @@
package com.fastbee.enums;
public enum ConnectorType {
MASTER, // 上位机(主动绑定别人)
SLAVE, // 下位机(只能被动绑定)
GENERAL // 不区分角色(将来扩展用)
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,17 @@
package com.fastbee.enums;
public enum RespondCode {
/**
* 账号已登录
*/
have_logged_in,
/**
* 设备状态改变
*/
device_status_changed,
/**
* 设备错误推送
*/
device_error_push
}

View File

@@ -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<DevicePlan> query = new LambdaQueryWrapperX<>();
List<String> list = Arrays.asList(DeviceTaskStaus.NEW.getCode(),DeviceTaskStaus.EXECUTING.getCode(),DeviceTaskStaus.PAUSE.getCode());
query.in(DevicePlan::getTaskStaus, list);
List<DevicePlan> devicePlanList = devicePlanMapper.selectList(query);
if(!CollectionUtils.isEmpty(devicePlanList)){
devicePlanList.forEach(GlobalMemory::addDevicePlan);
}
}
}

View File

@@ -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<String, NettyDevice> deviceMap = new ConcurrentHashMap<>();
// 设备ID -> Channel(通信通道)
private final ConcurrentHashMap<String, Channel> channelMap = new ConcurrentHashMap<>();
// Channel -> 设备ID(反查)
private final ConcurrentHashMap<Channel, String> channelToDeviceIdMap = new ConcurrentHashMap<>();
public void clearChannelToDeviceIdMap(String deviceId) {
if (StringUtils.isEmpty(deviceId) || CollectionUtils.isEmpty(channelToDeviceIdMap)) {
return;
}
// 获取 entrySet 迭代器,安全遍历+删除
Iterator<Map.Entry<Channel, String>> iterator = channelToDeviceIdMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Channel, String> 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<NettyDevice> getDevicesLike(String query) {
return deviceMap.values().stream()
.filter(device -> device.getConnectorId().contains(query))
.collect(Collectors.toList());
}
public List<NettyDevice> 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<NettyDevice> 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<NettyDevice> 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();
}
}

View File

@@ -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<ErrorIdentificationStandard> {
}

View File

@@ -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<ErrorIdentificationStandard> standard = null;
//key deviceId
public static ConcurrentHashMap<String, List<DevicePlan>> devicePlanMap = new ConcurrentHashMap<>();
//key deviceId prepare
public static ConcurrentHashMap<String, List<DevicePlanTask>> devicePlanTaskPrepareMap = new ConcurrentHashMap<>();
//key deviceId execute
public static ConcurrentHashMap<String, DevicePlanTask> devicePlanTaskExecuteMap = new ConcurrentHashMap<>();
// 设备当前控制权在哪里key->deviceId value:userName:app/web/pc:token
// public static final ConcurrentHashMap<String, String> controlLockMap = new ConcurrentHashMap<>();
public static void addDevicePlan(DevicePlan devicePlan) {
if (devicePlan != null && !StringUtils.isEmpty(devicePlan.getDeviceId())) {
String deviceId = devicePlan.getDeviceId();
List<DevicePlan> 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<DevicePlan> 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<DevicePlanTask> 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<DevicePlanTask> 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);
}
}
}

View File

@@ -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<SocketChannel>() {
@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();
}
}
}

View File

@@ -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<String> {
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<Connector> 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<NettyDevice> 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
}
}
}

View File

@@ -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<String> {
@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<DeviceRunningStatusHistory> 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;
}
}

View File

@@ -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<String> {
private static final Logger logger = LoggerFactory.getLogger(DeviceConnectHandler.class);
public static final AttributeKey<String> ATT_DEVICE_ID = AttributeKey.valueOf("deviceId");
public static final AttributeKey<ConnectionCloseReason> ATT_CLOSE_REASON = AttributeKey.valueOf("closeReason");
//记录切换控制权请求来源 key 是请求的masterId,value是控制当前设备的主机
private final ConcurrentHashMap<String, String> 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<NettyDevice> 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<UserClient> 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<UserClient> 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<NettyDevice> 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<LatAndLngEntity> 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<NettyDevice> 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<NettyDevice> 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();
}
}

View File

@@ -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<Object> 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Object> 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();
}
}

View File

@@ -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<String> {
@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
byte[] bytes = Hex.decodeHex(msg.toCharArray()); // 16进制字符串转字节数组
out.writeBytes(bytes);
}
}

View File

@@ -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<String> keys = redisCache.keys(key);
Collection<String> 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<String, List<DevicePlan>> 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点定时刷新状态执行完成");
}
}

View File

@@ -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 {
}

View File

@@ -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<String, List<DevicePlan>> planMap = GlobalMemory.devicePlanMap;
if (!CollectionUtils.isEmpty(planMap.values())) {
Iterator<String> iterator = planMap.keySet().iterator();
while (iterator.hasNext()) {
String key = iterator.next();
List<DevicePlan> 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<DayOfWeek> 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<Integer> 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<String, List<DevicePlanTask>> 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<DevicePlanTask> 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<String, DevicePlanTask> 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);
}
}

View File

@@ -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<String> 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<DevicePlan> getDeviceTaskByUser(Long userId) {
if (userId != null) {
LambdaQueryWrapper<DevicePlan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DevicePlan::getUserId, userId);
return transferDevicePlanData(devicePlanMapper.selectList(queryWrapper));
} else {
return new ArrayList<>();
}
}
public List<DevicePlan> getDeviceTask(DeviceTaskQueryDTO dto) {
if (dto != null) {
LambdaQueryWrapper<DevicePlan> 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<DevicePlan> 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<DevicePlanStatisticsDTO> getDevicePlanStatistics(Long userId) {
List<DevicePlanStatisticsDTO> 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<DevicePlan> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.ge(DevicePlan::getCreateTime, monthStart)
.le(DevicePlan::getCreateTime, monthEnd)
.eq(DevicePlan::getUserId,userId);
List<DevicePlan> devicePlans = transferDevicePlanData(devicePlanMapper.selectList(queryWrapper));
if (!CollectionUtils.isEmpty(devicePlans)) {
List<LocalDate> dates = getMonthDays();
for (LocalDate date : dates) {
List<DevicePlan> 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<LocalDate> getMonthDays() {
LocalDate today = LocalDate.now();
int totalDays = today.lengthOfMonth();
LocalDate firstDayOfMonth = LocalDate.of(today.getYear(), today.getMonth(), 1);
List<LocalDate> allDaysOfCurrentMonth = new ArrayList<>();
for (int i = 0; i < totalDays; i++) {
// 每次累加1天,获取当月每一天
LocalDate currentDay = firstDayOfMonth.plusDays(i);
allDaysOfCurrentMonth.add(currentDay);
}
return allDaysOfCurrentMonth;
}
public List<DevicePlanTaskStatisticsDTO> getDevicePlanTaskStatistics(Long userId) {
List<DevicePlanTaskStatisticsDTO> 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<DevicePlan> query = new LambdaQueryWrapper<>();
query.eq(DevicePlan::getUserId,userId);
List<DevicePlan> devicePlans = transferDevicePlanData(devicePlanMapper.selectList(query));
if(CollectionUtils.isEmpty(devicePlans)) return new ArrayList<>();
List<Long> planIds = devicePlans.stream().map(DevicePlan::getId).collect(Collectors.toList());
if(CollectionUtils.isEmpty(planIds)) return new ArrayList<>();
LambdaQueryWrapper<DevicePlanTask> queryWrapper = new LambdaQueryWrapper<>();
// 核心:筛选 createTime 大于等于起始时间,且小于等于结束时间
queryWrapper.ge(DevicePlanTask::getCreateTime, monthStart)
.le(DevicePlanTask::getCreateTime, monthEnd)
.in(DevicePlanTask::getPlanId,planIds);
List<DevicePlanTask> devicePlanTasks = transferDeviceTaskData(devicePlanTaskMapper.selectList(queryWrapper));
if (!CollectionUtils.isEmpty(devicePlanTasks)) {
List<LocalDate> dates = getMonthDays();
for (LocalDate date : dates) {
List<DevicePlanTask> 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<DevicePlanTask> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DevicePlanTask::getDeviceId, deviceId)
.eq(DevicePlanTask::getTaskStaus, DeviceTaskStaus.EXECUTING);
List<DevicePlanTask> devicePlanTasks = devicePlanTaskMapper.selectList(queryWrapper);
Long routId = CollectionUtils.isEmpty(devicePlanTasks) ? null : devicePlanTasks.get(0).getRouteId();
return routId == null ? null : workRecordMapper.selectWorkRecordById(routId);
}
public List<DeviceWorkStatisticsDTO> workStatistics(Long userId) {
//todo update
List<DeviceWorkStatisticsDTO> 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<DevicePlanTask> deviceTaskPool(DeviceTaskQueryDTO dto) {
List<DevicePlanTask> 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<DevicePlan> transferDevicePlanData(List<DevicePlan> list) {
if (CollectionUtils.isEmpty(list)) return list;
list.forEach(x -> {
DeviceTaskStaus taskStaus = x.getTaskStaus();
x.setTaskStausTranslate(taskStaus.getDescription());
});
return list;
}
public List<DevicePlanTask> transferDeviceTaskData(List<DevicePlanTask> list) {
if (CollectionUtils.isEmpty(list)) return list;
list.forEach(x -> {
DeviceTaskStaus taskStaus = x.getTaskStaus();
x.setTaskStausTranslate(taskStaus.getDescription());
});
return list;
}
}

View File

@@ -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<String> 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<DeviceErrorPushDetail> 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<ErrorIdentificationStandard> 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<DeviceErrorPushDetail> compareErrorStandard(DeviceRunningStatusHistory history, List<ErrorIdentificationStandard> standards) {
if (history == null || CollectionUtils.isEmpty(standards)) return null;
List<DeviceErrorPushDetail> 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;
}
}

View File

@@ -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<DeviceRunningStatusHistory> statusHistory(DeviceRunningStatusHistory history) {
LambdaQueryWrapper<DeviceRunningStatusHistory> queryWrapper = new LambdaQueryWrapper<>();
if (!StringUtils.isEmpty(history.getDeviceId())) {
queryWrapper.eq(DeviceRunningStatusHistory::getDeviceId, history.getDeviceId());
}
return deviceStatusHistoryMapper.selectList(queryWrapper);
}
public List<ErrorIdentificationStandard> allErrorIdentificationStandard() {
return errorIdentificationStandardMapper.selectList(null);
}
public int updateErrorIdentification(ErrorIdentificationStandard standard) {
return errorIdentificationStandardMapper.updateById(standard);
}
}

View File

@@ -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<UserClient> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(UserClient::getDeviceName, deviceId);
List<UserClient> 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<Connector> 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<NettyDevice> 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<NettyDevice> list = sessionManager.getSlaveControl(slaveId);
// if (!CollectionUtils.isEmpty(list)) {
// dto.setOwner(list.get(0).getConnectorId());
// }
// }
// } else {
// dto.setRemoteControl(false);
// dto.setReason("no control");
// List<NettyDevice> list = sessionManager.getSlaveControl(slaveId);
// if (!CollectionUtils.isEmpty(list)) {
// dto.setOwner(list.get(0).getConnectorId());
// }
// }
// } else {
// dto.setRemoteControl(false);
// dto.setReason("not active");
// List<NettyDevice> list = sessionManager.getSlaveControl(slaveId);
// if (!CollectionUtils.isEmpty(list)) {
// dto.setOwner(list.get(0).getConnectorId());
// }
// }
// } else {
// dto.setRemoteControl(false);
// dto.setReason("not exist");
// List<NettyDevice> 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<NettyDevice> 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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -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+)?$");
}
}

View File

@@ -0,0 +1,16 @@
[
{
"field": "voltage",
"compareValues": 58,
"compareType": "GT",
"errorName" : "电压过高",
"errorDescription" : "当前电池电压过高,超过正常范围"
},
{
"field": "battery",
"compareValues": 20,
"compareType": "LT",
"errorName" : "电量过低",
"errorDescription" : "当前电池电量过低,请及时充电"
}
]

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<!-- 定义每个日志级别对应的文件输出 -->
<File name="TraceFile" fileName="logs/trace.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="TRACE" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
<File name="DebugFile" fileName="logs/debug.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
<File name="InfoFile" fileName="logs/info.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
<File name="WarnFile" fileName="logs/warn.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
<File name="ErrorFile" fileName="logs/error.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
<File name="FatalFile" fileName="logs/fatal.log">
<PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %c{1} - %msg%n"/>
<Filters>
<ThresholdFilter level="FATAL" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</File>
</Appenders>
<Loggers>
<Root level="trace">
<AppenderRef ref="TraceFile"/>
<AppenderRef ref="DebugFile"/>
<AppenderRef ref="InfoFile"/>
<AppenderRef ref="WarnFile"/>
<AppenderRef ref="ErrorFile"/>
<AppenderRef ref="FatalFile"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_HOME" value="logs"/>
<appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/trace.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/trace.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>TRACE</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/debug.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/warn.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_HOME}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 控制台输出可选 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%d{HH:mm:ss}] [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="TRACE_FILE"/>
<appender-ref ref="DEBUG_FILE"/>
<appender-ref ref="INFO_FILE"/>
<appender-ref ref="WARN_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>