资讯详情

Onboard SDK的使用

注意事项

  • 在实际测试或执行飞行任务之前DJI Assistant 2 中模拟使用DJI OSDK 应用程序的开发。
  • 在使用OSDK 基于开发应用程序或测试OSDK 在开发应用程序时,取下无人机上的桨叶。
  • 使用USB 连接无人机和用户电脑时,无人机将进入安全保护模式全保护模式,不能使用遥控器或OSDK 控制无人机解或启动无人机电机。
  • 确保无人机和遥控器的电池电量超过50%。
  • 确认DJI Assistant 2 、应用程序与终端工具的波特率一致。重新启动飞行平台、遥控器和计算平台,以改变波特率。
  • 使用OSDK 开发应用程序时,需要先使用DJI Assistant2升级无人机固件。
  • 为使基于OSDK 开发的程序能够与飞行平台间正常通信,在DJI Assistant2 中启用OSDK API 控制功能,Enable API Control 打勾。

控制权限

无人机控制权限的等级由高至低依次为遥控器,基于MSDK 开发的移动终端APP 和基于OSDK 应用程序的开发。

断连控制

  • 控制DJI 如果无人机执行飞行任务,DJI 中断无人机和遥控信号,DJI 的无人机会按照开发者在DJI Pilot 或基于MSDK 开发的移动终端APP 上设置的执行相应的控制动作。
  • 并用遥控器在控制无人机飞行时,无人机将按照机载计算机中的逻辑自动执行飞行任务。如果需要使用遥控器控制无人机,在控制无人机之前切换齿轮(随机切换);如果机载计算机和DJI 如果无人机信号中断,开发人员需要指定相应的控制策略,如悬停、降落或返,,确保机载计算机和DJI 的无人机在按照规定的返航策略安全返航,

设备连接

在Matrice 300 RTK 安装在无人机上Manifold 2 或者当第三方开发平台时,使用OSDK 转接板。提供了转接板XT30 24V 供电接口、OSDK串口以及OSDK USB 2.0 接口。

购买链接:

https://store.dji.com/cn/product/matrice-300-rtk-osdk-expansion-module?from=autocomplete&position=3

机载计算机转接板使用说明书:

https://dl.djicdn.com/downloads/matrice-300/20200617/OSDK_Expansion_Module_Product_Information.pdf

转接板其他连接详解:

https://developer.dji.com/cn/document/f1b72093-0a02-460c-ae2d-dce9bf37a60e

配置Linux 开发环境

  • C 编译器:GCC 5.4.0/5.5.0 版本
  • CMake:2.8 及以上版本
  • Linux:Ubuntu 16.04 (如需使用高级传感功能,请使用Libusb库)

添加UART 读写权限

  1. 使用sudo usermod -a -G dialout $USER将用户添加到命令中dialout组中。
  2. 重新登录添加的账户后,可以获得该账户UART 读写权限。

添加DJI USB 设备节点

  1. /etc/udev/rules.d/目录下创建文件DJIDevice.rules
  2. DJIDevice.rules文件中添加SUBSYSTEM=="usb", ATTRS{idVendor}=="2ca3", MODE="0666"
  3. 重新启动计算机后,系统可以识别它DJI USB 设备。

配置ROS开发环境 /配置STM32开发环境

https://developer.dji.com/cn/document/32bc1c7e-6c55-421f-afc9-fc99437b5392

参数设置

  • 在ROS 通过平台开发的应用程序UART 接口与无人机通信时,波特率应设置为921600,否则可能会丢包。
  • 使用Manifold 2-G 上的ttyTHS2.与无人机通信时,将波特率设置为1万;如果使用,USB 转TTL 串口模块和无人机通信,建议选用芯片为FT232 的模块。

操作示例程序

https://developer.dji.com/cn/document/dc0b69f6-e234-4390-87a5-97df7d96612d

使用OSDK

引入OSDK 开发包

通过应用程序DJI Onboard SDK 中的Vehicle类调用DJI OSDK 因此使用了功能OSDK 在开发应用程序时,首先引入OSDK 开发包。

引入OSDK 可在头文件后使用OSDK 开发应用程序。

#include <dji_vehicle.hpp>

引入OSDK 帮助文件后,在Linux 可以读取用户的配置文件,激活平台上开发的应用程序DJI 的无人机。

#include <dji_linux_helpers.hpp>

配置依赖项

在Linux上使用CMake 开发基于OSDK 应用程序时,请配置相应的依赖项:

  1. 指定CMake 最小版本: version 2.8
  2. 指定工程名称
  3. 配置编译选项(可选)set(CMAKE_CXX_FLAGS"${CMAKE_CXX_FLAGS} -std=C 11 -pthread -g -O0")
  4. 包含所需头文件的路径
  5. 生成并链接可执行文件

OSDK 接口调用

  • 同步调用接口

同步接口调用,开发人员调用接口时,接口将根据应用程序的实际情况获得相应的返回值,调用人员需要等待调用的接口发送返回值。相机模式如下:

    ErrorCode::ErrorCodeType retCode;     CameraManager *pm = vehicle->cameraManager;     /*set camera work mode as SHOOT_PHOTO*/     DSTATUS("set camera work mode as SHOOT_PHOTO");     retCode == pm->setModeSync(index, camreamodule::WorkMode::SHOOT_PHOTO, 3);     if (retCode != ErrorCode::SysCommonErr::Success){         DERROR("Set camera as SHOOT_PHOTO fail. Error code : 0x%lX", retCode);         ErrorCode::prntErrorCodeMsg(retCode);
        return retCode;
    }
  • 异步接口调用

