20251229:tcp通信探究

This commit is contained in:
George 2025-12-30 08:02:52 +08:00
parent b35a7b8162
commit 8a87041e71
12 changed files with 2029 additions and 141 deletions

185
TCP通信流程说明.md Normal file
View File

@ -0,0 +1,185 @@
# TCP通信流程详解
## 核心问题解答
**问题:Message对象是设备直接回复的,还是底层框架封装的?**
**答案:Message对象是底层框架封装的,不是设备直接回复的。**
设备只发送原始的字节数据(ByteBuf),框架负责:
1. 接收设备的原始字节数据
2. 通过协议解码器解析为Message对象
3. 匹配请求和响应
4. 将响应传递给等待的request
## 完整通信流程
### 1. 平台下发指令(requestR调用)
```
PowerOnTriggerTaskExecutor.execute()
↓
MessageManager.requestR(serialNumber, msg, Message.class)
↓
Session.request(request, Message.class)
├─ 生成requestKey(基于Message类型和serNo)
├─ 订阅等待响应(subscribe(key))
└─ 发送指令到设备(channel.writeAndFlush())
```
**关键代码:**
```java
// Session.request()方法
public <T> Mono<T> request(Message message, Class<T> responseClass) {
String key = requestKey(message, responseClass); // 生成匹配key
Mono<T> subscribe = this.subscribe(key); // 订阅等待响应
ChannelFuture channelFuture = channel.writeAndFlush(Packet.of(this, message));
return Mono.create(...).then(subscribe); // 等待设备回复
}
```
### 2. 设备发送原始数据
设备通过TCP连接发送**原始字节数据**(ByteBuf),例如:
- Modbus RTU协议:`7E 01 03 00 00 00 02 C4 0B 7E`
- 设备不知道Message对象,只发送协议规定的字节流
### 3. 框架接收和处理(Netty Handler链)
```
TCPMessageAdapter.channelRead()
↓ 接收原始ByteBuf
MessageDecoderWrapper.channelRead()
↓ 协议解码:ByteBuf → DeviceReport(Message)
DispatcherHandler.channelRead()
↓ 消息分发处理
```
**关键代码:**
```java
// TCPMessageAdapter - 步骤1:接收原始数据
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg; // 设备发送的原始字节
Session session = getSession(ctx);
ctx.fireChannelRead(Packet.of(session, buf)); // 传递给下一个处理器
}
// MessageDecoderWrapper - 步骤2:协议解码
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Packet packet = (Packet) msg;
ByteBuf input = packet.take();
DeviceReport message = decoder.decode(input, packet.session.getClientId());
// 协议解码器将ByteBuf解析为DeviceReport对象
ctx.fireChannelRead(packet.replace(message));
}
// DispatcherHandler - 步骤3:消息分发
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Packet packet = (Packet) msg;
Message request = packet.message; // 已经是Message对象了
// 处理消息...
}
```
### 4. 协议解码器的工作
协议解码器(如ModbusProtocol)负责:
1. 解析原始字节数据
2. 识别是否是设备回复(通过协议特征,如Modbus功能码)
3. 设置isReply标志
4. 封装为DeviceReport对象
**关键代码:**
```java
// ModbusProtocol.decode()
case Write06:
case Write05:
// 如果返回06编码,说明是设备回复
report.setIsReply(true); // 标记为回复消息
report.setStatus(FunctionReplyStatus.SUCCESS);
return report;
```
### 5. 响应匹配机制(关键!)
**问题:设备回复如何匹配到等待的request?**
**答案:通过Session的requestKey和responseKey机制**
```java
// Session.request() - 发送请求时
private static String requestKey(Message request, Class responseClass) {
String className = responseClass.getName();
if (Response.class.isAssignableFrom(responseClass)) {
String serNo = request.getSerNo(); // 使用序列号
return className + '.' + serNo;
}
return className; // 普通Message使用类名
}
// Session.response() - 接收响应时
private static String responseKey(Object response) {
String className = response.getClass().getName();
if (response instanceof Response) {
int serialNo = ((Response) response).getResponseSerialNo();
return className + '.' + serialNo;
}
return className;
}
```
**匹配流程:**
1. 发送请求时,生成requestKey并订阅
2. 设备回复后,协议解码为Message对象
3. **需要调用Session.response(message)**来匹配
4. 如果key匹配,Mono会收到响应,requestR返回成功
### 6. 当前代码的问题(重要发现!)
**重要发现:在当前代码中,Session.response()方法没有被调用!**
这意味着:
- 设备回复的Message对象被解码后,**没有被匹配到等待的request**
- requestR()的Mono会一直等待,直到超时
- **这就是为什么你的代码中,即使设备回复了,requestR()也可能返回超时**
**实际流程:**
1. requestR()发送请求,订阅等待响应
2. 设备回复原始字节数据
3. 协议解码器解码为DeviceReport对象,设置isReply=true
4. **但是,DispatcherHandler将消息发送到MQ,而不是调用Session.response()**
5. requestR()的Mono一直等待,200ms后超时
**关键代码:**
```java
// DispatcherHandler.handleReport() - 设备回复被发送到MQ
private void handleReport(Message request) {
DeviceReport report = (DeviceReport) request;
DeviceReportBo reportBo = new DeviceReportBo();
// ...
reportBo.setIsReply(report.getIsReply()); // 标记为回复
MessageProducer.sendPublishMsg(reportBo); // 发送到MQ,而不是Session.response()
}
```
**这意味着:**
- requestR()机制在当前实现中**可能无法正确工作**
- 设备回复通过MQ异步处理,而不是同步匹配到request
- 你的代码中检查AjaxResult的code和data是正确的方向
- 但需要确认:如果设备回复了,requestR()是否能正确返回?
## 总结
1. **设备只发送原始字节数据**,不知道Message对象
2. **框架负责解码**:ByteBuf → Message对象
3. **协议解码器识别回复**:通过协议特征(如Modbus功能码)设置isReply标志
4. **响应匹配机制**:通过requestKey/responseKey匹配请求和响应
5. **关键问题**:需要确认Session.response()是否被正确调用
## 建议
对于上电触发任务,如果Session.response()没有被调用,那么:
- requestR()可能无法正确识别设备回复
- 需要检查是否有其他地方调用了Session.response()
- 或者需要修改代码,在协议解码后主动调用Session.response()

View File

