在這篇文章中,我們將把前面提到過(guò)的內(nèi)容組織起來(lái)構(gòu)成我們的導(dǎo)航器應(yīng)用,這個(gè) iPhone 應(yīng)用將裝載在我們的的無(wú)人機(jī)上,你可以在 Github 下載應(yīng)用的源碼,盡管這個(gè)應(yīng)用是計(jì)劃在沒(méi)有直接的交互操作下來(lái)使用的,但在測(cè)試過(guò)程中我們做了一個(gè)簡(jiǎn)單的 UI 界面來(lái)顯示其無(wú)人機(jī)狀態(tài)并方便我們手動(dòng)操作。
在我們的應(yīng)用中,我們有幾個(gè)類(lèi)它們分別是:
DroneCommunicator 這個(gè)類(lèi)關(guān)注于利用 UDP 和無(wú)人機(jī)通訊。這個(gè)話(huà)題全部在 Daniel 的文章中詳細(xì)介紹過(guò)
RemoteClient 使用 Multipeer Connectivity 技術(shù)和我們的遠(yuǎn)程客戶(hù)端進(jìn)行交互,具體客戶(hù)端的操作,請(qǐng)看 Florian 的文章。Navigator 用來(lái)設(shè)定目標(biāo)位置,計(jì)算飛行航線(xiàn),以及飛行距離。DroneController 用來(lái)把從 Navigator 獲取的導(dǎo)航的距離和方向發(fā)送命令到DroneCommunicator。ViewController 有一個(gè)簡(jiǎn)單的界面,用來(lái)初始化其他的類(lèi)并把它們連接起來(lái),這部分應(yīng)該用不同的類(lèi)來(lái)完成,但是在我們的設(shè)想中,我們的app足夠簡(jiǎn)單所以放到一個(gè)類(lèi)就可以了。View Controller 中最重要的一個(gè)部分是初始化方法,在這里我們創(chuàng)建了 DroneCommunicator, Navigator, DroneController 以及RemoteClient 的實(shí)例化對(duì)象,換句話(huà)說(shuō):我們建立了無(wú)人機(jī)和我們的客戶(hù)端應(yīng)用溝通的整個(gè)橋梁。
- (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 同時(shí)是 RemoteClient 的委托。 這就說(shuō)明無(wú)論我們的客戶(hù)端發(fā)送了一個(gè)新位置或者著陸,重置以及關(guān)機(jī)的命令,我們都需要在這里處理它。舉個(gè)例子,當(dāng)我們收到一個(gè)新的位置的命令的時(shí)候,我們這樣來(lái)做:
- (void)remoteClient:(RemoteClient *)client didReceiveTargetLocation:(CLLocation *)location
{
self.droneController.droneActivity = DroneActivityFlyToTarget;
self.navigator.targetLocation = location;
}
這段代碼是用來(lái)確保無(wú)人機(jī)開(kāi)始飛行(而不是徘徊)并且更新目標(biāo)位置。
導(dǎo)航類(lèi)用來(lái)指定目標(biāo)位置,并且計(jì)算從當(dāng)前位置到目標(biāo)位置的距離,為了完成整個(gè)工作我們首先需要監(jiān)聽(tīng) 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)航類(lèi)中,我們有兩種方向,絕對(duì)和相對(duì)方向,絕對(duì)方向是兩個(gè)地點(diǎn)之間的方向。比如說(shuō),阿姆斯特丹和柏林間的絕對(duì)方向幾乎處于同一緯度,相對(duì)位置則是我們?cè)趨⒖贾改厢樅罂梢缘贸龅穆肪€(xiàn)方向,要從阿姆斯特丹一直向東到柏林,兩地之間的相對(duì)方向?yàn)榱?。在操作無(wú)人機(jī)的時(shí)候我們就需要使用相對(duì)方向。方向值為零,飛機(jī)直行;方向角度小于零,飛機(jī)向右傾斜轉(zhuǎn)彎;方向角度大于零,飛機(jī)則向左傾斜轉(zhuǎn)彎。
計(jì)算到目的地的絕對(duì)方向,我們需要?jiǎng)?chuàng)建一個(gè)基于 CLLocation 的Helper方法用來(lái)計(jì)算兩個(gè)點(diǎn)的方向:
- (OBJDirection *)directionToLocation:(CLLocation *)otherLocation;
{
return [[OBJDirection alloc] initWithFromLocation:self toLocation:otherLocation];
}
由于我們的無(wú)人機(jī)只能飛很小的距離(電池只能支持10分鐘),所以我們需要一個(gè)幾何的假設(shè),我們是在一個(gè)平面而不是在地球表面:
- (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),然后我們把這兩個(gè)值存到屬性中,比如,計(jì)算我們需要飛行的兩點(diǎn)之間的距離,我們需要將絕對(duì)航向減去當(dāng)前航向(這與你看到指南針上的值是一樣的意思),然后將結(jié)果換算到 -180 度和 180 度之間。如果你希望知道為什么我們要減去 90 度,那是因?yàn)槲覀?iPhone 和無(wú)人機(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)前的位置和航向,計(jì)算出到目標(biāo)的距離和無(wú)人機(jī)應(yīng)當(dāng)飛行的方向。并且監(jiān)聽(tīng)這兩個(gè)屬性。
Drone controller 用來(lái)初始化 navigator 和 communicator,并且發(fā)送距離和方向的命令到無(wú)人機(jī),因?yàn)槊钚枰掷m(xù)發(fā)送,所以我們創(chuàng)建一個(gè)計(jì)時(shí)器:
self.updateTimer = [NSTimer scheduledTimerWithTimeInterval:0.25
target:self
selector:@selector(updateTimerFired:)
userInfo:nil
repeats:YES];
當(dāng)計(jì)時(shí)器觸發(fā)后,假設(shè)我們飛向一個(gè)目標(biāo),我們需要發(fā)送給無(wú)人機(jī)適當(dāng)?shù)闹噶睿绻覀冏銐蚪?,無(wú)人機(jī)盤(pán)旋,否則,我們轉(zhuǎn)向目標(biāo),在大致方向正確的情況下飛過(guò)去!
- (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 類(lèi)關(guān)注于和我們的客戶(hù)端通訊,我們利用了一個(gè)很方便 Multipeer Connectivity 框架。首先,我們需要和附近的創(chuàng)建一個(gè)會(huì)話(huà)以及 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;
}
在我們的項(xiàng)目中,我們不需要處理單獨(dú)設(shè)備的安全問(wèn)題,因?yàn)槲覀兛偸茄?qǐng)所有的對(duì)等網(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é)議方法,否則這個(gè)應(yīng)用將會(huì)崩潰。唯一一個(gè)方法我們需要實(shí)現(xiàn)的是 session:didReceiveData:fromPeer: 。我們解析對(duì)等客戶(hù)端發(fā)送來(lái)的命令并且調(diào)用合適的委托方法,在我們簡(jiǎn)易的應(yīng)用中,View Controller 實(shí)現(xiàn)了這些委托,當(dāng)我們接收到了新的位置我們更新導(dǎo)航,并且讓無(wú)人機(jī)飛向新的位置。
這篇文章描述了這個(gè)簡(jiǎn)易的 app ,最初我們把所有的委托和代碼都加入到了 View Controller 中,這是被證明最簡(jiǎn)單的編碼和測(cè)試方式,其實(shí)寫(xiě)代碼是一個(gè)容易的事情,但是閱讀代碼非常困難。因此我們需要重構(gòu)所有的代碼讓其合理的分配到不同類(lèi)中。
硬件方面的工作,測(cè)試非常的耗時(shí),比如,在我們的 quadcopter 項(xiàng)目中,需要一段時(shí)間來(lái)啟動(dòng)設(shè)備,發(fā)送命令,并讓它飛起來(lái)。因此我們盡可能多在離線(xiàn)狀況下測(cè)試。我們還添加了大量的的日志語(yǔ)句,這樣我們調(diào)試起來(lái)更加方便。