鍍金池/ 教程/ iOS/ 導(dǎo)航應(yīng)用
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

導(dǎo)航應(yīng)用

在這篇文章中,我們將把前面提到過的內(nèi)容組織起來構(gòu)成我們的導(dǎo)航器應(yīng)用,這個 iPhone 應(yīng)用將裝載在我們的的無人機(jī)上,你可以在 Github 下載應(yīng)用的源碼,盡管這個應(yīng)用是計劃在沒有直接的交互操作下來使用的,但在測試過程中我們做了一個簡單的 UI 界面來顯示其無人機(jī)狀態(tài)并方便我們手動操作。

概要

在我們的應(yīng)用中,我們有幾個類它們分別是:

  • DroneCommunicator 這個類關(guān)注于利用 UDP 和無人機(jī)通訊。這個話題全部在 Daniel 的文章中詳細(xì)介紹過

  • RemoteClient 使用 Multipeer Connectivity 技術(shù)和我們的遠(yuǎn)程客戶端進(jìn)行交互,具體客戶端的操作,請看 Florian 的文章。
  • Navigator 用來設(shè)定目標(biāo)位置,計算飛行航線,以及飛行距離。
  • DroneController 用來把從 Navigator 獲取的導(dǎo)航的距離和方向發(fā)送命令到DroneCommunicator
  • ViewController 有一個簡單的界面,用來初始化其他的類并把它們連接起來,這部分應(yīng)該用不同的類來完成,但是在我們的設(shè)想中,我們的app足夠簡單所以放到一個類就可以了。

View Controller

View Controller 中最重要的一個部分是初始化方法,在這里我們創(chuàng)建了 DroneCommunicator, Navigator, DroneController 以及RemoteClient 的實例化對象,換句話說:我們建立了無人機(jī)和我們的客戶端應(yīng)用溝通的整個橋梁。

- (void)setup
{
    self.communicator = [[DroneCommunicator alloc] init];
    [self.communicator setupDefaults];

    self.navigator = [[Navigator alloc] init];
    self.droneController = [[DroneController alloc] initWithCommunicator:self.communicator navigator:self.navigator];
    self.droneController.delegate = self;
    self.remoteClient = [[RemoteClient alloc] init];
    [self.remoteClient startBrowsing];
    self.remoteClient.delegate = self;
}

View Controller 同時是 RemoteClient 的委托。 這就說明無論我們的客戶端發(fā)送了一個新位置或者著陸,重置以及關(guān)機(jī)的命令,我們都需要在這里處理它。舉個例子,當(dāng)我們收到一個新的位置的命令的時候,我們這樣來做:

- (void)remoteClient:(RemoteClient *)client didReceiveTargetLocation:(CLLocation *)location
    {
        self.droneController.droneActivity = DroneActivityFlyToTarget;
        self.navigator.targetLocation = location;
    }

這段代碼是用來確保無人機(jī)開始飛行(而不是徘徊)并且更新目標(biāo)位置。

Navigator

導(dǎo)航類用來指定目標(biāo)位置,并且計算從當(dāng)前位置到目標(biāo)位置的距離,為了完成整個工作我們首先需要監(jiān)聽 core location 的改變:

- (void)startCoreLocation
{
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;

    self.locationManager.distanceFilter = kCLDistanceFilterNone;
    self.locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
    [self.locationManager startUpdatingLocation];
    [self.locationManager startUpdatingHeading];
}

在我們的導(dǎo)航類中,我們有兩種方向,絕對和相對方向,絕對方向是兩個地點之間的方向。比如說,阿姆斯特丹和柏林間的絕對方向幾乎處于同一緯度,相對位置則是我們在參考指南針后可以得出的路線方向,要從阿姆斯特丹一直向東到柏林,兩地之間的相對方向為零。在操作無人機(jī)的時候我們就需要使用相對方向。方向值為零,飛機(jī)直行;方向角度小于零,飛機(jī)向右傾斜轉(zhuǎn)彎;方向角度大于零,飛機(jī)則向左傾斜轉(zhuǎn)彎。

計算到目的地的絕對方向,我們需要創(chuàng)建一個基于 CLLocation 的Helper方法用來計算兩個點的方向:

- (OBJDirection *)directionToLocation:(CLLocation *)otherLocation;
{
    return [[OBJDirection alloc] initWithFromLocation:self toLocation:otherLocation];
}

由于我們的無人機(jī)只能飛很小的距離(電池只能支持10分鐘),所以我們需要一個幾何的假設(shè),我們是在一個平面而不是在地球表面:

