客戶端程序是這個項目的一個組成部分,這個項目能將目的地的坐標(biāo)發(fā)送給無人機上綁定的電話。 整個過程是個很簡單的任務(wù),而其中又不乏有趣的部分,例如使用新的( iOS7 上的)Multipeer Connectivity API 和 NSSecureCoding。
這個程序最后表現(xiàn)的界面很簡單,但卻不太漂亮:
http://wiki.jikexueyuan.com/project/objc/images/8-4.jpg" alt="" />
為了創(chuàng)建客戶端和無人機上的導(dǎo)航程序之間的連接,我們打算使用新的 Multipeer Connectivity 的 API。 為了做到這一點,我們只需相互連接兩個設(shè)備, 所以 multipeer API 在這里并不能完全發(fā)揮它的潛力。但是如果更多的客戶端要加入的話,代碼實際上是一樣的。
我們決定讓客戶端程序作為發(fā)送端,而無人機上的導(dǎo)航程序作為瀏覽接收端??蛻舳耸褂靡韵潞唵蔚恼Z句來開始發(fā)送:
NSString *displayName = [UIDevice currentDevice].name;
self.peer = [[MCPeerID alloc] initWithDisplayName:displayName];
self.advertiser = [[MCNearbyServiceAdvertiser alloc] initWithPeer:self.peer discoveryInfo:nil serviceType:ServiceTypeIdentifier];
self.advertiser.delegate = self;
[self.advertiser startAdvertisingPeer];
一旦另一臺有同樣服務(wù)類型的瀏覽客戶端的設(shè)備發(fā)現(xiàn)了發(fā)送端, 我們就會收到一個代理回調(diào)函數(shù)以便建立連接:
- (void)advertiser:(MCNearbyServiceAdvertiser *)advertiser didReceiveInvitationFromPeer:(MCPeerID *)peerID withContext:(NSData *)context invitationHandler:(void (^)(BOOL accept, MCSession *session))invitationHandler
{
self.session = [[MCSession alloc] initWithPeer:self.peer];
self.session.delegate = self;
invitationHandler(YES, self.session);
}
一旦我們收到邀請,我們就建立一個新的會話對象,設(shè)置我們自己為會話的代理,并且通過調(diào)用 invitationHandler
并將 YES
和會話做為參數(shù)傳遞,來接受邀請。
為了能在屏幕上顯示連接狀態(tài),我們要實現(xiàn)另一個會話代理方法。因為我們只能連接到一個另外的設(shè)備,所以我們僅使用當(dāng)前已連接的節(jié)點數(shù)量作為標(biāo)示,標(biāo)示大于 0 代表已連接:
- (void)session:(MCSession *)session peer:(MCPeerID *)peerID didChangeState:(MCSessionState)state
{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSString *notificationName = session.connectedPeers.count > 0 ?
MultiPeerConnectionDidConnectNotification :
MultiPeerConnectionDidDisconnectNotification;
[[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:self];
}];
}
因為六分之五的 MCSessionDelegate
協(xié)議里的方法都是必須的,所以盡管我們沒有特別的目的要使用他們,我們也必須把那些協(xié)議都加上。
這時候,連接建立好以后,我們就能使用會話的 sendData:toPeers:withMode:error:
方法傳遞數(shù)據(jù)。我們會在后面部分更多的探討這個內(nèi)容。
運行在飛行器手機上的導(dǎo)航程序必須通過給客戶端發(fā)送邀請來初始化連接。做法同樣直白,第一步是啟動掃描節(jié)點。
MCPeerID* peerId = [[MCPeerID alloc] initWithDisplayName:@"Drone"];
self.browser = [[MCNearbyServiceBrowser alloc] initWithPeer:peerId serviceType:ServiceTypeIdentifier];
self.browser.delegate = self;
[self.browser startBrowsingForPeers];
一個節(jié)點被發(fā)現(xiàn)以后, 我們就獲得一個代理回調(diào)函數(shù),而且能邀請該節(jié)點加入我們的會話:
- (void)browser:(MCNearbyServiceBrowser *)browser foundPeer:(MCPeerID *)peerID withDiscoveryInfo:(NSDictionary *)info
{
self.session = [[MCSession alloc] initWithPeer:peerId];
self.session.delegate = self;
[browser invitePeer:peerID toSession:self.session withContext:nil timeout:0];
}
一旦客戶端發(fā)送數(shù)據(jù),我們就能從會話的代理方法 session:didReceiveData:fromPeer:
接收到這些數(shù)據(jù)。
多點會話中的每個節(jié)點能很方便地使用 sendData:toPeers:withMode:error:
方法發(fā)送數(shù)據(jù)。 我們只需要考慮如何打包數(shù)據(jù)來傳輸。
一個通常的選擇是簡單的編碼為 JSON。 盡管這對于我們的目的簡單易行, 但是我們想要做一些更有趣的辦法,使用 NSSecureCoding
. 這對于我們的例子來講實際上并沒有什么差別, 但是如果你想要傳輸更多的數(shù)據(jù),這將是比編解碼 JSON 更有效的方式。
首先,我們創(chuàng)建一個類,用來打包我們要發(fā)送的數(shù)據(jù):
@interface RemoteControlCommand : NSObject <NSSecureCoding>
+ (instancetype)commandFromNetworkData:(NSData *)data;
- (NSData *)encodeAsNetworkData;
@property (nonatomic) CLLocationCoordinate2D coordinate;
@property (nonatomic) BOOL stop;
@property (nonatomic) BOOL takeoff;
@property (nonatomic) BOOL reset;
@end
為了使 secure coding 有效(確保收到的數(shù)據(jù)使我們期望收到的類型),我們需要添加 supportsSecureCoding
類方法到我們的實現(xiàn)中:
+ (BOOL)supportsSecureCoding;
{
return YES;
}
接下來,我們要添加方法來編碼一個對象的實例并把它打包成一個NSData對象使其能夠通過多點連接發(fā)送。
- (NSData *)encodeAsNetworkData;
{
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
archiver.requiresSecureCoding = YES;
[archiver encodeObject:self forKey:@"command"];
[archiver finishEncoding];
return data;
}
- (void)encodeWithCoder:(NSCoder *)coder;
{
[coder encodeDouble:self.coordinate.latitude forKey:@"coordinate.latitude"];
[coder encodeDouble:self.coordinate.longitude forKey:@"coordinate.longitude"];
[coder encodeBool:self.stop forKey:@"stop"];
[coder encodeBool:self.stop forKey:@"takeoff"];
[coder encodeBool:self.stop forKey:@"reset"];
}
現(xiàn)在我們能簡單地用幾行代碼發(fā)送控制指令:
RemoteControlCommand *command = [RemoteControlCommand alloc] init];
command.coordinate = self.location.coordinate;
NSData *data = [command encodeAsNetworkData];
NSError *error;
[self.session sendData:data toPeers:self.session.connectedPeers withMode:MCSessionSendDataReliable error:&error];
為了使接收端能夠解碼數(shù)據(jù),我們要添加另一個類方法到我們的 RemoteControlCommand
中:
+ (instancetype)commandFromNetworkData:(NSData *)data;
{
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
unarchiver.requiresSecureCoding = YES;
RemoteControlCommand *result = [unarchiver decodeObjectOfClass:self forKey:@"command"];
return result;
}
最后,我們需要實現(xiàn) initWithCoder:
來讓已被編碼的對象能從數(shù)據(jù)中解碼出來。
- (id)initWithCoder:(NSCoder *)coder;
{
self = [super init];
if (self != nil) {
CLLocationCoordinate2D coordinate = {};
coordinate.latitude = [coder decodeDoubleForKey:@"coordinate.latitude"];
coordinate.longitude = [coder decodeDoubleForKey:@"coordinate.longitude"];
self.coordinate = coordinate;
self.stop = [coder decodeBoolForKey:@"stop"];
self.takeoff = [coder decodeBoolForKey:@"takeoff"];
self.reset = [coder decodeBoolForKey:@"reset"];
}
return self;
}
現(xiàn)在,我們在這有了多點連接并且我們能編解碼遠(yuǎn)程控制指令,我們已經(jīng)為無線發(fā)送位置坐標(biāo)和控制指令做好了準(zhǔn)備。 為了解釋這個例子,因為其他指令也是完全一樣的,我們只是看一下坐標(biāo)的傳輸。
像在項目概述里討論過的那樣,為了使這個飛行器導(dǎo)航測試簡單一點,這個客戶端程序可以發(fā)送當(dāng)前的地理位置或者是地圖上的選點。 在第一種情況,我們僅需要實現(xiàn) CLLocationManager
的代理方法 locationManager:didUpdateLocations:
并在屬性里存儲當(dāng)前坐標(biāo):
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
self.location = locations.lastObject;
}
我們設(shè)置一個定時器來定期發(fā)送當(dāng)前位置:
- (void)startBroadcastingLocation
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(broadcastLocation) userInfo:nil repeats:YES];
}
最后,broadcastLocation
方法每一秒調(diào)用一次,會創(chuàng)建一個 RemoteControlCommand
對象而且把它發(fā)送到已連接的節(jié)點:
- (void)broadcastLocation
{
RemoteControlCommand *command = [RemoteControlCommand alloc] init];
command.coordinate = self.location.coordinate;
NSData *data = [command encodeAsNetworkData];
NSError *error;
[self.session sendData:data toPeers:self.session.connectedPeers withMode:MCSessionSendDataReliable error:&error];
if (error) {
NSLog(@"Error transmitting location: %@", error);
}
}
大概就是這樣。跟隨著閱讀關(guān)于飛行器上導(dǎo)航軟件和用于與飛行器通信的 Core Foundation
網(wǎng)路 API 的其他幾篇文章能了解這些與飛行器交互的指令接收端并且能真正讓它起飛!