#优化路径规划下发 任务状态接收相关

This commit is contained in:
2026-08-28 06:32:11 +00:00
parent 9ff685e0c1
commit a92ec91cd2
10 changed files with 169 additions and 90 deletions

View File

@@ -0,0 +1,16 @@
package com.maibu.core.business.dto;
import java.util.List;
import com.maibu.core.business.path.LatAndLngEntity;
import com.maibu.core.enums.MesType;
import lombok.Data;
@Data
public class WebRouteStatusMessageDTO {
private List<LatAndLngEntity> data;
private String deviceId;
private Long taskId;
private MesType type;
}

View File

@@ -14,5 +14,6 @@ public enum MesType {
remoteControl,
detectionReport, // 障碍物消息
switchPermission, // 切换控制申请
switchResult;// 切换结果
switchResult,// 切换结果
route_post;// 路径规划 点位推送
}

View File

@@ -1,6 +1,5 @@
package com.maibu.memory;
import cn.hutool.core.util.ObjectUtil;
import com.maibu.core.business.device.Connector;
import com.maibu.core.business.device.NettyDevice;
@@ -30,7 +29,7 @@ public class DeviceSessionManager {
// 设备ID -> 设备对象(业务实体)
private final ConcurrentHashMap<String, NettyDevice> deviceMap = new ConcurrentHashMap<>();
//当前设备的控制权在哪 登录的账号 deviceId(下位机) -> token key 唯一保证一个deviceId 只有一个控制权
// 当前设备的控制权在哪 登录的账号 deviceId(下位机) -> token key 唯一保证一个deviceId 只有一个控制权
private final ConcurrentHashMap<String, String> controllerMap = new ConcurrentHashMap<>();
// 设备ID -> Channel(通信通道)
@@ -39,7 +38,6 @@ public class DeviceSessionManager {
// Channel -> 设备ID(反查)
private final ConcurrentHashMap<Channel, String> channelToDeviceIdMap = new ConcurrentHashMap<>();
public void clearChannelToDeviceIdMap(String deviceId) {
if (StringUtils.isEmpty(deviceId) || CollectionUtils.isEmpty(channelToDeviceIdMap)) {
return;
@@ -67,7 +65,6 @@ public class DeviceSessionManager {
}
}
/**
* 注册设备连接(设备上线)
*
@@ -75,7 +72,8 @@ public class DeviceSessionManager {
* @param channel 通信通道
* @param deviceType 设备类型(MASTER/SLAVE)
*/
public void registerDevice(String deviceId, Channel channel, ConnectorType deviceType, ConnectorStatus status, String slaveId) {
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
@@ -109,7 +107,8 @@ public class DeviceSessionManager {
*/
public void bindConnector(String masterId, String slaveId) {
NettyDevice master = deviceMap.get(masterId);
if (master == null) return;
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);
@@ -147,7 +146,6 @@ public class DeviceSessionManager {
}
}
public boolean unLock(String token) {
if (!StringUtils.isEmpty(token)) {
Iterator<String> iterator = controllerMap.keySet().iterator();
@@ -179,7 +177,6 @@ public class DeviceSessionManager {
}
// 模糊匹配
public List<NettyDevice> getDevicesLike(String query) {
return deviceMap.values().stream()
@@ -187,7 +184,6 @@ public class DeviceSessionManager {
.collect(Collectors.toList());
}
public List<NettyDevice> getSlaveControl(String slaveId) {
// 获取所有连接的主机 且是active状态的
return deviceMap.values().stream()
@@ -196,7 +192,6 @@ public class DeviceSessionManager {
.collect(Collectors.toList());
}
public List<NettyDevice> getAllSlaveControl(String slaveId) {
// 获取所有连接的主机 不区分状态
return deviceMap.values().stream()
@@ -212,7 +207,6 @@ public class DeviceSessionManager {
.findFirst().orElse(null);
}
/// 模糊匹配
public NettyDevice getUserControl(String userName) {
return deviceMap.values().stream()
@@ -221,7 +215,6 @@ public class DeviceSessionManager {
.findFirst().orElse(null);
}
/**
* 根据设备ID获取Channel
*/
@@ -236,7 +229,6 @@ public class DeviceSessionManager {
return channelToDeviceIdMap.get(channel);
}
/**
* 根据从机查控制的主机
*/
@@ -249,7 +241,8 @@ public class DeviceSessionManager {
* 根据从机查所有主机
*/
public List<NettyDevice> getMastersBySlaveId(String slaveId) {
return deviceMap.values().stream().filter(x -> Objects.equals(x.getCurrentSlaveId(), slaveId)).collect(Collectors.toList());
return deviceMap.values().stream().filter(x -> Objects.equals(x.getCurrentSlaveId(), slaveId))
.collect(Collectors.toList());
}
/**
@@ -257,12 +250,12 @@ public class DeviceSessionManager {
*/
public void removeDevice(String deviceId) {
NettyDevice nettyDevice = deviceMap.remove(deviceId);
// Channel channel = channelMap.remove(deviceId);
// Channel channel = channelMap.remove(deviceId);
clearChannelToDeviceIdMap(deviceId);
// if (channel != null) {
// channelToDeviceIdMap.remove(channel);
// channel.close();
// }
// if (channel != null) {
// channelToDeviceIdMap.remove(channel);
// channel.close();
// }
if (nettyDevice != null) {
// 仅当设备是上位机时,才解绑绑定关系
if (nettyDevice.getDeviceType() == ConnectorType.MASTER) {
@@ -281,7 +274,6 @@ public class DeviceSessionManager {
}
}
/**
* 判断设备是否在线
*/
@@ -306,5 +298,20 @@ public class DeviceSessionManager {
return nettyDevice != null && nettyDevice.canReceive();
}
/**
* 获取客户端当前 控制的设备
*/
public String getDeviceByTaskId(Long taskId) {
if (taskId == null) {
return null;
}
for (String key : deviceMap.keySet()) {
NettyDevice nettyDevice = deviceMap.get(key);
if (nettyDevice != null && Objects.equals(nettyDevice.getCurrentTaskId(), taskId)) {
return nettyDevice.getConnectorId();
}
}
return null;
}
}

View File

@@ -108,6 +108,7 @@ public class GlobalMemory {
MqttTopic.MOWER_HEARTBEAT,//心跳
MqttTopic.MOWER_WILDCARD_LOCATION,//位置 经纬度
MqttTopic.MOWER_WILDCARD_TASK_ROUTE, //路径回复
MqttTopic.MOWER_WILDCARD_TASK_STATUS,//任务状态
MqttTopic.MOWER_WILDCARD_ERROR,//错误
MqttTopic.MOWER_WILDCARD_REALTIME, //状态消息
MqttTopic.MOWER_WILDCARD_TASK_TARGET_REPLY //路径规划下发回复

View File

@@ -74,21 +74,17 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
case "realtime_post":
handleRealtimePost(deviceId, topic, payload);
break;
case "status_post":
handleStatusPost(deviceId, topic, payload);
break;
case "heartbeat_post":
handleHeartbeatPost(deviceId, topic, payload);
break;
case "error_post":
handleErrorPost(deviceId, topic, payload);
break;
case "route_post":
case "point_post":
handleTaskPost(deviceId, topic, payload, action);
break;
// case "route_post":
// case "point_post":
// handleTaskPost(deviceId, topic, payload, action);
// break;
case "task_status_post":
case "task_finish_post":
handleTaskPost(deviceId, topic, payload, action);
break;
case "config_reply":
@@ -357,13 +353,11 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
if (remaining.equals("property/location/post")) return "location_post";
if (remaining.equals("property/realtime/post")) return "realtime_post";
if (remaining.equals("event/status/post")) return "status_post";
if (remaining.equals("event/heartbeat/post")) return "heartbeat_post";
if (remaining.equals("event/error/post")) return "error_post";
if (remaining.equals("task/route/post")) return "route_post";
if (remaining.equals("task/point/post")) return "point_post";
// if (remaining.equals("task/route/post")) return "route_post";
// if (remaining.equals("task/point/post")) return "point_post";
if (remaining.equals("task/status/post")) return "task_status_post";
if (remaining.equals("task/finish/post")) return "task_finish_post";
if (remaining.equals("action/config/reply")) return "config_reply";
if (remaining.equals("action/control/reply")) return "control_reply";
if (remaining.equals("task/target/reply")) return "route_reply";

View File

@@ -1,6 +1,5 @@
package com.maibu.mqtt;
public class MqttTopic {
/** @deprecated 通用设备主题,建议使用 MOWER_* 系列主题 */
@@ -13,6 +12,7 @@ public class MqttTopic {
public static final String DEVICE_TASK_ARRIVE_TOPIC = "task/%s/arrive";
/** @deprecated 通用设备主题,建议使用 MOWER_* 系列主题 */
public static final String DEVICE_ERROR_PUSH_TOPIC = "device/%s/error";
public static final String DEVICE_ROUTE_PUSH_TOPIC = "device/%s/route/post";
/** 产品标识:割草机 */
public static final String MOWER_PRODUCT = "mower";
@@ -47,8 +47,9 @@ public class MqttTopic {
// ==================== 任务管理 ====================
/** 路径规划下发(平台->设备),对应 TCP 0x01 path */
// public static final String MOWER_TASK_ROUTE_SET = "mower/%s/task/route/set";
// public static final String MOWER_TASK_ROUTE_POST = "mower/%s/task/route/post";
// public static final String MOWER_TASK_ROUTE_SET = "mower/%s/task/route/set";
// public static final String MOWER_TASK_ROUTE_POST =
// "mower/%s/task/route/post";
public static final String MOWER_TASK_ROUTE_SET = "mower/%s/task/target/set";
@@ -57,28 +58,32 @@ public class MqttTopic {
public static final String MOWER_TASK_POINT_POST = "mower/%s/task/point/post";
/** 任务状态变更上报:执行中/暂停/完成等(设备->平台) */
public static final String MOWER_TASK_STATUS_POST = "mower/%s/task/status/post";
/** 任务完成上报(设备->平台) */
public static final String MOWER_TASK_FINISH_POST = "mower/%s/task/finish/post";
// ==================== 错误告警 ====================
/** 设备错误告警上报:电机故障/传感器异常等(设备->平台) */
public static final String MOWER_ERROR_POST = "mower/%s/event/error/post";
// ==================== OTA 固件升级 ====================
// /** 下发 OTA 升级指令(平台->设备) */
// public static final String MOWER_OTA_UPGRADE_SET = "mower/%s/ota/upgrade/set";
// /** OTA 升级进度上报(设备->平台) */
// public static final String MOWER_OTA_PROGRESS_POST = "mower/%s/ota/progress/post";
// /** OTA 升级结果上报(设备->平台) */
// public static final String MOWER_OTA_RESULT_POST = "mower/%s/ota/result/post";
// /** 下发 OTA 升级指令(平台->设备) */
// public static final String MOWER_OTA_UPGRADE_SET =
// "mower/%s/ota/upgrade/set";
// /** OTA 升级进度上报(设备->平台) */
// public static final String MOWER_OTA_PROGRESS_POST =
// "mower/%s/ota/progress/post";
// /** OTA 升级结果上报(设备->平台) */
// public static final String MOWER_OTA_RESULT_POST =
// "mower/%s/ota/result/post";
// ==================== 设备信息 ====================
// /** 查询设备信息(平台->设备) */
// public static final String MOWER_INFO_DEVICE_GET = "mower/%s/info/device/get";
// /** 设备信息响应(设备->平台) */
// public static final String MOWER_INFO_DEVICE_REPLY = "mower/%s/info/device/reply";
// /** 查询设备信息(平台->设备) */
// public static final String MOWER_INFO_DEVICE_GET =
// "mower/%s/info/device/get";
// /** 设备信息响应(设备->平台) */
// public static final String MOWER_INFO_DEVICE_REPLY =
// "mower/%s/info/device/reply";
/** 设备版本信息上报(设备->平台) */
// public static final String MOWER_INFO_VERSION_POST = "mower/%s/info/version/post";
// public static final String MOWER_INFO_VERSION_POST =
// "mower/%s/info/version/post";
// ==================== 通配符订阅 ====================
/** 订阅所有设备的状态变更事件 */
@@ -98,9 +103,6 @@ public class MqttTopic {
public static final String MOWER_HEARTBEAT = "mower/+/event/heartbeat/post";
/**
* 格式化 Topic 模板,将 %s 替换为设备ID
*

View File

@@ -1,6 +1,5 @@
package com.maibu.netty.handler;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
@@ -8,7 +7,6 @@ import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.Resource;
import com.maibu.memory.DeviceSessionManager;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.MqttException;
@@ -24,8 +22,6 @@ import com.maibu.constant.Constant;
import com.maibu.core.business.Device;
import com.maibu.core.business.device.NettyDevice;
import com.maibu.core.business.inter.WebsocketMesDispather;
import com.maibu.core.domain.entity.SysUser;
import com.maibu.core.domain.entity.UserClient;
import com.maibu.core.enums.CommandRequestType;
import com.maibu.core.enums.ConnectionCloseReason;
import com.maibu.core.enums.ConnectorStatus;
@@ -34,12 +30,12 @@ import com.maibu.core.enums.DeviceStatus;
import com.maibu.core.enums.DeviceTaskStaus;
import com.maibu.core.enums.MesType;
import com.maibu.core.enums.RespondCode;
import com.maibu.dto.DeviceLoginResponseDTO;
import com.maibu.dto.DeviceRequestDTO;
import com.maibu.dto.DeviceRespondDTO;
import com.maibu.dto.DeviceStatusChangeDTO;
import com.maibu.dto.DeviceTaskStatusMessageDTO;
import com.maibu.mapper.DeviceRunStatisticsMapper;
import com.maibu.memory.DeviceSessionManager;
import com.maibu.memory.GlobalMemory;
import com.maibu.mqtt.MqttTopic;
import com.maibu.service.IDeviceService;
@@ -74,9 +70,6 @@ public class DeviceConnectHandler extends SimpleChannelInboundHandler<String> {
@Autowired
private IDeviceService deviceService;
@Autowired
private ISysUserService userService;
@Autowired
private DeviceRunStatisticsMapper deviceRunStatisticsMapper;
@@ -190,7 +183,7 @@ public class DeviceConnectHandler extends SimpleChannelInboundHandler<String> {
NettyDevice masterDevice = sessionManager.getDevice(sessionId);
if (masterDevice == null) {
// sessionManager.getControl(sessionId);
// sessionManager.getControl(sessionId);
handleMasterConnect(ctx, sessionId, null, ConnectorStatus.ACTIVE);
} else {
ctx.channel().attr(Constant.ATT_DEVICE_ID).set(sessionId);

View File

@@ -333,7 +333,7 @@ public class TransferDeviceService {
}
masterDevice.setCurrentSlaveId(slaveId);
masterDevice.setConnectorStatus(ConnectorStatus.ACTIVE);
sysUserClientService.updateAliveStatus(masterDevice.getConnectorId(), slaveId, 1);
// sysUserClientService.updateAliveStatus(masterDevice.getConnectorId(), slaveId, 1);
dto.setSwitchControl(true);
dto.setHolder(platform);

View File

@@ -1,16 +1,5 @@
package com.maibu.controller;
import com.maibu.core.business.path.LatAndLngEntity;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.R;
import com.maibu.memory.GlobalMemory;
import com.maibu.service.impl.HostComputerService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
@@ -18,6 +7,32 @@ import java.util.Arrays;
import java.util.List;
import java.util.zip.GZIPInputStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.maibu.core.business.device.NettyDevice;
import com.maibu.core.business.dto.WebRouteStatusMessageDTO;
import com.maibu.core.business.inter.WebsocketMesDispather;
import com.maibu.core.business.path.LatAndLngEntity;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.R;
import com.maibu.core.enums.MesType;
import com.maibu.memory.DeviceSessionManager;
import com.maibu.memory.GlobalMemory;
import com.maibu.mqtt.MqttTopic;
import com.maibu.service.impl.HostComputerService;
import com.maibu.utils.json.JsonUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/iot/host")
@@ -28,10 +43,21 @@ public class HostComputerController extends BaseController {
@Autowired
private HostComputerService hostComputerService;
@Autowired
private WebsocketMesDispather websocketMesDispather;
@Autowired
private DeviceSessionManager deviceSessionManager;
@Value("${mqtt.client-id}")
private String mqttClientId;
/**
* 上传 csv.gz 轨迹文件并解析为 LatAndLngEntity 列表(分批处理,每批 BATCH_SIZE 条)
*
* <p>CSV 格式:第 1 列=cell_index,第 2 列=x(lat),第 3 列=y(lng)</p>
* <p>
* CSV 格式:第 1 列=cell_index,第 2 列=x(lat),第 3 列=y(lng)
* </p>
*
* @param file csv.gz 压缩文件
* @param taskId 任务ID
@@ -40,8 +66,7 @@ public class HostComputerController extends BaseController {
@PostMapping(value = "/upload-path-csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R uploadCsv(
@RequestPart("file") MultipartFile file,
@RequestParam("taskId") Long taskId
) {
@RequestParam("taskId") Long taskId) {
if (file == null || file.isEmpty()) {
return R.fail("文件不能为空");
}
@@ -61,7 +86,7 @@ public class HostComputerController extends BaseController {
int batchIndex = 0;
try (GZIPInputStream gzipInputStream = new GZIPInputStream(file.getInputStream());
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream))) {
BufferedReader reader = new BufferedReader(new InputStreamReader(gzipInputStream))) {
String line;
int lineNum = 0;
@@ -74,7 +99,7 @@ public class HostComputerController extends BaseController {
String[] values = line.split(",");
if (lineNum == 1) {
if (lineNum < 4) {
log.info("CSV 表头: {}", Arrays.toString(values));
continue;
}
@@ -109,7 +134,8 @@ public class HostComputerController extends BaseController {
log.info("csv 解析完成: taskId={}, 共 {} 个轨迹点, 分 {} 批处理", taskId, allPath.size(), batchIndex);
GlobalMemory.taskPathMap.put(taskId, allPath);
sendMessage(taskId, allPath);
// TODO 推送数据到前端
return R.ok();
@@ -119,14 +145,46 @@ public class HostComputerController extends BaseController {
}
}
/**
* 定位信息 经纬度
*/
private void sendMessage(Long taskId, List<LatAndLngEntity> data) {
try {
WebRouteStatusMessageDTO dto = new WebRouteStatusMessageDTO();
String deviceId = deviceSessionManager.getDeviceByTaskId(taskId);
List<NettyDevice> controlMasters = deviceSessionManager
.getAllSlaveControl(deviceId);
dto.setData(data);
dto.setDeviceId(deviceId);
dto.setTaskId(taskId);
dto.setType(MesType.route_post);
if (!CollectionUtils.isEmpty(controlMasters)) {
for (NettyDevice x : controlMasters) {
try {
websocketMesDispather.dispather(x.getConnectorId(), JsonUtils.toJsonString(dto));
} catch (Exception e) {
log.error("位置数据websocket推送失败: connectorId={}, error={}", x.getConnectorId(),
e.getMessage());
}
}
}
GlobalMemory.customMqttDeviceMonitor.publish(mqttClientId,
String.format(MqttTopic.DEVICE_ROUTE_PUSH_TOPIC, deviceId),
JsonUtils.toJsonString(dto));
} catch (Exception e) {
log.error("sendMessage失败: taskId={}", taskId, e);
}
}
/**
* 从 CSV 行解析 LatAndLngEntity
* 第 2 列(x) → lat,第 3 列(y) → lng
* 第 3 列(x) → lat,第 4 列(y) → lng
*/
private LatAndLngEntity parseLatLng(String[] values) {
try {
String xStr = values[1].trim();
String yStr = values[2].trim();
String xStr = values[2].trim();
String yStr = values[3].trim();
if (xStr.isEmpty() || yStr.isEmpty()) {
return null;
@@ -145,11 +203,13 @@ public class HostComputerController extends BaseController {
/**
* 分批处理轨迹数据
*
* <p>每批 BATCH_SIZE 条数据触发一次,可用于:</p>
* <p>
* 每批 BATCH_SIZE 条数据触发一次,可用于:
* </p>
* <ul>
* <li>批量入库(insertBatch)</li>
* <li>发布 MQTT 消息到上位机</li>
* <li>数据校验/统计</li>
* <li>批量入库(insertBatch)</li>
* <li>发布 MQTT 消息到上位机</li>
* <li>数据校验/统计</li>
* </ul>
*
* @param taskId 任务ID
@@ -166,10 +226,14 @@ public class HostComputerController extends BaseController {
double minLng = Double.MAX_VALUE, maxLng = -Double.MAX_VALUE;
for (LatAndLngEntity p : path) {
if (p.getLat() < minLat) minLat = p.getLat();
if (p.getLat() > maxLat) maxLat = p.getLat();
if (p.getLng() < minLng) minLng = p.getLng();
if (p.getLng() > maxLng) maxLng = p.getLng();
if (p.getLat() < minLat)
minLat = p.getLat();
if (p.getLat() > maxLat)
maxLat = p.getLat();
if (p.getLng() < minLng)
minLng = p.getLng();
if (p.getLng() > maxLng)
maxLng = p.getLng();
}
log.info("【第 {} 批处理】taskId={}, 本批 {} 条, lat范围=[{}, {}], lng范围=[{}, {}]",

View File

@@ -67,6 +67,7 @@ public class WebMiddlewareController extends BaseController {
});
res.put("nettyClientMap", nettyClients);
res.put("controllerMap", deviceSessionManager.getControllerMap());
res.put("taskPathMap", GlobalMemory.taskPathMap);
res.put("siteMemory", GlobalMemory.getAllSiteMemory());
return AjaxResult.success(res);
}