大疆上云API-1.10.0版本本地启动代码-设备上下线源码解析

大疆上云api介绍 目前对于 DJI 无人机接入第三方云平台,主要是基于 MSDK 开发定制 App,然后自己定义私有上云通信协议连接到云平台中。这样对于核心业务是开发云平台,无人机只是其中一个接入硬件设备的开发者来说,重新基于 MSDK 开发 App 工作量大、成本高,同时还需要花很多精力在无人机功能接口适配上,从而导致整体的方案开发进度较为缓慢。上云 API 的推出主要期望解决开发者重新造轮子的苦恼。 阅读详情
AI摘要

本文解析了大疆上云API-1.10.0版本中设备上下线的功能实现,包括消息路由、消息处理流程及核心代码示例。

有一段时间没做无人机项目了,最近从新拉了最新的上云API-1.10.0的源码,在本地跑起来,有很多变化,今天新写一篇最新版本的本地启动文章,接下来一段时间,连载基于1.10.0这个版本做源码解析,敬请关注。上云API最新版本请关注 上云API文档地址

一、准备

上云API前端代码
上云API后端代码
本地安装Mysql、Redis、Emqx、Node.js等请自行安装。
注意: EMQX安装完毕,请安装如下插件,基于Redis做EMQX的ACL,
新版上云API使用了该功能做权限管理,请参照如下步骤配置。
在这里插入图片描述
在这里插入图片描述

二、服务的配置文件和前端配置文件说明

1、java的application.yml配置文件

配置如下:基于最简单的运行服务端代码,只需要配置mysql,redis,mqtt
注意:
1、修改gb28181下方对应的 serverPort和 localPort修改为任意数字就好,因为这两个字段代码使用的Integer类型不修改启动会报错。
2、整个livestream不配置启动时不会报错的,只会在使用到直播功能的时候才会出现业务报错,可根据业务选择的播放类型进行配置。
3、cloud-api配置请前往上云创建对应项目,添加项目对应的参数

server:
  port: 6789
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: cloud-api-sample
  datasource:
    druid:
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/cloud_sample?useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      initial-size: 10
      min-idle: 10
      max-active: 20
      max-wait: 60000

  redis:
    host: 192.168.148.100
    port: 6379
    database: 0
    username: # if you enable
    password:
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

  servlet:
    multipart:
      max-file-size: 2GB
      max-request-size: 2GB

jwt:
  issuer: DJI
  subject: CloudApiSample
  secret: CloudApiSample
  age: 86400

mqtt:
  # @see com.dji.sample.component.mqtt.model.MqttUseEnum
  # BASIC parameters are required.
  BASIC:
    protocol: MQTT # @see com.dji.sample.component.mqtt.model.MqttProtocolEnum
    host: 192.168.148.100
    port: 1883
    username: JavaServer
    password: 123456
    client-id: 123456
    # If the protocol is ws/wss, this value is required.
    path:
  DRC:
    protocol: WS # @see com.dji.sample.component.mqtt.model.MqttProtocolEnum
    host: 192.168.148.100
    port: 8083
    path: /mqtt
    username: JavaServer
    password: 123456

cloud-sdk:
  mqtt:
    # Topics that need to be subscribed when initially connecting to mqtt, multiple topics are divided by ",".
    inbound-topic: sys/product/+/status,thing/product/+/requests

url:
  manage:
    prefix: manage
    version: /api/v1
  map:
    prefix: map
    version: /api/v1
  media:
    prefix: media
    version: /api/v1
  wayline:
    prefix: wayline
    version: /api/v1
  storage:
    prefix: storage
    version: /api/v1
  control:
    prefix: control
    version: /api/v1