异步接口调用,开发者在调用接口时,该接口会根据应用程序实际的情况获得对应的返回值,但开发者可能无法立刻得到对应的结果,当调用的接口获得结果后,该接口会通过状态或通知向开发者告知该结果,开发这可通过回调函数处理该调用结果。如异步调用的方式设置相机的模式:

  • 构造回调函数
    /*set camera work mode as RECORD_VIDEO*/
    DSTATUS("set camera mode to RECORD_VIDEO");
    pm->setModeAsync(index, camreamodule::WorkMode::RECORD_VIDEO, 
                     setCameraModeForRecordVideoCb, &udata);

  void CameraManagerAsyncSample::setCameraModeForRecordVideoCb(
       ErrorCode ::ErrCodeType retCode, UserData userData){
       AsyncSampleData *uData = (AsyncSampleData *)userData;
       
       DSTATUS("retCode : 0x%lX", retCode);
    if (!uData) {
       DERROR("User data is a null value.");
    return;
  }
  if (retCode == ErrorCode::SysCommonErr::Success) {
    DSTATUS("set camera work mode successfully");
    if (uData->pm) {
        DSTATUS("start to RECORD_VIDEO");
        uData->pm->startRecordVideoAsync) {
        uData->index,
        (void(*)ErrprCode::ErrorCodeType, UserData))uData->userCallBack,
        uData-> userData);
        }
      } else {
        DERROR("start to record video error. Error code : 0x%lX", retCode);
        ErrorCode::printErrorCodeMsg(retCode);
      if (uData->userCallBack) {
      void (*cb)(ErrorCode::ErrorCodeType, UserData);
      cb = (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack;
      cb(retCode, uData->userData);
    }
  }
}
  • 注册回调函数函数

开发者调用OSDK 中的异步接口后,将会接收到相应的数据,开发者需要注册回调函数处理所接收的数据。

void NMEA Callback(Vehicle* vehiclePtr, RecvContainer recvFrame, UserData userdata)
{
  int length =recvFrame.recvinfo.len-OpenProtocol::PackageMin-4;
  uint8_t rawBuf[length];
  memcpy(rawBuf, recvFrame.recvData.raw_ack_array, length);
  DSTATUS("&s/n",std string((char*)rawBuf, length).c_str())
}

使用 Vehicle

无人机对象实例化

使用OSDK 开发应用程序时,应用程序需要先读取环境配置参数、初始化无人机并创建实例对象vehicle

  • 读取环境配置参数(Userconfig.txt)编译OSDK 提供的示例代码时,基于OSDK 开发的应用程序需要读取如第三方库、波特率以及驱动权限等环境配置参数。
  LinuxSetup linuxEnvironment(argc, argv);    
  • 创建实例对象vehicle,并完成初始化。
 Vehicle *vehicle = linuxEnvironment.getVehicle(); 
  if (vehicle == NULL) {
    std::cout << "Vehicle not initialized, exiting. \n";
    return -1;
  }
  std::string sampleCase = linuxEnvironment.getEnvironment()->getSampleCase(); 

功能模块实例化

无人机对象实例化后,开发者可根据实际的使用需求,实例化所需使用的功能模块。 如下代码以实例化相机模块,并初始化PAYLOAD_INDEX_0PAYLOAD_INDEX_1为例。

ErrorCode::ErrorCodeType 
ret = vehicle->cameraManager->initCameraModule(PAYLOAD_INDEX_0,"Sample_camera_1");
  if (ret != ErrorCode::SysCommonErr::Success) {
    DERROR("Init Camera module Sample_camera_1 failed.");
    ErrorCode::printErrorCodeMsg(ret);
  }
ret = vehicle->cameraManager->initCameraModule(PAYLOAD_INDEX_1,"Sample_camera_2");
  if (ret != ErrorCode::SysCommonErr::Success) {
    DERROR("Init Camera module Sample_camera_2 failed.");
    ErrorCode::printErrorCodeMsg(ret);
  }

跨平台移植

https://developer.dji.com/cn/document/4f57e7f7-b946-4f43-bb7b-2c7ff077c31a

基于OSDK 开发的应用程序能够运行在Linux 和ROS 系统上,为满足开发者在不同的操作系统和开发平台上使用OSDK 开发应用程序,OSDK 提供了Hal 层和Osal 层库,通过Hal(Hardware Abstraction Layer,硬件接口层)适配不同的硬件平台,通过Osal(Operating System Abstraction Layer,操作系统抽象层)实现与不同操作系统的兼容。

  • 使用版本,在STM32上开发的应用程序运行在FreeRTOS上;
  • 使用版本,在STM32上开发的应用程序运行在裸机上。

基础控制

无人机基础信息

  • 开启或关闭RTK功能和避障功能(水平避障和顶部避障)
  • 设置无人机的返航点和返航高度
  • 获取RTK功能和避障功能的状态(水平避障和顶部避障)
  • 获取无人机返航点和返航高度

无人机基础飞行动作

  1. 不同型号的无人机在执行起飞、降落和返航动作时的高度可能会有差异,详情参见选购机型的说明书。
  2. 无人机解锁后,若未接收到起飞指令,无人机将自动上锁,螺旋桨会停止旋转
  • 无人机锁定
  • 解锁:无人机解锁后,无人机的螺旋桨会怠速旋转,但不会飞离地面

  • 上锁:无人机上锁后,无人机的螺旋桨由怠速旋转状态变为静止状态

  • 无人机起飞降落
  • 自动起飞:无人机会自动起飞1.2 米(该高度不可调整)

  • 自动降落:无人机会自动降落

  • 取消自动降落:无人机在下降多过程中会停止降落,并悬停在空中

  • 降落确认:当无人机已降落到离地面一定距离时,用户使用该功能可确认无人机降落到地面

  • 强制降落:无视降停面的状态,强制无人机降落(降落速度较快)

  • 无人机返航
  • 返航:无人机会自动返航

  • 取消返航:无人机会悬停在空中

Joystick 功能

Joystick 是一个无人机综合控制功能,使用Joystick 功能时,开发者根据实际的应用需要,通过调用Joystick 中的接口,,才能设计出满足使用需求的无人机飞行控制逻辑。

设置坐标系

  • 机体坐标系 机体坐标系以无人机的重心为原点,无人机机头前进的方向为X轴,机头前进方向的右侧为Y轴,Z轴与X轴、Y轴相互垂直交于重心且指向无人机下方(遵循“右手法则”)。在机体坐标下,无人机围绕X轴、Y轴和Z轴旋转时的飞行动作,可称为横滚(无人机仅绕X轴旋转)、俯仰(无人机仅绕Y轴旋转)和偏航(无人机仅绕Z轴旋转)。

  • 大地坐标系 大地坐标系也称世界坐标系或当地水平坐标系,在该坐标系中,无人机指向地球正北的方向为X轴,正东的方向为Y轴,X轴与Y轴相互垂直,Z轴竖直指向无人机下方,在满足“右手法则”的前提下,Z轴将根据无人机飞行的实际情况调节角度,因此该坐标系也称为“北-东-地(N-E-D)坐标系”。

