20251223:物模型支持计算(以另一个物模型上报数据计算得出)

This commit is contained in:
George 2025-12-23 08:21:19 +08:00
parent 2c44850c38
commit 898687ea09
26 changed files with 4188 additions and 219 deletions

View File

@ -127,7 +127,7 @@ emqx:
ApiSecret: emqx.api.123
# sip 配置
sip:
enabled: true # 是否启用视频监控SIP,true为启用
enabled: false # 是否启用视频监控SIP,true为启用
## 本地调试时,绑定网卡局域网IP,设备在同一局域网,设备接入IP填写绑定IP
## 部署服务端时,默认绑定容器IP,设备接入IP填写服务器公网IP
ip: 192.168.110.78 # 本服务启动的 IP

View File

@ -275,6 +275,11 @@ public interface FastBeeConstant {
*/
String DEVICE_OTA_DATA = "device:ota:";
/**
* 设备上电触发指令缓存
*/
String POWER_ON_TRIGGER_COMMAND = "power:on:trigger:command:";
}
interface PROTOCOL {

View File

@ -134,5 +134,13 @@ public class RedisKeyBuilder {
return CacheConstants.DEVICE_OTA_DATA + serialNumber;
}
/**
* 设备上电触发指令缓存key
* 用于存储设备上电触发任务下发的指令片段(索引2到11位)
*/
public static String buildPowerOnTriggerCommandKey(String serialNumber){
return FastBeeConstant.REDIS.POWER_ON_TRIGGER_COMMAND + serialNumber.toUpperCase();
}
}

View File

@ -22,6 +22,9 @@ public class ThingsModelValuesInput
/** 设备物模型值的集合 **/
private List<ThingsModelSimpleItem> thingsModelSimpleItem;
/** 原始消息数据包(用于判断设备上电触发回包等) **/
private Object sources;
public Long getDeviceId() {
return deviceId;
}
@ -53,4 +56,12 @@ public class ThingsModelValuesInput
public void setThingsModelValueRemarkItem(List<ThingsModelSimpleItem> thingsModelSimpleItem) {
this.thingsModelSimpleItem = thingsModelSimpleItem;
}
public Object getSources() {
return sources;
}
public void setSources(Object sources) {
this.sources = sources;
}
}

View File

@ -19,9 +19,9 @@ public class CaculateUtils {
/**
* /*
* 暂时只支持加减乘除及括号的应用
* 支持加减乘除、幂运算、取余及括号的应用
*/
private static final String symbol = "+-,*/,(),%";
private static final String symbol = "+-,*/,(),%^";
/**
@ -105,6 +105,10 @@ public class CaculateUtils {
}
int length = b.toString().split("\\.")[1].length();
return a.divide(b, length, BigDecimal.ROUND_HALF_UP);
case '^':
// 幂运算:a的b次方
double result = Math.pow(a.doubleValue(), b.doubleValue());
return BigDecimal.valueOf(result).stripTrailingZeros();
default:
throw new RuntimeException("操作符号异常!");
}
@ -212,7 +216,19 @@ public class CaculateUtils {
public static String caculateReplace(String str, Map<String, String> map) {
for (Map.Entry<String, String> entry : map.entrySet()) {
str = str.replaceAll(entry.getKey(), entry.getValue()==null ? "1" : entry.getValue());
// 对于包含特殊字符的占位符(如 {identifier}),需要转义正则表达式特殊字符
String key = entry.getKey();
String value = entry.getValue() == null ? "1" : entry.getValue();
// 如果占位符包含正则表达式特殊字符,使用 Pattern.quote() 转义
if (key.contains("{") || key.contains("}") || key.contains("(") || key.contains(")")
|| key.contains("[") || key.contains("]") || key.contains("*") || key.contains("+")
|| key.contains("?") || key.contains("^") || key.contains("$") || key.contains("|")) {
// 转义正则表达式特殊字符
String escapedKey = java.util.regex.Pattern.quote(key);
str = str.replaceAll(escapedKey, value);
} else {
str = str.replaceAll(key, value);
}
}
return str;
}

View File

@ -53,7 +53,10 @@ public class TraceIdUtils {
public static long generateTraceId() {
long id = getIdWorker().nextId();
// 使用绝对值确保TraceId为正数,避免负数显示
// 注意:Math.abs(id) * 1000 可能会溢出,所以需要对最终结果再次使用Math.abs()
long traceId = Math.abs(id) * 1000 + ThreadLocalRandom.current().nextInt(1000);
// 确保最终结果为非负数(处理溢出情况)
traceId = Math.abs(traceId);
MDC.put(TRACE_ID_KEY, String.valueOf(traceId));
return traceId;
}

View File

@ -54,7 +54,8 @@ public class IdUtils
}
if(lastTimestamp == timestamp) {
this.sequence = this.sequence + 1L & 4095L;
// 修复:使用括号确保正确的运算顺序
this.sequence = (this.sequence + 1L) & 4095L;
if(this.sequence == 0L) {
timestamp = this.tilNextMillis(lastTimestamp);
}
@ -63,7 +64,18 @@ public class IdUtils
}
lastTimestamp = timestamp;
return timestamp - 1288834974657L << 22 | this.datacenterId << 17 | this.workerId << 12 | this.sequence;
// 修复:使用括号确保正确的运算符优先级,避免负数
// 原代码:timestamp - 1288834974657L << 22 会被解析为 timestamp - (1288834974657L << 22)
// 修复后:((timestamp - 1288834974657L) << 22) 确保先减法后位移
long id = ((timestamp - 1288834974657L) << 22)
| (this.datacenterId << 17)
| (this.workerId << 12)
| this.sequence;
// 确保返回的ID为正数(通过清除符号位)
// 如果id为负数,说明计算结果超出了预期,使用绝对值
return id < 0 ? Math.abs(id) : id;
}
private long tilNextMillis(long lastTimestamp) {

View File

@ -0,0 +1,122 @@
package com.fastbee.common.utils.uuid;
import lombok.extern.slf4j.Slf4j;
/**
* 系统编码生成器(改进版)
* 修复了原版本中可能导致负数的问题
*
* @author fastbee
* @date 2025-01-20
*/
@Slf4j
public class IdWorker {
private final long workerId;
/** 起始时间戳:2015-01-20 00:00:00 */
private final static long twepoch = 1421976968066L;
private long sequence = 0L;
/** 机器ID占用的位数 */
private final static long workerIdBits = 2L;
/** 支持的最大机器ID */
public final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 序列号占用的位数 */
private final static long sequenceBits = 9L;
/** 机器ID向左移的位数 */
private final static long workerIdShift = sequenceBits;
/** 时间戳向左移的位数 */
private final static long timestampLeftShift = sequenceBits + workerIdBits;
/** 生成序列的掩码 */
public final static long sequenceMask = -1L ^ (-1L << sequenceBits);
private long lastTimestamp = -1L;
/**
* 构造函数
*
* @param workerId 机器ID (0-3)
*/
public IdWorker(final long workerId) {
super();
if (workerId > IdWorker.maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format(
"worker Id can't be greater than %d or less than 0",
IdWorker.maxWorkerId));
}
this.workerId = workerId;
}
/**
* 生成下一个ID
* 修复了原代码中的运算符优先级问题,确保不会产生负数
*
* @return 生成的ID(始终为正数)
*/
public synchronized long nextId() {
long timestamp = this.timeGen();
// 时钟回拨检查
if (timestamp < this.lastTimestamp) {
long offset = this.lastTimestamp - timestamp;
log.warn("Clock moved backwards. Refusing to generate id for {} milliseconds", offset);
// 时钟回拨时,等待时钟追上
timestamp = this.tilNextMillis(this.lastTimestamp);
}
if (this.lastTimestamp == timestamp) {
// 修复:使用括号确保正确的运算顺序
// 原代码:this.sequence + 1 & IdWorker.sequenceMask 会被解析为 this.sequence + (1 & IdWorker.sequenceMask)
// 修复后:(this.sequence + 1) & IdWorker.sequenceMask 确保先加法后按位与
this.sequence = (this.sequence + 1) & IdWorker.sequenceMask;
if (this.sequence == 0) {
// 序列号溢出,等待下一毫秒
timestamp = this.tilNextMillis(this.lastTimestamp);
}
} else {
this.sequence = 0;
}
this.lastTimestamp = timestamp;
// 修复:使用括号确保正确的运算符优先级,避免负数
// 原代码:timestamp - twepoch << timestampLeftShift 会被解析为 timestamp - (twepoch << timestampLeftShift)
// 修复后:((timestamp - twepoch) << timestampLeftShift) 确保先减法后位移
long id = ((timestamp - twepoch) << timestampLeftShift)
| (this.workerId << IdWorker.workerIdShift)
| (this.sequence);
// 确保返回的ID为正数
// 正常情况下id应该为正数,但如果计算结果为负数,使用绝对值
if (id < 0) {
log.warn("Generated negative ID: {}, using absolute value", id);
return Math.abs(id);
}
return id;
}
/**
* 等待下一毫秒
*
* @param lastTimestamp 上次生成ID的时间戳
* @return 新的时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
/**
* 获取当前时间戳(毫秒)
*
* @return 当前时间戳
*/
private long timeGen() {
return System.currentTimeMillis();
}
}