# Tutorial: https://www.alibabacloud.com/help/en/object-storage-service/latest/use-a-temporary-credential-provided-by-sts-to-access-oss
oss:
  enable: false
  provider: ALIYUN # @see com.dji.sample.component.OssConfiguration.model.enums.OssTypeEnum
  endpoint: https://oss-cn-hangzhou.aliyuncs.com
  access-key: Please enter your access key.
  secret-key: Please enter your secret key.
  expire: 3600
  region: Please enter your oss region. # cn-hangzhou
  role-session-name: cloudApi
  role-arn: Please enter your role arn. # acs:ram::123456789:role/stsrole
  bucket: Please enter your bucket name.
  object-dir-prefix: Please enter a folder name.

#oss:
#  enable: true
#  provider: aws
#  endpoint: https://s3.us-east-1.amazonaws.com
#  access-key:
#  secret-key:
#  expire: 3600
#  region: us-east-1
#  role-session-name: cloudApi
#  role-arn:
#  bucket: cloudapi-bucket
#  object-dir-prefix: wayline

#oss:
#  enable: true
#  provider: minio
#  endpoint: http://192.168.1.1:9000
#  access-key: minioadmin
#  secret-key: minioadmin
#  bucket: cloud-bucket
#  expire: 3600
#  region: us-east-1
#  object-dir-prefix: wayline

logging:
  level:
    com.dji: debug
  file:
    name: logs/cloud-api-sample.log

ntp:
  server:
    host: Google.mzr.me

# To create a license for an application: https://developer.dji.com/user/apps/#all
cloud-api:
  app:
    id: xxxxx
    key: xxxxx
    license: xxxxx

livestream:
  url:
    # It is recommended to use a program to create Token. https://github.com/AgoraIO/Tools/blob/master/DynamicKey/AgoraDynamicKey/java/src/main/java/io/agora/media/RtcTokenBuilder2.java
    agora:
      channel: Please enter the agora channel.
      token: Please enter the agora temporary token.
      uid:  654321

    # RTMP  Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
    rtmp:
      url: Please enter the rtmp access address.  # Example: 'rtmp://192.168.1.1/live/'
    rtsp:
      username: Please enter the username.
      password: Please enter the password.
      port: 8554

    # GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
    gb28181:
      serverIP: Please enter the server ip.
      serverPort: 8889
      serverID: Please enter the server id.
      agentID: Please enter the agent id.
      agentPassword: Please enter the agent password.
      localPort: 8888
      channel: Please enter the channel.

    # Webrtc: Only supports using whip standard
    whip:
      url: Please enter the rtmp access address. #  Example:http://192.168.1.1:1985/rtc/v1/whip/?app=live&stream=

2、web端配置文件/src/api/http/config.ts

配置如下:license配置和服务的一致。http配置你的服务端ip和端口,我使用的是本机所以简单使用的127.0.0.1,如果部署服务器,请配置服务器地址,其他暂时可以不配置。

export const CURRENT_CONFIG = {

  // license
  appId: 'xxxxx', // You need to go to the development website to apply.
  appKey: 'xxxxx', // You need to go to the development website to apply.
  appLicense: 'xxxxx', // You need to go to the development website to apply.

  // http
  baseURL: 'http://127.0.0.1:6789/', // This url must end with "/". Example: 'http://192.168.1.1:6789/'
  websocketURL: 'ws://127.0.0.1:6789/api/v1/ws', // Example: 'ws://192.168.1.1:6789/api/v1/ws'

  // livestreaming
  // RTMP  Note: This IP is the address of the streaming server. If you want to see livestream on web page, you need to convert the RTMP stream to WebRTC stream.
  rtmpURL: 'rtmp://192.168.1.1/live/', // Example: 'rtmp://192.168.1.1/live/'
  // GB28181 Note:If you don't know what these parameters mean, you can go to Pilot2 and select the GB28181 page in the cloud platform. Where the parameters same as these parameters.
  gbServerIp: 'Please enter the server ip.',
  gbServerPort: 'Please enter the server port.',
  gbServerId: 'Please enter the server id.',
  gbAgentId: 'Please enter the agent id',
  gbPassword: 'Please enter the agent password',
  gbAgentPort: 'Please enter the local port.',
  gbAgentChannel: 'Please enter the channel.',
  // RTSP
  rtspUserName: 'Please enter the username.',
  rtspPassword: 'Please enter the password.',
  rtspPort: '8554',
  // Agora
  agoraAPPID: 'Please enter the agora app id.',
  agoraToken: 'Please enter the agora temporary token.',
  agoraChannel: 'Please enter the agora channel.',

  // map
  // You can apply on the AMap website.
  amapKey: 'Please enter the amap key.',

}

