Compare commits

...

4 Commits

Author SHA1 Message Date
9cd3b8e496 #update 设备在线升级的功能 2026-09-23 15:24:49 +08:00
25a05ae9d8 #update 设备在线升级的功能 2026-09-23 14:38:12 +08:00
5c0ee1f811 #temp 2026-09-23 13:17:39 +08:00
fce873b662 #update ota 在线升级 2026-09-23 09:55:05 +08:00
31 changed files with 515 additions and 592 deletions

View File

@@ -9,6 +9,7 @@ import com.maibu.core.enums.DeviceTaskStaus;
import com.maibu.core.host.HostWorkRecordCreateDTO;
import com.maibu.core.host.MutiPointEntity;
import com.maibu.core.host.PointEntity;
import com.maibu.core.ota.DeviceOtaFirmware;
import com.maibu.mapper.DevicePlanTaskMapper;
import com.maibu.mapper.WorkRecordMapper;
import com.maibu.memory.GlobalMemory;
@@ -26,10 +27,7 @@ import org.eclipse.paho.client.mqttv3.MqttException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.*;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@@ -182,7 +180,7 @@ public class NettyDevice extends Connector {
return true;
}
public synchronized boolean failedTask() {
public synchronized boolean failedTask() {
if (task != null) {
task.setTaskStaus(DeviceTaskStaus.FAILED);
task.setUpdateTime(LocalDateTime.now());
@@ -216,7 +214,7 @@ public class NettyDevice extends Connector {
// 配置左右轮增益系数
public void sendWheelGainCoefficientsCommand(double leftForwardGain, double leftBackwardGain,
double rightForwardGain, double rightBackwardGain) {
double rightForwardGain, double rightBackwardGain) {
ByteBuffer buffer = ByteBuffer.allocate(39);
buffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -403,4 +401,21 @@ public class NettyDevice extends Connector {
}
}
public void upgrade(String deviceId, DeviceOtaFirmware firmware) throws MqttException {
Map<String, Object> payload = new HashMap<>();
payload.put("action", "ota_upgrade");
payload.put("version", firmware.getVersion());
payload.put("file_url", firmware.getFileUrl());
payload.put("firmware_id", firmware.getId());
payload.put("sn", deviceId);
payload.put("timestamp", System.currentTimeMillis());
payload.put("message_id", firmware.getId() + "_" + System.currentTimeMillis());
GlobalMemory.customMqttDeviceMonitor.publish(
mqttClientId,
String.format(MqttTopic.MOWER_OTA_UPGRADE_SET, deviceId),
JsonUtils.toJsonString(payload)
);
}
}

View File

@@ -0,0 +1,18 @@
package com.maibu.core.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OtaOperate {
upload("upload", "上传"),
upgrade("upgrade", "升级"),
rollback("rollback", "回滚"),
reboot("reboot", "重启");
private final String name;
private final String value;
}

View File

@@ -1,43 +0,0 @@
package com.maibu.core.mq.message;
import com.maibu.core.ota.OtaPackageCode;
import com.maibu.core.protocol.Message;
import com.maibu.core.protocol.modbus.ModbusCode;
import io.netty.buffer.ByteBuf;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 消息解析model
* @author gsb
* @date 2022/10/10 15:53
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
public class DeviceData extends Message {
/*topic*/
private String topicName;
/*设备编号*/
private String serialNumber;
/*原数据*/
private byte[] data;
private ByteBuf buf;
private Object body;
/*MQTT OR 其他*/
private int type;
/*Modbus*/
private ModbusCode code;
/**产品id*/
private Long productId;
private OtaPackageCode netModbusCode;
private int bitCount;
}

View File

@@ -1,59 +0,0 @@
package com.maibu.core.mq.message;
import com.maibu.core.protocol.modbus.ModbusCode;
import com.maibu.enums.ServerType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 设备下发指令model
*
* @author gsb
* @date 2022/10/10 16:18
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DeviceDownMessage {
private String messageId;
/**
* 时间戳,单位毫秒
*/
private Long timestamp;
/**
* 消息体
*/
private Object body;
/*下发的指令,服务调用的时候就是服务标识符*/
private String identifier;
/*产品id*/
private Long productId;
/**
* 设备编码
*/
private String serialNumber;
/**
* 从机编号
*/
private Integer slaveId;
private ModbusCode code;
private String protocolCode;
private List<PropRead> values;
private String topic;
private String subCode;
private ServerType serverType;
public DeviceDownMessage(List<PropRead> values, String topic, String subCode,String transport) {
this.values = values;
this.topic = topic;
this.subCode = subCode;
this.serverType = ServerType.explain(transport);
}
}

View File

@@ -1,33 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
/**
* 平台下发指令数据model
* @author bill
*/
@Data
public class DeviceFunctionMessage {
/*流水号,兼容modbus标准协议没有消息流水号*/
private String seqNo;
/*平台时间*/
private Long pfTimestamp;
/*下发的消息体*/
private Object body;
/*下发的指令物模型标识符*/
private String identifier;
/*下发的数据寄存器地址*/
private String hexAddress;
/*产品ID*/
private Long productId;
/*设备编号*/
private String serialNumber;
/*网关设备编号*/
private String protocolCode;
/*是否有子设备 0-否,1-是*/
private Integer hasSub;
/*子设备从机编号 例如 02 编号从机。通过主机集控下发的指定从机编号*/
private String subDeviceCode;
}

View File

@@ -1,31 +0,0 @@
package com.maibu.core.mq.message;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.util.Date;
/**
* 设备消息
* @author bill
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DeviceMessage {
/** 下发的数据*/
private Object message;
/** 下发的topic*/
private String topicName;
/** 设备编号*/
private String serialNumber;
private String dataType;
/** 时间 */
@JsonFormat(pattern ="yyyy-MM-dd HH:mm:ss:SSS")
private Date time;
}

View File

@@ -1,24 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
/**
* 指令下发组将的model
* @author bill
*/
@Data
public class FunctionCallBackBo {
/*下发的数据*/
private byte[] message;
/*MQTt-下发的topic*/
private String topicName;
/*设备编号*/
private String serialNumber;
/**
* 原数据包
*/
private String sources;
}

View File

@@ -1,20 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
/**
* 指令下发组将的model
* @author bill
*/
@Data
public class InstructionsMessage {
/*下发的数据*/
private byte[] message;
/*MQTt-下发的topic*/
private String topicName;
/*设备编号*/
private String serialNumber;
}

View File

@@ -1,32 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
import java.util.List;
/**
* @author gsb
* @date 2024/6/20 10:53
*/
@Data
public class ModbusPollMsg {
/**
* 下发指令
*/
private List<String> commandList;
/**
* 服务端类型
*/
private Integer serverType;
/**
* 产品id
*/
private Long productId;
/**
* 设备编码
*/
private String serialNumber;
private String transport;
}

View File

@@ -1,26 +0,0 @@
package com.maibu.core.mq.message;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* @author bill
*/
@Data
public class MqttBo {
/*主题*/
private String topic;
/*数据*/
private String data;
/*消息质量*/
private int qos = 1;
/*发送方向*/
private String direction;
/*时间*/
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date ts;
}

View File

@@ -1,39 +0,0 @@
package com.maibu.core.mq.message;
import com.maibu.core.protocol.modbus.ModbusCode;
import lombok.Data;
/**
* @author gsb
* @date 2022/12/9 10:15
*/
@Data
public class PropRead {
/**设备编号*/
private String serialNumber;
/**寄存器起始地址*/
private int address;
/**
* 读取寄存器个数
*/
private int count;
/**数据结果长度计算值*/
private int length;
/**
* 从机地址
*/
private int slaveId;
/**
* 读取个数
*/
private int quantity;
/**
* 数据
*/
private String data;
/**
* 功能码
*/
private ModbusCode code;
}

View File

@@ -1,21 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
/**
* 协议bean
* @author gsb
* @date 2022/10/25 14:54
*/
@Data
public class ProtocolDto {
/**协议编号*/
private String code;
private String name;
/*外部协议url*/
private String protocolUrl;
private String description;
/**协议类型 协议类型 0:系统协议 1:jar,2.js,3.c*/
private Integer protocolType;
}

View File

@@ -1,18 +0,0 @@
package com.maibu.core.mq.message;
import lombok.Data;
/**
* 网关子设备model
* @author gsb
* @date 2022/10/10 10:18
*/
@Data
public class SubDeviceMessage {
/*子设备编号或编码*/
private String serialNumber;
/*数据*/
private byte[] data;
/*消息id*/
private String messageId;
}

View File

@@ -1,21 +0,0 @@
package com.maibu.core.mq.ota;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.math.BigDecimal;
/**
* OTA升级回复model
* @author gsb
* @date 2022/10/24 17:20
*/
@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class OtaReplyMessage {
private Long taskId;
private BigDecimal version;
private int status;
private int progress;
}

View File

@@ -1,40 +0,0 @@
package com.maibu.core.mq.ota;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Data;
import java.math.BigDecimal;
/**
* OTA远程升级
* @author gsb
* @date 2022/10/10 10:22
*/
@Data
@Builder
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class OtaUpgradeBo {
// 设备编码
private String serialNumber;
// 升级任务ID
private Long taskId;
// 消息内容
private byte[] msg;
// 设备状态
private int status;
// topic
private String topicName;
// 固件包版本
private BigDecimal version;
// 固件包http地址
private String url;
// 固件包分包传输大小
private int packageSize;
// 固件包传输偏移量
private int offset;
// 升级进度
private int progress;
}

View File

@@ -1,63 +0,0 @@
package com.maibu.core.mq.ota;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.maibu.utils.DateUtils;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* ota升级发送,实现Delayed延时接口
*
* @author bill
*/
@Data
public class OtaUpgradeDelayTask implements Delayed {
/**
* 固件id
*/
private Long firmwareId;
/**
* 1:指定产品 2:指定设备
*/
private Long upgradeType;
private List<String> devices;
/**
* 任务id
*/
private Long taskId;
/**
* 开始升级时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
/**
* 设置延迟执行时间 开始升级时间 -当前时间
*
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return startTime.getTime() - DateUtils.getTimestamp();
}
@Override
public int compareTo(Delayed o) {
OtaUpgradeDelayTask delayTask = (OtaUpgradeDelayTask) o;
//比较
long diff = this.startTime.getTime() - delayTask.startTime.getTime();
if (diff <= 0) {
return -1;
} else {
return 1;
}
}
}

View File

@@ -0,0 +1,32 @@
package com.maibu.core.ota;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.maibu.core.domain.BaseDO;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@TableName("iot_ota_firmware")
public class DeviceOtaFirmware extends BaseDO {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String version; //版本号
private String fileName; //文件名
private String minioObjectName; //服务器文件名
private String fileUrl; //存放地址
private String deviceType;
}

View File

@@ -0,0 +1,38 @@
package com.maibu.core.ota;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.maibu.core.domain.BaseDO;
import com.maibu.core.enums.OtaOperate;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode(callSuper = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
@TableName(value = "iot_ota_operate_record", autoResultMap = true)
public class DeviceOtaOperateRecord extends BaseDO {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private OtaOperate operateType; //操作类型
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> deviceModels; //文件名
private String fileName; //名称
private Long firmwareId; //id
private String deviceType;
}

View File

@@ -1,45 +0,0 @@
package com.maibu.core.ota;
import com.maibu.exception.ServiceException;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum OtaPackageCode {
OTA_01("查询产品工装号",(byte) 0x01),
OTA_0A("OTA升级启动",(byte) 0x0A),
OTA_0B("OTA升级包传输",(byte) 0x0B)
;
private String desc;
private byte code;
public static OtaPackageCode getInstance(int code) {
switch ((byte)code) {
case 0x01:
return OTA_01;
case 0x0A:
return OTA_0A;
case 0x0B:
return OTA_0B;
default:
throw new ServiceException("功能码[" + code + "],未定义");
}
}
public static String getDes(int code){
switch ((byte)code) {
case 0x01:
return OTA_01.desc;
case 0x0A:
return OTA_0A.desc;
case 0x0B:
return OTA_0B.desc;
default:
return "UNKOWN";
}
}
}

View File

@@ -0,0 +1,11 @@
package com.maibu.mapper;
import com.maibu.core.ota.DeviceOtaFirmware;
import com.maibu.mybatis.mapper.BaseMapperX;
import org.springframework.stereotype.Repository;
@Repository
public interface DeviceOtaFirmwareMapper extends BaseMapperX<DeviceOtaFirmware> {
}

View File

@@ -0,0 +1,11 @@
package com.maibu.mapper;
import com.maibu.core.ota.DeviceOtaOperateRecord;
import com.maibu.mybatis.mapper.BaseMapperX;
import org.springframework.stereotype.Repository;
@Repository
public interface DeviceOtaOperateRecordMapper extends BaseMapperX<DeviceOtaOperateRecord> {
}

View File

@@ -111,7 +111,8 @@ public class GlobalMemory {
MqttTopic.MOWER_WILDCARD_TASK_STATUS,//任务状态
MqttTopic.MOWER_WILDCARD_ERROR,//错误
MqttTopic.MOWER_WILDCARD_REALTIME, //状态消息
MqttTopic.MOWER_WILDCARD_TASK_TARGET_REPLY //路径规划下发回复
MqttTopic.MOWER_WILDCARD_TASK_TARGET_REPLY, //路径规划下发回复
MqttTopic.MOWER_OTA_RESULT_POST
));
deviceConfig.setHandlerType(Constant.mqttHandleKey); //唯一的 用于处理上位机mqtt的
customMqttDeviceMonitor.registerDevice(deviceConfig);

View File

@@ -1,30 +1,18 @@
package com.maibu.mqtt;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.paho.client.mqttv3.MqttException;
import com.maibu.core.business.dto.DeviceMapPushDTO;
import org.springframework.util.CollectionUtils;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.maibu.constant.Constant;
import com.maibu.constant.NettyCacheKey;
import com.maibu.core.business.DeviceRunStatistics;
import com.maibu.core.business.device.DeviceStatusDetail;
import com.maibu.core.business.device.NettyDevice;
import com.maibu.core.business.dto.DeviceMapPushDTO;
import com.maibu.core.business.dto.WebDeviceTaskStatusMessageDTO;
import com.maibu.core.business.dto.WebStatusMessageDTO;
import com.maibu.core.business.inter.WebsocketMesDispather;
import com.maibu.core.enums.DeviceTaskStaus;
import com.maibu.core.enums.MesType;
import com.maibu.core.host.HeartBeatDTO;
import com.maibu.core.host.HostLocationRelTimeDTO;
import com.maibu.core.host.HostNavigationReplyDTO;
import com.maibu.core.host.HostTaskStatusDTO;
import com.maibu.core.host.HostVehicleRelTimeDTO;
import com.maibu.core.host.LocationMessage;
import com.maibu.core.host.*;
import com.maibu.core.redis.RedisCache;
import com.maibu.enums.DeviceStatusEnum;
import com.maibu.memory.DeviceSessionManager;
@@ -33,17 +21,19 @@ import com.maibu.memory.SiteMemory;
import com.maibu.utils.StringUtils;
import com.maibu.utils.json.JsonUtils;
import com.maibu.utils.spring.SpringUtils;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* mqtt相关的 上位机处理器
*/
@Slf4j
public class HostMessageHandler implements CustomMqttMessageHandler {
private final ObjectMapper objectMapper = new ObjectMapper();
private final DeviceSessionManager deviceSessionManager = SpringUtils.getBean(DeviceSessionManager.class);
private final WebsocketMesDispather websocketMesDispather = SpringUtils.getBean(WebsocketMesDispather.class);
@@ -114,6 +104,9 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
case "route_reply":
handleRouteReply(deviceId, topic, payload);
break;
case "upgrade_reply":
handleUpgradeReply(deviceId, topic, payload);
break;
default:
log.debug("割草机消息: deviceId={}, topic={}, action={}", deviceId, topic, action);
break;
@@ -162,7 +155,7 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
}
public WebStatusMessageDTO createWebDeviceLocationMessage(HostLocationRelTimeDTO locationRelTimeDTO,
String deviceId) {
String deviceId) {
WebStatusMessageDTO webStatusMessageDTO = new WebStatusMessageDTO();
List<DeviceStatusDetail> transferData = new ArrayList<>();
@@ -447,6 +440,23 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
}
}
/**
* 处理控制指令响应
*/
private void handleUpgradeReply(String deviceId, String topic, String payload) {
// log.debug("【路径任务下发响应】deviceId={}, payload={}", deviceId, payload);
//
// // todo 更新任务接收状态
// HostNavigationReplyDTO replyDTO = JsonUtils.parseObject(payload, HostNavigationReplyDTO.class);
// NettyDevice device = deviceSessionManager.getDevice(deviceId);
// if (replyDTO.isAccepted()) {
// if (device != null && device.getTask() != null
// && String.valueOf(device.getTask().getId()).equals(replyDTO.getTask_id())) {
// device.getTask().setTaskAccepted(true);
// }
// }
}
/**
* 从 Topic 中解析 deviceId
* Topic 格式:mower/{deviceId}/...
@@ -497,6 +507,9 @@ public class HostMessageHandler implements CustomMqttMessageHandler {
return "control_reply";
if (remaining.equals("task/target/reply"))
return "route_reply";
if (remaining.equals("/ota/result/post"))
return "upgrade_reply";
return null;
}

View File

@@ -64,15 +64,12 @@ public class MqttTopic {
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/+/ota/progress/post";
/** OTA 升级结果上报(设备->平台) */
public static final String MOWER_OTA_RESULT_POST = "mower/+/ota/result/post";
// ==================== 设备信息 ====================
// /** 查询设备信息(平台->设备) */

View File

@@ -1,9 +1,6 @@
package com.maibu.utils;
import io.minio.BucketExistsArgs;
import io.minio.MakeBucketArgs;
import io.minio.MinioClient;
import io.minio.PutObjectArgs;
import io.minio.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -44,6 +41,15 @@ public class MinioUtil {
// 拼接 public URL
return endpoint + "/" + defaultBucket + "/" + objectName;
}
public void deleteFile(String objectName) throws Exception {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(defaultBucket)
.object(objectName)
.build()
);
}
}