设置水平控制模式

  • 姿态角控制模式:在该模式下,水平方向的指令为无人机的姿态角。(在机体坐标系下,该角度为roll 和pitch)
  • 速度控制模式:在该模式下,水平方向的指令为无人机的速度
  • 位置控制模式:在该模式下,水平方向的指令为无人机的位置,当位置指令的模不为0时,无人机会以指定的速度向前飞行,否则,无人机将悬停在指定的位置
  • 角速度控制:在该模式下,水平方向的指令为无人机的旋转角速度

设置无人机悬停模式

仅在水平控制模式中的速度控制模式下,开发者可以设置无人机的悬停模式:

  • 开启稳定模式:开启稳定模式后,无人机将在指定的位置上悬停
  • 关闭稳定模式:关闭稳定模式后,无人机将按照速度命令飞行,当无人机的前进速度为0时候,无人机可能会随风飘动

设置垂直控制模式

  • 速度控制模式:控制无人机垂直方向的速度
  • 位置控制模式:控制无人机垂直方向的位置,该位置为相对于起飞点的绝对位置
  • 油门控制模式:控制无人机的油门

设置yaw角度控制模式

yaw偏航角,绕着重力方向

  • 角度控制模式:在该模式下,yaw方向旋转的指令为yaw 的角度
  • 角速率控制模式:在该模式下,yaw方向旋转的指令为yaw 的角速率

使用无人机飞行控制功能

使用无人机飞行控制功能需要先实例化对象,再设置无人机的基础参数,最后再使用Joystick 中的功能。

1.实例化对象

使用OSDK 开发应用程序前先实例化无人机的对象。

FlightSample* flightSample = new FlightSample(vehicle);

2.设置无人机基础参数

使用flightSample 和 flightController 中的接口设置无人机的基础参数。

flightSample->monitoredTakeoff();
vehicle->flightController->setCollisionAvoidanceEnabledSync(
  FlightController::AvoidEnable::AVOID_ENABLE, 1);

/*! 飞行较长的距离 */
flightSample->moveByPositionOffset((FlightSample::Vector3f){0, 0, 30}, 0);

/*! 飞行较短的距离 */
flightSample->moveByPositionOffset((FlightSample::Vector3f){10, 0, 0}, 0);

/*!设置无人机当前位置为返航点 */
flightSample->setNewHomeLocation();

/*! 设置无人机返航高度 */
flightSample->setGoHomeAltitude(50);

/*! 飞至另一个位置 */
flightSample->moveByPositionOffset((FlightSample::Vector3f){20, 0, 0}, 0);

vehicle->flightController->setCollisionAvoidanceEnabledSync(
FlightController::AvoidEnable::AVOID_DISABLE, 1);
/*! 返航并确认降落*/
flightSample->goHomeAndConfirmLanding();

vehicle->flightController->setCollisionAvoidanceEnabledSync(
FlightController::AvoidEnable::AVOID_ENABLE, 1);

3. 使用Joystick 中的功能

使用Joystick 功能需要先设置Joystick 的模式和对应的控制指令,再执行Joystick 指令,实现对无人机的控制。如下代码以在flightSample->moveByPositionOffset()中,使用Joystick 功能控制无人机在水平和垂直方向上运动到指定的位置。

1.设置joystick的模式

  FlightController::JoystickMode joystickMode = {
    FlightController::HorizontalLogic::HORIZONTAL_POSITION,
    FlightController::VerticalLogic::VERTICAL_POSITION,
    FlightController::YawLogic::YAW_ANGLE,
    FlightController::HorizontalCoordinate::HORIZONTAL_GROUND,
    FlightController::StableMode::STABLE_ENABLE,
  };
  vehicle->flightController->setJoystickMode(joystickMode);

2.设置Joystick 控制指令并执行Joystick 功能

while (elapsedTimeInMs < timeoutInMilSec) {

限定无人机飞行范围
......
// 设置Joystick 控制指令
FlightController::JoystickCommand joystickCommand = {
                    positionCommand.x, positionCommand.y,
                    offsetDesired.z + originHeightBaseHomepoint, yawDesiredInDeg};
vehicle->flightController->setJoystickCommand(joystickCommand);

// 执行Joystick 功能
vehicle->flightController->joystickAction();
......
判断无人机是否进入设定位置的阈值
                                         }

广播与订阅

OSDK提供了数据广播和数据订阅功能,具有广播功能的应用程序能够实时向使用移动端APP(基于MSDK 开发)的用户、基于OSDK 开发的地面设备及其他设备发送无人机当前的飞行状态以及无人机上传感器产生的数据信息;具有消息订阅功能的应用程序,能够实时获取无人机上传感器产生的数据和无人机的飞行状态。

消息订阅(建议使用)

无人机上的各个部件以及第三方传感器根据无人机实际的飞行状况,会实时产生大量的数据信息并被无人机推送给其他模块,具有消息订阅功能的应用程序,能够记录用户所需订阅的数据。

订阅项

使用OSDK 消息订阅功能可订阅的数据信息如下:

数据类型 订阅项(Topic) 最大订阅频率(Hz)
无人机基础信息 姿态四元数 200
硬件同步 400
返航点信息和设置状态 50
遥控器信息 50
无人机状态信息 无人机飞行状态 50
无人机飞行模式 50
起落架状态 50
电调 50
电池 50
解锁电机错误码 50
飞行异常 50
无人机速度信息 飞行速度 200
融合角速度 200
原始角速度 400
原始加速度 400
大地坐标系下的加速度 200
机体坐标系下的加速度 200
传感器信息
气压计高度 200
融合气压计高度 200
返航点气压计高度 1
融合相对高度 100
指南针 100
VO视觉位置 200
避障信息 100
云台信息
云台状态 50
云台角度 200
云台控制模式 50
定位信息
GPS信号水平 50
GPS原始信息 5
GPS融合信息 50
RTK连接状态 50
RTK原始信息 5