三、运行项目

1、在根目录下打开控制台,运行命令安装依赖。

npm install

2、安装完成可以在根目录下看到一个node_modules的目录。
3、运行命令启动服务。

npm run serve

4、服务端启动前,在mysql创建对应的数据库,运行代码中的cloud_sample.sql创建对应的表。
5、直接启动服务在这里插入图片描述

四、测试

使用EMQX的WEB客户端使用websoket连接,发送设备上线指令sys/product/{遥控器sn或者机场sn}/status 如下:

{
	"tid": "11111",
	"bid": "222222",
	"method": "update_topo",
	"timestamp": 1234567890123,
	"data": {
		"domain": 2,
		"type": 119,
		"sub_type": 0,
		"device_secret": "secret",
		"nonce": "nonce",
		"version": "1",
		"sub_devices": [
			{
				"sn": "无人机sn",
				"domain": 0,
				"type": 60,
				"sub_type": 0,
				"index": "A",
				"device_secret": "secret",
				"nonce": "nonce",
				"version": "1"
			}
		]
	}
}

在这里插入图片描述
在这里插入图片描述
系统运行正常,数据库存储上报的数据
在这里插入图片描述
redis存储设备在线数据在这里插入图片描述

五、设备上下线源码解析(中文注释)

上云API使用了spring-integration-mqtt这个组件,相关知识请自行学习。直接上代码:

1、第一步路由根据通道转发

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);

    /**
     * 所有 mqtt broker 消息都会到达这里,然后再将它们分发到不同的通道。
     * @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);
    }
}

2、第二次路由转发

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;
    /**
     * 用于从INBOUND_STATUS通道接收MQTT消息,解析消息内容TopicStatusRequest<UpdateTopo>,并根据消息中的特定条件
     * (UpdateTopo对象的subDevices集合是否为空)将消息路由到不同的通道(INBOUND_STATUS_OFFLINE或INBOUND_STATUS_ONLINE)
     * @param  
     * @return org.springframework.integration.dsl.IntegrationFlow
     * @author chenyuanyuan 
     * @date 2024/8/14 10:11 
     **/

    @Bean
    public IntegrationFlow statusRouterFlow() {
        return IntegrationFlows
                .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();
    }
    /**
     * 监听OUTBOUND_STATUS通道上的消息,对每条消息调用当前类的publish方法进行处理,然后丢弃这些消息
     * @param
     * @return org.springframework.integration.dsl.IntegrationFlow
     * @author chenyuanyuan
     * @date 2024/8/14 10:12
     **/
    @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、消息切入点

package com.dji.sdk.cloudapi.device.api;

import com.dji.sdk.annotations.CloudSDKVersion;
import com.dji.sdk.cloudapi.device.*;
import com.dji.sdk.cloudapi.property.DockDroneCommanderFlightHeight;
import com.dji.sdk.cloudapi.property.DockDroneCommanderModeLostAction;
import com.dji.sdk.cloudapi.property.DockDroneRthMode;
import com.dji.sdk.config.version.CloudSDKVersionEnum;
import com.dji.sdk.config.version.GatewayTypeEnum;
import com.dji.sdk.mqtt.ChannelName;
import com.dji.sdk.mqtt.MqttReply;
import com.dji.sdk.mqtt.osd.TopicOsdRequest;
import com.dji.sdk.mqtt.state.TopicStateRequest;
import com.dji.sdk.mqtt.state.TopicStateResponse;
import com.dji.sdk.mqtt.status.TopicStatusRequest;
import com.dji.sdk.mqtt.status.TopicStatusResponse;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;