@ -26,6 +26,7 @@ public interface FastBeeConstant {
String WS_PREFIX = "web-"; String WS_PREFIX = "web-";
String WM_PREFIX = "server-"; String WM_PREFIX = "server-";
String FAST_PHONE = "phone-"; String FAST_PHONE = "phone-";
String POWER = "powerOnTriggerHandler";
/*MQTT平台判定离线时间 keepAlive*1.5 */ /*MQTT平台判定离线时间 keepAlive*1.5 */
Long DEVICE_PING_EXPIRED = 90000L; Long DEVICE_PING_EXPIRED = 90000L;

View File

@ -9,6 +9,8 @@ import com.fastbee.common.core.redis.RedisCache;
import com.fastbee.common.core.redis.RedisKeyBuilder; import com.fastbee.common.core.redis.RedisKeyBuilder;
import com.fastbee.common.enums.ServerType; import com.fastbee.common.enums.ServerType;
import com.fastbee.common.enums.TopicType; import com.fastbee.common.enums.TopicType;
import com.fastbee.common.constant.HttpStatus;
import com.fastbee.common.core.domain.AjaxResult;
import com.fastbee.common.utils.DateUtils; import com.fastbee.common.utils.DateUtils;
import com.fastbee.common.utils.StringUtils; import com.fastbee.common.utils.StringUtils;
import com.fastbee.common.utils.gateway.mq.TopicsUtils; import com.fastbee.common.utils.gateway.mq.TopicsUtils;
@ -31,10 +33,8 @@ import javax.annotation.Resource;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
@ -135,13 +135,16 @@ public class PowerOnTriggerTaskExecutor {
} }
String pendingKey = RedisKeyBuilder.buildPowerOnTriggerPendingKey(serialNumber); String pendingKey = RedisKeyBuilder.buildPowerOnTriggerPendingKey(serialNumber);
// 关键:检查是否存在"等待第一个数据包"标记 // 关键:使用原子删除操作来防止竞态条件
Object pendingFlag = redisCache.getCacheObject(pendingKey); // 只有第一个成功删除 pendingKey 的线程才能继续执行任务
if (pendingFlag == null) { // 其他线程删除时会返回 false,直接返回,避免重复执行
// 标记不存在,说明不是第一个数据包,或者已经处理过了 boolean deleted = redisCache.deleteObject(pendingKey);
log.debug("设备上电触发任务标记不存在,不是第一个数据包,设备编号:{}", serialNumber); if (!deleted) {
// 删除失败,说明已经被其他线程处理了,直接返回
log.debug("设备上电触发任务标记已被其他线程处理,不是第一个数据包,设备编号:{}", serialNumber);
return; return;
} }
// 成功删除,说明是第一个线程,继续执行任务
// 补偿查询设备信息(获取产品ID) // 补偿查询设备信息(获取产品ID)
if (productId == null) { if (productId == null) {
@ -219,10 +222,9 @@ public class PowerOnTriggerTaskExecutor {
redisCache.deleteObject(taskIdsKey); redisCache.deleteObject(taskIdsKey);
} }
// 关键:无论任务是否成功,都删除"等待第一个数据包"标记 // 注意:pendingKey 已经在方法开始时原子删除了,这里不需要再次删除
// 这样后续数据包就不会再触发任务了 // 如果执行到这里,说明是第一个成功删除 pendingKey 的线程
redisCache.deleteObject(pendingKey); log.info("设备上电触发任务处理完成,设备编号:{}", serialNumber);
log.info("设备上电触发任务处理完成,已删除第一个数据包标记,设备编号:{}", serialNumber);
} catch (Exception e) { } catch (Exception e) {
@ -272,11 +274,6 @@ public class PowerOnTriggerTaskExecutor {
} }
} }
// 记录设备上电触发任务下发的指令片段(索引2到3位,2个字符)
String cacheKey = RedisKeyBuilder.buildPowerOnTriggerCommandKey(serialNumber);
// 收集所有指令片段(用于支持多个指令的情况)
Set<String> commandSegments = new HashSet<>();
// 执行所有相关的modbus任务 // 执行所有相关的modbus任务
int successCount = 0; int successCount = 0;
int timeoutCount = 0; int timeoutCount = 0;
@ -296,14 +293,6 @@ public class PowerOnTriggerTaskExecutor {
} }
try { try {
// 记录下发的指令片段(索引2到3位,即第3到第4个字符,2个字符)
String cleanCommand = command.replaceAll("\\s+", "").toUpperCase();
if (cleanCommand.length() >= 4) {
String commandSegment = cleanCommand.substring(2, 4);
commandSegments.add(commandSegment);
log.debug("收集设备上电触发任务指令片段,设备编号:{},指令片段:{}", serialNumber, commandSegment);
}
// 转换指令为字节数组 // 转换指令为字节数组
byte[] commandData = ByteUtils.convertToHexByteArray(command); byte[] commandData = ByteUtils.convertToHexByteArray(command);
if (commandData.length == 0) { if (commandData.length == 0) {
@ -339,19 +328,50 @@ public class PowerOnTriggerTaskExecutor {
Message msg = new Message(); Message msg = new Message();
msg.setClientId(serialNumber); msg.setClientId(serialNumber);
msg.setPayload(Unpooled.wrappedBuffer(commandData)); msg.setPayload(Unpooled.wrappedBuffer(commandData));
// 为每个请求设置唯一的serNo,避免多个并发请求使用相同的requestKey
Session session = sessionStore.getSession(serialNumber);
if (session != null) {
int serNo = session.nextSerialNO();
msg.setSerNo(String.valueOf(serNo));
}
try { try {
messageManager.requestR(serialNumber, msg, Message.class) // 等待200ms内的响应,直接检查设备是否回复了Message对象
.timeout(Duration.ofMillis(20000)) // 200ms超时 // DispatcherHandler会在协议解码后,如果识别为设备回复(isReply=true),
.doOnSuccess(result -> log.info("=>上电触发-TCP-线程=[{}],轮询指令:[{}],响应成功", Thread.currentThread().getName(), command)) // 主动调用Session.response()来匹配等待的requestR()调用
AjaxResult result = messageManager.requestR(serialNumber, msg, Message.class)
.timeout(Duration.ofMillis(200)) // 200ms超时
.doOnError(error -> log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],响应超时或失败:{}", Thread.currentThread().getName(), command, error.getMessage())) .doOnError(error -> log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],响应超时或失败:{}", Thread.currentThread().getName(), command, error.getMessage()))
.block(); // 阻塞等待响应,最多200ms .block(); // 阻塞等待响应,最多200ms
responseReceived = true;
log.info("=>上电触发-TCP-线程=[{}],轮询指令:[{}],已收到响应", Thread.currentThread().getName(), command); // 检查返回结果:code为200(SUCCESS)且data字段有Message对象,说明设备回复了
if (result != null) {
Integer code = (Integer) result.get(AjaxResult.CODE_TAG);
Object data = result.get(AjaxResult.DATA_TAG);
// 只有code为200且data是Message对象时,才认为设备回复了
responseReceived = (code != null && code == HttpStatus.SUCCESS && data != null && data instanceof Message);
if (responseReceived) {
log.info("=>上电触发-TCP-线程=[{}],轮询指令:[{}],设备已回复,code:{}", Thread.currentThread().getName(), command, code);
// 释放Message对象的ByteBuf(DeviceReport的payload)
Message responseMessage = (Message) data;
if (responseMessage != null && responseMessage.getPayload() != null) {
responseMessage.getPayload().release();
}
} else {
log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],设备未回复,code:{}, data:{}", Thread.currentThread().getName(), command, code, data != null ? data.getClass().getSimpleName() : "null");
}
} else {
responseReceived = false;
log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],响应结果为空", Thread.currentThread().getName(), command);
}
} catch (Exception e) { } catch (Exception e) {
// 200ms内未收到响应,视为超时 // 200ms内未收到响应或发生异常,视为超时
long elapsed = System.currentTimeMillis() - sendTime; long elapsed = System.currentTimeMillis() - sendTime;
log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],200ms内未收到响应,耗时:{}ms", Thread.currentThread().getName(), command, elapsed); log.warn("=>上电触发-TCP-线程=[{}],轮询指令:[{}],200ms内未收到响应,耗时:{}ms,异常:{}", Thread.currentThread().getName(), command, elapsed, e.getMessage());
responseReceived = false; responseReceived = false;
// 超时后的回复会被DispatcherHandler识别为isReply=true,但匹配失败(订阅已取消)
// 会继续发送到MQ,但不会误判为普通数据包,因为isReply=true
} }
break; break;
default: default:
@ -376,14 +396,6 @@ public class PowerOnTriggerTaskExecutor {
} }
} }
// 所有指令下发完成后,缓存指令片段集合(用于识别回包)
if (!commandSegments.isEmpty()) {
// 将指令片段集合转换为JSON字符串存储,过期时间5秒(足够设备响应)
String segmentsJson = JSON.toJSONString(new ArrayList<>(commandSegments));
redisCache.setCacheObject(cacheKey, segmentsJson, 5, TimeUnit.SECONDS);
log.debug("缓存设备上电触发任务指令片段集合,设备编号:{},片段数量:{},片段:{}", serialNumber, commandSegments.size(), commandSegments);
}
log.info("设备上电触发任务执行完成,设备编号:{},总任务数:{},成功:{},超时:{}", serialNumber, totalCount, successCount, timeoutCount); log.info("设备上电触发任务执行完成,设备编号:{},总任务数:{},成功:{},超时:{}", serialNumber, totalCount, successCount, timeoutCount);
} }

View File