订阅规则

  • 消息订阅功能最多支持订阅0Hz、1Hz、10Hz、50Hz、100Hz,,每个订阅项只能被订阅一次。
  • 指定订阅频率时,任何参数的订阅频率不能小于或等于0 ,相同订阅频率的主题的数据长度总和须小于或等于242。

使用消息订阅功能

1. 验证消息订阅功能

 ACK::ErrorCode subscribeStatus;
    subscribeStatus = vehicle->subscribe->verify(timeout);
    if (ACK::getError(subscribeStatus) != ACK::SUCCESS) {
      ACK::getErrorCodeMessage(subscribeStatus, __func__);
      return false;
    }

2. 消息订阅功能初始化

初始化消息订阅功能后,开发者需指定订阅项、订阅频率、包编号、数据大小。

bool enableTimestamp = false;
    bool pkgStatus = vehicle->subscribe->initPackageFromTopicList(
        pkgIndex, topicSize, topicList, enableTimestamp, freq);
    if (!(pkgStatus)) {
      return pkgStatus;
    }

3. 获取订阅数据

   subscribeStatus = vehicle->subscribe->startPackage(pkgIndex, timeout);
    if (ACK::getError(subscribeStatus) != ACK::SUCCESS) {
      ACK::getErrorCodeMessage(subscribeStatus, __func__);

4. 清除订阅项

      ACK::ErrorCode ack = vehicle->subscribe->removePackage(pkgIndex, timeout);
      if (ACK::getError(ack)) {
        DERROR(
            "Error unsubscription; please restart the drone/FC to get "
            "back to a clean state");
      }
      return false;
    }
    return true;
  } else {
    DERROR("vehicle haven't been initialized", __func__);
    return false;
  }

开发者可选择一组“主题”或“订阅”数据集,将它们添加到“订阅”包中,并配置相应的订阅频率。用户可以通过DJI Onboard API配置五个数据包。同时可为每个数据包设置单独的频率,OSDK 为开发者提供了300 字节的缓冲区,允许用户根据需要为每个软件包添加较多的订阅项。

广播(不建议使用)

无人机上的各个部件以及第三方传感器根据无人机实际的飞行状况,会实时产生大量的数据信息,OSDK 的广播功能,不仅能够接收到无人机主动推送给其他模块的数据,还能够接收第三方传感器的数据,并通过广播功能广播所接收到的数据,基于MSDK 开发的移动端APP、基于OSDK 开发的地面设备、可接收无人机广播信息的设备或云端应用平台等可记录无人机广播的数据信息。

广播项

具有OSDK 广播功能的应用程序可广播如下数据信息:

  • 传感器数据信息:陀螺仪和磁力计等传感器读数
  • 融合数据,例如态度和全球位置
  • 无人机基础信息:电池,云台和飞行状态

广播频率

  • OSDK 的广播功能最多支持以0Hz、1Hz、10Hz、50Hz、100Hz,广播数据,每个广播项只能被广播一次。例如,如果将GPS指定为50Hz,将陀螺仪指定为10Hz,则来自广播的整个数据包将以50Hz传入,但是陀螺仪将仅每5个数据包中出现一次。
  • 使用OSDK 的广播功能后,基于OSDK 开发的应用程序将以默认频率广播数据,如需更改广播频率,请调用广播功能中的API 或使用DJI Assistant 2 更改广播频率。
  • 指定订阅频率时,任何参数的订阅频率不能小于或等于0 ,相同订阅频率的主题的数据长度总和须小于或等于242。

时间同步

时间同步是一种使用PPS 信号,通过与通信,实现的功能;具有“时间同步”功能的应用程序,不仅方便用户快速分析传感器采样的数据,还能提高相机曝光时间的精准度,以及实现获取精准定位信息等高级功能;时间同步功能支持以1Hz 的频率获取RTK 数据,支持以5Hz 的频率获取GPS 数据。

OSDK 时间同步的流程如下所示:

  1. 无人机通过指定的硬件接口发送PPS 信号和UTC 时间戳,用于同步机载计算机和传感器上的时间;
  2. 无人机与RTK 或GPS 卫星在通信状态良好的情况下,无人机将以1Hz 的频率发送RTK 数据包,以5Hz 的频率发送GPS 数据包,其中包含NMEA 数据。

无人机脉冲在上升沿处(从0V上升到3.3V)产生UTC 时间戳。

获取NMEA 信息

  • 以异步的方式获取NMEA 信息
void subscribeNMEAMsgs(VehicleCallBack cb, void *userData);
void unsubscribeNMEAMsgs();
  • 以同步的方式获取NMEA 信息
bool getNMEAMsg(NMEAType type, NMEAData &nmea);

获取UTC 时间戳

  • 以异步的方式
void subscribeUTCTime(VehicleCallBack cb, void *userData);
void unsubscribeUTCTime();
  • 以同步的方式获取UTC 时间戳
bool getUTCTime(NMEAData &utc);

获取无人机上的时间

  • 以异步的方式获取无人机上的时间
void subscribeFCTimeInUTCRef(VehicleCallBack cb, void *userData);
void unsubscribeFCTimeInUTCRef();
  • 以同步的方式获取无人机上的时间
bool getFCTimeInUTCRef(DJI::OSDK::ACK::FCTimeInUTC &fcTimeInUTC);

获取无人机上的PPS 信息

  • 以异步的方式获取无人机上的PPS 信息
void subscribePPSSource(VehicleCallBack cb, void *userData);
void unsubscribePPSSource();
  • 以同步的方式获取无人机上的PPS 信息
bool getPPSSource(PPSSource &source);

断连重连

M210 V2系列和M300飞机在飞行时,由于使用USB A口的抗抖动性欠佳,可能会偶现OSDK USB短暂断连的情况(类似于USB拔插情况)。在OSDK 4.0及以前版本,一旦USB短暂断连发生,OSDK程序将无法重新恢复通信。为解决该问题,从OSDK 4.1起OSDK程序将提供USB的断连重连的支持,该功能作为可选项在程序编译时选择是否启用。目前STM32平台已经默认支持USB断连重连,ROS平台的断连重连依赖Linux版本程序,因此本次支持的重点主要是Linux平台。

断连重连监控线程