/**
 * @author sean
 * @version 1.7
 * @date 2023/6/30
 */
public class AbstractDeviceService {

 /**
     * 网关设备+子设备上线   (遥控器+无人机)
     * @param request  data
     * @param headers   The headers for a {@link Message}.
     * @return status_reply
     */
    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_ONLINE, outputChannel = ChannelName.OUTBOUND_STATUS)
    public TopicStatusResponse<MqttReply> updateTopoOnline(TopicStatusRequest<UpdateTopo> request, MessageHeaders headers) {
        throw new UnsupportedOperationException("updateTopoOnline not implemented");
    }
 }

4、具体消息处理实现

package com.dji.sample.manage.service.impl;

import com.dji.sample.component.websocket.model.BizCodeEnum;
import com.dji.sample.component.websocket.service.IWebSocketMessageService;
import com.dji.sample.manage.model.dto.DeviceDTO;
import com.dji.sample.manage.model.dto.DevicePayloadReceiver;
import com.dji.sample.manage.model.enums.DeviceFirmwareStatusEnum;
import com.dji.sample.manage.model.param.DeviceQueryParam;
import com.dji.sample.manage.service.IDeviceDictionaryService;
import com.dji.sample.manage.service.IDevicePayloadService;
import com.dji.sample.manage.service.IDeviceRedisService;
import com.dji.sample.manage.service.IDeviceService;
import com.dji.sdk.cloudapi.device.*;
import com.dji.sdk.cloudapi.device.api.AbstractDeviceService;
import com.dji.sdk.cloudapi.tsa.DeviceIconUrl;
import com.dji.sdk.cloudapi.tsa.IconUrlEnum;
import com.dji.sdk.config.version.GatewayManager;
import com.dji.sdk.common.SDKManager;
import com.dji.sdk.mqtt.MqttReply;
import com.dji.sdk.mqtt.osd.TopicOsdRequest;
import com.dji.sdk.mqtt.state.TopicStateRequest;
import com.dji.sdk.mqtt.status.TopicStatusRequest;
import com.dji.sdk.mqtt.status.TopicStatusResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.MessageHeaders;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

/**
 * @author sean
 * @version 1.7
 * @date 2023/7/4
 */
@Service
@Slf4j
public class SDKDeviceService extends AbstractDeviceService {


    @Autowired
    private IDeviceRedisService deviceRedisService;

    @Autowired
    private IDeviceService deviceService;

    @Autowired
    private IDeviceDictionaryService dictionaryService;

    @Autowired
    private IWebSocketMessageService webSocketMessageService;

    @Autowired
    private IDevicePayloadService devicePayloadService;
@Override
    public TopicStatusResponse<MqttReply> updateTopoOnline(TopicStatusRequest<UpdateTopo> request, MessageHeaders headers) {
        UpdateTopoSubDevice updateTopoSubDevice = request.getData().getSubDevices().get(0);
        String deviceSn = updateTopoSubDevice.getSn();
		//获取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());

        DeviceDTO gateway = deviceGatewayConvertToDevice(request.getFrom(), request.getData());
        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);
        //设备上下并保存
        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();
        //机场上线操作
        dockGoOnline(gateway, subDevice);
        //订阅网关设备topic
        deviceService.gatewayOnlineSubscribeTopic(gatewayManager);

        if (!StringUtils.hasText(subDevice.getWorkspaceId())) {
            return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
        }

        // 订阅无人机设备相关话题。
        deviceService.subDeviceOnlineSubscribeTopic(gatewayManager);
        //发送设备上线的消息到PILOT
        deviceService.pushDeviceOnlineTopo(gateway.getWorkspaceId(), gateway.getDeviceSn(), subDevice.getDeviceSn());

