在這篇文章中,我們將把前面提到過的內(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 中最重要的一個部分是初始化方法,在這里我們創(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)位置。
導(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 用來初始化 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 類關(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];
}
我們需要加入 MCNearbyServiceBrowserDelegate
和 MCSessionDelegate
全部的協(xié)議方法,否則這個應(yīng)用將會崩潰。唯一一個方法我們需要實現(xiàn)的是 session:didReceiveData:fromPeer:
。我們解析對等客戶端發(fā)送來的命令并且調(diào)用合適的委托方法,在我們簡易的應(yīng)用中,View Controller 實現(xiàn)了這些委托,當(dāng)我們接收到了新的位置我們更新導(dǎo)航,并且讓無人機(jī)飛向新的位置。
這篇文章描述了這個簡易的 app ,最初我們把所有的委托和代碼都加入到了 View Controller 中,這是被證明最簡單的編碼和測試方式,其實寫代碼是一個容易的事情,但是閱讀代碼非常困難。因此我們需要重構(gòu)所有的代碼讓其合理的分配到不同類中。
硬件方面的工作,測試非常的耗時,比如,在我們的 quadcopter 項目中,需要一段時間來啟動設(shè)備,發(fā)送命令,并讓它飛起來。因此我們盡可能多在離線狀況下測試。我們還添加了大量的的日志語句,這樣我們調(diào)試起來更加方便。