- (double)heading;
{
    double y = self.toLocation.coordinate.longitude - self.fromLocation.coordinate.longitude;
    double x = self.toLocation.coordinate.latitude - self.fromLocation.coordinate.latitude;

    double degree = radiansToDegrees(atan2(y, x));
    return fmod(degree + 360., 360.);
}

在導(dǎo)航器中,我們將得到位置和航向的回調(diào),然后我們把這兩個值存到屬性中,比如,計算我們需要飛行的兩點之間的距離,我們需要將絕對航向減去當(dāng)前航向(這與你看到指南針上的值是一樣的意思),然后將結(jié)果換算到 -180 度和 180 度之間。如果你希望知道為什么我們要減去 90 度,那是因為我們 iPhone 和無人機(jī)之間有 90 度的夾角。

- (CLLocationDirection)directionDifferenceToTarget;
{
    CLLocationDirection result = (self.direction.heading - self.lastKnownSelfHeading.trueHeading - 90);
    // Make sure the result is in the range -180 -> 180
    result = fmod(result + 180. + 360., 360.) - 180.;
    return result;
}

這就是我們導(dǎo)航做的事情?;诋?dāng)前的位置和航向,計算出到目標(biāo)的距離和無人機(jī)應(yīng)當(dāng)飛行的方向。并且監(jiān)聽這兩個屬性。

Drone Controller

Drone controller 用來初始化 navigator 和 communicator,并且發(fā)送距離和方向的命令到無人機(jī),因為命令需要持續(xù)發(fā)送,所以我們創(chuàng)建一個計時器:

self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.25
                                                    target:self
                                                  selector:@selector(updateTimerFired:)
                                                  userInfo:nil
                                                   repeats:YES];

當(dāng)計時器觸發(fā)后,假設(shè)我們飛向一個目標(biāo),我們需要發(fā)送給無人機(jī)適當(dāng)?shù)闹噶?,如果我們足夠近,無人機(jī)盤旋,否則,我們轉(zhuǎn)向目標(biāo),在大致方向正確的情況下飛過去!

- (void)updateDroneCommands;
{
    if (self.navigator.distanceToTarget < 1) {
        self.droneActivity = DroneActivityHover;
    } else {
        static double const rotationSpeedScale = 0.01;
        self.communicator.rotationSpeed = self.navigator.directionDifferenceToTarget * rotationSpeedScale;
        BOOL roughlyInRightDirection = fabs(self.navigator.directionDifferenceToTarget) < 45.;
        self.communicator.forwardSpeed = roughlyInRightDirection ? 0.2 : 0;
    }
}

Remote Client

Remote Client 類關(guān)注于和我們的客戶端通訊,我們利用了一個很方便 Multipeer Connectivity 框架。首先,我們需要和附近的創(chuàng)建一個會話以及 MCNearbyServiceBrowser :

- (void)startBrowsing
{
    MCPeerID* peerId = [[MCPeerID alloc] initWithDisplayName:@"Drone"];

    self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:peerId serviceType:@"loc-broadcaster"];
    self.browser.delegate = self;
    [self.browser startBrowsingForPeers];

    self.session = [[MCSession alloc] initWithPeer:peerId];
    self.session.delegate = self;
}

在我們的項目中,我們不需要處理單獨設(shè)備的安全問題,因為我們總是邀請所有的對等網(wǎng)絡(luò)的設(shè)備。

- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
    [browser invitePeer:peerID toSession:self.session withContext:nil timeout:0];
}

我們需要加入 MCNearbyServiceBrowserDelegateMCSessionDelegate 全部的協(xié)議方法,否則這個應(yīng)用將會崩潰。唯一一個方法我們需要實現(xiàn)的是 session:didReceiveData:fromPeer: 。我們解析對等客戶端發(fā)送來的命令并且調(diào)用合適的委托方法,在我們簡易的應(yīng)用中,View Controller 實現(xiàn)了這些委托,當(dāng)我們接收到了新的位置我們更新導(dǎo)航,并且讓無人機(jī)飛向新的位置。

總結(jié)

這篇文章描述了這個簡易的 app ,最初我們把所有的委托和代碼都加入到了 View Controller 中,這是被證明最簡單的編碼和測試方式,其實寫代碼是一個容易的事情,但是閱讀代碼非常困難。因此我們需要重構(gòu)所有的代碼讓其合理的分配到不同類中。

硬件方面的工作,測試非常的耗時,比如,在我們的 quadcopter 項目中,需要一段時間來啟動設(shè)備,發(fā)送命令,并讓它飛起來。因此我們盡可能多在離線狀況下測試。我們還添加了大量的的日志語句,這樣我們調(diào)試起來更加方便。