        log.debug("{} online.", subDevice.getDeviceSn());
        return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
    }
}    

重要的方法调用如下:

    //设备上线并保存数据库redis
    private Optional<DeviceDTO> onlineSaveDevice(DeviceDTO device, String childSn, String parentSn) {

        device.setChildDeviceSn(childSn);
        device.setLoginTime(LocalDateTime.now());

        Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(device.getDeviceSn());
        //首次上线创建设备在数据库
        if (deviceOpt.isEmpty()) {
            device.setIconUrl(new DeviceIconUrl());
            // 设置 TSA 模块中需要的飞行员地图中显示的网关设备的图标。
            device.getIconUrl().setNormalIconUrl(IconUrlEnum.NORMAL_PERSON.getUrl());
            // 设置 TSA 模块中需要的网关设备图标,当它被选中时,它会显示在飞行员的地图中。
            device.getIconUrl().setSelectIconUrl(IconUrlEnum.SELECT_PERSON.getUrl());
            device.setBoundStatus(false);

            // 查询该网关设备的型号信息。
            dictionaryService.getOneDictionaryInfoByTypeSubType(
                    device.getDomain().getDomain(), device.getType().getType(), device.getSubType().getSubType())
                    .ifPresent(entity -> {
                        device.setDeviceName(entity.getDeviceName());
                        device.setNickname(entity.getDeviceName());
                        device.setDeviceDesc(entity.getDeviceDesc());
                    });
        }
        boolean success = deviceService.saveOrUpdateDevice(device);
        if (!success) {
            return Optional.empty();
        }

        deviceOpt = deviceService.getDeviceBySn(device.getDeviceSn());
        DeviceDTO redisDevice = deviceOpt.get();
        redisDevice.setStatus(true);
        redisDevice.setParentSn(parentSn);
        //存储设备在线到redis
        deviceRedisService.setDeviceOnline(redisDevice);
        return deviceOpt;
    }
 //修改遥控器和无人机配对信息,当无人机在数据库存储的对应遥控器与当前遥控器不一样 ,修改之前的遥控器无人机信息及下线
    private void changeSubDeviceParent(String deviceSn, String gatewaySn) {
        List<DeviceDTO> gatewaysList = deviceService.getDevicesByParams(
                DeviceQueryParam.builder()
                        .childSn(deviceSn)
                        .build());
        gatewaysList.stream()
                .filter(gateway -> !gateway.getDeviceSn().equals(gatewaySn))
                .forEach(gateway -> {
                    gateway.setChildDeviceSn("");
                    deviceService.updateDevice(gateway);
                    deviceRedisService.getDeviceOnline(gateway.getDeviceSn())
                            .ifPresent(device -> {
                                device.setChildDeviceSn(null);
                                deviceRedisService.setDeviceOnline(device);
                            });
                });
    }

	private void dockGoOnline(DeviceDTO gateway, DeviceDTO subDevice) {
        if (DeviceDomainEnum.DOCK != gateway.getDomain()) {
            return;
        }
        if (!StringUtils.hasText(gateway.getWorkspaceId())) {
            log.error("The dock is not bound, please bind it first and then go online.");
            return;
        }
        if (!Objects.requireNonNullElse(subDevice.getBoundStatus(), false)) {
            //直接将 Dock 的无人机绑定到与 Dock 相同的工作空间。
            deviceService.bindDevice(DeviceDTO.builder().deviceSn(subDevice.getDeviceSn()).workspaceId(gateway.getWorkspaceId()).build());
            subDevice.setWorkspaceId(gateway.getWorkspaceId());
        }
        //保存到redis
        deviceRedisService.setDeviceOnline(subDevice);
    }

6、设备下线

发送设备下线指令sys/product/{遥控器sn或者机场sn}/status

