创建Map View
3. 添加Annotations到MapView中
设置智能飞行任务
3. 创建 DJIWaypointConfigViewController
设置地面站任务
1. 添加 DJIWaypointConfigViewController 到 DJIRootViewController中
如果您在本教程中遇到任何错误或者bug,请使用Github issue,在DJI论坛发帖或者在Gitbook中评论告知我们。您可以随时给我们发送Github pull request来帮助我们修复错误。关于文档的修改需要符合 格式标准
在本教程中, 你将学到如何安装使用 DJI PC 模拟器, 如何升级你的 Inspire 1, Phantom 3 Professional 和 Phantom 3 Advanced的固件到Beta版本, 以及如何使用DJI PC 模拟器去测试地面站的API. 同时, 你将学到地面站预设航点功能的基本使用流程. 现在开始学习吧!
你可以在这里下载到本教程的全部代码: https://github.com/DJI-Mobile-SDK/iOS-GSDemo.git
使用DJI PC模拟器
1. 介绍
DJI PC 模拟器专为 SDK 开发者所设计。模拟器通过 UDP 广播的方式传输飞行器模拟的参数, 在 PC 端实现飞行器的 3D 虚拟显示,并且可以进行数据分析。
适用操作系统 : Windows 7, Windows 8, 和 Windows 8.1
适用飞行器型号 : Matrice 100, Inspire 1, Phantom 3 Professional 和 Phantom 3 Advanced
2. 安装DJI PC模拟器
首先, 请从这里下载DJI PC模拟器和DJI WIN 驱动程序: https://developer.dji.com/mobile-sdk/downloads :
你必须在使用模拟器前安装好驱动程序. 因为目前模拟器只支持Windows系统, 你必须找到一台 PC 或者有运行着Windows系统虚拟机(像 VMWare 或者 Parallels Desktop)的Mac才能使用模拟器. 现在, 双击 DJI_WIN_Driver_Installer.exe 文件进行安装. 如果弹出一个写着“请给主控供电并用USB连接电脑”的对话框, 点击是 , 然后按照指引安装好即可。
接着, 双击 DJISimulator-Installer.exe 文件并按指引安装好模拟器即可。
3. 使用DJI PC模拟器
1 . 运行DJI PC模拟器时,会首先弹出Simulator Config 对话框. 设置好 Latitude(纬度) 和 Longitude(经度) 的值. SN 代表连接到PC模拟器上的飞机序列号. 如果 SN 的值为空, 请检查你的飞机是否和PC连接好,或者你的飞机的固件版本是否属于开发版本.
Note :
在Log Settings选项中勾选show log window , 可以弹出log调试界面:
2 . 通过Micro-USB线将你的飞机连接到PC上, 启动遥控器然后启动飞机。 点击Display Simulator , 会出现以下界面(它会显示你正在使用的飞机). 当前使用的是Inspire 1:
注意 :
使用模拟器时不需要连接 DJI Pilot App.
3 . 点击 Start Simulation 开始模拟飞行。用户可以使用遥控器(档位拨到F档)和 SDK API 串口来控制飞行器的起飞、降落、飞行方向和返航。飞行状态栏 World X,Y,Z 分别表示北-南,东-西 和上-下方向。其中,北、东和上的数值为正值,南、西和下的数值为负值。
4 . 单击和拖曳鼠标可以调整观测飞行器的角度。滚动鼠标可以放大或缩小飞行视图。
注意 : 你还可以在模拟器上看到飞机的飞行轨迹线。右键点击模拟器对话框,选中setup ,如下所示:
然后勾选simulator UI Config选项卡下面的show trace ,以显示飞行轨迹,如下所示:
以下是效果图:
5 . 点击 Stop Simulation 结束模拟飞行。使用完毕后关闭飞行器、遥控器和模拟器。
重要 : 如果你想中途停止地面站任务, 你必须点击 Stop Simulation 来停止飞行任务, 否则在你下次启动地面站任务时,模拟器可能会继续执行上次的地面站任务。
要了解更多关于 DJI PC模拟器的内容, 请阅读 DJI PC 模拟器用户手册.pdf 文件, 你可以从下载的DJI PC模拟器包里面找到该文件。
升级飞机固件
确保你的飞机固件支持DJI Mobile SDK的开发,对于接下来的教程操作很重要. 请从这里下载beta版本的飞机固件: https://developer.dji.com/mobile-sdk/downloads :
Phantom 3 Professional 固件升级包
一般不必升级遥控器的固件, 只需把下载好的 bin 文件放到SD卡中, 把SD卡插到飞机的相机上面, 然后重启飞机升级即可. 升级过程可能会花10 ~ 30分钟的时间.
对于Inspire 1, 你可以通过检查飞机发出的声音来判断升级状态:
对于Phantom 3系列, 你可以检查云台上面的指示灯来判断升级状态:
根据你目前使用的飞机, 你可以通过检查升级过程生成的 txt 文件来获取固件升级结果。 对于Phantom 3 Professional, txt文件的名字为 "P3X_FW_RESULT_AB.txt" . 对于Inspire 1, txt文件的名字为 "WM610_FW_RESULT_AB.txt" . 以下是 txt 文件内容例子:
创建Map View
1. 导入Framework 和 库文件
你已经完成了以上步骤, 可以开始开发应用程序了. 在之前的教程创建航拍相机App , 你学会了如何导入DJI Mobile SDK到你的Xcode工程中,并进行激活操作。如果你还没有阅读, 请先看下再进行以下操作。
1 . 在Xcode中创建一个工程并命名为 "GSDemo ", 复制 DJISDK.framework 到你的 Xcode 工程文件夹中. 然后, 选择工程,并进入 Build Phases -> Link Binary With Libraries. 点击底部的 "+" 按钮添加以下两个库文件到工程中: libstdc++.6.0.9.dylib 和 libz.dylib. 以下是截图:
2. 创建Map View
接着, 我们删除 ViewController.h 和 ViewController.m 文件, 他们在你创建Xcode工程时,自动帮你生成了. 然后, 创建一个名字为"DJIRootViewController"的viewController,并在Main.storyboard 中将它设置为 Root View Controller. 接着, 从Object Library中拖一个 MKMapView 到 DJIRootViewController 中, 设置它的AutoLayout constraints, 以及它的delegate到DJIRootViewController , 如下图所示:
紧接着, 打开DJIRootViewController.h 文件, 为MKMapView创建一个IBOutlet, 命名为 "mapView", 然后把它连接到Main.storyboard 的“MKMapView”. 导入以下头文件并实现MKMapView的delegate方法:
Copy #import <DJISDK/DJISDK.h>
#import <MapKit/MapKit.h>
@interface DJIRootViewController : UIViewController<MKMapViewDelegate>
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end
现在, 编译运行下工程. 如果一切正常, 你会看到以下截屏画面:
3. 添加Annotations到MapView中
目前的map view比较简单. 我们给它加点有趣的东西吧. 新建一个NSObject 文件并命名为"DJIMapController", 它会被用来处理地图上的MKAnnotations(对于本教程来说,是预设航点设置)逻辑. 打开 DJIMapController.h 文件,然后添加以下代码:
Copy #import <UIKit/UIKit.h>
#import <MapKit/MapKit.h>
@interface DJIMapController : NSObject
@property (strong, nonatomic) NSMutableArray *editPoints;
/**
* Add Waypoints in Map View
*/
- (void)addPoint:(CGPoint)point withMapView:(MKMapView *)mapView;
/**
* Clean All Waypoints in Map View
*/
- (void)cleanAllPointsWithMapView:(MKMapView *)mapView;
/**
* Current Edit Points
*
* @return Return an NSArray contains multiple CCLocation objects
*/
- (NSArray *)wayPoints;
@end
这里, 我们创建了一个叫做"editPoints"的NSMutableArray来存放waypoint对象。并且添加了两个方法,用于添加和删除waypoint. 最后的方法是用来返回添加到当前地图上的waypoint对象.
来到DJIMapController.m 文件,并用以下代码替换掉原来的内容:
Copy #import "DJIMapController.h"
@implementation DJIMapController
- (instancetype)init
{
if (self = [super init]) {
self.editPoints = [[NSMutableArray alloc] init];
}
return self;
}
- (void)addPoint:(CGPoint)point withMapView:(MKMapView *)mapView
{
CLLocationCoordinate2D coordinate = [mapView convertPoint:point toCoordinateFromView:mapView];
CLLocation *location = [[CLLocation alloc] initWithLatitude:coordinate.latitude longitude:coordinate.longitude];
[_editPoints addObject:location];
MKPointAnnotation* annotation = [[MKPointAnnotation alloc] init];
annotation.coordinate = location.coordinate;
[mapView addAnnotation:annotation];
}
- (void)cleanAllPointsWithMapView:(MKMapView *)mapView
{
[_editPoints removeAllObjects];
NSArray* annos = [NSArray arrayWithArray:mapView.annotations];
for (int i = 0; i < annos.count; i++) {
id<MKAnnotation> ann = [annos objectAtIndex:i];
[mapView removeAnnotation:ann];
}
}
- (NSArray *)wayPoints
{
return self.editPoints;
}
@end
首先, 我们在init方法中初始化editPoints 数组对象, 然后从CGPoint创建 MKPointAnnotation 对象并把它添加到我们的 mapView 上, 最后实现 cleanAllPointsWithMapView 方法来清空eidtPoints数组,以及mapview上的annotations对象.
回到DJIRootViewController.h 文件, 导入DJIMapController.h头文件, 然后创建一个叫“mapController”的DJIMapController 属性. 因为我们希望通过点击地图来添加航点标注钉, 我们还需要创建一个叫"tapGesture"的 UITapGestureRecognizer 属性. 最后, 添加一个 UIButton 到 Main.storyboard 的 DJIRootViewController 视图中, 设置它的IBOutlet名字为 "editBtn ", 然后为它添加一个名为"editBtnAction "的IBAction方法, 如下所示:
Copy @property (nonatomic, strong) DJIMapController *mapController;
@property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
@property (weak, nonatomic) IBOutlet UIButton *editBtn;
- (IBAction)editBtnAction:(id)sender;
完成以上步骤后, 打开DJIMapController.m文件, 初始化mapController 和 tapGesture 变量, 然后添加 tapGesture 到 mapView 上用于航点添加手势. 进一步的, 我们需要一个命名为"isEditingPoints "的布尔值去存储编辑 waypoint 的状态, 它也会用来判断修改editBtn 的标题. 最后, 实现 tapGesture 的action方法 addWayPoints , 如下图所示:
Copy #import "DJIRootViewController.h"
@interface DJIRootViewController ()
@property (nonatomic, assign)BOOL isEditingPoints;
@end
@implementation DJIRootViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.mapController = [[DJIMapController alloc] init];
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addWaypoints:)];
[self.mapView addGestureRecognizer:self.tapGesture];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registerAppSuccess:) name:@"RegisterAppSuccess" object:nil];
}
#pragma mark Custom Methods
- (void)registerAppSuccess:(NSNotification *)notification
{
}
- (void)addWaypoints:(UITapGestureRecognizer *)tapGesture
{
CGPoint point = [tapGesture locationInView:self.mapView];
if(tapGesture.state == UIGestureRecognizerStateEnded){
if (self.isEditingPoints) {
[self.mapController addPoint:point withMapView:self.mapView];
}
}
}
- (IBAction)editBtnAction:(id)sender {
if (self.isEditingPoints) {
[self.mapController cleanAllPointsWithMapView:self.mapView];
[self.editBtn setTitle:@"Edit" forState:UIControlStateNormal];
}else
{
[self.editBtn setTitle:@"Reset" forState:UIControlStateNormal];
}
self.isEditingPoints = !self.isEditingPoints;
}
#pragma mark MKMapViewDelegate Method
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKPointAnnotation class]]) {
MKPinAnnotationView* pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Pin_Annotation"];
pinView.pinColor = MKPinAnnotationColorPurple;
return pinView;
}
return nil;
}
在以上代码中, 我们添加了一个NSNotification通知监听器去检查DJI Mobile SDK的注册状态, 确保app注册成功. 同时, 我们通过调用DJIMapController 的
Copy - (void)addPoint:(CGPoint)point withMapView:(MKMapView *)mapView
方法来实现addWaypoints gesture action 以添加 waypoints 到map上. 接着, 我们实现了IBAction方法editBtn , 更新按钮的标题,并且根据isEditingPoints 的布尔值去清空waypoints. 最后, 我们实现 MKMapViewDelegate的方法来修改pin(航点钉)的颜色为紫色.
完成以上步骤后, 编译运行工程,尝试通过点击屏幕添加航点到地图上. 如果一切顺利, 你会看到以下动画:
4. 定位MKMapView到特定位置
你也许发现了地图的位置和你当前的坐标位置不一致,而且很难找到你在地图上的当前位置。如果有一个功能,能让我们迅速定位到你在地图上的当前位置,app会人性化很多。 要实现该功能,我们需要使用CLLocationManager .
打开DJIRootViewController.h 文件并且导入CoreLocation的头文件. 创建一个命名为"locationManager"的 CLLocationManager 属性. 然后创建一个命名为"userLocation"的CLLocationCoordinate2D 的属性来存储用户当前的坐标位置信息. 接着, 在类里面实现CLLocationManager的 CLLocationManagerDelegate 协议, 如下图所示:
Copy #import <DJISDK/DJISDK.h>
#import <MapKit/MapKit.h>
#import <CoreLocation/CoreLocation.h>
@interface DJIRootViewController : UIViewController<MKMapViewDelegate, CLLocationManagerDelegate>
@property (nonatomic, weak) IBOutlet MKMapView *mapView;
@property (nonatomic, strong) CLLocationManager* locationManager;
@property (nonatomic, assign) CLLocationCoordinate2D userLocation;
@property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
@property (nonatomic, weak) IBOutlet UIButton *editBtn;
- (IBAction)editBtnAction:(id)sender;
- (IBAction)focusMapAction:(id)sender;
@end
这里我们在Main.storyboard 的DJIRootViewController中添加了一个命名为"Focus Map"的UIButton, 并且添加了一个叫"focusMapAction"的IBAction方法. 以下是Main.storyboard 的截图:
完成以上步骤后, 打开DJIRootViewController.m 文件并添加以下代码:
Copy - (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self startUpdateLocation];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.locationManager stopUpdatingLocation];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.userLocation = kCLLocationCoordinate2DInvalid;
self.mapController = [[DJIMapController alloc] init];
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addWaypoints:)];
[self.mapView addGestureRecognizer:self.tapGesture];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(registerAppSuccess:) name:@"RegisterAppSuccess" object:nil];
}
- (BOOL)prefersStatusBarHidden {
return NO;
}
#pragma mark CLLocation Methods
-(void) startUpdateLocation
{
if ([CLLocationManager locationServicesEnabled]) {
if (self.locationManager == nil) {
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
self.locationManager.distanceFilter = 0.1;
if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)]) {
[self.locationManager requestAlwaysAuthorization];
}
[self.locationManager startUpdatingLocation];
}
}else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Location Service is not available" message:@"" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
}
}
- (IBAction)focusMapAction:(id)sender {
{
if (CLLocationCoordinate2DIsValid(self.userLocation)) {
MKCoordinateRegion region = {0};
region.center = self.userLocation;
region.span.latitudeDelta = 0.001;
region.span.longitudeDelta = 0.001;
[self.mapView setRegion:region animated:YES];
}
}
#pragma mark - CLLocationManagerDelegate
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
CLLocation* location = [locations lastObject];
self.userLocation = location.coordinate;
}
首先, 我们在viewDidLoad方法中初始化 userLocation 的值为 kCLLocationCoordinate2DInvalid. 然后我们添加了一个新的方法 startUpdateLocation 来初始化 locationManger , 设置好它的参数值并且开始更新位置信息. 如果app的定位服务不可用, 我们可以添加一个UIAlertView来展示警告信息. 在viewWillAppear 方法中调用startUpdateLocation ,然后在 viewWillDisappear方法中停止它. 进一步的, 我们需要实现 CLLocationManagerDelegate 方法来更新 userLocation 属性. 最后, 我们实现 focusMapAction 方法来定位地图到用户当前的坐标位置.
在iOS8中, 我们必须首先调用 locationManager的 requestAlwaysAuthorization 方法, 我们在startUpdateLocation 方法中实现了该调用. 接着, 在工程的Info.plist中添加 NSLocationAlwaysUsageDescription 或者 NSLocationWhenInUseUsageDescription key, 你可以对这两个key设置具体的值。当出现提示信息,询问你是否允许应用程序使用你当前的地理位置信息时,你对这两个key设置的值也会出现。这里我们设置两个key的值为空:
编译运行工程,检查下地图定位的功能. 当你第一次启动app时, app会弹出一个对话框,询问是否允许访问你的地理位置信息. 选择允许 然后点击 Focus Map 按钮. 如果效果和以下动画一样,地图自动定位到你当前的位置, 恭喜, 你已经完成了Focus Map功能!
5. 在地图上显示飞机
现在, 我们可以定位地图到我们当前的位置, 这是一个好的开端! 让我们来做一些更有趣的事情吧. 我们将用DJI PC 模拟器来模拟飞机的GPS位置并将其显示在地图上.
你可能已经知道如何设置DJI PC 模拟器以及它的基本使用方法. 如果你想要将飞机位置设置为你当前的GPS位置,你可以在Simulator Config 里将经纬度改成你当前的经纬度. 在接下来的例子中,我们将使用模拟器的初始数值.
我们回到代码. 创建一个MKAnnotationView 的新子类名为"DJIAircraftAnnotationView"和一个NSObject 的新子类名为DJIAircraftAnnotation . 下面是代码:
DJIAircraftAnnotationView.h
Copy #import <MapKit/MapKit.h>
@interface DJIAircraftAnnotationView : MKAnnotationView
-(void) updateHeading:(float)heading;
@end
DJIAircraftAnnotationView.m
Copy #import "DJIAircraftAnnotationView.h"
@implementation DJIAircraftAnnotationView
- (instancetype)initWithAnnotation:(id <MKAnnotation>)annotation reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithAnnotation:annotation reuseIdentifier:reuseIdentifier];
if (self) {
self.enabled = NO;
self.draggable = NO;
self.image = [UIImage imageNamed:@"aircraft.png"];
}
return self;
}
-(void) updateHeading:(float)heading
{
self.transform = CGAffineTransformIdentity;
self.transform = CGAffineTransformMakeRotation(heading);
}
@end
在上面的代码中,我们为飞机创造了一个MKAnnotationView, 加入了一个名为updateHeading 的方法来改变飞机的头部方向, 并在init方法里设置它的图片为 "aircraft.png "(你可以在此教程的demo里获取图片) . 同时我们禁用了DJIAircraftAnnotationView'的draggable属性. 看看下面的代码:
Copy #import <MapKit/MapKit.h>
#import "DJIAircraftAnnotationView.h"
@interface DJIAircraftAnnotation : NSObject<MKAnnotation>
@property(nonatomic, readonly) CLLocationCoordinate2D coordinate;
@property(nonatomic, weak) DJIAircraftAnnotationView* annotationView;
-(id) initWithCoordiante:(CLLocationCoordinate2D)coordinate;
-(void)setCoordinate:(CLLocationCoordinate2D)newCoordinate;
-(void) updateHeading:(float)heading;
@end
Copy #import "DJIAircraftAnnotation.h"
@implementation DJIAircraftAnnotation
-(id) initWithCoordiante:(CLLocationCoordinate2D)coordinate
{
self = [super init];
if (self) {
_coordinate = coordinate;
}
return self;
}
- (void)setCoordinate:(CLLocationCoordinate2D)newCoordinate
{
_coordinate = newCoordinate;
}
-(void)updateHeading:(float)heading
{
if (self.annotationView) {
[self.annotationView updateHeading:heading];
}
}
@end
DJIAircraftAnnotation 类实现了 MKAnnotation 协议. 它被用来储存并更新CLLocationCoordinate2D属性. 同时,我们用updateHeading 来改变DJIAircraftAnnotationView的方向.
当你完成以上操作后,打开 DJIMapController.h 文件并导入 DJIAircraftAnnotation.h 文件:
Copy #import "DJIAircraftAnnotation.h"
然后创建一个DJIAircraftAnnotation 的属性,命名为aircraftAnnotation .
Copy @property (nonatomic, strong) DJIAircraftAnnotation* aircraftAnnotation;
进一步的, 添加以下两个新方法来更新飞机在地图上的位置和头部转向:
Copy /**
* Update Aircraft's location in Map View
*/
-(void)updateAircraftLocation:(CLLocationCoordinate2D)location withMapView:(MKMapView *)mapView;
/**
* Update Aircraft's heading in Map View
*/
-(void)updateAircraftHeading:(float)heading;
接下来,回到 DJIMapController.m 文件并实现我们刚加入的两个方法:
Copy -(void)updateAircraftLocation:(CLLocationCoordinate2D)location withMapView:(MKMapView *)mapView
{
if (self.aircraftAnnotation == nil) {
self.aircraftAnnotation = [[DJIAircraftAnnotation alloc] initWithCoordiante:location];
[mapView addAnnotation:self.aircraftAnnotation];
}
[self.aircraftAnnotation setCoordinate:location];
}
-(void)updateAircraftHeading:(float)heading
{
if (self.aircraftAnnotation) {
[self.aircraftAnnotation updateHeading:heading];
}
}
这里,因为我们不希望DJIMapController.m文件里的cleanAllPointsWithMapView 方法将aircraftAnnotation (它代表在地图上面的飞机)删除掉,我们需要修改它,如下所示:
Copy - (void)cleanAllPointsWithMapView:(MKMapView *)mapView
{
[_editPoints removeAllObjects];
NSArray* annos = [NSArray arrayWithArray:mapView.annotations];
for (int i = 0; i < annos.count; i++) {
id<MKAnnotation> ann = [annos objectAtIndex:i];
if (![ann isEqual:self.aircraftAnnotation]) {
[mapView removeAnnotation:ann];
}
}
}
我们加入一个if语句来检查地图的annotation是否等于aircraftAnnotation 属性, 如果不等于,我们移除它. 这样做,可以防止飞机的annotation被移除.
为了提供一个更好的用户体验,我们需要在地图的上方加入一个信息栏来显示飞机当前的状态:当前的GPS卫星数,垂直飞行速度,水平飞行速度以及飞机的高度. 让我们在Main.storyboard* 的 RootViewController Scene画面里加入UI,如下所示:
当以上任务完成时。打开 DJIRootViewController.h 文件, 为以上UI元素创建IBOutlets, 导入DJISDK的头文件并且实现 DJIDroneDelegate , DJIMainControllerDelegate , DJIAppManagerDelegate 和 DJINavigationDelegate 协议. 我们在demo里使用的是Inspire 1,所以我们需要创建三个属性,一个是 DJIDrone 的属性,一个是DJIInspireMainController 的属性,另外一个是 NSObject 的属性. 同时我们需要创建一个名为 “droneLocation”的CLLocationCoordinate2D 属性来记录飞机的位置, 如下所示:
Copy #import <DJISDK/DJISDK.h>
@interface DJIRootViewController : UIViewController<MKMapViewDelegate, CLLocationManagerDelegate, DJIDroneDelegate, DJIMainControllerDelegate, DJIAppManagerDelegate>
@property(nonatomic, strong) IBOutlet UILabel* modeLabel;
@property(nonatomic, strong) IBOutlet UILabel* gpsLabel;
@property(nonatomic, strong) IBOutlet UILabel* hsLabel;
@property(nonatomic, strong) IBOutlet UILabel* vsLabel;
@property(nonatomic, strong) IBOutlet UILabel* altitudeLabel;
@property(nonatomic, strong) DJIDrone* inspireDrone;
@property(nonatomic, strong) DJIInspireMainController* inspireMainController;
@property(nonatomic, weak) NSObject<DJINavigation>* navigationManager;
@property(nonatomic, assign) CLLocationCoordinate2D droneLocation;
现在,我们来到 DJIRootViewController.m 文件并在initUI 方法里初始化UI元素的值. 同时加入一个 initDrone 新方法来初始化 inspireDrone 属性. 在viewDidLoad 方法里运行 initUI 以及 initDrone 方法. 最后,创建一个新方法"registerApp", 并且在initDrone方法中调用它来注册app,如下所示:
Copy -(void) initUI
{
self.modeLabel.text = @"N/A";
self.gpsLabel.text = @"0";
self.vsLabel.text = @"0.0 M/S";
self.hsLabel.text = @"0.0 M/S";
self.altitudeLabel.text = @"0 M";
}
- (void)initDrone
{
self.inspireDrone = [[DJIDrone alloc] initWithType:DJIDrone_Inspire];
self.inspireDrone.delegate = self;
self.navigationManager = self.inspireDrone.mainController.navigationManager;
self.navigationManager.delegate = self;
self.inspireMainController = (DJIInspireMainController*)self.inspireDrone.mainController;
self.inspireMainController.mcDelegate = self;
[self registerApp];
}
- (void)registerApp
{
NSString *appKey = @"Enter Your App Key Here";
[DJIAppManager registerApp:appKey withDelegate:self];
}
在 initDrone 方法中, 我们初始化了inspireDrone 和 navigationManager 属性,然后设置它们的delegate为self. 接着,初始化inspireMainController 属性,并设置它的mcDelegate为self. DJIInspireMainController 是DJIMainController的子类,它是控制飞机的一个mainController, 可以在起飞,降落等情况下获取DJIMCSystemState. 你可以在SDK中查看它的头文件获取更多信息。
进一步的,我们来重构 ViewDidLoad 方法,如下所示:
Copy - (void)viewDidLoad {
[super viewDidLoad];
[self initUI];
[self initData];
[self initDrone];
}
-(void)initData
{
self.userLocation = kCLLocationCoordinate2DInvalid;
self.droneLocation = kCLLocationCoordinate2DInvalid;
self.mapController = [[DJIMapController alloc] init];
self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(addWaypoints:)];
[self.mapView addGestureRecognizer:self.tapGesture];
}
接着,实现"DJIAppManagerDelegate"方法:
Copy #pragma mark DJIAppManagerDelegate Method
-(void)appManagerDidRegisterWithError:(int)error
{
NSString* message = @"Register App Successed!";
if (error != RegisterSuccess) {
message = @"Register App Failed! Please enter your App Key and check the network.";
}else
{
[self.inspireDrone connectToDrone];
[self.inspireDrone.mainController startUpdateMCSystemState];
}
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Register App" message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alertView show];
}
在以上方法中,当app成功注册时,我们可以运行 connectToDrone 方法来连接飞机, 并且运行 startUpdateMCSystemState 方法来更新飞机的系统状态, 这些信息会被用来更新我们飞机的位置及方向. 更进一步,在 viewWillDisappear 方法里, 我们需要中断与飞机的连接,并且销毁drone对象。另外,我们还需要调用inspireDrone的mainController的"stopUpdateMCSystemState"方法,去停止更新DJIMainController的system state, 如下所示:
Copy - (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.locationManager stopUpdatingLocation];
[self.inspireDrone.mainController stopUpdateMCSystemState];
[self.inspireDrone disconnectToDrone];
}
在viewDidLoad 方法里, 把 droneLocation 属性的值改为 kCLLocationCoordinate2DInvalid.
Copy self.droneLocation = kCLLocationCoordinate2DInvalid;
同时,更新 focusMapAction 方法来设置 droneLocation 为地图的中心区域,如下所示:
Copy - (IBAction)focusMapAction:(id)sender {
if (CLLocationCoordinate2DIsValid(self.droneLocation)) {
MKCoordinateRegion region = {0};
region.center = self.droneLocation;
region.span.latitudeDelta = 0.001;
region.span.longitudeDelta = 0.001;
[self.mapView setRegion:region animated:YES];
}
}
接下来,我们需要将 MKMapViewDelegate 方法修改成下面的样子. 它会检查 annotation 变量的类并将它的annotationView属性设置为一个 DJIAircraftAnnotationView 类的实例:
Copy - (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
if ([annotation isKindOfClass:[MKPointAnnotation class]]) {
MKPinAnnotationView* pinView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Pin_Annotation"];
pinView.pinColor = MKPinAnnotationColorPurple;
return pinView;
}else if ([annotation isKindOfClass:[DJIAircraftAnnotation class]])
{
DJIAircraftAnnotationView* annoView = [[DJIAircraftAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:@"Aircraft_Annotation"];
((DJIAircraftAnnotation*)annotation).annotationView = annoView;
return annoView;
}
return nil;
}
接着,我们来实现 DJIMainControllerDelegate 方法:
Copy #pragma mark - DJIMainControllerDelegate Method
-(void) mainController:(DJIMainController*)mc didUpdateSystemState:(DJIMCSystemState*)state
{
self.droneLocation = state.droneLocation;
if (!state.isMultipleFlightModeOpen) {
[self.inspireMainController setMultipleFlightModeOpen:YES withResult:nil];
}
self.modeLabel.text = state.flightModeString;
self.gpsLabel.text = [NSString stringWithFormat:@"%d", state.satelliteCount];
self.vsLabel.text = [NSString stringWithFormat:@"%0.1f M/S",state.velocityZ];
self.hsLabel.text = [NSString stringWithFormat:@"%0.1f M/S",(sqrtf(state.velocityX*state.velocityX + state.velocityY*state.velocityY))];
self.altitudeLabel.text = [NSString stringWithFormat:@"%0.1f M",state.altitude];
[self.mapController updateAircraftLocation:self.droneLocation withMapView:self.mapView];
double radianYaw = (state.attitude.yaw * M_PI / 180.0);
[self.mapController updateAircraftHeading:radianYaw];
}
首先,他会更新 droneLocation ,将其变为飞机当前的位置. 然后它会打开inspireMainController的multipleFlightMode.
接下来,更新DJIMCSystemState的状态Label的文字. 用DJIMapController 里的相关方法来更新飞机的位置以及方向.
最后,实现DJIDroneDelegate 方法,如下所示:
Copy
- (void)enterNavigationMode
{
[self.navigationManager enterNavigationModeWithResult:^(DJIError *error) {
if (error.errorCode != ERR_Succeeded) {
NSString* message = [NSString stringWithFormat:@"Enter navigation mode failed:%@", error.errorDescription];
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Enter Navigation Mode" message:message delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil];
alertView.tag = kEnterNaviModeFailedAlertTag;
[alertView show];
}else
{
NSString* message = @"Enter navigation mode Success";
UIAlertView* alertView = [[UIAlertView alloc] initWithTitle:@"Enter Navigation Mode" message:message delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alertView show];
}
}];
}
#pragma mark - DJIDroneDelegate Method
-(void) droneOnConnectionStatusChanged:(DJIConnectionStatus)status
{
if (status == ConnectionSucceeded) {
[self enterNavigationMode];
}
}
如果它成功连接上飞机,运行 DJIInspireMainController 的 enterNavigationModeWithResult 方法来检查飞机是否成功进入地面站模式. 如果没有成功,弹出UIAlertView提醒用户. 这里我们实现UIAlertView的delegate方法,如下所示:
Copy #pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (alertView.tag == kEnterNaviModeFailedAlertTag) {
if (buttonIndex == 1) {
[self enterNavigationMode];
}
}
}
现在我们测试下这个应用吧! 编译运行工程,将此app安装到你的移动设备上。 然后,用Micro-USB连接线将你的飞机连接上装有Windows系统的PC或者虚拟机, 并且启动飞机和遥控器. 按下Display Simulator按钮. 如果你喜欢,可以在Simulator Config里输入你当前位置的经纬度.
然后运行app,并且将你的移动设备连接到你的遥控器上. 以下是截图画面:
重要事项 : 为了解决以上提醒框的问题,将遥控器上的模式到 F 位置上 (在之前的版本中,模式在A位置上) 并且按下 Retry 按钮. 如果当自动巡航开始时模式选择已在F位置,用户需要拨动其他位置再调回 F 位置,然后按下 Retry 按键.
在DJI Mobile SDK里使用地面站,热点环绕,以及JoyStick功能都必须将模式调到 F 位置.
接着,打开PC上的DJI PC 模拟器并按下 Start Simulation 按键. 如果你现在查看app,一架小型红色飞机将会在地图上显示:
如果你无法找到飞机,按下 "Focus Map " 按钮,然后地图会以飞机为中心放大定位显示,如下动画所示:
现在,如果你在Simulator Config里按下 Stop Simulation ,飞机会从地图上消失,因为模拟器停止向飞机传送GPS模拟信息.
重构UI
如你所见,目前的代码结构比较简单,不够健壮. 为了便于本教程代码工程的继续开发,这里需要重构下代码,而且加入更多的UI元素.
1. 添加并设置新的UIButton
首先,我们新建一个的名为DJIGSButtonController 的UIViewController 的子类. 确保你在创建文件时勾选上 Also create XIB file . 然后打开 DJIGSButtonController.xib 文件,并在Simulated Metrics部分的Size 选项中选择 Freeform .在View部分,调整宽度为 110 , 高为 260 . 请看下面的效果图:
接下来,拖动八个按钮到DJIGSButtonViewController.xib 上,并将它们的名字改为 Edit , Back , Clear , Focus Map , Start , Stop , Add 和 Config . Edit覆盖在Back上面, Focus Map覆盖在Add上面. 并且确保 Back , Clear , Start , Add 和 Config 按钮是隐藏状态.
然后在DJIGSButtonViewController.h 文件里为八个按钮添加IBOutlets 和 IBActions. 同时,我们将加入一个名为 DJIGSViewMode 的枚举值,用来记录app的两种工作模式(View和Edit). 接着,我们添加几个delegate方法,用于响应IBAction的方法调用. 最后,加入一个 - (void)switchToMode:(DJIGSViewMode)mode inGSButtonVC:(DJIGSButtonViewController *)GSBtnVC; 方法,当DJIGSViewMode的值改变时,更新多个按钮的状态. 请看下面的代码:
Copy #import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, DJIGSViewMode) {
DJIGSViewMode_ViewMode,
DJIGSViewMode_EditMode,
};
@class DJIGSButtonViewController;
@protocol DJIGSButtonViewControllerDelegate <NSObject>
- (void)stopBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)clearBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)focusMapBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)startBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)addBtn:(UIButton *)button withActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)configBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
- (void)switchToMode:(DJIGSViewMode)mode inGSButtonVC:(DJIGSButtonViewController *)GSBtnVC;
@end
@interface DJIGSButtonViewController : UIViewController
@property (weak, nonatomic) IBOutlet UIButton *backBtn;
@property (weak, nonatomic) IBOutlet UIButton *stopBtn;
@property (weak, nonatomic) IBOutlet UIButton *clearBtn;
@property (weak, nonatomic) IBOutlet UIButton *focusMapBtn;
@property (weak, nonatomic) IBOutlet UIButton *editBtn;
@property (weak, nonatomic) IBOutlet UIButton *startBtn;
@property (weak, nonatomic) IBOutlet UIButton *addBtn;
@property (weak, nonatomic) IBOutlet UIButton *configBtn;
@property (assign, nonatomic) DJIGSViewMode mode;
@property (weak, nonatomic) id <DJIGSButtonViewControllerDelegate> delegate;
- (IBAction)backBtnAction:(id)sender;
- (IBAction)stopBtnAction:(id)sender;
- (IBAction)clearBtnAction:(id)sender;
- (IBAction)focusMapBtnAction:(id)sender;
- (IBAction)editBtnAction:(id)sender;
- (IBAction)startBtnAction:(id)sender;
- (IBAction)addBtnAction:(id)sender;
- (IBAction)configBtnAction:(id)sender;
@end
当你完成后,打开 DJIGSButtonViewController.m 文件,替换已有的代码为以下代码:
Copy #import "DJIGSButtonViewController.h"
@implementation DJIGSButtonViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setMode:DJIGSViewMode_ViewMode];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - Property Method
- (void)setMode:(DJIGSViewMode)mode
{
_mode = mode;
[_editBtn setHidden:(mode == DJIGSViewMode_EditMode)];
[_focusMapBtn setHidden:(mode == DJIGSViewMode_EditMode)];
[_backBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
[_clearBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
[_startBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
[_stopBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
[_addBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
[_configBtn setHidden:(mode == DJIGSViewMode_ViewMode)];
}
#pragma mark - IBAction Methods
- (IBAction)backBtnAction:(id)sender {
[self setMode:DJIGSViewMode_ViewMode];
if ([_delegate respondsToSelector:@selector(switchToMode:inGSButtonVC:)]) {
[_delegate switchToMode:self.mode inGSButtonVC:self];
}
}
- (IBAction)stopBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(stopBtnActionInGSButtonVC:)]) {
[_delegate stopBtnActionInGSButtonVC:self];
}
}
- (IBAction)clearBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(clearBtnActionInGSButtonVC:)]) {
[_delegate clearBtnActionInGSButtonVC:self];
}
}
- (IBAction)focusMapBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(focusMapBtnActionInGSButtonVC:)]) {
[_delegate focusMapBtnActionInGSButtonVC:self];
}
}
- (IBAction)editBtnAction:(id)sender {
[self setMode:DJIGSViewMode_EditMode];
if ([_delegate respondsToSelector:@selector(switchToMode:inGSButtonVC:)]) {
[_delegate switchToMode:self.mode inGSButtonVC:self];
}
}
- (IBAction)startBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(startBtnActionInGSButtonVC:)]) {
[_delegate startBtnActionInGSButtonVC:self];
}
}
- (IBAction)addBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(addBtn:withActionInGSButtonVC:)]) {
[_delegate addBtn:self.addBtn withActionInGSButtonVC:self];
}
}
- (IBAction)configBtnAction:(id)sender {
if ([_delegate respondsToSelector:@selector(configBtnActionInGSButtonVC:)]) {
[_delegate configBtnActionInGSButtonVC:self];
}
}
@end
这些改变使得代码架构变得更加简洁与健壮,这将会方便以后的维护.
现在,我们回到 DJIRootViewController.h 文件并删除 editButton IBOutlet, resetPointsAction 方法, 以及 focusMapAction 方法. 删除这些以后,创建一个名为 "topBarView" 的UIView IBOutlet并将其连接到 Main.storyboard 的 RootViewController的对应View上, 如下所示:
然后打开 DJIRootViewController.m 文件, 导入DJIGSButtonViewController.h 头文件. 接着创建一个DJIGSButtonViewController类型的属性并命名为 gsButtonVC ,然后在类里实现 DJIGSButtonViewController的 DJIGSButtonViewControllerDelegate 协议:
Copy #import "DJIRootViewController.h"
#import "DJIGSButtonViewController.h"
#define kEnterNaviModeFailedAlertTag 1001
@interface DJIRootViewController ()<DJIGSButtonViewControllerDelegate>
@property (nonatomic, assign)BOOL isEditingPoints;
@property (nonatomic, strong)DJIGSButtonViewController *gsButtonVC;
@end
下一步,初始化initUI 方法里的 gsButtonVC 属性并将原来的 focusMapAction 方法中的代码移到一个名为 focusMap 的新方法里, 如下所示:
Copy self.gsButtonVC = [[DJIGSButtonViewController alloc] initWithNibName:@"DJIGSButtonViewController" bundle:[NSBundle mainBundle]];
[self.gsButtonVC.view setFrame:CGRectMake(0, self.topBarView.frame.origin.y + self.topBarView.frame.size.height, self.gsButtonVC.view.frame.size.width, self.gsButtonVC.view.frame.size.height)];
self.gsButtonVC.delegate = self;
[self.view addSubview:self.gsButtonVC.view];
Copy - (void)focusMap
{
if (CLLocationCoordinate2DIsValid(self.droneLocation)) {
MKCoordinateRegion region = {0};
region.center = self.droneLocation;
region.span.latitudeDelta = 0.001;
region.span.longitudeDelta = 0.001;
[self.mapView setRegion:region animated:YES];
}
}
最后,实现 DJIGSButtonViewController 的 delegate 方法, 如下所示:
Copy #pragma mark - DJIGSButtonViewController Delegate Methods
- (void)stopBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
}
- (void)clearBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
[self.mapController cleanAllPointsWithMapView:self.mapView];
}
- (void)focusMapBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
[self focusMap];
}
- (void)configBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
}
- (void)startBtnActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
}
- (void)switchToMode:(DJIGSViewMode)mode inGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
if (mode == DJIGSViewMode_EditMode) {
[self focusMap];
}
}
- (void)addBtn:(UIButton *)button withActionInGSButtonVC:(DJIGSButtonViewController *)GSBtnVC
{
if (self.isEditingPoints) {
self.isEditingPoints = NO;
[button setTitle:@"Add" forState:UIControlStateNormal];
}else
{
self.isEditingPoints = YES;
[button setTitle:@"Finished" forState:UIControlStateNormal];
}
}
在 - (void)switchToMode:(DJIGSViewMode)mode inGSButtonVC:(DJIGSButtonViewController *)GSBtnVC delegate方法里, 我们调用 focusMap 方法. 进行这样的操作,我们可以实现在按下edit按钮时,将地图定位到飞机的位置,省去用户每次都需要放大地图才能编辑的麻烦,提升用户体验. 同时,当应用在edit模式下, isEditingPoints 属性的值会被设置为 YES .
现在,我们编译并运行该工程,并尝试按下Edit 和 Back 按钮. 以下是当你按下按钮时的动画:
设置智能飞行任务
1. DJIWaypoint
我们先看下 DJIWaypoint.h 文件. 比如说,你可以使用:
Copy -(id) initWithCoordinate:(CLLocationCoordinate2D)coordinate;
来创建一个有特定坐标的waypoint对象. 当你创建了waypoint后,可以通过调用以下代码,向其添加一个DJIWaypointAction :
Copy -(BOOL) addWaypointAction:(DJIWaypointAction*)action;
更进一步的,有了waypoints,你可以设置坐标,高度,朝向甚至更多参数. 要了解更多细节,请查看DJIWaypoint.h 头文件.
2. DJIWaypointMission
当你想要上传,开始和停止一个GroundStation Waypoint 任务(Task)时,你需要使用DJIWaypointMission . 你可以使用下面的方法来添加 DJIWaypoint 类型的waypoint:
Copy -(void) addWaypoint:(DJIWaypoint*)waypoint;
相反地, 你也可以调用以下方法来删除任务中的waypoint:
Copy -(void) removeWaypoint:(DJIWaypoint*)waypoint;
更多的, 你可以设置类型为DJIWaypointMissionFinishedAction 枚举值的finishedAction 属性,来设置飞机完成任务后要进行的动作. 最后,你可以设置类型为DJIWaypointMissionHeadingMode 枚举值的 headingMode 属性来设置飞机执行任务时的机头朝向. 下面是一部分头文件内容:
Copy
typedef NS_ENUM(uint8_t, DJIWaypointMissionFinishedAction)
{
/**
* No action will be taken. The aircraft will exit the task and hover in the
* air where the task was completed. After that, the aircraft will be able
* to be controlled by the remote controller.
*/
DJIWaypointMissionFinishedNoAction,
/**
* The aicraft will go home.
*/
DJIWaypointMissionFinishedGoHome,
/**
* The aircraft will land automatically.
*/
DJIWaypointMissionFinishedAutoLand,
/**
* The aircraft will go back to its first waypoint.
*/
DJIWaypointMissionFinishedGoFirstWaypoint,
/**
* If the user attempts to pull the aircraft back along the flight path as the
* mission is being executed, the aircarft will move towards the previous waypoint
* and will continue to do so until there are no more waypoint to move back to or
* the user has stopped attempting to move the aircraft back. In the process of moving the
* aircraft back, if the user ever stops attempting to do so the aircraft will,
* automatically continue the mission until the end.