基础概念
- 网关设备:能够直接连接云平台的设备,如遥控器、大疆机场均为网关设备
- MQTT: 物联网通用的MQTT5.0标准协议,是一个客户端服务端架构的发布/定义模式的消息传输协议。
Pilot
当pilot2与云平台建立mqtt连接后,进行设备绑定后。
设备向云平台推送消息,topic为sys/product/{gateway_sn}/status 进行设备上线
// 设备上线
{
"tid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
"bid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
"method": "update_topo",
"timestamp": 1234567890123,
"data": {
"domain": 2,
"type": 119,
"sub_type": 0,
"device_secret": "secret",
"nonce": "nonce",
"version": "1",
"sub_devices": [
{
"sn": "drone001",
"domain": 0,
"type": 60,
"sub_type": 0,
"index": "A",
"device_secret": "secret",
"nonce": "nonce",
"version": "1"
}
]
}
}
//设备下线
{
"tid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
"bid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
"method": "update_topo",
"timestamp": 1234567890123,
"data": {
"domain": 2,
"type": 119,
"sub_type": 0,
"device_secret":"secret",
"nonce":"nonce",
"version": "1",
"sub_devices":[]
}
}
Java代码部分
- 继承org.springframework.integration.router.AbstractMessageRouter重写determineTargetChannels方法,根据不同的topic进入不同的MessageChannel
package com.dji.sdk.mqtt;
import com.dji.sdk.common.SpringBeanUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.annotation.Router;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.integration.router.AbstractMessageRouter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
/**
*
* @author sean.zhou
* @date 2021/11/10
* @version 0.1
*/
@Component
public class InboundMessageRouter extends AbstractMessageRouter {
private static final Logger log = LoggerFactory.getLogger(InboundMessageRouter.class);
/**
* All mqtt broker messages will arrive here before distributing them to different channels.
* @param message message from mqtt broker
* @return channel
*/
@Override
@Router(inputChannel = ChannelName.INBOUND)
protected Collection<MessageChannel> determineTargetChannels(Message<?> message) {
MessageHeaders headers = message.getHeaders();
String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString();
byte[] payload = (byte[])message.getPayload();
log.debug("received topic: {} \t payload =>{}", topic, new String(payload));
CloudApiTopicEnum topicEnum = CloudApiTopicEnum.find(topic);
MessageChannel bean = (MessageChannel) SpringBeanUtils.getBean(topicEnum.getBeanName());
return Collections.singleton(bean);
}
}
- 根据上述topic 消息处理进入statusRouter中,最终根据消息中的sub_devices是不是空进入ChannelName.INBOUND_STATUS_OFFLINE或者ChannelName.INBOUND_STATUS_ONLINE
package com.dji.sdk.mqtt.status;
import com.dji.sdk.cloudapi.device.UpdateTopo;
import com.dji.sdk.common.Common;
import com.dji.sdk.exception.CloudSDKException;
import com.dji.sdk.mqtt.ChannelName;
import com.dji.sdk.mqtt.MqttGatewayPublish;
import com.fasterxml.jackson.core.type.TypeReference;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.mqtt.support.MqttHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Objects;
import java.util.Optional;
import static com.dji.sdk.mqtt.TopicConst.*;
/**
*
* @author sean.zhou
* @date 2021/11/12
* @version 0.1
*/
@Configuration
public class StatusRouter {
@Resource
private MqttGatewayPublish gatewayPublish;
@Bean
public IntegrationFlow statusRouterFlow() {
return IntegrationFlows
// 从ChannelName.INBOUND_STATUS 中来的消息都由我处理
.from(ChannelName.INBOUND_STATUS)
.transform(Message.class, source -> {
try {
TopicStatusRequest<UpdateTopo> response = Common.getObjectMapper().readValue((byte[]) source.getPayload(), new TypeReference<TopicStatusRequest<UpdateTopo>>() {});
String topic = String.valueOf(source.getHeaders().get(MqttHeaders.RECEIVED_TOPIC));
return response.setFrom(topic.substring((BASIC_PRE + PRODUCT).length(), topic.indexOf(STATUS_SUF)));
} catch (IOException e) {
throw new CloudSDKException(e);
}
}, null)
.<TopicStatusRequest<UpdateTopo>, Boolean>route(
response -> Optional.ofNullable(response.getData()).map(UpdateTopo::getSubDevices).map(CollectionUtils::isEmpty).orElse(true),
mapping -> mapping.channelMapping(true, ChannelName.INBOUND_STATUS_OFFLINE)
.channelMapping(false, ChannelName.INBOUND_STATUS_ONLINE))
.get();
}
@Bean
public IntegrationFlow replySuccessStatus() {
return IntegrationFlows
.from(ChannelName.OUTBOUND_STATUS)
.handle(this::publish)
.nullChannel();
}
private TopicStatusResponse publish(TopicStatusResponse request, MessageHeaders headers) {
if (Objects.isNull(request)) {
return null;
}
gatewayPublish.publishReply(request, headers);
return request;
}
}
3.进行上线逻辑后,最终进入ChannelName.OUTBOUND_STATUS中
@Override
public TopicStatusResponse<MqttReply> updateTopoOnline(TopicStatusRequest<UpdateTopo> request, MessageHeaders headers) {
// 获取子设备
UpdateTopoSubDevice updateTopoSubDevice = request.getData().getSubDevices().get(0);
// 获取sn
String deviceSn = updateTopoSubDevice.getSn();
// 从redis中查询是否在线 (如果之前已经上线,会将它们存放到Redis中,防止重复上线)
Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(deviceSn);
// 查询网关设备是否在线
Optional<DeviceDTO> gatewayOpt = deviceRedisService.getDeviceOnline(request.getFrom());
// 将设备相关信息注册到网关管理器中
GatewayManager gatewayManager = SDKManager.registerDevice(request.getFrom(), deviceSn,
request.getData().getDomain(), request.getData().getType(),
request.getData().getSubType(), request.getData().getThingVersion(), updateTopoSubDevice.getThingVersion());
// 如果设备和网关都已经上线了
if (deviceOpt.isPresent() && gatewayOpt.isPresent()) {
// 再次上线推送上线成功消息
deviceOnlineAgain(deviceOpt.get().getWorkspaceId(), request.getFrom(), deviceSn);
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
// 修改飞行器的绑定网关(有可能之前绑定在其它网关)
changeSubDeviceParent(deviceSn, request.getFrom());
// 转换成DTO
DeviceDTO gateway = deviceGatewayConvertToDevice(request.getFrom(), request.getData());
// 保存网关信息到数据库,存入redis中,设备上线
Optional<DeviceDTO> gatewayEntityOpt = onlineSaveDevice(gateway, deviceSn, null);
if (gatewayEntityOpt.isEmpty()) {
log.error("Failed to go online, please check the status data or code logic.");
return null;
}
DeviceDTO subDevice = subDeviceConvertToDevice(updateTopoSubDevice);
// 保存飞行器信息到数据库,存入redis,设备上线
Optional<DeviceDTO> subDeviceEntityOpt = onlineSaveDevice(subDevice, null, gateway.getDeviceSn());
if (subDeviceEntityOpt.isEmpty()) {
log.error("Failed to go online, please check the status data or code logic.");
return null;
}
subDevice = subDeviceEntityOpt.get();
gateway = gatewayEntityOpt.get();
// 如果是机场,将飞行器绑定到机场的workspace中
dockGoOnline(gateway, subDevice);
// 订阅网关设备相关消息topic
deviceService.gatewayOnlineSubscribeTopic(gatewayManager);
if (!StringUtils.hasText(subDevice.getWorkspaceId())) {
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
// 订阅飞行器相关消息topic
deviceService.subDeviceOnlineSubscribeTopic(gatewayManager);
// 发布设备上线消息,通过websocket连接通知其他设备更新设备拓扑
deviceService.pushDeviceOnlineTopo(gateway.getWorkspaceId(), gateway.getDeviceSn(), subDevice.getDeviceSn());
log.debug("{} online.", subDevice.getDeviceSn());
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
- 下线功能消息路由跳转同上,主要看不同的地方
@Override
public TopicStatusResponse<MqttReply> updateTopoOffline(TopicStatusRequest<UpdateTopo> request, MessageHeaders headers) {
// 获取网关管理器
GatewayManager gatewayManager = SDKManager.registerDevice(request.getFrom(), null,
request.getData().getDomain(), request.getData().getType(),
request.getData().getSubType(), request.getData().getThingVersion(), null);
// 订阅网关相关topic 主题
deviceService.gatewayOnlineSubscribeTopic(gatewayManager);
// 查找网关对应的上线设备
Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(request.getFrom());
if (deviceOpt.isEmpty()) {
// 如果是null 表示第一次上线,保存网关信息
DeviceDTO gatewayDevice = deviceGatewayConvertToDevice(request.getFrom(), request.getData());
Optional<DeviceDTO> gatewayDeviceOpt = onlineSaveDevice(gatewayDevice, null, null);
if (gatewayDeviceOpt.isEmpty()) {
return null;
}
// 推送设备拓扑消息
deviceService.pushDeviceOnlineTopo(gatewayDeviceOpt.get().getWorkspaceId(), request.getFrom(), null);
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
String deviceSn = deviceOpt.get().getChildDeviceSn();
if (!StringUtils.hasText(deviceSn)) {
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
// 取消订阅飞行器相关主题
deviceService.subDeviceOffline(deviceSn);
return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
}
- 存在全局定时任务检查设备在线状态
@Scheduled(initialDelay = 10, fixedRate = 30, timeUnit = TimeUnit.SECONDS)
private void deviceStatusListen() {
int start = RedisConst.DEVICE_ONLINE_PREFIX.length();
// 获取所有在线的设备
RedisOpsUtils.getAllKeys(RedisConst.DEVICE_ONLINE_PREFIX + "*").forEach(key -> {
long expire = RedisOpsUtils.getExpire(key);
// 如果过期时间小于30s
if (expire <= 30) {
DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(key);
if (null == device) {
return;
}
if (DeviceDomainEnum.DRONE == device.getDomain()) {
// 飞行器下线
deviceService.subDeviceOffline(key.substring(start));
} else {
// 网关设备下线
deviceService.gatewayOffline(key.substring(start));
}
RedisOpsUtils.del(key);
}
});
log.info("Subscriptions: {}", Arrays.toString(topicService.getSubscribedTopic()));
}