{
    "tid": "1111",
    "bid": "2222",
    "method": "update_topo",
    "timestamp": 1234567890123,
    "data": {
        "domain": 2,
        "type": 119,
        "sub_type": 0,
        "device_secret":"secret",
        "nonce":"nonce",
        "version": "1",
        "sub_devices":[]
    }
}

消息切入点如下

 /**
     * 子设备离线
     * @param request  data
     * @param headers   The headers for a {@link Message}.
     * @return status_reply
     */
    @ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_OFFLINE, outputChannel = ChannelName.OUTBOUND_STATUS)
    public TopicStatusResponse<MqttReply> updateTopoOffline(TopicStatusRequest<UpdateTopo> request, MessageHeaders headers) {
        throw new UnsupportedOperationException("updateTopoOffline not implemented");
    }

实际实现如下

 @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);
        deviceService.gatewayOnlineSubscribeTopic(gatewayManager);
        // 只有遥控器登录,飞行器未连接。
        Optional<DeviceDTO> deviceOpt = deviceRedisService.getDeviceOnline(request.getFrom());
        if (deviceOpt.isEmpty()) {
            // 第一次设备连接时
            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());
        }
        //设备离线操作,修改redis,数据库,取消订阅,删除载荷等
        deviceService.subDeviceOffline(deviceSn);
        return new TopicStatusResponse<MqttReply>().setData(MqttReply.success());
    }

结语

其实在我看来,整个上云的代码是不难的,只是代码提取的比较多。很多人急于求成,急于完成公司的任务,没有时间详细的去跟代码,今天就先写到这里,后期我会继续更新相关源码解析,如果有志同道合的朋友希望我解析对应的知识点的,请留言,我会提前解析对应的代码。

本文作者手打,请勿转载

避坑指南:ArcGIS 做莫兰指数分析,你的‘空间关系概念化’选对了吗? 本文深入解析了ArcGIS中莫兰指数分析的关键参数——'空间关系概念化'的选择策略。通过对比反距离法、固定距离法和K最近邻法的适用场景与影响机制,帮助研究者避免常见误区,提升空间自相关分析的准确性和解释力。特别针对全域莫兰指数和局域莫兰指数的计算差异提供了实用建议。 阅读详情

相关推荐

说说PSTN、ISDN、 ADSL三种互联网接入方式的区别。

互联网的接入方式: 电话拨号接入(PSTN) ISDN接入 ADSL接入 Cable MODEM接入 局域网接入 光纤接入(FDDI) 卫星接入 DDN专线 目前来说,ADSL和Cable MODEM比较常用,今天我们就先来说说PSTN、ISDN和ADSL的区别。 电话拨号接入(PSTN/POTS,Public Switched Telephone Network / Public Oold Telephone Service) 数据终端设备(DTE)通过数据通信设备(DCE)再连

Winyaz的博客 2万+

大疆上云API本地部署与飞机上云

大疆上云API官方文档有些写的不是很详细,踩了很多坑,自己实际部署了一遍,做此纪录!

恋上、小幸福的博客 1万+

Java实战:绿盾加密文件批量解密工具Ldterm的实现与优化

本文详细介绍了基于Java开发的绿盾加密文件批量解密工具Ldterm的实现与优化过程。通过分析绿盾加密机制,结合Java流处理能力,实现了高效的文件解密方案,并分享了多线程处理、内存优化等性能提升技巧,适用于企业级数据迁移与备份场景。

weixin_29314405的博客 489

大疆上云API本地部署配置

大疆上云API

yychen_java的博客 1万+

EDA课程实验:《数字逻辑电路》实验四___基于Minisys 平台 VIVADO工具与Verilog语言