View File

@@ -0,0 +1,129 @@
package com.maibu.controller;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.AjaxResult;
import com.maibu.core.domain.R;
import com.maibu.core.enums.OtaOperate;
import com.maibu.core.ota.DeviceOtaFirmware;
import com.maibu.core.ota.DeviceOtaOperateRecord;
import com.maibu.core.page.TableDataInfo;
import com.maibu.dto.BaseQueryDTO;
import com.maibu.dto.DeviceOtaOperateDTO;
import com.maibu.service.impl.DeviceOtaService;
import com.maibu.utils.MinioUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.List;
@Api(tags = "OTA固件管理")
@RestController
@RequestMapping("/iot/deviceOta")
public class DeviceOtaController extends BaseController {
@Autowired
private DeviceOtaService service;
@Autowired
private MinioUtil minioUtil;
// ==================== 固件管理 ====================
@ApiOperation("新增或修改固件")
@PostMapping("/firmware/save")
public R saveFirmware(
@RequestPart("file") MultipartFile file, // 接收上传的图片
@RequestPart("firmware") DeviceOtaFirmware firmware // 接收表单里的 JSON 对象
) {
try (InputStream inputStream = file.getInputStream()) {
String fileName = "ota/firmware" + firmware.getVersion();
// 上传到 MinIO
minioUtil.uploadFile(fileName, inputStream, "image/png");
// 获取访问 URL
String url = minioUtil.getPublicUrl(fileName);
System.out.println("✅ 图片上传成功,访问地址: " + url);
// 这里可以把 URL 存到 workRecord 里
firmware.setMinioObjectName(fileName);
firmware.setFileUrl(url);
boolean b = service.saveFirmware(firmware);
DeviceOtaOperateRecord record = new DeviceOtaOperateRecord();
record.setFileName(firmware.getVersion());
record.setFirmwareId(firmware.getId());
record.setOperateType(OtaOperate.upload);
record.setCreateBy(getUsername());
record.setCreateTime(LocalDateTime.now());
service.saveOperateRecord(record);
return !b ? R.fail("新增失败") : R.ok("新增成功");
} catch (Exception e) {
return R.fail("新增失败: " + e.getMessage());
}
}
@ApiOperation("固件分页列表")
@PostMapping("/firmware/list")
public TableDataInfo listFirmware() {
startPage();
return getDataTable(service.listFirmware());
}
@ApiOperation("根据ID查询固件")
@GetMapping("/firmware/detail")
public AjaxResult selectFirmwareById(@RequestParam Long id) {
return AjaxResult.success(service.selectFirmwareById(id));
}
@ApiOperation("更新固件")
@PostMapping("/firmware/upgrade")
public AjaxResult upgrade(@RequestBody DeviceOtaOperateDTO dto) {
return service.upgrade(dto);
}
@ApiOperation("批量删除固件")
@PostMapping("/firmware/delete")
public AjaxResult deleteFirmware(@RequestBody List<Long> ids) {
return AjaxResult.success(service.deleteFirmware(ids));
}
// ==================== 操作记录管理 ====================
@ApiOperation("新增或修改操作记录")
@PostMapping("/record/save")
public AjaxResult saveOperateRecord(@RequestBody DeviceOtaOperateRecord record) {
return AjaxResult.success(service.saveOperateRecord(record));
}
@ApiOperation("操作记录分页列表")
@PostMapping("/record/list")
public TableDataInfo listOperateRecord(@RequestBody BaseQueryDTO dto) {
startPage();
return getDataTable(service.listOperateRecord(dto));
}
@ApiOperation("根据ID查询操作记录")
@GetMapping("/record/detail")
public AjaxResult selectOperateRecordById(@RequestParam Long id) {
return AjaxResult.success(service.selectOperateRecordById(id));
}
@GetMapping("/record/selectListByDeviceId")
public AjaxResult selectListByDeviceId(@RequestParam String deviceId) {
return AjaxResult.success(service.selectListByDeviceId(deviceId));
}
@ApiOperation("批量删除操作记录")
@PostMapping("/record/delete")
public AjaxResult deleteOperateRecord(@RequestBody List<Long> ids) {
return AjaxResult.success(service.deleteOperateRecord(ids));
}
}