@ -123,6 +123,11 @@ public class Session {
String serNo = request.getSerNo(); String serNo = request.getSerNo();
return new StringBuffer(34).append(className).append('.').append(serNo).toString(); return new StringBuffer(34).append(className).append('.').append(serNo).toString();
} }
// 对于Message.class类型,如果请求有serNo,也使用serNo来生成唯一key
// 这样可以支持多个并发请求使用相同的响应类型
if (request != null && request.getSerNo() != null && !request.getSerNo().isEmpty()) {
return new StringBuffer(34).append(className).append('.').append(request.getSerNo()).toString();
}
return className; return className;
} }
@ -133,6 +138,19 @@ public class Session {
int serialNo = ((Response) response).getResponseSerialNo(); int serialNo = ((Response) response).getResponseSerialNo();
return new StringBuffer(34).append(className).append('.').append(serialNo).toString(); return new StringBuffer(34).append(className).append('.').append(serialNo).toString();
} }
// 对于Message类型(如DeviceReport),如果响应有serNo,也使用serNo来匹配
// 这样可以匹配到带serNo的请求
if (response instanceof Message) {
Message message = (Message) response;
if (message.getSerNo() != null && !message.getSerNo().isEmpty()) {
// 关键:如果响应是Message的子类(如DeviceReport),但请求类型是Message.class
// 需要同时尝试使用Message.class的类名来匹配,因为requestKey使用的是Message.class.getName()
String messageClassName = Message.class.getName();
// 优先使用Message.class的类名匹配(用于匹配Message.class类型的请求)
// 注意:这里返回Message.class的key,因为requestR()使用的是Message.class
return new StringBuffer(34).append(messageClassName).append('.').append(message.getSerNo()).toString();
}
}
return className; return className;
} }
@ -249,11 +267,44 @@ public class Session {
* 消息响应 * 消息响应
*/ */
public boolean response(Message message) { public boolean response(Message message) {
MonoSink monoSink = topicSubscribers.get(responseKey(message)); // 先尝试精确匹配
String key = responseKey(message);
MonoSink monoSink = topicSubscribers.get(key);
if (monoSink != null) { if (monoSink != null) {
monoSink.success(message); monoSink.success(message);
return true; return true;
} }
// 如果精确匹配失败,且响应是Message类型,尝试匹配所有等待Message.class类型的请求
// 这是因为设备回复可能没有serNo,或者类名不匹配(如DeviceReport vs Message)
if (message instanceof Message) {
String messageClassName = Message.class.getName();
// 如果响应有serNo,尝试使用Message.class的类名匹配
if (message.getSerNo() != null && !message.getSerNo().isEmpty()) {
String messageKey = messageClassName + "." + message.getSerNo();
monoSink = topicSubscribers.get(messageKey);
if (monoSink != null) {
monoSink.success(message);
return true;
}
}
// 如果还是没有匹配到,尝试匹配所有等待Message.class类型的请求(不依赖serNo)
// 找到第一个等待Message.class类型的请求(key以Message.class.getName()开头)
// 这样可以匹配到设备回复,即使设备回复没有serNo
synchronized (topicSubscribers) {
for (Map.Entry<String, MonoSink> entry : topicSubscribers.entrySet()) {
String entryKey = entry.getKey();
// 如果key是Message.class的类名,或者是Message.class的类名+serNo
if (entryKey.equals(messageClassName) || entryKey.startsWith(messageClassName + ".")) {
entry.getValue().success(message);
return true;
}
}
}
}
return false; return false;
} }

View File

@ -75,7 +75,7 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter {
*/ */
// 4.单个数据包上传无标识位 // 4.单个数据包上传无标识位
if (null == request.getMessageId()) { if (null == request.getMessageId()) {
this.handleReport(request); this.handleReport(request, session);
} }
//处理数据调试 //处理数据调试
@ -100,7 +100,7 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter {
this.handleMessage(request, packet, ctx); this.handleMessage(request, packet, ctx);
} }
//处理消息转发 //处理消息转发
this.handleReport(request); this.handleReport(request, session);
// 2/3 单个注册包,心跳,数据包上传,匹配消息id处理 // 2/3 单个注册包,心跳,数据包上传,匹配消息id处理
} else if (null != request.getMessageId() && !"0".equals(request.getMessageId())) { } else if (null != request.getMessageId() && !"0".equals(request.getMessageId())) {
@ -134,8 +134,29 @@ public class DispatcherHandler extends ChannelInboundHandlerAdapter {
} }
} }
private void handleReport(Message request) { private void handleReport(Message request, Session session) {
DeviceReport report = (DeviceReport) request; DeviceReport report = (DeviceReport) request;
// 尝试匹配到等待的requestR()调用(无论isReply是否为true)
// 这是因为读操作(Read01、Read03等)的回复可能没有设置isReply=true
// 但仍然是设备对下发指令的回复,需要匹配到等待的请求
// Session.response()方法已经包含了回退机制,可以匹配到设备回复,即使设备回复没有serNo
if (session != null) {
boolean matched = session.response(report);
if (matched) {
// 匹配成功,requestR()已经收到响应,不再发送到MQ,避免重复处理
// 注意:这里不发送到MQ,因为requestR()已经处理了
// 同时,如果原来没有设置isReply=true,现在设置为true,表示这是设备回复
if (report.getIsReply() == null || !report.getIsReply()) {
report.setIsReply(true);
}
return;
}
// 匹配失败,说明不是等待的请求的回复,继续正常流程(发送到MQ)
}
// 正常流程:发送到MQ
DeviceReportBo reportBo = new DeviceReportBo(); DeviceReportBo reportBo = new DeviceReportBo();
reportBo.setThingsModelSimpleItem(report.getThingsModelSimpleItem()); reportBo.setThingsModelSimpleItem(report.getThingsModelSimpleItem());
reportBo.setPlatformDate(DateUtils.getNowDate()); reportBo.setPlatformDate(DateUtils.getNowDate());

View File

@ -687,10 +687,8 @@ public class DeviceServiceImpl implements IDeviceService {
} }
// 缓存最新一条数据到redis // 缓存最新一条数据到redis
redisCache.hashPutAll(key, maps); redisCache.hashPutAll(key, maps);
// 判断是否是设备上电触发的回包,如果是则跳过保存历史数据
boolean isPowerOnTriggerReply = isPowerOnTriggerReply(input.getSources(), input.getDeviceNumber());
//存储历史数据 //存储历史数据
if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow && !isPowerOnTriggerReply) { if (!CollectionUtils.isEmpty(deviceLogList) && !isShadow) {
for (DeviceLog deviceLog : deviceLogList) { for (DeviceLog deviceLog : deviceLogList) {
logService.saveDeviceLog(deviceLog); logService.saveDeviceLog(deviceLog);
} }
@ -722,7 +720,7 @@ public class DeviceServiceImpl implements IDeviceService {
Map<String, String> calculatedMaps = new HashMap<>(); Map<String, String> calculatedMaps = new HashMap<>();
List<DeviceLog> calculatedDeviceLogList = new ArrayList<>(); List<DeviceLog> calculatedDeviceLogList = new ArrayList<>();
calculateDependentThingsModels(input, updatedIdentifiers, key, calculatedMaps, calculatedDeviceLogList, type, isPowerOnTriggerReply); calculateDependentThingsModels(input, updatedIdentifiers, key, calculatedMaps, calculatedDeviceLogList, type);
// 将计算后的值更新到缓存 // 将计算后的值更新到缓存
if (!calculatedMaps.isEmpty()) { if (!calculatedMaps.isEmpty()) {
@ -732,7 +730,7 @@ public class DeviceServiceImpl implements IDeviceService {
} }
// 保存计算型物模型的历史数据 // 保存计算型物模型的历史数据
if (!CollectionUtils.isEmpty(calculatedDeviceLogList) && !isPowerOnTriggerReply) { if (!CollectionUtils.isEmpty(calculatedDeviceLogList)) {
for (DeviceLog deviceLog : calculatedDeviceLogList) { for (DeviceLog deviceLog : calculatedDeviceLogList) {
logService.saveDeviceLog(deviceLog); logService.saveDeviceLog(deviceLog);
} }
@ -1942,96 +1940,6 @@ public class DeviceServiceImpl implements IDeviceService {
return deviceMapper.selectSerialNumbersByProductId(productId); return deviceMapper.selectSerialNumbersByProductId(productId);
} }
/**
* 判断是否是设备上电触发的回包
* 从Redis缓存中获取下发的指令片段(索引2到3位,2个字符),与上报的指令片段进行比较
*
* @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);
Object cachedSegments = redisCache.getCacheObject(cacheKey);
// 如果缓存中没有下发的指令片段,说明不是设备上电触发的回包
if (cachedSegments == null) {
return false;
}
// 解析指令片段(可能是单个字符串或JSON数组)
Set<String> sentCommandSegments = new HashSet<>();
try {
if (cachedSegments instanceof String) {
String segmentsStr = (String) cachedSegments;
// 尝试解析为JSON数组
if (segmentsStr.startsWith("[")) {
List<String> segmentsList = JSON.parseArray(segmentsStr, String.class);
sentCommandSegments.addAll(segmentsList);
} else {
// 单个片段(兼容旧数据)
sentCommandSegments.add(segmentsStr);
}
}
} catch (Exception e) {
log.warn("解析指令片段失败,设备编号:{}", serialNumber, e);
return false;
}
if (sentCommandSegments.isEmpty()) {
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();
}
// 至少需要4个字符才能提取索引2到3位
if (hexStr.length() < 4) {
return false;
}
// 提取上报指令的索引2到3位(即第3到第4个字符,2个字符)
String actualSegment = hexStr.substring(2, 4);
// 与下发的指令片段集合进行比较(支持多个指令)
boolean isMatch = sentCommandSegments.contains(actualSegment);
if (isMatch) {
// 匹配成功,删除缓存(避免重复匹配)
redisCache.deleteObject(cacheKey);
log.info("检测到设备上电触发回包,设备编号:{},指令片段:{}", serialNumber, actualSegment);
}
return isMatch;
} catch (Exception e) {
log.warn("判断设备上电触发回包时发生异常: {}", e.getMessage());
return false;
}
}
/** /**
* 自动计算依赖已更新物模型的其他物模型(纯计算型物模型) * 自动计算依赖已更新物模型的其他物模型(纯计算型物模型)
@ -2043,15 +1951,13 @@ public class DeviceServiceImpl implements IDeviceService {
* @param maps 物模型值映射(用于批量更新缓存) * @param maps 物模型值映射(用于批量更新缓存)
* @param deviceLogList 设备日志列表(用于存储历史数据) * @param deviceLogList 设备日志列表(用于存储历史数据)
* @param type 物模型类型(1=属性,2=功能,3=事件) * @param type 物模型类型(1=属性,2=功能,3=事件)
* @param isPowerOnTriggerReply 是否是设备上电触发回包
*/ */
private void calculateDependentThingsModels(ThingsModelValuesInput input, private void calculateDependentThingsModels(ThingsModelValuesInput input,
Set<String> updatedIdentifiers, Set<String> updatedIdentifiers,
String key, String key,
Map<String, String> maps, Map<String, String> maps,
List<DeviceLog> deviceLogList, List<DeviceLog> deviceLogList,
int type, int type) {
boolean isPowerOnTriggerReply) {
try { try {
// 查询产品下所有物模型 // 查询产品下所有物模型
com.fastbee.iot.domain.ThingsModel queryModel = new com.fastbee.iot.domain.ThingsModel(); com.fastbee.iot.domain.ThingsModel queryModel = new com.fastbee.iot.domain.ThingsModel();

View File

@ -0,0 +1,6 @@
-- 为iot_modbus_job表添加execute_time字段
-- 用于记录设备上电触发任务的执行时间
ALTER TABLE `iot_modbus_job`
ADD COLUMN `execute_time` datetime NULL DEFAULT NULL COMMENT '执行时间(仅设备上电触发任务使用,记录任务执行的时间)' AFTER `remark`;

View File

@ -0,0 +1,17 @@
-- 为 iot_modbus_job 表添加 cycle_type 字段
-- 时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发)
ALTER TABLE `iot_modbus_job`
ADD COLUMN `cycle_type` int(2) NULL DEFAULT NULL COMMENT '时间周期类型(1-周期循环,2-自定义时间段,3-设备上电触发)' AFTER `device_type`;
-- 迁移设备上电触发任务:将 remark 字段中包含 'power_on_trigger' 的任务设置为 cycle_type = 3
-- remark 字段保持不变,因为代码中还在使用它来识别设备上电触发任务
UPDATE `iot_modbus_job`
SET `cycle_type` = 3
WHERE `remark` IS NOT NULL AND `remark` LIKE '%power_on_trigger%';
-- 为其他现有数据设置默认值(默认为1-周期循环)
UPDATE `iot_modbus_job`
SET `cycle_type` = 1
WHERE `cycle_type` IS NULL;

View File

@ -0,0 +1,163 @@
# 上电触发响应处理优化说明
## 优化目标
在上电触发场景下,**在协议解码之前拦截设备回复**,直接匹配到等待的`requestR()`调用,避免走完整的协议解码和MQ流程,减少通信负担,提高响应速度。
## 实现方案
### 1. 新增Handler:PowerOnTriggerResponseHandler
**位置:** `fastbee-server/iot-server-core/src/main/java/com/fastbee/server/handler/PowerOnTriggerResponseHandler.java`
**功能:**
- 在协议解码之前拦截设备回复的原始ByteBuf数据
- 创建简单的Message对象(不解析内容)
- 调用`Session.response()`尝试匹配到等待的`requestR()`调用
- 如果匹配成功,`requestR()`会立即返回,无需等待协议解码
### 2. Handler链顺序
```
TCPMessageAdapter (接收原始ByteBuf)
↓
PowerOnTriggerResponseHandler (拦截并匹配响应) ← 新增
↓
MessageDecoderWrapper (协议解码)
↓
DispatcherHandler (消息分发)
```
### 3. 工作原理
```
1. 平台下发指令
PowerOnTriggerTaskExecutor.execute()
→ MessageManager.requestR()
→ Session.request() 订阅等待响应
2. 设备回复原始字节数据
设备发送:ByteBuf (例如:7E 01 03 00 00 00 02 C4 0B 7E)
3. PowerOnTriggerResponseHandler拦截
- 接收原始ByteBuf
- 创建简单的Message对象(不解析内容)
- 调用Session.response()尝试匹配
4. 匹配成功
- Session.response()找到等待的request
- 立即唤醒等待的Mono
- requestR()返回成功(code=200, data=Message对象)
- PowerOnTriggerTaskExecutor收到响应,更新任务状态
5. 继续传递(可选)
- 如果继续传递,协议解码也会处理(但上电触发任务已完成)
- 如果不想走协议解码,可以注释掉ctx.fireChannelRead(msg)
```
## 优势
1. **减少通信负担**
- 不需要走完整的协议解码流程
- 不需要发送到MQ异步处理
- 直接同步匹配,响应更快
2. **提高精准度**
- 直接匹配等待的request,不依赖协议特征识别
- 避免协议解码误判导致的问题
3. **降低延迟**
- 200ms内设备回复,立即匹配成功
- 无需等待协议解码和MQ处理
## 代码修改
### 1. PowerOnTriggerResponseHandler.java
```java
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
Packet packet = (Packet) msg;
Session session = packet.session;
ByteBuf byteBuf = packet.byteBuf;
// 创建简单的Message对象,只用于匹配响应
Message responseMessage = new Message();
responseMessage.setClientId(session.getClientId());
responseMessage.setPayload(byteBuf.retain());
// 尝试匹配到等待的request
boolean matched = session.response(responseMessage);
if (matched) {
log.debug("=>上电触发-设备回复已匹配,设备编号:{}", session.getClientId());
// 匹配成功,继续传递(可选)
ctx.fireChannelRead(msg);
} else {
// 没有匹配到,继续正常流程
responseMessage.getPayload().release();
ctx.fireChannelRead(msg);
}
}
```
### 2. TCPServer.java
```java
channel.pipeline()
.addLast(FastBeeConstant.SERVER.ADAPTER, adapter)
.addLast("powerOnTriggerHandler", powerOnTriggerHandler) // 新增
.addLast(FastBeeConstant.SERVER.DECODER, decoder)
// ...
```
## 使用说明
### 当前实现
- **匹配成功**:`requestR()`立即返回,`PowerOnTriggerTaskExecutor`收到响应
- **继续传递**:消息继续传递给协议解码器(可选,如果不想走协议解码可以注释掉)
### 如果不想走协议解码
如果完全不想走协议解码,可以修改`PowerOnTriggerResponseHandler`:
```java
if (matched) {
log.debug("=>上电触发-设备回复已匹配,设备编号:{}", session.getClientId());
// 匹配成功,直接消费掉,不走协议解码
byteBuf.release(); // 释放ByteBuf
return; // 不继续传递
}
```
### 注意事项
1. **ByteBuf引用计数**
- 使用`byteBuf.retain()`增加引用计数
- 如果匹配失败,需要`release()`释放
- 如果匹配成功,后续处理器会负责释放
2. **兼容性**
- 如果匹配失败,继续走正常流程(协议解码等)
- 不影响其他功能
3. **性能**
- 只在上电触发场景下生效
- 其他场景继续走正常流程
## 测试建议
1. **验证匹配成功**
- 下发上电触发指令
- 设备回复后,检查`requestR()`是否立即返回
- 检查`PowerOnTriggerTaskExecutor`是否正确更新任务状态
2. **验证性能**
- 对比优化前后的响应时间
- 检查是否减少了MQ消息量
3. **验证兼容性**
- 确保其他功能不受影响
- 确保协议解码仍然正常工作

View File

@ -0,0 +1,606 @@
# FastBee IoT平台 - 规则引擎原理与操作说明
## 一、规则引擎概述
**规则引擎**是FastBee IoT平台的核心组件,用于处理设备数据、实现场景联动、数据转发等功能。它基于**LiteFlow**框架,支持Groovy脚本,通过EL表达式构建规则链,实现灵活的业务逻辑处理。
### 核心特性
- **基于LiteFlow**: 轻量级规则引擎框架
- **Groovy脚本**: 支持动态脚本执行
- **EL表达式**: 使用表达式语言构建规则链
- **异步执行**: 非阻塞式规则执行
- **动态构建**: 运行时动态创建规则链和脚本节点
## 二、规则引擎工作原理
### 2.1 整体架构
```
设备上报数据
↓
数据处理器 (DataHandler)
↓
规则引擎处理器 (RuleEngineHandler)
↓
查询关联规则 (Script/Scene)
↓
构建规则链 (LiteFlow)
↓
执行规则脚本 (Groovy)
↓
执行动作 (设备控制/告警/转发等)
```
### 2.2 核心组件
#### 1. FlowLogExecutor(规则执行器)
- **位置**: `fastbee-plugs/fastbee-ruleEngine/src/main/java/com/fastbee/ruleEngine/core/FlowLogExecutor.java`
- **作用**: 封装LiteFlow执行器,提供日志记录功能
- **功能**:
- 执行规则链
- 记录执行日志
- 处理执行结果
#### 2. SceneContext(场景上下文)
- **位置**: `fastbee-iot-data/src/main/java/com/fastbee/iot/data/ruleEngine/SceneContext.java`
- **作用**: 场景联动的执行上下文
- **包含数据**:
- 设备编号
- 产品ID
- 数据类型(属性/功能/事件)
- 物模型数据列表
#### 3. MsgContext(消息上下文)
- **位置**: `fastbee-plugs/fastbee-ruleEngine/src/main/java/com/fastbee/ruleEngine/context/MsgContext.java`
- **作用**: 规则脚本的消息上下文
- **包含数据**:
- 主题(topic)
- 负载(payload)
- 设备编号
- 产品ID
- 协议编码
### 2.3 规则链构建原理
#### EL表达式语法
规则引擎使用EL(Expression Language)表达式来构建规则链,支持以下语法:
1. **IF-THEN结构**
```
IF(条件, THEN(动作1, 动作2))
```
2. **逻辑运算符**
- `AND(条件1, 条件2, ...)`: 所有条件都满足
- `OR(条件1, 条件2, ...)`: 任一条件满足
- `NOT(条件)`: 条件不满足
3. **执行模式**
- `THEN(动作1, 动作2, ...)`: 串行执行(顺序执行)
- `WHEN(动作1, 动作2, ...)`: 并行执行(同时执行)
#### 规则链示例
**场景联动规则链**:
```
IF(AND(T1, T2, T3), THEN(A1, A2, A3))
```
- `T1, T2, T3`: 触发器脚本(条件判断)
- `A1, A2, A3`: 执行动作脚本(执行操作)
**数据流规则链**:
```
THEN(script1, script2, httpBridge)
```
- `script1, script2`: 数据处理脚本
- `httpBridge`: HTTP桥接节点
### 2.4 脚本类型
#### 1. 脚本用途(scriptPurpose)
- `1`: 数据流 - 处理设备上报数据
- `2`: 触发器 - 条件判断脚本(返回boolean)
- `3`: 执行动作 - 执行操作脚本
#### 2. 脚本类型(scriptType)
- `script`: 普通脚本
- `boolean_script`: 条件脚本(返回boolean值)
- `switch_script`: 选择脚本
- `for_script`: 循环脚本
#### 3. 脚本事件(scriptEvent)
- `1`: 设备上报
- `2`: 平台下发
- `3`: 设备上线
- `4`: 设备离线
- `5`: HTTP桥接数据
- `6`: MQTT桥接数据
#### 4. 脚本动作(scriptAction)
- `1`: 消息重发
- `2`: 消息通知
- `3`: HTTP推送
- `4`: MQTT桥接
- `5`: 数据库存储
### 2.5 执行流程详解
#### 场景联动执行流程
```
1. 设备上报数据
↓
2. RuleEngineHandler.ruleMatch() 接收数据
↓
3. 查询设备关联的场景
sceneDeviceService.selectTriggerDeviceRelateScenes()
↓
4. 遍历场景列表
↓
5. 创建SceneContext上下文
↓
6. 执行规则链
flowExecutor.execute2FutureWithRid(chainName, context)
↓
7. LiteFlow解析EL表达式
例如: IF(AND(T1, T2), THEN(A1, A2))
↓
8. 执行触发器脚本(T1, T2)
- 检查静默时间
- 匹配设备数据
- 判断条件是否满足
↓
9. 如果条件满足,执行动作脚本(A1, A2)
- 设备控制
- 告警推送
- 场景恢复等
```
#### 规则脚本执行流程
```
1. 设备上报数据
↓
2. RuleProcess.processRuleScript() 处理
↓
3. 查询匹配的规则脚本
selectExecRuleScriptList(scriptCondition)
↓
4. 构建规则链
LiteFlowChainELBuilder.createChain()
↓
5. 执行脚本
flowExecutor.execute2RespWithRid()
↓
6. 执行脚本动作
- HTTP推送
- MQTT桥接
- 数据库存储等
```
## 三、规则引擎操作指南
### 3.1 规则脚本管理
**功能位置**: 规则引擎 → 规则脚本
**路径**: `/iot/scene/script`
**权限**: `iot:script:list`
#### 创建规则脚本
1. **进入规则脚本页面**
- 点击"新增"按钮
2. **配置脚本基本信息**
- **脚本名称**: 脚本的显示名称
- **脚本标识**: 唯一标识(字母开头,可包含数字)
- **执行顺序**: 值越大优先级越高
- **脚本事件**: 选择触发事件(设备上报/平台下发/设备上线/设备离线等)
- **脚本动作**: 选择执行动作(消息重发/消息通知/HTTP推送/MQTT桥接/数据库存储)
- **脚本状态**: 启用/禁用
3. **选择关联对象**
- **产品**: 选择关联的产品(设备上报/平台下发时)
- **桥接**: 选择HTTP/MQTT桥接(桥接数据时)
4. **编写脚本代码**
- 使用Groovy语言编写
- 脚本中可以使用上下文变量
- 支持脚本验证
5. **脚本验证**
- 点击"验证"按钮检查脚本语法
- 验证通过后才能保存
6. **保存脚本**
- 点击"确定"保存脚本
- 系统会自动构建规则链
#### 脚本编写示例
**数据流脚本示例**:
```groovy
// 获取消息上下文
def msgContext = _meta.getContextBean("com.fastbee.ruleEngine.context.MsgContext")
// 获取设备编号
def serialNumber = msgContext.getSerialNumber()
// 获取产品ID
def productId = msgContext.getProductId()
// 获取消息负载
def payload = msgContext.getPayload()
// 处理数据逻辑
// ...
// 设置处理后的数据
msgContext.setPayload(processedData)
```
**条件脚本示例**:
```groovy
// 获取场景上下文
def sceneContext = _meta.getContextBean("com.fastbee.iot.data.ruleEngine.SceneContext")
// 处理规则脚本
String json = "..."; // JSON格式的脚本模板
return sceneContext.process(json);
```
#### 查看脚本日志
1. 在脚本列表中点击"日志"按钮
2. 查看脚本执行日志
3. 日志包含:
- 执行时间
- 上下文数据
- 执行步骤
- 错误信息
### 3.2 场景联动中的规则引擎
场景联动会自动使用规则引擎,用户无需直接操作规则引擎,但需要了解其工作原理:
#### 场景创建时的规则构建
1. **创建场景时**
- 系统自动为每个触发器创建规则脚本(T开头)
- 系统自动为每个执行动作创建规则脚本(A开头)
- 自动生成EL表达式
2. **规则链构建**
- 根据执行条件(或/且/非)构建IF表达式
- 根据执行方式(串行/并行)构建THEN/WHEN表达式
- 生成完整的规则链名称(chainName)
3. **动态加载**
- 规则链和脚本节点动态构建到内存
- 无需重启服务即可生效
#### EL表达式生成规则
**执行条件为"且"(所有条件满足)**:
```
IF(AND(T1, T2, T3), THEN(A1, A2))
```
**执行条件为"或"(任一条件满足)**:
```
IF(OR(T1, T2, T3), THEN(A1, A2))
```
**执行条件为"非"(条件不满足)**:
```
IF(NOT(T1), THEN(A1, A2))
```
**执行方式为并行**:
```
IF(AND(T1, T2), WHEN(A1, A2))
```
### 3.3 规则脚本使用场景
#### 场景1: 数据转发
**需求**: 将设备上报的数据转发到第三方系统
**操作步骤**:
1. 创建规则脚本
2. 脚本事件选择"设备上报"
3. 脚本动作选择"HTTP推送"或"MQTT桥接"
4. 编写脚本处理数据
5. 配置桥接信息
6. 保存并启用脚本
#### 场景2: 数据转换
**需求**: 转换设备数据格式
**操作步骤**:
1. 创建规则脚本
2. 脚本事件选择"设备上报"
3. 脚本动作选择"消息重发"
4. 编写脚本转换数据格式
5. 保存并启用脚本
#### 场景3: 数据存储
**需求**: 将设备数据存储到数据库
**操作步骤**:
1. 创建规则脚本
2. 脚本事件选择"设备上报"
3. 脚本动作选择"数据库存储"
4. 编写脚本处理数据
5. 保存并启用脚本
## 四、规则引擎核心代码
### 4.1 规则执行器
```java
// FlowLogExecutor.java
@Component
public class FlowLogExecutor {
@Resource
private FlowExecutor flowExecutor;
// 执行规则链(带请求ID)
public LiteflowResponse execute2FutureWithRid(
String chainId,
Object param,
String requestId,
Object... contextBeanArray
) {
// 打印上下文
printContextBeanWithRid(requestId, contextBeanArray);
// 执行规则链
Future<LiteflowResponse> future =
flowExecutor.execute2FutureWithRid(chainId, param, requestId, contextBeanArray);
LiteflowResponse response = future.get();
// 打印执行结果
printResponseWithRid(requestId, response);
return response;
}
}
```
### 4.2 规则链构建
```java
// SceneServiceImpl.java
private void dynamicBuildRule(List<Script> ruleScripts, Scene scene) {
for (Script ruleScript : ruleScripts) {
if (ruleScript.getScriptPurpose() == 2) {
// 构建条件脚本节点
LiteFlowNodeBuilder.createScriptBooleanNode()
.setId(ruleScript.getScriptId())
.setName(ruleScript.getScriptName())
.setScript(ruleScript.getScriptData())
.build();
} else if (ruleScript.getScriptPurpose() == 3) {
// 构建普通脚本节点
LiteFlowNodeBuilder.createScriptNode()
.setId(ruleScript.getScriptId())
.setName(ruleScript.getScriptName())
.setScript(ruleScript.getScriptData())
.build();
}
}
// 构建规则链
LiteFlowChainELBuilder.createChain()
.setChainName(scene.getChainName())
.setEL(scene.getElData())
.build();
}
```
### 4.3 EL表达式构建
```java
// SceneServiceImpl.java
private String buildElData(Scene scene, ...) {
StringBuilder triggerBuilder = new StringBuilder();
StringBuilder actionBuilder = new StringBuilder();
// 根据执行条件构建IF表达式
switch (scene.getCond()) {
case 1: // 或
triggerBuilder.append("IF(OR(");
break;
case 2: // 且
triggerBuilder.append("IF(AND(");
break;
case 3: // 非
triggerBuilder.append("IF(NOT(");
break;
}
// 根据执行方式构建THEN/WHEN表达式
switch (scene.getExecuteMode()) {
case 1: // 串行
actionBuilder.append(",THEN(");
break;
case 2: // 并行
actionBuilder.append(",WHEN(");
break;
}
// 拼接触发器ID
for (SceneScript trigger : scene.getTriggers()) {
triggerBuilder.append(scriptId + ",");
}
// 拼接动作ID
for (SceneScript action : scene.getActions()) {
actionBuilder.append(scriptId + ",");
}
// 生成最终EL表达式
// 例如: IF(AND(T1,T2),THEN(A1,A2))
return triggerBuilder.append(actionBuilder).toString();
}
```
## 五、规则引擎API接口
### 5.1 规则脚本接口
- `GET /iot/script/list` - 查询规则脚本列表
- `GET /iot/script/{scriptId}` - 获取规则脚本详情
- `POST /iot/script` - 新增规则脚本
- `PUT /iot/script` - 修改规则脚本
- `DELETE /iot/script/{scriptIds}` - 删除规则脚本
- `GET /iot/script/log/{scriptId}` - 获取规则脚本日志
- `POST /iot/script/validate` - 验证规则脚本
### 5.2 场景联动接口
- `GET /iot/scene/list` - 查询场景列表
- `GET /iot/scene/{sceneId}` - 获取场景详情
- `POST /iot/scene` - 新增场景
- `PUT /iot/scene` - 修改场景
- `DELETE /iot/scene/{sceneIds}` - 删除场景
- `GET /iot/scene/log/{chainName}` - 获取场景执行日志
- `POST /iot/runtime/runScene` - 手动执行场景
## 六、规则引擎最佳实践
### 6.1 脚本编写规范
1. **脚本标识命名**
- 使用字母开头
- 可包含字母和数字
- 建议使用有意义的名称
2. **脚本代码规范**
- 使用Groovy语法
- 添加必要的注释
- 处理异常情况
- 避免死循环
3. **性能优化**
- 避免在脚本中执行耗时操作
- 使用缓存减少重复计算
- 合理使用脚本执行顺序
### 6.2 规则链设计原则
1. **触发器设计**
- 条件判断要准确
- 避免过于复杂的条件
- 合理使用静默周期
2. **执行动作设计**
- 动作要独立可执行
- 避免动作之间的依赖
- 合理选择串行/并行执行
3. **规则链优化**
- 减少不必要的规则链
- 合并相似的规则
- 定期清理无效规则
### 6.3 调试技巧
1. **查看执行日志**
- 在脚本列表中查看日志
- 在场景列表中查看日志
- 日志包含详细的执行信息
2. **脚本验证**
- 保存前先验证脚本
- 检查语法错误
- 验证上下文变量
3. **测试规则**
- 使用测试接口测试规则
- 模拟设备数据上报
- 验证规则执行结果
## 七、规则引擎与场景联动的关系
### 7.1 关系说明
- **场景联动**是规则引擎的**应用场景**
- **规则引擎**是场景联动的**执行引擎**
- 场景联动配置会自动转换为规则引擎的规则链
### 7.2 工作流程
```
用户配置场景联动
↓
系统自动构建规则脚本
↓
生成EL表达式规则链
↓
设备上报数据触发规则
↓
规则引擎执行规则链
↓
执行场景联动动作
```
### 7.3 规则脚本分类
在场景联动中,规则脚本分为两类:
1. **触发器脚本(T开头)**
- 用途:条件判断
- 类型:boolean_script
- 返回:true/false
2. **执行动作脚本(A开头)**
- 用途:执行操作
- 类型:script
- 执行:设备控制、告警推送等
## 八、注意事项
1. **脚本安全**
- 脚本执行有安全限制
- 不允许执行危险操作
- 系统会自动替换危险包引用
2. **性能考虑**
- 规则脚本执行是异步的
- 避免在脚本中执行耗时操作
- 大量规则可能影响性能
3. **规则链管理**
- 规则链动态构建到内存
- 修改场景后规则链会自动更新
- 删除场景会清理相关规则链
4. **日志管理**
- 规则执行日志存储在logs/rule目录
- 定期清理日志文件
- 日志文件可能较大
5. **脚本语言**
- 目前主要支持Groovy
- 未来可能支持其他语言(JS、Python等)
6. **规则链命名**
- 场景规则链:C + 雪花算法ID
- 数据流规则链:dataChain_ + scriptId
- 规则链名称唯一
---
**文档版本**: v1.0
**最后更新**: 2024-12-20
**维护者**: FastBee开发团队

495
视频中心功能说明.md Normal file
View File

@ -0,0 +1,495 @@
# FastBee IoT平台 - 视频中心功能说明
## 一、视频中心概述
**视频中心**是FastBee IoT平台中专门用于视频监控管理的功能模块。它基于GB28181协议,支持SIP视频设备的接入、直播、录像、回放等功能,提供完整的视频监控解决方案。
### 核心特性
- **GB28181协议支持**: 符合国标GB28181视频监控协议
- **多路视频播放**: 支持1/4/9分屏同时播放
- **录像管理**: 支持录像查询、回放、下载
- **流媒体服务器**: 集成ZLM流媒体服务器
- **设备管理**: 支持SIP设备注册、通道管理
## 二、视频中心功能模块
### 2.1 分屏显示
**功能位置**: 视频中心 → 分屏显示
**路径**: `/iot/sip/splitview`
**权限**: 无特殊权限要求
#### 作用
- 多路视频同时播放
- 实时监控多个摄像头
- 支持1/4/9分屏切换
- 设备树快速选择通道
#### 主要功能
1. **分屏模式**
- **单屏模式**: 1路视频全屏播放
- **4分屏模式**: 同时播放4路视频
- **9分屏模式**: 同时播放9路视频
2. **设备树**
- 左侧显示设备树结构
- 按产品、设备、通道层级展示
- 点击通道即可开始播放
3. **视频播放**
- 自动获取流媒体地址
- 支持FLV/WS-FLV/WSS-FLV协议
- 自动适配HTTP/HTTPS协议
- 支持截图功能
4. **播放控制**
- 点击分屏区域选择播放器
- 自动保存播放状态
- 支持播放器销毁和重建
#### 使用场景
- 监控中心大屏展示
- 多路视频实时监控
- 设备巡检
- 安防监控
#### 操作步骤
1. 进入分屏显示页面
2. 在左侧设备树选择要播放的通道
3. 选择分屏模式(1/4/9)
4. 点击通道开始播放
5. 点击分屏区域切换播放器
---
### 2.2 录像管理
**功能位置**: 视频中心 → 录像管理
**路径**: `/iot/record/record`
**权限**: `iot:sip:record:query`
#### 作用
- 查看流媒体服务器上的录像文件
- 录像回放
- 录像下载
- 录像上传到OSS
#### 主要功能
1. **录像列表**
- 按流媒体服务器查看录像
- 显示应用名称(app)
- 显示流ID(stream)
- 显示录像时间
2. **录像详情**
- 按日期查看录像文件
- 显示具体录像时间段
- 支持录像文件列表浏览
3. **录像回放**
- 点击录像文件开始回放
- 支持时间轴选择
- 支持播放控制(播放/暂停/快进/快退)
4. **录像下载**
- 下载录像文件到本地
- 支持MP4格式下载
- 支持批量下载
5. **录像上传**
- 上传录像到OSS对象存储
- 支持云存储备份
#### 录像类型
- **实时录像**: 设备推流时自动录制
- **历史录像**: 从设备拉取的录像文件
- **云端录像**: 存储在流媒体服务器的录像
#### 使用场景
- 查看历史监控录像
- 事件回放分析
- 录像备份归档
- 证据提取
#### 操作步骤
1. 选择流媒体服务器
2. 查看录像列表
3. 点击"查看"进入录像详情
4. 选择日期查看录像文件
5. 点击录像文件开始回放
6. 可下载或上传录像
---
### 2.3 配置管理(媒体服务器)
**功能位置**: 视频中心 → 配置管理
**路径**: `/iot/sip/mediaServer`
**权限**: `iot:video:list`
#### 作用
- 管理流媒体服务器配置
- 添加/编辑/删除媒体服务器
- 配置服务器参数
- 管理服务器状态
#### 主要功能
1. **服务器列表**
- 显示所有配置的流媒体服务器
- 显示服务器ID、IP地址、协议类型
- 显示创建时间
2. **服务器配置**
- **服务器ID**: 唯一标识
- **IP地址**: 服务器IP
- **协议**: HTTP/HTTPS
- **域名**: HTTPS访问域名
- **端口配置**:
- HTTP端口
- HTTPS端口
- RTSP端口
- RTP端口
- 录像端口
- **密钥**: ZLM服务器密钥
- **状态**: 启用/禁用
3. **服务器管理**
- 新增服务器
- 编辑服务器配置
- 删除服务器
- 查看服务器详情
- 服务器管理(高级配置)
#### 流媒体服务器类型
- **ZLM (ZLMediaKit)**: 支持RTSP/RTMP/HTTP-FLV/HLS等协议
- **SRS**: 流媒体服务器(规划中)
- **M7S**: 流媒体服务器(规划中)
#### 使用场景
- 部署流媒体服务器
- 配置多台服务器负载均衡
- 服务器参数调整
- 服务器监控管理
#### 操作步骤
1. 进入配置管理页面
2. 点击"新增"添加服务器
3. 填写服务器配置信息
4. 保存配置
5. 可编辑、删除或管理服务器
---
### 2.4 通道管理
**功能位置**: 视频中心 → 通道管理
**路径**: `/iot/sip/index`
**权限**: `iot:video:list`
#### 作用
- 管理SIP设备通道
- 设备注册和绑定
- 通道信息查询
- 设备状态监控
#### 主要功能
1. **通道列表**
- 显示设备SIP ID
- 显示通道SIP ID
- 显示设备状态(在线/离线)
- 显示产品名称
- 显示设备类型
- 显示通道类型
- 显示城市编码
- 显示注册时间
2. **通道创建**
- 选择省市区
- 选择设备类型(DVR/NVR/摄像机/IPC等)
- 选择通道类型
- 选择产品
- 批量创建通道(最多10个)
3. **通道绑定**
- 绑定到设备
- 绑定到场景模型
- 解绑操作
4. **设备类型**
- `0`: DVR(数字视频录像机)
- `1`: NVR(网络视频录像机)
- `2`: 报警控制器
- `4`: 摄像机
- `5`: IPC(网络摄像机)
- `6`: 显示器
- `7`: 报警输入设备
- `8`: 报警输出设备
- `9`: 语音输入设备
- `10`: 语音输出设备
- `11`: 移动传输设备
5. **通道类型**
- 视频通道
- 音频通道
- 告警通道
- 其他类型
#### 使用场景
- SIP设备注册
- 通道信息管理
- 设备绑定配置
- 设备状态监控
#### 操作步骤
1. 进入通道管理页面
2. 点击"新增"创建通道
3. 选择省市区、设备类型、通道类型
4. 选择产品并设置创建数量
5. 保存创建通道
6. 可绑定到设备或场景模型
---
## 三、视频中心技术架构
### 3.1 协议支持
#### GB28181协议
- **SIP信令**: 设备注册、心跳、目录查询
- **RTP传输**: 视频流传输
- **RTSP推流**: 流媒体服务器接收
- **HTTP-FLV播放**: Web端播放
#### 流媒体协议
- **RTSP**: 实时流传输协议
- **RTMP**: 实时消息传输协议
- **HTTP-FLV**: HTTP FLV流
- **HLS**: HTTP Live Streaming
- **WebRTC**: Web实时通信(规划中)
### 3.2 系统架构
```
SIP设备
↓
SIP服务器 (fastbee-server/sip-server)
↓
流媒体服务器 (ZLM)
↓
Web播放器 (Jessibuca/EasyPlayer)
```
### 3.3 核心组件
1. **SIP服务器**
- 处理GB28181协议
- 设备注册管理
- 信令转发
- 流媒体控制
2. **流媒体服务器 (ZLM)**
- 接收RTP流
- 转码和分发
- 录像存储
- 流媒体API
3. **Web播放器**
- **Jessibuca**: 支持FLV/WS-FLV播放
- **EasyPlayer**: 支持多种格式播放
- **WebRTC**: 低延迟播放(规划中)
## 四、视频中心使用场景
### 4.1 实时监控场景
1. **单路监控**
- 进入分屏显示
- 选择单屏模式
- 选择通道开始播放
2. **多路监控**
- 进入分屏显示
- 选择4分屏或9分屏
- 依次选择多个通道播放
3. **监控大屏**
- 使用9分屏模式
- 全屏显示
- 实时监控多个点位
### 4.2 录像回放场景
1. **查看历史录像**
- 进入录像管理
- 选择流媒体服务器
- 查看录像列表
- 选择日期查看录像文件
2. **事件回放**
- 根据时间查找录像
- 播放录像文件
- 分析事件经过
3. **录像下载**
- 选择录像文件
- 点击下载
- 保存到本地
### 4.3 设备管理场景
1. **设备注册**
- 在通道管理中创建通道
- 配置设备信息
- 设备自动注册
2. **设备绑定**
- 将通道绑定到设备
- 关联场景模型
- 配置设备参数
3. **设备监控**
- 查看设备在线状态
- 监控设备注册情况
- 处理设备异常
## 五、核心代码位置
### 5.1 前端代码
1. **分屏显示**
- `vue/src/views/iot/sip/splitview.vue`
- `vue/src/views/components/player/jessibuca.vue`
- `vue/src/views/components/player/DeviceTree.vue`
2. **录像管理**
- `vue/src/views/iot/record/record.vue`
- `vue/src/views/iot/record/record-detail.vue`
- `vue/src/views/components/player/easyplayer.vue`
3. **配置管理**
- `vue/src/views/iot/sip/mediaServer.vue`
- `vue/src/views/iot/sip/mediaServer-edit.vue`
4. **通道管理**
- `vue/src/views/iot/sip/index.vue`
- `vue/src/views/iot/sip/channel.vue`
### 5.2 后端代码
1. **SIP服务器**
- `fastbee-server/sip-server/`
- SIP协议处理
- 设备管理
- 流媒体控制
2. **录像服务**
- `fastbee-server/sip-server/src/main/java/com/fastbee/sip/service/IRecordService.java`
- `fastbee-open-api/src/main/java/com/fastbee/controller/media/RecordController.java`
3. **流媒体服务**
- `fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IMediaServerService.java`
4. **通道服务**
- `fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IChannelService.java`
## 六、API接口说明
### 6.1 视频播放接口
- `POST /iot/channel/startPlay` - 开始播放视频流
- `POST /iot/channel/stopPlay` - 停止播放视频流
- `GET /iot/channel/getStream` - 获取流信息
### 6.2 录像管理接口
- `GET /iot/record/list` - 获取录像列表
- `GET /iot/record/file/{stream}/{period}` - 获取录像文件列表
- `GET /iot/record/play/{deviceId}/{channelId}` - 播放录像
- `GET /iot/record/download/{deviceId}/{channelId}` - 下载录像
- `POST /iot/record/start/{stream}` - 开始录像
- `POST /iot/record/stop/{stream}` - 停止录像
### 6.3 媒体服务器接口
- `GET /iot/mediaServer/list` - 获取媒体服务器列表
- `POST /iot/mediaServer` - 新增媒体服务器
- `PUT /iot/mediaServer` - 修改媒体服务器
- `DELETE /iot/mediaServer/{ids}` - 删除媒体服务器
### 6.4 通道管理接口
- `GET /iot/channel/list` - 获取通道列表
- `POST /iot/channel` - 新增通道
- `PUT /iot/channel` - 修改通道
- `DELETE /iot/channel/{ids}` - 删除通道
- `POST /iot/channel/binding` - 绑定通道
## 七、注意事项
1. **流媒体服务器配置**
- 需要先配置流媒体服务器才能使用视频功能
- 确保服务器网络可达
- 配置正确的端口和密钥
2. **设备注册**
- 设备需要支持GB28181协议
- 配置正确的SIP服务器地址
- 确保设备网络连通
3. **视频播放**
- 浏览器需要支持WebSocket
- HTTPS环境使用WSS协议
- 建议使用Chrome或Edge浏览器
4. **录像管理**
- 录像文件存储在流媒体服务器
- 定期清理过期录像
- 录像文件较大,注意存储空间
5. **性能优化**
- 多路视频播放会占用较多资源
- 建议根据服务器性能调整分屏数量
- 使用硬件加速提升播放性能
6. **安全配置**
- 配置HTTPS访问
- 设置流媒体服务器密钥
- 控制视频访问权限
## 八、未来规划
根据README.md中的规划,视频中心将支持:
1. **协议扩展**
- TCP传输信令
- SRS、M7S流媒体服务器支持
- 百度EVS、七牛云QVS等云平台对接
2. **功能增强**
- PTZ云台控制
- 设备自动拉流
- 云端录像
- 事件处理机制
3. **性能优化**
- WebRTC低延迟播放
- 硬件加速
- 负载均衡
---
**文档版本**: v1.0
**最后更新**: 2024-12-20
**维护者**: FastBee开发团队

425
运维管理功能说明.md Normal file
View File

@ -0,0 +1,425 @@
# FastBee IoT平台 - 运维管理功能说明
## 一、运维管理概述
**运维管理**是FastBee IoT平台中用于系统运维、监控和管理的功能模块集合。它提供了协议管理、日志查看、客户端监控、固件管理、通知管理等核心运维功能,帮助管理员更好地维护和管理IoT平台。
## 二、运维管理功能模块
### 2.1 协议管理
**功能位置**: 运维管理 → 协议管理
**路径**: `/iot/protocol/index`
**权限**: `iot:protocol:list`
#### 作用
- 管理IoT设备通信协议
- 支持多种协议类型(JAR、JS、C程序)
- 配置协议解析规则
- 管理协议文件上传和版本控制
#### 主要功能
1. **协议列表查询**
- 查看所有已配置的协议
- 支持按协议名称、协议编码搜索
2. **协议详情查看**
- 查看协议详细信息
- 查看协议数据格式配置(Groovy脚本)
3. **协议配置**
- 编辑协议数据格式
- 支持Groovy脚本编写
- 配置协议显示状态
4. **协议类型**
- `0`: 未知类型
- `1`: JAR包协议
- `2`: JS脚本协议
- `3`: C程序协议
#### 使用场景
- 新增设备协议支持
- 修改协议解析规则
- 查看协议配置信息
- 管理协议版本
---
### 2.2 设备服务下发日志
**功能位置**: 运维管理 → 设备服务下发日志
**路径**: `/iot/log/index`
**权限**: `iot:log:list`
#### 作用
- 记录所有设备服务下发操作的详细日志
- 追踪设备控制指令的执行情况
- 排查设备控制问题
- 审计设备操作记录
#### 主要功能
1. **日志查询**
- 按日志名称、类型、设备ID等条件查询
- 支持多条件组合查询
- 分页显示日志列表
2. **日志信息**
- 日志名称
- 日志值
- 日志类型(属性上报、事件上报、调用功能等)
- 日志级别
- 设备信息(设备ID、设备名称)
- 用户信息(用户ID、用户昵称)
- 租户信息
- 触发源
- 告警信息(是否生成告警、告警处理状态)
- 创建时间
3. **日志管理**
- 新增日志(手动记录)
- 修改日志
- 删除日志
- 导出日志
#### 日志类型
- `1`: 属性上报
- `2`: 事件上报
- `3`: 调用功能
- `4`: 设备升级
- `5`: 设备上线
- `6`: 设备离线
- `7`: 运算数据上报
#### 使用场景
- 查看设备控制指令是否成功下发
- 排查设备响应异常问题
- 审计设备操作历史
- 分析设备使用情况
---
### 2.3 产品固件管理
**功能位置**: 运维管理 → 产品固件
**路径**: `/iot/firmware/index`
**权限**: `iot:firmware:list`
#### 作用
- 管理产品固件版本
- 固件升级任务管理
- 固件详情查看
- 设备固件升级
#### 主要功能
1. **固件管理**
- 上传固件文件
- 管理固件版本
- 查看固件详情
2. **固件任务**
- 创建固件升级任务
- 管理升级任务状态
- 查看升级进度
3. **固件详情**
- 查看固件版本信息
- 查看固件文件信息
- 查看升级记录
#### 使用场景
- 设备固件版本管理
- 批量设备固件升级
- 固件升级任务监控
---
### 2.4 设备模拟
**功能位置**: 运维管理 → 设备模拟
**路径**: `/iot/simulate/index`
**权限**: `iot:simulate:list`
#### 作用
- 模拟设备数据上报
- 测试设备功能
- 调试设备协议
- 开发测试支持
#### 主要功能
1. **设备模拟**
- 创建模拟设备
- 模拟设备数据上报
- 模拟设备响应
2. **模拟日志**
- 查看模拟设备日志
- 追踪模拟数据
#### 使用场景
- 开发阶段测试
- 协议调试
- 功能验证
- 系统演示
---
### 2.5 通知管理
#### 2.5.1 通知渠道
**功能位置**: 运维管理 → 通知渠道
**路径**: `/notify/channel/index`
**权限**: `notify:channel:list`
#### 作用
- 配置通知发送渠道
- 管理短信、邮件、微信等通知方式
- 配置渠道参数
#### 主要功能
- 添加通知渠道(短信、邮件、微信、钉钉等)
- 配置渠道参数
- 测试渠道连接
- 启用/禁用渠道
---
#### 2.5.2 通知模板
**功能位置**: 运维管理 → 通知模板
**路径**: `/notify/template/index`
**权限**: `notify:template:list`
#### 作用
- 管理通知消息模板
- 配置告警通知模板
- 自定义通知内容格式
#### 主要功能
- 创建通知模板
- 编辑模板内容
- 配置模板变量
- 预览模板效果
---
#### 2.5.3 通知日志
**功能位置**: 运维管理 → 通知日志
**路径**: `/notify/log/index`
**权限**: `notify:log:list`
#### 作用
- 查看所有通知发送记录
- 追踪通知发送状态
- 排查通知发送问题
#### 主要功能
- 查询通知日志
- 查看通知详情
- 查看发送状态(成功/失败)
- 查看发送时间
---
### 2.6 指令权限
**功能位置**: 运维管理 → 指令权限
**路径**: `/iot/order/index`
**权限**: `order:control:list`
#### 作用
- 控制设备指令下发权限
- 管理用户对设备的控制权限
- 防止未授权操作
#### 主要功能
- 配置设备控制权限
- 管理用户指令权限
- 查看权限配置
- 权限审计
---
## 三、Netty管理(独立模块)
**功能位置**: Netty管理(独立主菜单)
**路径**: `/iot/netty/clients`
**权限**: `monitor:server:list`
### 3.1 客户端管理
#### 作用
- 监控MQTT/TCP客户端连接
- 查看在线客户端信息
- 管理客户端连接
- 排查连接问题
#### 主要功能
1. **客户端列表**
- 查看所有连接的客户端
- 显示客户端ID、类型、连接状态
- 查看客户端订阅的主题
- 查看连接时间、心跳间隔
2. **客户端类型**
- `server`: 服务器端(红色标签)
- `web`: Web客户端(绿色标签)
- `phone`: 手机客户端(黄色标签)
- `test`: 测试客户端(灰色标签)
- 其他: 设备客户端(蓝色标签)
3. **客户端操作**
- 查看客户端详情
- 查看订阅主题列表
- 强制断开客户端连接
4. **连接状态**
- 已连接(绿色标签)
- 未连接(灰色标签)
5. **客户端信息**
- 客户端ID
- 客户端类型
- 连接状态
- 心跳间隔(KeepAlive)
- 用户名
- 订阅主题数量
- 连接时间
#### 使用场景
- 监控设备连接状态
- 查看在线设备数量
- 排查设备连接问题
- 强制断开异常连接
- 查看设备订阅的主题
---
### 3.2 事件日志
**功能位置**: IoT管理 → 事件日志
**路径**: `/iot/log/index`
**权限**: `iot:event:list`
#### 作用
- 记录系统事件日志
- 追踪系统运行状态
- 排查系统问题
#### 主要功能
- 查询事件日志
- 查看事件详情
- 导出事件日志
- 事件统计分析
---
## 四、运维管理使用场景
### 4.1 日常运维场景
1. **协议问题排查**
- 设备数据解析异常 → 查看协议管理,检查协议配置
- 新增设备协议支持 → 在协议管理中配置新协议
2. **设备控制问题排查**
- 设备控制失败 → 查看设备服务下发日志,检查下发记录
- 设备无响应 → 查看Netty客户端管理,检查设备连接状态
3. **系统监控**
- 查看在线设备数量 → Netty管理 → 客户端
- 查看系统事件 → 事件日志
- 查看通知发送情况 → 通知日志
4. **固件升级管理**
- 批量升级设备固件 → 产品固件 → 固件任务
- 查看升级进度 → 固件详情
### 4.2 故障排查流程
```
设备异常
↓
1. 检查设备连接状态(Netty管理 → 客户端)
↓
2. 查看设备服务下发日志(设备服务下发日志)
↓
3. 检查协议配置(协议管理)
↓
4. 查看事件日志(事件日志)
↓
5. 查看通知日志(通知日志)
```
### 4.3 系统维护场景
1. **协议升级**
- 在协议管理中更新协议配置
- 测试协议解析是否正确
2. **通知配置**
- 配置通知渠道(短信、邮件等)
- 创建通知模板
- 测试通知发送
3. **权限管理**
- 配置设备控制权限
- 管理用户指令权限
## 五、核心代码位置
### 5.1 协议管理
- **控制器**: `fastbee-open-api/src/main/java/com/fastbee/controller/protocol/ProtocolController.java`
- **服务**: `fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/service/IProtocolService.java`
- **前端**: `vue/src/views/iot/protocol/index.vue`
### 5.2 设备服务下发日志
- **前端**: `vue/src/views/iot/log/index.vue`
- **实体**: `fastbee-service/fastbee-iot-service/src/main/java/com/fastbee/iot/domain/FunctionLog.java`
### 5.3 Netty客户端管理
- **前端**: `vue/src/views/iot/netty/clients.vue`
- **后端**: MQTT Broker相关代码在 `fastbee-server/mqtt-broker/`
### 5.4 通知管理
- **通知渠道**: `vue/src/views/notify/channel/index.vue`
- **通知模板**: `vue/src/views/notify/template/index.vue`
- **通知日志**: `vue/src/views/notify/log/index.vue`
## 六、注意事项
1. **协议管理**
- 修改协议配置需要谨慎,可能影响设备数据解析
- 协议文件需要正确上传和配置
2. **日志管理**
- 日志数据量可能很大,建议定期清理
- 重要日志建议导出备份
3. **客户端管理**
- 强制断开客户端连接会影响设备通信
- 建议在确认异常后再断开连接
4. **通知管理**
- 通知渠道需要正确配置参数
- 通知模板变量需要正确配置
5. **权限管理**
- 指令权限配置会影响设备控制功能
- 建议根据实际需求合理配置权限
---
**文档版本**: v1.0
**最后更新**: 2024-12-20
**维护者**: FastBee开发团队