(4)使用 Verilog HDL 实现一个分频器 clock_div,输入的是系统提供的 100MHz 频率,输出的是 1Hz 频率,占空比是 1:2,(实际上可以用一个加 1 计数器来实现降频,输入的是一个 100MHz 频率的方波,对这 个方波计数 1 亿次刚好到 1 秒(1Hz),题目要求输出频率占空比是 1:2,也就是这个 1Hz 的 方波高电平和低电平各 500ms,可以采用计数到中间值的时候输出电平翻转。(1)1) 拨动 SW1~0 为 01,按下 P4 按钮,GLD1~0 为 01;

ssjy_的博客 1296

2024 年高教社杯全国大学生数学建模竞赛题目【A/B/C/D/E题】完整论文+代码+结果

2024国赛A题参考论文。2024国赛D题参考论文。2024国赛E题参考论文。2024国赛B题参考论文。2024国赛C题参考论文。

qq_52590045的博客 5355

大疆上云api开发

涉及到使用大疆机场上云api开发遇到的一系列问题

qq_48403611的博客 8088

别再乱用标签了!深度解析Unity Addressables标签(Labels)的三种高级用法与性能影响

本文深度解析Unity Addressables标签(Labels)的三种高级用法与性能影响,包括标签组合查询、Pack Together by Label打包策略及运行时性能优化。通过实战代码示例和性能数据对比,帮助开发者实现精细化资源加载控制与包体优化,提升项目效率。

weixin_30500105的博客 508

大疆上云API基于源码部署

大疆上云API基于源码部署

m0_62317155的博客 1万+

大疆sdkV4以及V5webrtc推流

webrtcSDK中要用它的VideoCapturer来初始化推流 ViewdoCapturer是一个接口我们继承它 然后重建创建文件CapturerAircraft.java文件。CameraUtil.java这个是我们用来创建CapturerAircraft的一个工具。除了CapturerAircraft这个文件不一样外其他都是一样的。PlayBodyBean.java这个是我们的配置文件。下面的有个代码块第一个是v4的 第二个是v5的。接着我们来创建几个推流用到的webrtc工具。

weixin_41624865的博客 1188

手把手用ROS实现Ego-Planner动态避障:无人机撞树问题终结方案

本文详细介绍了如何在ROS和Gazebo仿真环境中,通过配置和调优Ego-Planner算法,实现无人机的动态避障功能。文章深入解析了其基于局部梯度优化的核心思想,提供了从环境搭建、参数详解到代码集成的完整实践指南,并分享了实战排错经验,旨在彻底解决无人机在复杂动态场景中的碰撞问题。

weixin_29031161的博客 1630

上云API对接大疆机场开发板

本文主要是如何调试大疆上云API对接大疆机场开发板进行内部测试。

yychen_java的博客 1万+

飞牛NAS新增虚拟机功能,如果使用虚拟机网卡直通安装ikuai软路由(如何解决OVS网桥绑定失败以及打开ovs后无法访问飞牛nas等问题)

飞牛NAS新增虚拟机功能,如果使用虚拟机网卡直通安装ikuai软路由

XiaoqiangClub的博客 1万+

DJI 上云API配置

本文参考DJI官方提供的上云API实现文档实现,记录在搭建过程中遇到的一些问题及解决方法。目前对于DJI 无人机接入第三方云平台,主要是基于 MSDK 开发定制 App,然后自己定义私有上云通信协议连接到云平台中。这样对于核心业务是开发云平台,无人机只是其中一个接入硬件设备的开发者来说,重新基于 MSDK 开发 App 工作量大、成本高,同时还需要花很多精力在无人机功能接口适配上,从而导致整体的方案开发进度较为缓慢。上云 API 的推出主要期望解决开发者重新造轮子的苦恼。

sym619的博客 8594

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

大疆上云api 源码解析(上下线功能)

设备向云平台推送消息,topic为sys/product/{gateway_sn}/status 进行设备上线。3.进行上线逻辑后,最终进入ChannelName.OUTBOUND_STATUS中。当pilot2与云平台建立mqtt连接后,进行设备绑定后。

qq617410272的博客 4226

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