View File

@@ -1,30 +1,20 @@
package com.maibu.controller;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import com.maibu.annotation.Log;
import com.maibu.core.domain.entity.SysUser;
import com.maibu.enums.BusinessType;
import com.maibu.utils.poi.ExcelUtil;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.maibu.core.business.DeviceRunParam;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.AjaxResult;
import com.maibu.core.page.TableDataInfo;
import com.maibu.dto.BaseQueryDTO;
import com.maibu.service.impl.DeviceRunParamService;
import com.maibu.utils.poi.ExcelUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
@RestController
@RequestMapping("/iot/deviceRunParam")
public class DeviceRunParamController extends BaseController {

View File

@@ -1,28 +1,20 @@
package com.maibu.controller;
import com.maibu.core.business.WorkRecord;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.AjaxResult;
import com.maibu.core.domain.R;
import com.maibu.memory.GlobalMemory;
import com.maibu.service.impl.WorkRecordService;
import com.maibu.utils.MinioUtil;
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.InputStream;
import java.util.List;
import com.maibu.core.domain.AjaxResult;
import com.maibu.memory.GlobalMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
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.WorkRecord;
import com.maibu.core.business.path.LatAndLngEntity;
import com.maibu.core.controller.BaseController;
import com.maibu.core.domain.R;
import com.maibu.service.impl.WorkRecordService;
import com.maibu.utils.MinioUtil;
@RestController
@RequestMapping("/iot/workRecord")
public class WorkRecordController extends BaseController {

View File

@@ -0,0 +1,15 @@
package com.maibu.dto;
import com.maibu.core.enums.OtaOperate;
import lombok.Data;
import java.util.List;
@Data
public class DeviceOtaOperateDTO {
private OtaOperate operateType;
private Long firmwareId;
private List<String> deviceIds;
}

View File

@@ -0,0 +1,171 @@
package com.maibu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.maibu.core.business.device.NettyDevice;
import com.maibu.core.domain.AjaxResult;
import com.maibu.core.enums.OtaOperate;
import com.maibu.core.ota.DeviceOtaFirmware;
import com.maibu.core.ota.DeviceOtaOperateRecord;
import com.maibu.dto.BaseQueryDTO;
import com.maibu.dto.DeviceOtaOperateDTO;
import com.maibu.mapper.DeviceOtaFirmwareMapper;
import com.maibu.mapper.DeviceOtaOperateRecordMapper;
import com.maibu.memory.DeviceSessionManager;
import com.maibu.utils.MinioUtil;
import com.maibu.utils.SecurityUtils;
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.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class DeviceOtaService {
@Autowired
private DeviceOtaFirmwareMapper deviceOtaFirmwareMapper;
@Autowired
private DeviceOtaOperateRecordMapper deviceOtaOperateRecordMapper;
@Autowired
private MinioUtil minioUtil;
@Autowired
private DeviceSessionManager deviceSessionManager;
private static final int BATCH_SIZE = 5;
public boolean saveFirmware(DeviceOtaFirmware firmware) {
return deviceOtaFirmwareMapper.saveOrUpdate(firmware);
}
public List<DeviceOtaFirmware> listFirmware() {
LambdaQueryWrapper<DeviceOtaFirmware> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(DeviceOtaFirmware::getCreateTime);
return deviceOtaFirmwareMapper.selectList(queryWrapper);
}
public DeviceOtaFirmware selectFirmwareById(Long id) {
return deviceOtaFirmwareMapper.selectById(id);
}
public AjaxResult upgrade(DeviceOtaOperateDTO dto) {
if (dto.getFirmwareId() == null) {
return AjaxResult.error("固件ID不能为空");
}
if (CollectionUtils.isEmpty(dto.getDeviceIds())) {
return AjaxResult.error("请选择要升级的设备");
}
DeviceOtaFirmware firmware = deviceOtaFirmwareMapper.selectById(dto.getFirmwareId());
if (firmware == null) {
return AjaxResult.error("固件不存在");
}
DeviceOtaOperateRecord record = new DeviceOtaOperateRecord();
record.setOperateType(OtaOperate.upgrade);
record.setFirmwareId(firmware.getId());
record.setFileName(firmware.getFileName());
record.setDeviceType(firmware.getDeviceType());
record.setDeviceModels(new ArrayList<>());
record.setCreateTime(LocalDateTime.now());
record.setCreateBy(SecurityUtils.getUsername());
int totalDevices = dto.getDeviceIds().size();
int successCount = 0;
int offlineCount = 0;
for (int i = 0; i < totalDevices; i += BATCH_SIZE) {
List<String> batch = dto.getDeviceIds().subList(i, Math.min(i + BATCH_SIZE, totalDevices));
log.info("OTA 灰度升级 第 {} 批, 本批设备数: {}", (i / BATCH_SIZE) + 1, batch.size());
for (String deviceId : batch) {
NettyDevice device = deviceSessionManager.getDevice(deviceId);
if (device == null || device.getOnlineStatus() == 0) {
log.warn("设备 {} 离线,跳过升级", deviceId);
offlineCount++;
continue;
}
try {
device.upgrade(deviceId, firmware);
record.getDeviceModels().add(deviceId);
successCount++;
log.info("OTA 升级指令已下发: deviceId={}, version={}", deviceId, firmware.getVersion());
} catch (Exception e) {
log.error("OTA 升级指令下发失败: deviceId={}", deviceId, e);
}
}
}
if (!record.getDeviceModels().isEmpty()) {
deviceOtaOperateRecordMapper.insert(record);
}
String msg = String.format("升级指令下发完成: 成功=%d, 离线=%d, 总计=%d",
successCount, offlineCount, totalDevices);
log.info(msg);
return AjaxResult.success(msg);
}
public int deleteFirmware(List<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return 0;
}
List<DeviceOtaFirmware> list = deviceOtaFirmwareMapper.selectBatchIds(ids);
if (!CollectionUtils.isEmpty(list)) {
list.forEach(l -> {
try {
minioUtil.deleteFile(l.getMinioObjectName());
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
return deviceOtaFirmwareMapper.deleteBatchIds(ids);
}
public boolean saveOperateRecord(DeviceOtaOperateRecord record) {
return deviceOtaOperateRecordMapper.saveOrUpdate(record);
}
public List<DeviceOtaOperateRecord> listOperateRecord(BaseQueryDTO dto) {
LambdaQueryWrapper<DeviceOtaOperateRecord> queryWrapper = new LambdaQueryWrapper<>();
if (dto != null) {
if (!StringUtils.isEmpty(dto.getDeviceId())) {
queryWrapper.eq(DeviceOtaOperateRecord::getDeviceType, dto.getDeviceId());
}
}
queryWrapper.orderByDesc(DeviceOtaOperateRecord::getCreateTime);
return deviceOtaOperateRecordMapper.selectList(queryWrapper);
}
public DeviceOtaOperateRecord selectOperateRecordById(Long id) {
return deviceOtaOperateRecordMapper.selectById(id);
}
public List<DeviceOtaOperateRecord> selectListByDeviceId(String deviceId) {
LambdaQueryWrapper<DeviceOtaOperateRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.apply(
"JSON_CONTAINS(device_models, {0})",
"\"" + deviceId + "\""
);
return deviceOtaOperateRecordMapper.selectList(wrapper);
}
public int deleteOperateRecord(List<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return 0;
}
return deviceOtaOperateRecordMapper.deleteBatchIds(ids);
}
}

View File

@@ -3,7 +3,6 @@ package com.maibu.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.maibu.core.business.DevicePlanTask;
import com.maibu.core.business.WorkRecord;
import com.maibu.core.business.path.LatAndLngEntity;
import com.maibu.mapper.DevicePlanTaskMapper;
import com.maibu.mapper.WorkRecordMapper;
import org.springframework.beans.factory.annotation.Autowired;