在启用断连重连功能后,OSDK Linux平台代码将新增一个线程监控断连重连情况是否发生。如果检测到断连和重连情况出现,OSDK将会有log输出提示。在重连发生后,监控线程将重新初始化硬件设备并更新Hal层对应相关的句柄,从而恢复线程。 具体的工作流程局部图如下:

Udev机制

OSDK通过Udev机制来监控Linux系统平台的热拔插事件,并与OSDK通信链路初始化时的硬件特征进行对比,从而来获取OSDK通信链路的通断事件。

断连重连配置

首先需要支持libudev

sudo apt-get install libudev-dev

在Onboard SDK执行CMake编译时需要加上 -DOSDK_HOTPLUG=ON

$:~/Onboard-SDK/build$ cmake .. -DOSDK_HOTPLUG=ON

如果出现以下log则表明编译OSDK Lib时,断链重连机制将生效:

-- Enable OSDK Hotplug monitoring.
-- Found libudev:
--  - Includes: /usr/include
--  - Libraries: /lib/x86_64-linux-gnu/libudev.so
  • Linux平台的断连重连功能仅支持USB设备,并且在USB设备初始化成功后才能生效。
  • OSDK工程在Linux平台默认不支持断连重连特性,需要在Cmake时加上宏定义-DOSDK_HOTPLUG=ON才生效。

云台管理

云台关节是云台上带动负载设备转动的结构件:云台电机,云台关节角即云台电机转动的角度。

根据用户的控制指令,云台能够调整姿态;云台姿态角即使用大地坐标系(NED,北东地坐标系)描述云台上的角度,该角度也称为欧拉角。

云台控制

控制方式

  • 绝对角度控制:使用OSDK 开发的云台根据用户的指令,在规定的时间内,从转动到指定的位置。
  • 在角度控制模式下,云台转动的时间受云台最大旋转速度和最大加速度限制,实际的转动角度受云台限位角度的限制。
  • 速度控制:用户可控制使用OSDK 开发的云台的转动速度。
  • 在速度控制模式下,云台根据用户指定的速度转动0.5s,当云台转动到限位角时,将会停止转动。

目前OSDK 的云台控制功能自由模式,在该模式下,当无人机的姿态改变时,云台将不会转动。

使用云台管理功能

1. 云台控制功能模块初始化

使用“云台控制”功能前,需要调用云台控制功能的类GimbalManagerSyncSample,创建云台控制功能的对象并初始化指定的云台。

GimbalManagerSyncSample *g = new GimbalManagerSyncSample(vehicle);
ret = vehicle->gimbalManager->initGimbalModule(PAYLOAD_INDEX_0,
                                                "Sample_gimbal_1");

if (ret != ErrorCode::SysCommonErr::Success)
{
  DERROR("Init Camera module Sample_gimbal_1 failed.");
  ErrorCode::printErrorCodeMsg(ret);
}
ret = vehicle->gimbalManager->initGimbalModule(PAYLOAD_INDEX_1,
                                                "Sample_gimbal_2");

2. 获取云台的状态信息

创建云台控制功能的对象并初始化指定的云台后,即可获取用户指定的云台的信息。

DSTATUS("Current gimbal %d angle (p,r,y) = (%0.2f°, %0.2f°, %0.2f°)", PAYLOAD_INDEX_0,
      g->getGimbalData(PAYLOAD_INDEX_0).pitch,
      g->getGimbalData(PAYLOAD_INDEX_0).roll,
      g->getGimbalData(PAYLOAD_INDEX_0).yaw);

3. 控制云台转动

通过GimbalManagerSyncSample中指定的接口控制云台的姿态。

GimbalModule::Rotation rotation;
        rotation.roll = 0.0f;
        rotation.pitch = 25.0f;
        rotation.yaw = 90.0f;
        rotation.rotationMode = 0; 
        rotation.time = 0.5;
        g->rotateSyncSample(PAYLOAD_INDEX_0, rotation);

4. 云台回中

使用OSDK 开发的应用程序支持将云台的俯仰轴和偏航轴转动至中位。

g->resetSyncSample(PAYLOAD_INDEX_0);

相机管理

OSDK 提供了相机管理功能,即CameraManager类。开发者使用OSDK 中的CameraManager类,能够同时设置并获取无人机上多个相机的感光度、光圈、快门和曝光等参数的值,控制相机实现拍照、录像及指点变焦等功能。

在使用相机管理功能时,开发者需要先OSDK 中的相机模块,CameraManager,再根据实际的使用需要,最后根据用户的使用逻辑实现所需使用的功能,如设置相机的参数或检查功能的状态等。

CameraManager 支持功能

  X4S X5S X7 Z30 XTS XT2 H20/H20T
设置/获取相机工作模式
设置/获取相机对焦模式 × ×
设置/获取曝光模式 × × ×
设置/获取ISO × ×
设置/获取光圈 × ×
设置/获取快门 × × ×
设置/获取EV(曝光补偿) × × ×
设置/获取分接缩放参数 × × × × ×
设置/获取分接缩放点 × × × × ×
设置/获取焦点 × × ×
设置/获取拍摄照片模式 × × ×
拍摄单张照片
设置/获取间隔拍摄参数 × × × ×
拍摄间隔照片 ×
设置/获取自动包围曝光拍摄参数 × × × ×
拍摄自动包围曝光照片 × × × ×
设置/获取连拍参数 × × × ×
拍摄连拍照片 × × ×
录制视频
下载文件列表(仅M300 RTK) × × × √(beta)
下载媒体文件(仅M300 RTK) × × × √(beta)

1. 相机模块初始化

无人机初始化后,需初始化无人机上的相机模块,并将所需控制的相机注册到vehicle->cameraManager中,如下为初始化无人机I 号云台和II 号云台上的相机模块。

ErrorCode::ErrorCodeType 
ret = vehicle->cameraManager->initCameraModule(PAYLOAD_INDEX_0,"Sample_camera_1");
  if (ret != ErrorCode::SysCommonErr::Success) {
    DERROR("Init Camera module Sample_camera_1 failed.");
    ErrorCode::printErrorCodeMsg(ret);
  }
ret = vehicle->cameraManager->initCameraModule(PAYLOAD_INDEX_1,"Sample_camera_2");
  if (ret != ErrorCode::SysCommonErr::Success) {
    DERROR("Init Camera module Sample_camera_2 failed.");
    ErrorCode::printErrorCodeMsg(ret);
  }