【亲测免费】 上云API SDK for Java:高效、易用的云端开发工具

上云API SDK for Java:高效、易用的云端开发工具 项目简介 是一个由UPYUN(又拍云)提供的Java库,旨在简化开发者对又拍云服务的集成和调用过程,包括对象存储、CDN加速、音视频处理等丰富的云服务。通过这个SDK,开发者可以更便捷地在自己的Java应用程序中利用又拍云的各项功能。 技术分析 架构设计 该SDK遵循Java的最佳实践,采用模块化设计,每个模块对应于又拍云的一项具体服...

gitblog_00032的博客 786

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

大疆无人机上云API:Java代码实战全攻略

本文介绍了基于大疆上云API的Java二次开发流程,涵盖开发准备、API基础认知、环境配置到核心功能实现。内容主要包括:1) 开发环境搭建与SDK配置;2) API身份验证与凭证获取;3) 四大核心功能实现:无人机信息获取、飞行控制、相机操作及实时监控;4) 物流配送场景案例实践。文章还提供了常见问题解决方案,如连接失败、权限不足等,帮助开发者快速定位和解决问题。通过详细的代码示例和原理说明,展示了如何利用Java语言高效开发无人机应用。

m0_73978383的博客 8806

零基础小白本地部署大疆上云api(个人记录供参考)

3.下载好emqx,redis等(这些必须要求是windows版本的)进入emqx安装目录的bin文件下执行下面指令(必须的管理员运行终端)主要修改(appId,appKey,appLicense这些)最后生成如下界面,再把内容替换到config.ts就行。1.保证电脑又node.js环境,可以正常使用npm。最后我们可以到浏览器看到界面可以成功登录,运行成功。进行如下操作(需要注意申请的是web端的)1.修改confit.ts文件(如下图)运行的host都修改为localhost。

aloha 5138

简述大疆无人机对接

获取无人机飞行过程中的实时画面,用来直播获取飞机飞行数据(如实时高度,实时经纬度,实时飞机姿态等)获取飞机静态数据(如飞机基本信息、飞行记录、电池信息等)操控飞机飞行(如起飞、降落,改变高度,操作操控杆,设定航飞任务,拍照,录视频等)媒体管理(下载飞机记载内存卡中的文件等)其它业务功能开发(如态势感知,固件升级,多飞机集中管控等)至于应用场景,那就太广泛了,如消防、测绘、林业等等,可以赋能各行各业。应用开发:MobileSDK,UXSDK,WindowsSDK。

Naylor的博客 7605

大疆上云API源码解析(远程日志)

远程日志支持机场日志与飞行器日志的上传。云端服务器向设备请求获取日志文件列表,设备响应并上传列表。

qq617410272的博客 1323

大疆上云API源码解析(态势感知)

服务端推送设备坐标信息到pilot端,pilot会把推送过来的信息显示在地图中。实现了同一工作空间下的所有设备之间的拓扑结构,实现设备间的态势信息分享。

qq617410272的博客 2032

大疆上云API连接遥控器和无人机

大疆上云API连接遥控器和无人机

m0_62317155的博客 9233

大疆上云api如何配置开放平台

大疆上云api中,需要在遥控器中配置开放平台地址,这样才可以将设备和负载接入到开放平台。

Naylor的博客 3240

大疆上云API源码解析(固件升级)

固件升级功能只在机场上支持。

qq617410272的博客 1270

基于上云api前端开发经验教训(loading...)

上云api开发学习过程中的错误教训积累总结

IIIIIAN 2629

上云API 机场执行航线任务流程详解

上云api 航线执行 大疆机场

qq617410272的博客 2028
上一篇: springboot对web项目post请求参数加密,响应数据加密
下一篇: 大疆上云API-1.10.0版本-设备位置数据上报处理源码解析
yychen_java
博客等级 码龄8年 1451粉丝 77原创
评论 12
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值