自动关逻辑初步验证完毕

master
lld 2026-03-11 16:12:57 +08:00
parent fd0fd6e61e
commit 12fe073189
12 changed files with 206 additions and 159 deletions

View File

@ -1,7 +1,9 @@
package com.agri.common.constant;
import java.math.BigDecimal;
import java.util.Locale;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Value;
/**
*
@ -173,4 +175,8 @@ public class Constants
public static final String JM1G = "jm1g";
public static final BigDecimal PER_LAP_LEN = BigDecimal.valueOf(2.13);
public static final BigDecimal PER_LAP_SEC = BigDecimal.valueOf(18);
}

View File

@ -8,6 +8,7 @@ package com.agri.common.utils;
* @Version 1.0
*/
import com.agri.common.constant.Constants;
import org.springframework.beans.factory.annotation.Value;
import java.math.BigDecimal;
@ -17,12 +18,6 @@ import java.math.RoundingMode;
* +
*/
public class RollerTimeCalculator {
// 基础常量:固化到工具类,统一维护
@Value("${agri.per-lap.len}")
private static BigDecimal perLapLen;
@Value("${agri.per-lap.sec}")
private static BigDecimal perLapSec;
// 私有化构造器,禁止实例化
private RollerTimeCalculator() {}
@ -38,8 +33,8 @@ public class RollerTimeCalculator {
return 0;
}
// 核心公式:时间 = (长度 / 每圈长度) × 每圈时间
BigDecimal cycleCount = targetLen.divide(perLapLen,2, RoundingMode.HALF_UP);
return cycleCount.multiply(perLapSec).setScale(2, RoundingMode.HALF_UP).intValue(); // 四舍五入取整
BigDecimal cycleCount = targetLen.divide(Constants.PER_LAP_LEN,2, RoundingMode.HALF_UP);
return cycleCount.multiply(Constants.PER_LAP_SEC).setScale(2, RoundingMode.HALF_UP).intValue(); // 四舍五入取整
}
/**

View File

@ -89,7 +89,7 @@ public class FrontendConfigHandler {
}
// 转发前端指令
String deviceTopic = "dtu/" + deviceId + "/down";
// mqttMessageSender.publish(deviceTopic, payload);
mqttMessageSender.publish(deviceTopic, payload);
LocalDateTime currentTime = LocalDateTime.now();
// 3. 记录日志
log.info("【指令处理】前端{}于{}控制设备{}的{}功能,指令:{}",

View File

@ -128,13 +128,13 @@ public class FrontendControlHandler {
});
} catch (Exception e) {
log.error("【指令处理】功能码解析失败payload={}", payload, e);
// String errorTopic = "frontend/" + clientId + "/dtu/" + deviceId + "/listener";
// mqttMessageSender.publish(errorTopic, "{\"msg\":\"指令格式错误\"}");
String errorTopic = "frontend/" + clientId + "/dtu/" + deviceId + "/listener";
mqttMessageSender.publish(errorTopic, "{\"msg\":\"指令格式错误\"}");
return;
}
if (funcCodeMap == null || funcCodeMap.isEmpty()) {
// String errorTopic = "frontend/" + clientId + "/dtu/" + deviceId + "/listener";
// mqttMessageSender.publish(errorTopic, "{\"msg\":\"功能码不能为空\"}");
String errorTopic = "frontend/" + clientId + "/dtu/" + deviceId + "/listener";
mqttMessageSender.publish(errorTopic, "{\"msg\":\"功能码不能为空\"}");
log.warn("【指令处理】前端{}操作设备{}失败:功能码为空", clientId, deviceId);
return;
}

View File

@ -280,25 +280,26 @@ public class MqttAutoOffManager {
JSONObject down = new JSONObject();
down.put(funcType, 0);
log.info("触发自动化条件");
// mqttMessageSender.publish(deviceTopic, down.toJSONString());
// SysAgriInfo agriInfo = sysAgriInfoService.lambdaQuery()
// .eq(SysAgriInfo::getImei, deviceId)
// .one();
// String agriName = (agriInfo!=null && ObjectUtils.isNotEmpty(agriInfo.getAgriName()))?agriInfo.getAgriName():null;
// SysDevOperLog logDto = new SysDevOperLog();
// logDto.setAgriName(agriName);
// logDto.setImei(deviceId);
// logDto.setFuncCode(funcType);
// logDto.setOpType(0);
// logDto.setOpSource(2);
// logDto.setPayload(down.toJSONString());
// logDto.setLockAcquired(1);
// logDto.setLockHolder("autoOff");
// logDto.setExecResult(1);
// logDto.setLatestState(latest);
// logDto.setCreateBy("自动关");
// logDto.setTaskStatus(getFutureStatus().toString());
// sysDevOperLogService.save(logDto);
//todo
mqttMessageSender.publish(deviceTopic, down.toJSONString());
SysAgriInfo agriInfo = sysAgriInfoService.lambdaQuery()
.eq(SysAgriInfo::getImei, deviceId)
.one();
String agriName = (agriInfo!=null && ObjectUtils.isNotEmpty(agriInfo.getAgriName()))?agriInfo.getAgriName():null;
SysDevOperLog logDto = new SysDevOperLog();
logDto.setAgriName(agriName);
logDto.setImei(deviceId);
logDto.setFuncCode(funcType);
logDto.setOpType(0);
logDto.setOpSource(2);
logDto.setPayload(down.toJSONString());
logDto.setLockAcquired(1);
logDto.setLockHolder("autoOff");
logDto.setExecResult(1);
logDto.setLatestState(latest);
logDto.setCreateBy("自动关");
logDto.setTaskStatus(getFutureStatus().toString());
sysDevOperLogService.save(logDto);
log.info("【自动关任务】检测仍在运行已下发关闭deviceId={}, funcType={}, payload={}", deviceId, funcType, down.toJSONString());
}

View File

@ -59,7 +59,7 @@ public class MqttMessageDispatcher {
public void handleMessage(String topic, String payload) {
try {
// log.info("【MQTT接收】topic={}, payload={}", topic, payload);
// if (env.acceptsProfiles("dev")) return;
if (env.acceptsProfiles("dev")) return;
// 设备状态主题dtu/{deviceId}/up
if (topic.matches("dtu/\\w+/\\w+")) {
deviceStatusHandler.handle(topic, payload);

View File

@ -1,7 +1,9 @@
package com.agri.quartz.task;
import cn.hutool.core.map.MapUtil;
import com.agri.common.enums.TempCommandStatus;
import com.agri.common.utils.*;
import com.agri.common.utils.wechat.WxUtil;
import com.agri.framework.config.MqttConfig;
import com.agri.framework.manager.MqttAutoOffManager;
import com.agri.system.domain.SysAgriInfo;
@ -23,6 +25,8 @@ import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -73,6 +77,9 @@ public class RollerAutoTask {
@Value("${spring.mqtt.dtu-ctl-lock-ttl}")
private int dtuCtlLockTTL;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
// ========== 常量定义(新增) ==========
private static final int WORK_MODE_AUTO = 1; // 自动模式
private static final int NOT_DELETED = 0; // 未删除
@ -83,7 +90,8 @@ public class RollerAutoTask {
private static final int LOCK_ACQUIRED = 1; // 是否获取锁
public void checkAutoTerm() {
try {
log.info("=============【定时任务】大棚自动模式监测开始执行,时间:{}=============", LocalDateTime.now());
// 查询自动模式的大棚
List<SysAgriInfo> agriInfos = agriInfoService.lambdaQuery()
.select(SysAgriInfo::getImei, SysAgriInfo::getAgriName)
@ -134,7 +142,7 @@ public class RollerAutoTask {
// 最后一条对应imei对应温度 及时间
Map<String, Object> dtuData = dtuDataInfo.get(0);
// 求温度上报时间
LocalDateTime dtuTime = TimeConvertUtil.strToLocalDateTimeSafe((String) dtuData.get("time"));
LocalDateTime dtuTime = (LocalDateTime) dtuData.get("time");
if (dtuTime == null) {
// todo 当前大棚温湿度时间为空 跳过
log.error("【定时任务-卷膜自动化控制】大棚『{}』温湿度时间「{}」为空, 跳过", imei, LocalDateTime.now().minusMinutes(1));
@ -143,9 +151,9 @@ public class RollerAutoTask {
// 获取当前imei下的所有参数设置以及卷膜自动化条件设置
Map<String, List<RollerTermVO>> configTermByRollerMap = rollerTermMap.get(imei);
if (configTermByRollerMap.isEmpty()) {
if (MapUtil.isEmpty(configTermByRollerMap)) {
// todo 当前大棚下没有设置条件或者参数
log.error("【定时任务-卷膜自动化控制】大棚『{}』当前大棚下没有设置条件或者参数",imei);
log.error("【定时任务-卷膜自动化控制】大棚『{}』当前大棚下没有设置条件或者参数, 跳过", imei);
continue;
}
@ -161,6 +169,10 @@ public class RollerAutoTask {
log.error("【定时任务-卷膜自动化控制】大棚『{}』当前卷膜「{}」无参数设置跳过当前roller", imei, roller);
continue;
}
LocalDateTime startTime = terms.stream()
.max(Comparator.comparing(RollerTermVO::getStartTime))
.get()
.getStartTime();
// 获取卷膜参数
RollerTermVO rollerConfig = terms.get(0);
String refTempCode = rollerConfig.getRefTempCode(); // 参考温度
@ -171,37 +183,58 @@ public class RollerAutoTask {
Object tempObj = dtuData.get(refTempCode);
if (tempObj == null) {
// todo 当前卷膜参考温度设置为空
log.error("【定时任务-卷膜自动化控制】大棚『{}』当前卷膜「{}」参考温度设置为空",imei, roller);
log.error("【定时任务-卷膜自动化控制】大棚『{}』当前卷膜「{}」参考温度设置为空,跳过!", imei, roller);
continue;
}
// 优化明确标注为当前roller的参考温度快照仅解析一次
BigDecimal currentTemp = new BigDecimal(tempObj.toString());
// 每个roller单独定义isFirstRun作用域当前roller
boolean isFirstRun = todayLogByRoller.getOrDefault(roller, 0) == 0;
// 遍历当前roller的所有term改用普通for循环可读性更高
for (RollerTermVO term : terms) {
// 判断该温度上报时间是否在该条件设置的时间范围内
boolean inRange = TimeRangeUtil.isTimeInRange(dtuTime, term.getStartTime(), term.getEndTime());
boolean isCancelOff = startTime.equals(term.getStartTime());
log.info("\n【定时任务-卷膜自动化控制】正在监测大棚『{}』-卷膜『{}』:" +
"条件设置详情,监控时间范围:『{}~{}』,适宜温度:{}℃,运行风口:{}cm。「{}」",
imei,roller,term.getStartTime().format(FORMATTER),term.getEndTime().format(FORMATTER),
term.getTemp(),term.getVent(), isCancelOff?"当前为卷膜「"+roller+"」最后一条自动化规则":"");
if (!inRange) {
log.info("【定时任务-卷膜自动化控制】大棚『{}』当前卷膜「{}」温湿度时间:「{}」不在监控时间范围内「{}~{}」,跳过!",
imei, roller, dtuTime,term.getStartTime().format(FORMATTER),term.getEndTime().format(FORMATTER));
continue;
}
// 在范围内
if (inRange) {
//判断温度是否在适宜温度内
TempCommandStatus tempCommandStatus = TempJudgeUtil.judgeTempCommand(currentTemp, term.getTemp());
// todo 开关指令需要通知用户 推送主题 && 更新数据 前端重新请求消息表
if (tempCommandStatus == TempCommandStatus.OPEN) {
log.info("【定时任务-卷膜自动化控制】大棚『{}』-卷膜『{}』当前温湿度:『{}℃』,适宜温度为:「{}℃」,触发自动化条件,即将执行『开』指令!",
imei, roller, currentTemp, term.getTemp());
// 判断是否首次开
Integer openLen = todayLogByRoller.getOrDefault(roller + "k1", 0);
// 开指令
sendOpenCommand(imei,agriName, roller, isFirstRun, term.getVent(), reservedLen);
isFirstRun = false;
sendOpenCommand(imei, agriName, roller, openLen == 0, term.getVent(), reservedLen);
// 每次后,数量累计+1 不需要因为只循环到一次
} else if (tempCommandStatus == TempCommandStatus.CLOSE) {
log.info("【定时任务-卷膜自动化控制】大棚『{}』-卷膜『{}』当前温湿度:『{}℃』,适宜温度为:「{}℃」,触发自动化条件,即将执行『关』指令!",
imei, roller, currentTemp, term.getTemp());
// 判断是否首次开
Integer closeLen = todayLogByRoller.getOrDefault(roller + "g1", 0);
// 关指令
sendCloseCommand(imei, agriName, roller, isFirstRun, term.getVent());
isFirstRun = false;
}
sendCloseCommand(imei, agriName, roller, closeLen == 0, term.getVent(), isCancelOff);
// 每次后,数量累计+1
} else {
log.info("【定时任务-卷膜自动化控制】大棚『{}』-卷膜『{}』当前温湿度:『{}℃』,适宜温度为:「{}℃」,温度适宜,无需操作!",
imei, roller, currentTemp, term.getTemp());
}
}
}
}
log.info("=============【定时任务】大棚自动模式监测执行完毕,时间:{}=============", LocalDateTime.now());
} catch (Exception e) {
WxUtil.pushText("【定时任务】大棚自动模式监测执行终端, \n发生错误.错误原因:"+e+"\n时间"+LocalDateTime.now());
log.error("\n=============【定时任务】大棚自动模式监测执行终端,\n发生错误.错误原因:{}\n时间{}=============", e, LocalDateTime.now());
}
}
@ -238,7 +271,7 @@ public class RollerAutoTask {
String message = buildMqttMessage(funcType);
// ========== 3. 执行核心指令逻辑 ==========
executeCommand(imei, agriName, roller, funcType, message, openLen, true);
executeCommand(imei, agriName, roller, funcType, message, openLen, true, false);
}
/**
@ -249,7 +282,8 @@ public class RollerAutoTask {
* @param isFirstRun
* @param vent
*/
private void sendCloseCommand(String imei, String agriName, String roller, boolean isFirstRun, BigDecimal vent) {
private void sendCloseCommand(String imei, String agriName, String roller,
boolean isFirstRun, BigDecimal vent, boolean isCancelOff) {
// ========== 1. 前置参数校验 ==========
validateBaseParams(imei, agriName, roller);
if (isFirstRun) {
@ -266,7 +300,7 @@ public class RollerAutoTask {
String message = buildMqttMessage(funcType);
// ========== 3. 执行核心指令逻辑 ==========
executeCommand(imei, agriName, roller, funcType, message, vent, false);
executeCommand(imei, agriName, roller, funcType, message, vent, false, isCancelOff);
}
// ========== 抽取公共方法:减少代码冗余 ==========
@ -309,7 +343,7 @@ public class RollerAutoTask {
* @param isOpen
*/
private void executeCommand(String imei, String agriName, String roller, String funcType,
String message, BigDecimal len, boolean isOpen) {
String message, BigDecimal len, boolean isOpen, boolean isCancelOff) {
String lockKey = LOCK_PREFIX + imei + ":" + funcType;
try {
// ========== 1. 获取分布式锁 ==========
@ -327,15 +361,18 @@ public class RollerAutoTask {
isOpen ? "开启" : "关闭", imei, funcType, message);
saveOperLog(imei, agriName, funcType, message, isOpen ? 1 : 0);
// ========== 3. 发布MQTT指令 ==========
// mqttMessageSender.publish("dtu/" + imei + "/down", message);
// ========== 3. 发布MQTT指令 ==========todo
mqttMessageSender.publish("dtu/" + imei + "/down", message);
if (isCancelOff) {
log.debug("【自动关调度】设备{}卷膜{}:触发最后一条自动化条件,下发关闭指令,无需暂停!时间:{}", imei, roller, LocalDateTime.now());
return;
}
// ========== 4. 计算运行时间并调度自动关 ==========
int runTime = RollerTimeCalculator.calculateRunTime(len);
if (runTime > 0) {
String autoOffKey = roller + (isOpen ? "k" : "g");
autoOffManager.scheduleAutoOff(imei, autoOffKey, runTime);
log.debug("【自动关调度】设备{}卷膜{}调度{}秒后自动关闭", imei, roller, runTime);
log.debug("【自动关调度】设备{}卷膜{}:{}」调度{}秒后自动关闭", imei, roller,(isOpen?"开":"关"), runTime);
}
}

View File

@ -1,10 +1,12 @@
package com.agri.system.mapper;
import java.util.List;
import java.util.Map;
import com.agri.system.domain.SysAgriAlarmRelation;
import com.agri.system.domain.SysDtuData;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
/**
* DTU湿Mapper
@ -63,4 +65,6 @@ public interface SysDtuDataMapper extends BaseMapper<SysDtuData>
* @return
*/
public int deleteSysDtuDataByIds(Long[] ids);
List<Map<String, Object>> getLastDtuDataByImeiList(@Param("imeiList") List<String> imeiList);
}

View File

@ -122,6 +122,7 @@ public class SysDevOperLogServiceImpl extends ServiceImpl<SysDevOperLogMapper, S
.in("imei", imeiList) // 过滤固定IMEI列表命中idx_imei_time/idx_func的imei前缀
.ge("create_time", todayStart) // 今日0点后命中idx_imei_time的create_time
.lt("create_time", tomorrowStart) // 明日0点前避免23:59:59.999漏查)
.notIn("func_code","jbk1","jbg1")
.groupBy("imei", "func_code"); // 按IMEI+func_code分组利用idx_func索引
// 3. 执行查询返回Map列表仅包含imei/func_code/count三个字段
@ -132,7 +133,7 @@ public class SysDevOperLogServiceImpl extends ServiceImpl<SysDevOperLogMapper, S
// 4.1 初始化所有固定IMEI的内层Map避免取值时NPE
for (String imei : imeiList) {
nestedCountMap.put(imei, Collections.emptyMap());
nestedCountMap.put(imei, new HashMap<>());
}
// 4.2 填充统计结果
for (Map<String, Object> row : sqlResult) {

View File

@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -115,28 +116,14 @@ public class SysDtuDataServiceImpl extends ServiceImpl<SysDtuDataMapper, SysDtuD
*/
@Override
public List<Map<String, Object>> getLastDtuDataByImeiList(List<String> imeiList) {
// 1. 计算最后一分钟的起始时间(当前时间 - 1分钟
LocalDateTime lastOneMinute = LocalDateTime.now().minusMinutes(1);
// 2. 子查询按imei分组 + 最后一分钟过滤 + 取每组最大ID
QueryWrapper<SysDtuData> subQuery = new QueryWrapper<>();
subQuery.select("imei", "MAX(id) as max_id") // 雪花ID的MAX(id)就是最新记录
.in("imei", imeiList)
.ge("time", lastOneMinute)
.groupBy("imei");
if (imeiList == null || imeiList.isEmpty()) {
log.error("imeiList列表为空");
return Collections.emptyList();
}
// 3. 主查询通过MAX(id)匹配完整数据 + 指定字段
LambdaQueryWrapper<SysDtuData> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.inSql(SysDtuData::getId,
"SELECT max_id FROM (" + subQuery.getCustomSqlSegment() + ") t")
.select(SysDtuData::getImei,
SysDtuData::getTemp1,
SysDtuData::getTemp2,
SysDtuData::getTemp3,
SysDtuData::getTemp4,
SysDtuData::getTime);
return baseMapper.selectMaps(queryWrapper);
// 3. 执行查询(如果用 ServiceImpl直接调 list(mainQueryWrapper) 即可)
return baseMapper.getLastDtuDataByImeiList(imeiList);
}
}

View File

@ -136,4 +136,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</where>
order by `time` desc limit 1
</select>
<select id="getLastDtuDataByImeiList" resultType="java.util.Map">
SELECT
id, imei, temp1, temp2, temp3, temp4, time
FROM
sys_dtu_data
WHERE id IN (
SELECT MAX(id) AS id
FROM sys_dtu_data
WHERE imei IN
<foreach collection="imeiList" item="imei" open="(" separator="," close=")">
#{imei}
</foreach>
AND time >= DATE_SUB(NOW(), INTERVAL 1 MINUTE)
GROUP BY imei
)
</select>
</mapper>

View File

@ -107,7 +107,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
at.start_time,
at.end_time
FROM sys_roller_param rp
LEFT JOIN sys_auto_term at
LEFT JOIN sys_auto_term `at`
ON rp.imei = at.imei
AND rp.roller = at.roller -- 关键按imei+roller双字段关联精准匹配
WHERE rp.imei IN