2. CameraManager 对象实例化

如需使用OSDK CameraManager 中功能,创建CameraManager实例。

  • 以同步的方式创建实例

CameraManagerSyncSample *p = new CameraManagerSyncSample(vehicle);

  • 以异步的方式创建实例

CameraManagerAsyncSample *p = new CameraManagerAsyncSample(vehicle);

3. 设置或获取相机参数

CameraManager 对象实例化后,开发者需根据使用相机功能的逻辑,设置或获取相机的模式,如设置相机的光圈模式前需先设置相机的曝光模式,详情请参见OSDK API 文档,如下以同步和异步的方法介绍获取相机光圈值的方法。

以同步的方式获取相机的光圈模式

  retCode = pm->getApertureSync(index, apertureGet, 1);

以异步的方式获取相机的光圈模式

  • 构造回调函数
void CameraManagerAsyncSample::getApertureCb(ErrorCode::ErrorCodeType retCode,
                                             CameraModule::Aperture apertureGet,
                                             UserData userData) {
  AsyncSampleData *uData = (AsyncSampleData *)userData;

  DSTATUS("retCode : 0x%lX", retCode);
  if (!uData) {
    DERROR("User data is a null value.");
    return;
  }
  if (retCode == ErrorCode::SysCommonErr::Success) {
    DSTATUS("Get aperture = %d", apertureGet);
    if (uData->pm) {
      if (*(CameraModule::Aperture *)uData->dataTarget == apertureGet) {
        DSTATUS("The aperture value is already %d.", apertureGet);
        if (uData->userCallBack) {
          void (*cb)(ErrorCode::ErrorCodeType, UserData);
          cb =
              (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack;
          cb(ErrorCode::SysCommonErr::Success, uData->userData);
        }
      } else {
        uData->pm->setApertureAsync(
            uData->index, *(CameraModule::Aperture *)uData->dataTarget,
            (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack,
            uData->userData);
      }
    }

  } else {
    DERROR("Get aperture error. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    if (uData->userCallBack) {
      void (*cb)(ErrorCode::ErrorCodeType, UserData);
      cb = (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack;
      cb(retCode, uData->userData);
    }
  }
}
  • 注册回调函数
  DSTATUS("Get aperture now ...");
  pm->getApertureAsync(index, getApertureCb, &uData);
}

4. 控制相机执行指定的动作

开发者通过CameraManager 能够控制相机执行指定的动作,如指点变焦等,详情请参见OSDK API 文档,如下以同步和异步的方法介绍控制相机执行指点变焦功能的方法。

以同步的方式控制相机指点变焦

 ErrorCode::ErrorCodeType CameraManagerSyncSample::setTapZoomPointSyncSample(
    PayloadIndexType index, uint8_t multiplier, float x, float y) {
  if (!vehicle || !vehicle->cameraManager) {
    DERROR("vehicle or cameraManager is a null value.");
    return ErrorCode::SysCommonErr::InstInitParamInvalid;
  }
  ErrorCode::ErrorCodeType retCode;
  CameraManager *pm = vehicle->cameraManager;

  /*开启指点变焦功能*/
  DSTATUS("Set tap zoom enable  = %d", true);
  retCode = pm->setTapZoomEnabledSync(index, true, 1);
  if (retCode != ErrorCode::SysCommonErr::Success) {
    DERROR("Set tap zoom enable fail. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    DERROR("It is only supported Z30 camera.");
    return retCode;
  }

  /*设置指点变焦功能的参数*/
  DSTATUS("Set tap zoom multiplier = %d", multiplier);
  retCode = pm->setTapZoomMultiplierSync(index, multiplier, 1);
  if (retCode != ErrorCode::SysCommonErr::Success) {
    DERROR("Set tap zoom multiplier fail. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    DERROR("It is only supported Z30 camera.");
    return retCode;
  }

  /*设置指点变焦对象*/
  DSTATUS("Set tap zoom target point : (%f,%f)", x, y);
  retCode = pm->tapZoomAtTargetSync(index, {x, y}, 1);
  if (retCode != ErrorCode::SysCommonErr::Success) {
    DERROR("Set tap zoom target fail. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    DERROR("It is only supported Z30 camera.");
    return retCode;
  } else {
    DSTATUS(
        "tap zoom at target (%0.2f, %0.2f) successfully, need several seconds "
        "to zoom.",
        x, y);
  }

  return retCode;
}

以异步的方式控制相机指点变焦

  • 构造回调函数
/*设置指点变焦系数*/
void CameraManagerAsyncSample::setTapZoomMultiplierCb(
    ErrorCode::ErrorCodeType retCode, UserData userData) {
  AsyncSampleData *uData = (AsyncSampleData *)userData;

  DSTATUS("retCode : 0x%lX", retCode);
  if (!uData) {
    DERROR("User data is a null value.");
    return;
  }
  if (retCode == ErrorCode::SysCommonErr::Success) {
    DSTATUS("Set tap zoom multiplier successfully ");
    if (uData->pm) {
      /*注册开启指点变焦功能的函数*/
      uData->pm->setTapZoomEnabledAsync(uData->index, true, setTapZoomEnableCb,
                                        uData);
    }
  } else {
    DERROR("Set tap zoom multiplier error. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    if (uData->userCallBack) {
      void (*cb)(ErrorCode::ErrorCodeType, UserData);
      cb = (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack;
      cb(retCode, uData->userData);
    }
  }
}

/*开启指点变焦功能*/
void CameraManagerAsyncSample::setTapZoomEnableCb(
    ErrorCode::ErrorCodeType retCode, UserData userData) {
  AsyncSampleData *uData = (AsyncSampleData *)userData;

  DSTATUS("retCode : 0x%lX", retCode);
  if (!uData) {
    DERROR("User data is a null value.");
    return;
  }
  if (retCode == ErrorCode::SysCommonErr::Success) {
    DSTATUS("Set tap zoom enable successfully ");
    if (uData->pm) {
      /* 设置指点变焦的对象 */
      uData->pm->tapZoomAtTargetAsync(
          uData->index, *(CameraModule::TapZoomPosData *)uData->dataTarget,
          (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack,
          uData->userData);
    }
  } else {
    DERROR("Tap zoom at enable error. Error code : 0x%lX", retCode);
    ErrorCode::printErrorCodeMsg(retCode);
    if (uData->userCallBack) {
      void (*cb)(ErrorCode::ErrorCodeType, UserData);
      cb = (void (*)(ErrorCode::ErrorCodeType, UserData))uData->userCallBack;
      cb(retCode, uData->userData);
    }
  }
}
  • 注册回调函数
  DSTATUS("Set tap zoom multiplier = %d", multiplier);
  pm->setTapZoomMultiplierAsync(index, multiplier, setTapZoomMultiplierCb, &uData);
}

SDK互联互通

https://developer.dji.com/cn/document/e37514a6-5803-4abf-a5b6-6da844bd56f6

HMS(健康管理系统)

HMS(健康管理系统)是对飞机各模块的运行健康状态的一个监控系统。在DJI Pilot上已经提供了交互界面查看各模块的工作状态是否有异常。OSDK则提供了基本接口来获取各模块异常的错误信息,便于OSDK程序获知飞机各模块的工作状态。

HMS错误信息

单条HMS的错误信息包括一个错误ID,错误的Index和一个错误的等级,在订阅HMS信息后,这些错误信息将会打包发送给OSDK。OSDK根据这些错误信息的数据解析成字符串,并从终端打印出来。

使用HMS

OSDK的HMS功能支持M300机型,其错误信息通过USB ACM口与飞机进行通信实现。

初始化HMS Sample

OSDK已经通过HMSSample这个类封装好了HMS的API的使用方法,在环境初始化之后仅需要创建该类即可快速使用HMS功能。

HMSSample* hmsSample = new HMSSample(vehicle);

订阅飞行状态信息

在HMS模块内会根据TOPIC_STATUS_FLIGHT的订阅数据来判断飞机是否在空中,从而区分HMS信息是在飞行过程中接收到的还是在地上接收到的。

//! 1.subscribe flight'status.(or default Error Information is about on the ground)
int pkgIndex = 0;
if (!hmsSample->subscribeFlightStatus(pkgIndex))
{
    return -1;
}

订阅HMS信息

订阅HMS信息,订阅成功后,HMS信息将推送给OSDK。该命令请求以及推送数据都是通过USB ACM口进行通信,需要确认USB ACM是否已经连接成功才能正常使用。

//! 2.subscribe HMS's pushing.(Then it will print all error information with 5HZ in English)
if (!hmsSample->subscribeHMSInf(true, timeOutMs))
{
    hmsSample->unsubscribeFlightStatus((pkgIndex));
    return -1;
}

打印HMS信息

OSDK接收到的HMS可以通过 void HMSSample::printAllError(void)进行打印。开发者也可以参考该函数的实现方式,调用接口 HMSPushPacket DJIHMS::getHMSPushPacket() 获取原始的HMS信息包进行自定义处理。

//! 2.1 print all pushed raw error data with 5HZ in 30 senconds
const int waitTimeMs = 200;
int timeSoFar = 0;
int totalTimeMs = 30*1000; // 30 secs
while(timeSoFar < totalTimeMs)
{
    OsdkLinux_TaskSleepMs(waitTimeMs);
    hmsSample->printAllError();
    DSTATUS("now flight status is %d\n", hmsSample->getFlightStatus());
    timeSoFar += waitTimeMs;
}

取消订阅

在HMS任务完成时可以取消HMS和飞行状态信息的订阅。

//! 3. reset osdk's subscription for HMS's pushing
hmsSample->subscribeHMSInf(false, timeOutMs);
//! 4. unsubscribe flight status
hmsSample->unsubscribeFlightStatus(pkgIndex);

运动规划

使用运动规划功能,可根据实际的使用需求设计相应的航点任务和热点任务,制定控制无人机自动化飞行的控制逻辑。

航点任务

航点规划是一个控制无人机按照指定的航线飞行,实现无人机飞行自动化的控制功能。开发者通过调用DJI OSDK 的接口,能够控制无人机以飞往并执行相应动作,根据实际的使用需求,还可控制无人机重复多次执行指定的任务,实现自动化巡航等功能。

航点

开发者在使用航点任务时,需要指定航点数量和对应的航点类型。

  • 航点数量 使用航点任务时,开发者需使用OSDK 中的API 接口指定无人机所需飞达的航点,DJI OSDK 支持开发者在一个任务中最多添加65535 个航点,至少2个航点。
  • 航点类型 航点类型是指无人机在执行航点任务时,飞向该航点的方式,其中包含曲率飞行、直线飞行和协调转弯。
    • 曲率飞行
      • 无人机以曲率连续的方式执行飞行任务时,到达指定的航点不停留。
      • 无人机以曲率连续的方式执行飞行任务时,在航点处停留。
      • 无人机以曲率不连续的方式执行飞行任务时,在航点处停留。
    • 直线飞行
      • 直线进入
      • 直线退出
    • 协调转弯:无人机在到达航点前提前转弯

动作

开发者在使用航点任务时,开发者或用户可为无人机在指定的航点处添加对应的动作,如拍照、录像或悬停等。

  • 动作信息 动作信息主要由动作ID、触发该动作的触发器和执行该动作的执行器组成。
    • 动作ID,开发者可指定全局唯一的动作ID 标识用户设置的动作。
    • 触发器:触发无人机执行动作的触发器,包含触发器的类型,以及该触发类型所对应的参数。每个触发器仅支持一种触发类型。
    • 执行器:执行用户指定动作的模块,包含执行器的类型和执行器的编号,以及该执行器所对应的参数。每个执行器仅支持一种执行类型
  • 动作数量 DJI OSDK 支持开发者最多在一个任务中总共可添加65535 个动作。
  • 动作管理:
    • 支持将相机对焦变焦的动作配置到地面站中自动执行。
    • 支持云台角度增量控制
    • 支持配置多个云台多个相机
  • 动作触发 如需在无人机执行航点任务的过程中,触发无人机执行指定的动作,需要添加触发该动作的条件:
    • 定时触发
    • 距离触发
    • 动作串行触发
    • 动作并行触发
    • 航点触发:无人机在航点飞行时,在第N个航点结束航点任务

速度控制

OSDK 为开发者提供了速度控制功能,能够使开发者为指定的航点配置不同的速度(为同一个航点设置多个速度),支持开发者在无人机执行航线飞行任务时,修改或查询无人机全局巡航速度。

断连控制

新增支持配置遥控器失联后继续执行航线任务的功能。

  • 基于OSDK 开发的应用程序在控制无人机执行任务时,用户可使用遥控器控制无人机如无人机的飞行速度、飞行高度和飞行航向等。
  • 无人机每次只能执行一个自动化飞行任务,上传新的任务后,已有的飞行任务将会被覆盖。
  • 在航点任务中,无人机的航点与无人机的动作没有必然关系,开发者可根据实际情况添加航点和无人机飞行时的动作。

工作流程

航点任务功能按如下流程,控制无人机执行航点任务:

  1. 上传航线任务的整体信息 一个航点任务包含航点任务的ID、航点任务的航点数、任务重复次数、航点任务结束后的动作、最大飞行速度和巡航速度。
  2. 上传航点信息
    • 基础参数:航点坐标(设置航点的经度、纬度和相对于起飞点的高度)、航点类型、航向类型和飞行速度。
    • 可选参数:缓冲距离、航向角度、转向模式、兴趣点、单点最大飞行速度、单点巡航速度。仅开发者设置航点信息所有的基础参数后,才能设置航点信息的可选参数。
  3. 设置动作信息(可选) 设置动作的ID、触发器和执行器
  4. 上传无人机的航点任务的信息
  5. 控制无人机执行航点任务 上传无人机航点和对应的动作信息后,开发者即可通过指定的接口控制航点任务,如开始、停止或暂停任务,设置或获取巡航速度等。

使用航点任务功能

如需使用航点任务,使用DJI OSDK 的WaypointV2MissionOperator 类实现航点任务功能。

在使用航点任务前,请先完成无人机飞行准备,如初始化无人机、获取控制无人机的权限,创建实例并执行航点任务。

  • 初始化无人机的飞行环境
LinuxSetup linuxEnvironment(argc, argv);
Vehicle* vehicle = linuxEnvironment.getVehicle();
if (vehicle == NULL)
{
 std::cout << "Vehicle not initialized, exiting.\n";
 return -1;
}
  • 获取无人机的控制权限
/*! Obtain Control Authority*/
vehicle->control->obtainCtrlAuthority(functionTimeout);
  • 创建航点任务的示例,并执行航点任务
/*! Initialize a new WaypointV2 mission sample*/
auto *sample = new WaypointV2MissionSample(vehicle);

/*! run a new WaypointV2 mission sample*/
sample->runWaypointV2Mission();

1. 航点任务初始化

初始化航点任务的信息,向无人机飞行控制器发送航点任务的整体信息和航点信息,其中航点任务的整体信息包括无人机的巡航速度、断连控制和航点信息等;航点信息包含航点高度,航点经纬度等。

ret = initMissionSetting(timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(timeout);
......
......
/*设置航点任务的整体信息*/
WayPointV2InitSettings missionInitSettings;
missionInitSettings.missionID = rand();
missionInitSettings.repeatTimes = 1;
missionInitSettings.finishedAction = DJIWaypointV2MissionFinishedGoHome;
missionInitSettings.maxFlightSpeed = 10;
missionInitSettings.autoFlightSpeed = 3;
missionInitSettings.exitMissionOnRCSignalLost = 1;
missionInitSettings.gotoFirstWaypointMode = DJIWaypointV2MissionGotoFirstWaypointModePointToPoint;
missionInitSettings.mission = generatePolygonWaypoints(radius, polygonNum);
missionInitSettings.missTotalLen = missionInitSettings.mission.size();
this->actions = generateWaypointActions(actionNum);
ErrorCode::ErrorCodeType ret = vehiclePtr->waypointV2Mission->init(&missionInitSettings,timeout);
/*设置航点任务中航点的信息*/
missionInitSettings.mission = generatePolygonWaypoints(radius, polygonNum);
/*设置无人机的动作信息(可选)*/
this->actions = generateWaypointActions(actionNum);

std::vector<DJIWaypointV2Action> actionVector;
for(uint16_t i = 0; i < actionNum; i++)
{
  DJIWaypointV2SampleReachPointTriggerParam sampleReachPointTriggerParam;
 sampleReachPointTriggerParam.waypointIndex = i;/*设置动作的编号*/
 sampleReachPointTriggerParam.terminateNum = 0;
 /*设置触发器*/
 auto *trigger = new DJIWaypointV2Trigger(DJIWaypointV2ActionTriggerTypeSampleReachPoint,&sampleReachPointTriggerParam);   
 /*设置执行器*/
 auto *cameraActuatorParam = new DJIWaypointV2CameraActuatorParam(DJIWaypointV2ActionActuatorCameraOperationTypeTakePhoto, nullptr);
 auto *actuator = new DJIWaypointV2Actuator(DJIWaypointV2ActionActuatorTypeCamera, 0, cameraActuatorParam);
 auto *action = new DJIWaypointV2Action(i, *trigger,*actuator);
 actionVector.push_back(*action);
}
return actionVector;

2. 上传或下载航点任务

  • 上传航点任务
uploadWaypointMission(timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(waitTime);
  • 下载航点任务
std::vector<DJIWaypointV2> mission;
dowloadWaypointMission(mission,timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(timeout);

3. 上传“动作”(可选)

在上传无人机所需执行的“动作”的过程中,无人机会检查“动作”的合法性检查,若检查不通过,则无法上传动作(但不影响无人机飞行),详情请参考相应的错误码。

uploadWapointActions(timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(timeout);

4. 控制无人机执行航点任务

  • 开始执行航点任务 无人机开始执行航点任务前,将先检查用户上传的任务信息,若检查失败,无人机将无法执行航点任务,详情请查看错误码信息。
startWaypointMission(timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(40);
  • 暂停执行航点任务 控制无人机暂停执行航点任务。
pauseWaypointMission(timeout);
if(ret != ErrorCode::SysCommonErr::Success)
return ret;
sleep(5);
  • 恢复执行航点任务 控制无人机从暂停的位置,继续执行为完成的航点任务。
resumeWaypointMission(timeout);
if(ret != ErrorCo

标签: dx4电量变送器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台