View File

@ -58,6 +58,15 @@ import java.util.Objects;
@Slf4j
public class DeviceDebugLogAspect {
/** 日志值最大长度 */
private static final int MAX_LOG_VALUE_LENGTH = 64;
/** 备注最大长度 */
private static final int MAX_REMARK_LENGTH = 200;
/** 错误信息显示最大长度 */
private static final int MAX_ERROR_MSG_DISPLAY_LENGTH = 50;
/** 其他模式 */
private static final int MODE_OTHER = 3;
@Resource
private ILogService logService;
@ -102,43 +111,21 @@ public class DeviceDebugLogAspect {
*/
@AfterThrowing(pointcut = "parseReportMsgPointcut()", throwing = "e")
public void afterThrowingParseReportMsg(JoinPoint joinPoint, Throwable e) {
try {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0 && args[0] instanceof DeviceReportBo) {
handleException(joinPoint, e, (args) -> {
if (args[0] instanceof DeviceReportBo) {
DeviceReportBo bo = (DeviceReportBo) args[0];
String serialNumber = bo.getSerialNumber();
if (StringUtils.isEmpty(serialNumber)) {
log.warn("设备编号为空,跳过错误日志记录");
return;
return null;
}
// 处理原始数据,可能是byte[]或String
String sourceData = "";
try {
if (bo.getData() != null && bo.getData().length > 0) {
sourceData = ByteBufUtil.hexDump(bo.getData());
} else if (StringUtils.isNotEmpty(bo.getSources())) {
sourceData = bo.getSources();
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
sourceData = bo.getSources() != null ? bo.getSources() : "";
}
String errorMsg = e.getMessage();
if (StringUtils.isEmpty(errorMsg)) {
errorMsg = e.getClass().getName();
}
// 直接从bo中获取协议编码,避免额外的查询
String protocolCode = bo.getProtocolCode();
String sourceData = extractSourceDataFromReportBo(bo);
String errorMsg = getErrorMessage(e);
Long productId = bo.getProductId();
saveErrorLog(serialNumber, sourceData, errorMsg, DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "DEBUG_ERROR", productId);
return new ErrorLogContext(serialNumber, sourceData, errorMsg,
DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "DEBUG_ERROR", productId);
}
} catch (Exception ex) {
log.error("记录数据解析错误日志失败: {}", ex.getMessage(), ex);
}
return null;
}, "记录数据解析错误日志失败");
}
/**
@ -146,58 +133,21 @@ public class DeviceDebugLogAspect {
*/
@AfterThrowing(pointcut = "messagePostPointcut()", throwing = "e")
public void afterThrowingMessagePost(JoinPoint joinPoint, Throwable e) {
try {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0 && args[0] instanceof DeviceMessage) {
handleException(joinPoint, e, (args) -> {
if (args[0] instanceof DeviceMessage) {
DeviceMessage deviceMessage = (DeviceMessage) args[0];
String serialNumber = deviceMessage.getSerialNumber();
if (StringUtils.isEmpty(serialNumber)) {
log.warn("设备编号为空,跳过错误日志记录");
return;
return null;
}
// 处理原始数据
String sourceData = "";
try {
if (deviceMessage.getMessage() != null) {
Object msg = deviceMessage.getMessage();
if (msg instanceof byte[]) {
sourceData = ByteBufUtil.hexDump((byte[]) msg);
} else {
String msgStr = msg.toString();
// 如果dataType是ascii,需要将ASCII字符串转换为HEX格式用于显示
if ("ascii".equalsIgnoreCase(deviceMessage.getDataType())) {
// ASCII字符串转HEX
sourceData = asciiToHex(msgStr);
} else {
// 其他情况直接使用原字符串(可能是HEX格式)
sourceData = msgStr;
}
}
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
// 如果转换失败,至少保存原始字符串
if (deviceMessage.getMessage() != null) {
sourceData = deviceMessage.getMessage().toString();
}
}
String errorMsg = e.getMessage();
if (StringUtils.isEmpty(errorMsg)) {
errorMsg = e.getClass().getName();
}
// 获取协议编码和产品ID(通过设备编号查询,有缓存)
String protocolCode = getProtocolCodeBySerialNumber(serialNumber);
String sourceData = extractSourceDataFromDeviceMessage(deviceMessage);
String errorMsg = getErrorMessage(e);
Long productId = getProductIdBySerialNumber(serialNumber);
saveErrorLog(serialNumber, sourceData, errorMsg,
return new ErrorLogContext(serialNumber, sourceData, errorMsg,
DeviceLogTypeEnum.COMMAND_SEND_ERROR.getType(), "SEND_ERROR", productId);
}
} catch (Exception ex) {
log.error("记录指令发送错误日志失败: {}", ex.getMessage(), ex);
}
return null;
}, "记录指令发送错误日志失败");
}
/**
@ -205,55 +155,24 @@ public class DeviceDebugLogAspect {
*/
@AfterThrowing(pointcut = "protocolDecodePointcut()", throwing = "e")
public void afterThrowingProtocolDecode(JoinPoint joinPoint, Throwable e) {
try {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0 && args[0] instanceof DeviceData) {
DeviceData deviceData =
(DeviceData) args[0];
handleException(joinPoint, e, (args) -> {
if (args[0] instanceof DeviceData) {
DeviceData deviceData = (DeviceData) args[0];
String serialNumber = deviceData.getSerialNumber();
if (StringUtils.isEmpty(serialNumber)) {
log.warn("设备编号为空,跳过错误日志记录");
return;
return null;
}
// 处理原始数据
String sourceData = "";
try {
ByteBuf buf = deviceData.getBuf();
if (buf != null) {
sourceData = ByteBufUtil.hexDump(buf);
} else if (deviceData.getData() != null && deviceData.getData().length > 0) {
sourceData = ByteBufUtil.hexDump(deviceData.getData());
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
}
String errorMsg = e.getMessage();
if (StringUtils.isEmpty(errorMsg)) {
errorMsg = e.getClass().getName();
}
// 获取协议编码和产品ID:优先从deviceData获取,否则通过设备编号查询
String protocolCode = null;
String sourceData = extractSourceDataFromDeviceData(deviceData);
String errorMsg = getErrorMessage(e);
Long productId = deviceData.getProductId();
if (productId != null) {
protocolCode = getProtocolCodeByProductId(productId);
if (productId == null) {
productId = getProductIdBySerialNumber(serialNumber);
}
if (StringUtils.isEmpty(protocolCode) && StringUtils.isNotEmpty(serialNumber)) {
protocolCode = getProtocolCodeBySerialNumber(serialNumber);
if (productId == null) {
productId = getProductIdBySerialNumber(serialNumber);
}
}
// 协议解析错误也归类为数据解析错误
saveErrorLog(serialNumber, sourceData, errorMsg,
return new ErrorLogContext(serialNumber, sourceData, errorMsg,
DeviceLogTypeEnum.DATA_PARSE_ERROR.getType(), "PARSE_ERROR", productId);
}
} catch (Exception ex) {
log.error("记录协议解析错误日志失败: {}", ex.getMessage(), ex);
}
return null;
}, "记录协议解析错误日志失败");
}
/**
@ -301,31 +220,140 @@ public class DeviceDebugLogAspect {
return null;
}
/**
* 统一异常处理入口
*/
private void handleException(JoinPoint joinPoint, Throwable e,
java.util.function.Function<Object[], ErrorLogContext> contextExtractor,
String errorLogPrefix) {
try {
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
return;
}
ErrorLogContext context = contextExtractor.apply(args);
if (context == null || StringUtils.isEmpty(context.getSerialNumber())) {
log.warn("设备编号为空,跳过错误日志记录");
return;
}
saveErrorLog(context);
} catch (Exception ex) {
log.error("{}: {}", errorLogPrefix, ex.getMessage(), ex);
}
}
/**
* 从DeviceReportBo提取原始数据
*/
private String extractSourceDataFromReportBo(DeviceReportBo bo) {
try {
if (bo.getData() != null && bo.getData().length > 0) {
return ByteBufUtil.hexDump(bo.getData());
} else if (StringUtils.isNotEmpty(bo.getSources())) {
return bo.getSources();
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
}
return bo.getSources() != null ? bo.getSources() : "";
}
/**
* 从DeviceMessage提取原始数据
*/
private String extractSourceDataFromDeviceMessage(DeviceMessage deviceMessage) {
try {
if (deviceMessage.getMessage() != null) {
Object msg = deviceMessage.getMessage();
if (msg instanceof byte[]) {
return ByteBufUtil.hexDump((byte[]) msg);
} else {
String msgStr = msg.toString();
if ("ascii".equalsIgnoreCase(deviceMessage.getDataType())) {
return asciiToHex(msgStr);
} else {
return msgStr;
}
}
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
if (deviceMessage.getMessage() != null) {
return deviceMessage.getMessage().toString();
}
}
return "";
}
/**
* 从DeviceData提取原始数据
*/
private String extractSourceDataFromDeviceData(DeviceData deviceData) {
try {
ByteBuf buf = deviceData.getBuf();
if (buf != null) {
return ByteBufUtil.hexDump(buf);
} else if (deviceData.getData() != null && deviceData.getData().length > 0) {
return ByteBufUtil.hexDump(deviceData.getData());
}
} catch (Exception ex) {
log.warn("处理原始数据失败: {}", ex.getMessage());
}
return "";
}
/**
* 获取错误信息
*/
private String getErrorMessage(Throwable e) {
String errorMsg = e.getMessage();
return StringUtils.isEmpty(errorMsg) ? e.getClass().getName() : errorMsg;
}
/**
* 检查设备是否已授权
*/
private boolean isDeviceAuthorized(String serialNumber, Long productId) {
if (productId == null) {
return true; // 产品ID为空,不进行授权校验
}
try {
Product product = productService.selectProductByProductId(productId);
if (product == null || product.getIsAuthorize() == null || product.getIsAuthorize() != 1) {
return true; // 产品未开启授权码,允许记录
}
// 产品开启了授权码,检查设备是否绑定了授权码
ProductAuthorize queryParam = new ProductAuthorize();
queryParam.setProductId(productId);
queryParam.setSerialNumber(serialNumber);
List<ProductAuthorize> authorizeList = productAuthorizeService.selectProductAuthorizeList(queryParam);
return authorizeList != null && !authorizeList.isEmpty();
} catch (Exception ex) {
log.warn("授权码校验失败: {}, 设备编号: {}, 继续记录日志", ex.getMessage(), serialNumber);
return true; // 校验失败时继续记录日志,避免影响正常功能
}
}
/**
* 保存错误日志到iot_device_log表,并推送到前端页面
*/
private void saveErrorLog(String serialNumber, String sourceData, String errorMsg,
Integer logType, String identity, Long productId) {
private void saveErrorLog(ErrorLogContext context) {
try {
String serialNumber = context.getSerialNumber();
if (StringUtils.isEmpty(serialNumber)) {
log.warn("设备编号为空,无法保存错误日志");
return;
}
// 获取设备信息
Device device = null;
try {
device = deviceService.selectDeviceBySerialNumber(serialNumber);
} catch (Exception ex) {
log.warn("获取设备信息失败: {}, 设备编号: {}", ex.getMessage(), serialNumber);
}
Device device = getDeviceBySerialNumber(serialNumber);
if (device == null) {
log.warn("设备不存在,无法保存错误日志: {}", serialNumber);
return;
}
// 如果productId为空,尝试从设备信息中获取
// 获取产品ID
Long productId = context.getProductId();
if (productId == null) {
productId = device.getProductId();
}
@ -333,64 +361,66 @@ public class DeviceDebugLogAspect {
productId = getProductIdBySerialNumber(serialNumber);
}
// 授权码校验:如果产品开启了授权码,但设备未绑定授权码,不记录日志也不推送消息
if (productId != null) {
try {
Product product = productService.selectProductByProductId(productId);
if (product != null && product.getIsAuthorize() != null && product.getIsAuthorize() == 1) {
// 产品开启了授权码,检查设备是否绑定了授权码
ProductAuthorize queryParam = new ProductAuthorize();
queryParam.setProductId(productId);
queryParam.setSerialNumber(serialNumber);
java.util.List<ProductAuthorize> authorizeList = productAuthorizeService.selectProductAuthorizeList(queryParam);
if (authorizeList == null || authorizeList.isEmpty()) {
log.debug("设备[{}]未绑定授权码,跳过错误日志记录和消息推送", serialNumber);
return; // 不记录日志,也不推送消息
}
}
} catch (Exception ex) {
log.warn("授权码校验失败: {}, 设备编号: {}, 继续记录日志", ex.getMessage(), serialNumber);
// 校验失败时继续记录日志,避免影响正常功能
}
// 授权码校验
if (!isDeviceAuthorized(serialNumber, productId)) {
log.debug("设备[{}]未绑定授权码,跳过错误日志记录和消息推送", serialNumber);
return;
}
DeviceLog deviceLog = new DeviceLog();
deviceLog.setSerialNumber(serialNumber);
deviceLog.setDeviceId(device.getDeviceId());
deviceLog.setDeviceName(device.getDeviceName());
deviceLog.setLogType(logType);
// 存储原始数据包(限制64字符)
deviceLog.setLogValue(StringUtils.substring(sourceData, 0, 64));
// 存储错误信息(限制500字符)
deviceLog.setRemark(StringUtils.substring(errorMsg, 0, 500));
deviceLog.setIdentity(identity);
deviceLog.setModelName(Objects.equals(logType, DeviceLogTypeEnum.DATA_PARSE_ERROR.getType()) ?
"数据解析错误" : "指令发送错误");
deviceLog.setCreateTime(DateUtils.getNowDate());
deviceLog.setUserId(device.getTenantId());
deviceLog.setUserName(device.getTenantName());
deviceLog.setTenantId(device.getTenantId());
deviceLog.setTenantName(device.getTenantName());
deviceLog.setIsMonitor(0);
deviceLog.setMode(3); // 3=其他
// 保存错误日志到数据库
// 构建并保存设备日志
DeviceLog deviceLog = buildDeviceLog(device, context);
logService.saveDeviceLog(deviceLog);
log.info("已保存错误日志到数据库 - 设备: {}, 错误类型: {}, 错误信息: {}",
serialNumber, identity, StringUtils.substring(errorMsg, 0, 50));
serialNumber, context.getIdentity(),
StringUtils.substring(context.getErrorMsg(), 0, MAX_ERROR_MSG_DISPLAY_LENGTH));
// 推送错误信息到前端页面
pushErrorToFrontend(serialNumber, productId, sourceData, errorMsg, logType, identity);
pushErrorToFrontend(serialNumber, productId, context);
} catch (Exception ex) {
log.error("保存错误日志失败: {}", ex.getMessage(), ex);
}
}
/**
* 获取设备信息
*/
private Device getDeviceBySerialNumber(String serialNumber) {
try {
return deviceService.selectDeviceBySerialNumber(serialNumber);
} catch (Exception ex) {
log.warn("获取设备信息失败: {}, 设备编号: {}", ex.getMessage(), serialNumber);
return null;
}
}
/**
* 构建设备日志对象
*/
private DeviceLog buildDeviceLog(Device device, ErrorLogContext context) {
DeviceLog deviceLog = new DeviceLog();
deviceLog.setSerialNumber(context.getSerialNumber());
deviceLog.setDeviceId(device.getDeviceId());
deviceLog.setDeviceName(device.getDeviceName());
deviceLog.setLogType(context.getLogType());
deviceLog.setLogValue(StringUtils.substring(context.getSourceData(), 0, MAX_LOG_VALUE_LENGTH));
deviceLog.setRemark(StringUtils.substring(context.getErrorMsg(), 0, MAX_REMARK_LENGTH));
deviceLog.setIdentity(context.getIdentity());
deviceLog.setModelName(Objects.equals(context.getLogType(), DeviceLogTypeEnum.DATA_PARSE_ERROR.getType()) ?
"数据解析错误" : "指令发送错误");
deviceLog.setCreateTime(DateUtils.getNowDate());
deviceLog.setUserId(device.getTenantId());
deviceLog.setUserName(device.getTenantName());
deviceLog.setTenantId(device.getTenantId());
deviceLog.setTenantName(device.getTenantName());
deviceLog.setIsMonitor(0);
deviceLog.setMode(MODE_OTHER);
return deviceLog;
}
/**
* 推送错误信息到前端页面
*/
private void pushErrorToFrontend(String serialNumber, Long productId, String sourceData,
String errorMsg, Integer logType, String identity) {
private void pushErrorToFrontend(String serialNumber, Long productId, ErrorLogContext context) {
try {
if (productId == null) {
log.warn("产品ID为空,无法推送错误信息到前端: {}", serialNumber);
@ -398,42 +428,68 @@ public class DeviceDebugLogAspect {
}
// 构建错误消息的物模型数据
List<ThingsModelSimpleItem> errorItems = new ArrayList<>();
ThingsModelSimpleItem errorItem = new ThingsModelSimpleItem();
errorItem.setId("ERROR_" + identity);
errorItem.setName(Objects.equals(logType, DeviceLogTypeEnum.DATA_PARSE_ERROR.getType()) ? "数据解析错误" : "指令发送错误");
errorItem.setValue(errorMsg);
errorItem.setRemark(errorMsg);
errorItem.setId("ERROR_" + context.getIdentity());
errorItem.setName(Objects.equals(context.getLogType(), DeviceLogTypeEnum.DATA_PARSE_ERROR.getType())
? "数据解析错误" : "指令发送错误");
errorItem.setValue(context.getErrorMsg());
errorItem.setRemark(context.getErrorMsg());
errorItem.setTs(new Date());
List<ThingsModelSimpleItem> errorItems = new ArrayList<>();
errorItems.add(errorItem);
// 构建推送消息
PushMessageBo messageBo = new PushMessageBo();
messageBo.setTopic(topicsUtils.buildTopic(productId, serialNumber.toUpperCase(), TopicType.WS_SERVICE_INVOKE));
// 将hex转换为ASCII
String sourceAscii = hexToAscii(sourceData);
String sourceAscii = hexToAscii(context.getSourceData());
JSONObject pushObj = new JSONObject();
pushObj.put("message", errorItems);
pushObj.put("sources", StringUtils.isNotEmpty(sourceData) ? sourceData : ""); // HEX格式
pushObj.put("sourcesAscii", sourceAscii); // ASCII格式
pushObj.put("errorType", identity); // 错误类型:PARSE_ERROR 或 SEND_ERROR
pushObj.put("isError", true); // 标记为错误消息
pushObj.put("sources", StringUtils.isNotEmpty(context.getSourceData()) ? context.getSourceData() : "");
pushObj.put("sourcesAscii", sourceAscii);
pushObj.put("errorType", context.getIdentity());
pushObj.put("isError", true);
messageBo.setMessage(JSON.toJSONString(pushObj));
// 推送消息到MQTT
mqttRemoteManager.pushCommon(messageBo);
log.info("已推送错误信息到前端 - 设备: {}, 产品ID: {}, 错误类型: {}, HEX: {}, ASCII: {}",
serialNumber, productId, identity,
StringUtils.isNotEmpty(sourceData) ? StringUtils.substring(sourceData, 0, 20) + "..." : "",
StringUtils.isNotEmpty(sourceAscii) ? StringUtils.substring(sourceAscii, 0, 20) + "..." : "");
log.info("已推送错误信息到前端 - 设备: {}, 产品ID: {}, 错误类型: {}",
serialNumber, productId, context.getIdentity());
} catch (Exception ex) {
log.error("推送错误信息到前端失败: {}", ex.getMessage(), ex);
}
}
/**
* 错误日志上下文对象
*/
private static class ErrorLogContext {
private final String serialNumber;
private final String sourceData;
private final String errorMsg;
private final Integer logType;
private final String identity;
private final Long productId;
public ErrorLogContext(String serialNumber, String sourceData, String errorMsg,
Integer logType, String identity, Long productId) {
this.serialNumber = serialNumber;
this.sourceData = sourceData;
this.errorMsg = errorMsg;
this.logType = logType;
this.identity = identity;
this.productId = productId;
}
public String getSerialNumber() { return serialNumber; }
public String getSourceData() { return sourceData; }
public String getErrorMsg() { return errorMsg; }
public Integer getLogType() { return logType; }
public String getIdentity() { return identity; }
public Long getProductId() { return productId; }
}
/**
* 将ASCII字符串转换为HEX字符串
* @param asciiStr ASCII字符串

View File

@ -95,6 +95,8 @@ public class DataHandlerImpl implements IDataHandler {
// 这里上报设备编号是转的大写,后面存缓存也是使用大写的,所以在查询物模型的值时添加把设备编号转大写后取值
input.setDeviceNumber(bo.getSerialNumber().toUpperCase());
input.setThingsModelValueRemarkItem(thingsModelSimpleItems);
// 传递原始消息数据包,用于判断设备上电触发回包等
input.setSources(bo.getSources());
List<ThingsModelSimpleItem> result = deviceService.reportDeviceThingsModelValue(input, bo.getType(), bo.isShadow());
// 只有设备上报进入规则引擎流程
if (bo.isRuleEngine() && !bo.getSerialNumber().startsWith("server-")){
@ -120,7 +122,7 @@ public class DataHandlerImpl implements IDataHandler {
// log.error("=>上报属性信息给小度音箱异常", e);
// }
} catch (Exception e) {
log.error("接收属性数据,解析数据时异常 message={},e={}", e.getMessage(),e);
log.error("接收属性数据,解析数据时异常 message={}", e.getMessage(),e);
}
}

View File

@ -55,7 +55,6 @@ public class MessageAdapter implements MessageDecoder, MessageEncoder{
//这里兼容一下TCP整包发送的数据(整包==设备编号,数据一起发送)
if (clientId == null){
if (StringUtils.isNotEmpty(dump)) {
assert dump != null;
if (dump.startsWith("68") && dump.endsWith("16")) {
protocol = managerService.getProtocolByProtocolCode(FastBeeConstant.PROTOCOL.FlowMeter);
}else if (dump.startsWith("7e")&& dump.endsWith("7e")){

View File

@ -130,7 +130,7 @@ public class ModbusProtocol implements IProtocol {
report.setSources(hexDump);
return report;
} catch (Exception e) {
log.error("=>解码异常[{}]", e, e.getMessage());
log.error("=>解码异常[{}]", e.getMessage(), e);
throw new RuntimeException(e);
}
}

View File

@ -80,4 +80,10 @@ public class ModbusJob extends BaseEntity {
@ApiModelProperty("时间周期类型")
private Integer cycleType;
/**
* 执行时间(仅设备上电触发任务使用,记录任务执行的时间)
*/
@ApiModelProperty("执行时间")
private java.util.Date executeTime;
}

View File

@ -45,6 +45,14 @@ public interface ModbusJobMapper
*/
public int updateModbusJob(ModbusJob modbusJob);
/**
* 清空执行时间
*
* @param taskId 任务ID
* @return 结果
*/
public int clearExecuteTime(Long taskId);
/**
* 删除轮训任务列
*

View File

@ -68,6 +68,7 @@ import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.fastbee.common.utils.SecurityUtils.getLoginUser;
@ -513,6 +514,7 @@ public class DeviceServiceImpl implements IDeviceService {
ThingsModelValueItem thingModels = thingsModelService.getSingleThingModels(input.getProductId(), identity);
if (Objects.isNull(thingModels)) {
log.warn("=>查询物模型为空[{}],标识符:[{}]", input.getProductId(), identity);
continue; // 跳过当前物模型的处理
}
// 获取设备实时值
ValueItem cacheIdentifier = itslValueCache.getCacheIdentifier(input.getProductId(), input.getDeviceNumber(), thingModels.getId());
@ -525,10 +527,50 @@ public class DeviceServiceImpl implements IDeviceService {
/* ★★★★★★★★★★★★★★★★★★★★★★ 数据计算 -开始 ★★★★★★★★★★★★★★★★★★★★★★*/
//有计算公式的,经过计算公式
if (thingModels.getFormula() != null && !"".equals(thingModels.getFormula())) {
if (thingModels != null && thingModels.getFormula() != null && !"".equals(thingModels.getFormula())) {
Map<String, String> params = new HashMap<>();
// %s 表示当前物模型的值(保持向后兼容)
params.put("%s", value);
value = String.valueOf(CaculateUtils.execute(thingModels.getFormula(), params));
// 解析公式中的其他物模型标识符(格式:{identifier} 或直接使用标识符)
String formula = thingModels.getFormula();
// 匹配 {identifier} 格式的占位符
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("\\{([^}]+)\\}");
java.util.regex.Matcher matcher = pattern.matcher(formula);
Set<String> referencedIdentifiers = new HashSet<>();
while (matcher.find()) {
String identifier = matcher.group(1);
referencedIdentifiers.add(identifier);
}
// 获取引用的其他物模型的值
for (String refIdentifier : referencedIdentifiers) {
if (!refIdentifier.equals(identity)) { // 避免循环引用自己
try {
ValueItem refValueItem = itslValueCache.getCacheIdentifier(
input.getProductId(),
input.getDeviceNumber(),
refIdentifier
);
if (refValueItem != null && StringUtils.isNotEmpty(refValueItem.getValue())) {
// 使用 {identifier} 作为占位符
params.put("{" + refIdentifier + "}", refValueItem.getValue());
} else {
// 如果引用的物模型值不存在,使用0作为默认值
params.put("{" + refIdentifier + "}", "0");
log.warn("=>计算公式引用的物模型值不存在[{}],标识符:[{}],使用默认值0",
input.getProductId(), refIdentifier);
}
} catch (Exception e) {
log.error("=>获取计算公式引用的物模型值失败[{}],标识符:[{}],错误:{}",
input.getProductId(), refIdentifier, e.getMessage());
params.put("{" + refIdentifier + "}", "0");
}
}
}
// 执行计算
value = String.valueOf(CaculateUtils.execute(formula, params));
item.setValue(value);
}
/* ★★★★★★★★★★★★★★★★★★★★★★ 数据计算 -结束 ★★★★★★★★★★★★★★★★★★★★★★*/
@ -642,8 +684,10 @@ public class DeviceServiceImpl implements IDeviceService {
}
// 缓存最新一条数据到redis
redisCache.hashPutAll(key, maps);
// 判断是否是设备上电触发的回包,如果是则跳过保存历史数据
boolean isPowerOnTriggerReply = isPowerOnTriggerReply(input.getSources(), input.getDeviceNumber());
//存储历史数据
if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow) {
if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow && !isPowerOnTriggerReply) {
for (DeviceLog deviceLog : deviceLogList) {
logService.saveDeviceLog(deviceLog);
}
@ -1355,10 +1399,26 @@ public class DeviceServiceImpl implements IDeviceService {
// 执行所有相关的modbus任务
boolean hasActiveTask = false;
// 记录设备上电触发任务下发的指令片段(索引2到11位)
String serialNumber = device.getSerialNumber().toUpperCase();
String cacheKey = RedisKeyBuilder.buildPowerOnTriggerCommandKey(serialNumber);
for (ModbusJob modbusJob : modbusJobList) {
if ("0".equals(modbusJob.getStatus())) { // 只执行正常状态的任务
hasActiveTask = true;
// log.info("发现设备上电触发任务,设备编号:{},任务ID:{},指令:{}", device.getSerialNumber(), deviceJob.getJobId(), modbusJob.getCommand());
// 记录下发的指令片段(索引2到11位,即第3到第12个字符)
String command = modbusJob.getCommand();
if (StringUtils.isNotEmpty(command) && command.length() >= 12) {
// 去除空格并转为大写
String cleanCommand = command.replaceAll("\\s+", "").toUpperCase();
if (cleanCommand.length() >= 12) {
// 提取索引2到11位(即第3到第12个字符)
String commandSegment = cleanCommand.substring(2, 12);
// 存储到Redis缓存,设置过期时间为5分钟(设备上报应该在5分钟内完成)
redisCache.setCacheObject(cacheKey, commandSegment, 300, TimeUnit.SECONDS);
log.info("记录设备上电触发任务下发指令片段,设备编号:{},指令片段:{}", serialNumber, commandSegment);
}
}
}
}
@ -1391,6 +1451,7 @@ public class DeviceServiceImpl implements IDeviceService {
return;
}
log.info("更新设备上电触发任务状态为暂停,任务数量:{}", modbusJobList.size());
java.util.Date executeTime = DateUtils.getNowDate();
modbusJobList.forEach(modbusJob -> {
// 只更新status=0的任务为status=1,如果已经是status=1则跳过
if (!"0".equals(modbusJob.getStatus())) {
@ -1400,10 +1461,12 @@ public class DeviceServiceImpl implements IDeviceService {
ModbusJob job = new ModbusJob();
job.setStatus("1");
job.setTaskId(modbusJob.getTaskId());
// 记录执行时间
job.setExecuteTime(executeTime);
try {
// 将任务状态改为暂停("1"=暂停,"0"=正常)
// 将任务状态改为暂停("1"=暂停,"0"=正常),并记录执行时间
modbusJobService.updateModbusJob(job);
log.info("设备上电触发任务已设置为暂停状态,任务ID:{}", modbusJob.getTaskId());
log.info("设备上电触发任务已设置为暂停状态并记录执行时间,任务ID:{},执行时间:{}", modbusJob.getTaskId(), executeTime);
} catch (Exception e) {
log.error("更新设备上电触发任务状态异常,任务ID:{}", modbusJob.getTaskId(), e);
}
@ -1972,4 +2035,71 @@ public class DeviceServiceImpl implements IDeviceService {
return deviceMapper.selectSerialNumbersByProductId(productId);
}
/**
* 判断是否是设备上电触发的回包
* 从Redis缓存中获取下发的指令片段(索引2到11位),与上报的指令片段进行比较
*
* @param sources 原始消息数据包
* @param deviceNumber 设备编号
* @return true表示是设备上电触发的回包
*/
private boolean isPowerOnTriggerReply(Object sources, String deviceNumber) {
if (sources == null || StringUtils.isEmpty(deviceNumber)) {
return false;
}
try {
// 从Redis缓存中获取下发的指令片段
String serialNumber = deviceNumber.toUpperCase();
String cacheKey = RedisKeyBuilder.buildPowerOnTriggerCommandKey(serialNumber);
String sentCommandSegment = redisCache.getCacheObject(cacheKey);
// 如果缓存中没有下发的指令片段,说明不是设备上电触发的回包
if (StringUtils.isEmpty(sentCommandSegment)) {
return false;
}
String hexStr = null;
// 将 sources 转换为十六进制字符串
if (sources instanceof String) {
hexStr = (String) sources;
// 如果是字符串,可能需要去除空格
hexStr = hexStr.replaceAll("\\s+", "").toUpperCase();
} else if (sources instanceof byte[]) {
// 如果是字节数组,转换为十六进制字符串
byte[] bytes = (byte[]) sources;
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02X", b & 0xFF));
}
hexStr = sb.toString();
} else {
// 其他类型,尝试转换为字符串
hexStr = sources.toString().replaceAll("\\s+", "").toUpperCase();
}
if (hexStr.length() < 12) {
return false;
}
// 提取上报指令的索引2到11位(即第3到第12个字符)
String actualSegment = hexStr.substring(2, 12);
// 与下发的指令片段进行比较
boolean isMatch = sentCommandSegment.equals(actualSegment);
if (isMatch) {
// 匹配成功,删除缓存(避免重复匹配)
redisCache.deleteObject(cacheKey);
log.info("检测到设备上电触发回包,设备编号:{},指令片段:{}", serialNumber, actualSegment);
}
return isMatch;
} catch (Exception e) {
log.warn("判断设备上电触发回包时发生异常: {}", e.getMessage());
return false;
}
}
}

View File

@ -201,6 +201,14 @@ public class ModbusJobServiceImpl implements IModbusJobService {
*/
@Override
public int updateModbusJob(ModbusJob modbusJob) {
// 如果状态从1(暂停)改为0(正常),需要清空执行时间
if (modbusJob.getTaskId() != null && modbusJob.getStatus() != null && "0".equals(modbusJob.getStatus())) {
ModbusJob existingJob = modbusJobMapper.selectModbusJobByTaskId(modbusJob.getTaskId());
if (existingJob != null && "1".equals(existingJob.getStatus())) {
// 状态从1改为0,清空执行时间
modbusJobMapper.clearExecuteTime(modbusJob.getTaskId());
}
}
return modbusJobMapper.updateModbusJob(modbusJob);
}

View File

@ -16,10 +16,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="createTime" column="create_time" />
<result property="remark" column="remark" />
<result property="deviceType" column="device_type" />
<result property="executeTime" column="execute_time" />
</resultMap>
<sql id="selectModbusJobVo">
select task_id,job_name, sub_device_id, sub_serial_number, command,device_type, job_id, status, create_by, create_time, remark from iot_modbus_job
select task_id,job_name, sub_device_id, sub_serial_number, command,device_type, job_id, status, create_by, create_time, remark, execute_time from iot_modbus_job
</sql>
<select id="selectModbusJobList" parameterType="com.fastbee.iot.domain.ModbusJob" resultMap="ModbusJobResult">
@ -56,6 +57,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createTime != null">create_time,</if>
<if test="remark != null">remark,</if>
<if test="deviceType != null">device_type,</if>
<if test="executeTime != null">execute_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="jobName != null">#{jobName},</if>
@ -68,6 +70,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createTime != null">#{createTime},</if>
<if test="remark != null">#{remark},</if>
<if test="deviceType != null">#{deviceType},</if>
<if test="executeTime != null">#{executeTime},</if>
</trim>
</insert>
@ -83,10 +86,16 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="executeTime != null">execute_time = #{executeTime},</if>
</trim>
where task_id = #{taskId}
</update>
<!-- 清空执行时间 -->
<update id="clearExecuteTime" parameterType="Long">
update iot_modbus_job set execute_time = null where task_id = #{taskId}
</update>
<delete id="deleteModbusJobByTaskId" parameterType="Long">
delete from iot_modbus_job where task_id = #{taskId}
</delete>

3538
sql/yack.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@ -15,7 +15,7 @@ VUE_APP_SERVER_API_URL = http://localhost:8080
VUE_APP_MQTT_SERVER_URL = ws://localhost:8083/mqtt
# 百度地图 API Key
VUE_APP_BAI_DU_AK = 0SD4KrYeMXVu8Ox2pxQGDOw3NmDYiKIw
VUE_APP_BAI_DU_AK = hxAMWdceoYQ2laSOzL1OEddHj4fx3Ah9
# OSS API 地址(生产环境使用)
VUE_APP_OSS_API =

View File

@ -297,14 +297,20 @@
"product.product-things-model.142341-51": "step",
"product.product-things-model.142341-52": "Please enter the step size, for example: 1",
"product.product-things-model.142341-53": "The upstream data of the device is displayed after being calculated using a formula.",
"product.product-things-model.142341-54": "The% s in the formula is a placeholder and a fixed field.",
"product.product-things-model.142341-54": "%s in the formula is a placeholder representing the current thing model value.",
"product.product-things-model.142341-54-1": "Use {identifier} to reference values from other thing models, supporting multiple thing models in calculations.",
"product.product-things-model.142341-54-2": "Supported operators: +, -, *, /, % (modulo), ^ (power), parentheses ().",
"product.product-things-model.142341-55": "For example:",
"product.product-things-model.142341-56": "Add:% s+10",
"product.product-things-model.142341-57": "Subtract:% s-10",
"product.product-things-model.142341-58": "Multiply:% s * 10",
"product.product-things-model.142341-59": "Except for:% s/10",
"product.product-things-model.142341-60": "except(Retain decimals)for:% s/10",
"product.product-things-model.142341-56": "Add: %s+10",
"product.product-things-model.142341-57": "Subtract: %s-10",
"product.product-things-model.142341-58": "Multiply: %s*10",
"product.product-things-model.142341-59": "Divide: %s/10",
"product.product-things-model.142341-60": "Divide (retain decimals): %s%10.00",
"product.product-things-model.142341-61": "Retain decimals",
"product.product-things-model.142341-61-1": "Examples of referencing other thing models:",
"product.product-things-model.142341-61-2": "Pressure = K * frequency^2 - constant: 1.5*{frequency}^2-100",
"product.product-things-model.142341-61-3": "Speed = distance / time: {distance}/{time}",
"product.product-things-model.142341-61-4": "Mixed calculation: %s*{factor}+{offset}",
"product.product-things-model.142341-62": "Control Formula",
"product.product-things-model.142341-63": "Boolean value",
"product.product-things-model.142341-64": "For example: Close",

View File

@ -297,7 +297,9 @@
"product.product-things-model.142341-51": "步长",
"product.product-things-model.142341-52": "请输入步长,例如:1",
"product.product-things-model.142341-53": "设备上行数据经计算公式计算后显示 。",
"product.product-things-model.142341-54": "公式中的%s为占位符,是固定字段。",
"product.product-things-model.142341-54": "公式中的%s为占位符,表示当前物模型的值。",
"product.product-things-model.142341-54-1": "使用{标识符}可以引用其他物模型的值,支持多个物模型参与计算。",
"product.product-things-model.142341-54-2": "支持运算符:+、-、*、/、%、^(幂运算)、括号()。",
"product.product-things-model.142341-55": "如:",
"product.product-things-model.142341-56": "加:%s+10",
"product.product-things-model.142341-57": "减:%s-10",
@ -305,6 +307,10 @@
"product.product-things-model.142341-59": "除:%s/10",
"product.product-things-model.142341-60": "除(保留小数):%s%10.00",
"product.product-things-model.142341-61": "保留小数",
"product.product-things-model.142341-61-1": "引用其他物模型示例:",
"product.product-things-model.142341-61-2": "压力=K*频率平方-常数:1.5*{frequency}^2-100",
"product.product-things-model.142341-61-3": "速度=路程/时间:{distance}/{time}",
"product.product-things-model.142341-61-4": "混合计算:%s*{factor}+{offset}",
"product.product-things-model.142341-62": "控制公式",
"product.product-things-model.142341-63": "布尔值",
"product.product-things-model.142341-64": "例如:关闭",

View File

@ -241,7 +241,8 @@ export default {
return {
name: this.identifierList.find((chil) => chil.identifier === item).modelName,
type: 'line',
stack: this.$t('dataCenter.analysis.349202-12'),
//stack: this.$t('dataCenter.analysis.349202-12'),
smooth: true,
data: this.deviceLineList.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Object.values(ide)[0];

View File

@ -515,7 +515,7 @@ export default {
return {
name: this.devIdentifierList.find((chil) => chil.identifier === item).modelName,
type: 'line',
stack: this.$t('dataCenter.history.384934-23'),
smooth: true,
data: this.devDatas.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Object.values(ide)[0];
@ -665,7 +665,7 @@ export default {
return {
name: this.sceneIdentifierList.find((chil) => chil.identifier === item).sourceName,
type: 'line',
stack: this.$t('dataCenter.history.384934-23'),
smooth: true,
data: this.sceneDatas.map((d) => {
const ide = Object.values(d)[0].find((f) => Object.keys(f)[0] === item);
return Object.values(ide)[0];

View File

@ -35,6 +35,14 @@
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
</template>
</el-table-column>
<el-table-column label="执行时间" align="center" prop="executeTime" width="180">
<template slot-scope="scope">
<span v-if="scope.row.cycleType === 3 && scope.row.status === '1' && scope.row.executeTime">
{{ parseTime(scope.row.executeTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column :label="$t('device.device-modbus-task.384302-59')" align="center" prop="remarkStr"></el-table-column>
<el-table-column :label="$t('device.device-modbus-task.384302-60')" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
@ -55,7 +63,7 @@
<!-- 从机地址 -->
<el-col :span="12">
<el-form-item :label="$t('device.device-modbus-task.384302-14')" prop="path">
<el-input v-model="createForm.path" class="input-item" disabled></el-input>
<el-input v-model="createForm.path" class="input-item"></el-input>
</el-form-item>
</el-col>
<!-- 功能码 -->
@ -307,6 +315,7 @@
<script>
import { hex2int, int2hex, copyText } from '@/utils/common';
import { parseTime } from '@/utils/ruoyi';
import { encode } from '@/api/iot/mqttTest';
import { listJob, getJob, delJob, addJob, updateJob, getSlaveId } from '@/api/iot/modbusJob';

View File

@ -93,7 +93,7 @@
<!-- 从机地址 -->
<el-col :span="24">
<el-form-item :label="$t('device.instruction-parsing.830424-19')" prop="path">
<el-input v-model="createForm.path" :disabled="true"></el-input>
<el-input v-model="createForm.path"></el-input>
</el-form-item>
</el-col>
<!-- 指令类型 -->

View File

@ -190,13 +190,18 @@
<el-form-item :label="$t('product.product-things-model.142341-19')" prop="formula">
<template slot="label">
<span>{{ $t('product.product-things-model.142341-19') }}</span>
<el-tooltip style="cursor: pointer" effect="light" placement="top">
<el-tooltip style="cursor: pointer" effect="light" placement="top" max-width="500px">
<div slot="content">
{{ $t('product.product-things-model.142341-53') }}
<br />
{{ $t('product.product-things-model.142341-54') }}
<br />
{{ $t('product.product-things-model.142341-55') }}
{{ $t('product.product-things-model.142341-54-1') }}
<br />
{{ $t('product.product-things-model.142341-54-2') }}
<br />
<br />
<strong>{{ $t('product.product-things-model.142341-55') }}</strong>
<br />
{{ $t('product.product-things-model.142341-56') }}
<br />
@ -208,6 +213,15 @@
<br />
{{ $t('product.product-things-model.142341-60') }}({{ $t('product.product-things-model.142341-61') }}):%s%10.00
<br />
<br />
<strong>{{ $t('product.product-things-model.142341-61-1') }}</strong>
<br />
{{ $t('product.product-things-model.142341-61-2') }}
<br />
{{ $t('product.product-things-model.142341-61-3') }}
<br />
{{ $t('product.product-things-model.142341-61-4') }}
<br />
</div>
<i class="el-icon-question" />
</el-tooltip>