這個框架能做什麼
顧名思義:External:外部的;Accessory:配件。應該是和外部設備相關的一個框架。
ExternalAccessory框架,就是可以用來和Lightning接口的硬件,或者藍牙(2.1)設備進行連接、通訊的這麼一個框架。(當然,也可以和30-pin接口的硬件連接、通訊——不過現在幾乎沒有這種接口的設備了吧~)
就是你現在有一個Lightning耳機(iPhone7, 7Plus的耳機~),或者有一個藍牙2.1的音箱,你要寫一個App去控制這些設備,你要選用的框架,就是ExternalAccessory。
比如我前公司,幫美國公司代工的一款藍牙2.1的音箱,寫了一個App進行控制(燈光、音效);還有現在公司,做Lightning設備的App,用來對耳機進行簡單的控制、固件升級。這都需要用到ExternalAccessory框架。
框架簡介
ExternalAccessory框架的主要功能,就是提供一個管道,讓外圍設備可以和基於iOS系統的設備進行通訊。
主要的幾個類:
EAAccessory:表示你連接的設備。
EAAccessoryManager:有一個重要的屬性connectedAccessories,用來獲取已經連接上手機的設備。
EASession:這個類主要用來建立通道,讓App和設備可以進行數據的傳輸(發送和接收)
設備的連接
其實設備的連接、斷開,都是系統自動完成的。
EAAccessoryManager類中有一個屬性connectedAccessories(一個array),裡面就已經包含了所有已經連接的外圍設備(EAAccessory對象)。像什麼設備名稱、制造廠商、硬件型號、固件型號等等信息,都可以在EAAccessory對象中拿得到。
但是,ExternalAccessory框架,並不會自動幫你監控設備的斷開、連接狀態。如果你想拿到設備連接、斷開的回調,則需要手動敲一些代碼了:
拿到連接、斷開的回調
需要注冊通告,即調用EAAccessoryManager的方法registerForLocalNotifications。
當有硬件連接,ExternalAccessory框架就會發送EAAccessoryDidConnectNotification這個通告,當有硬件斷開連接,就會發出EAAccessoryDidDisconnectNotification通告。所以,要監聽、接收這兩個通告。
// 注冊通告 [[EAAccessoryManager sharedAccessoryManager] registerForLocalNotifications]; // 監聽EAAccessoryDidConnectNotification通告(有硬件連接就會回調Block) [[NSNotificationCenter defaultCenter] addObserverForName:EAAccessoryDidConnectNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { // 從已經連接的外設中查找我們的設備(根據協議名稱來查找) [self searchOurAccessory]; }]; [[NSNotificationCenter defaultCenter] addObserverForName:EAAccessoryDidDisconnectNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) { // Do something what you want }];
此外,硬件斷開連接,除了通告回調,框架還提供了Delegate的回調方式,遵守EAAccessoryDelegate協議,並實現accessoryDidDisconnect:這個可選方法(這個協議中的唯一一個方法),也可以拿到硬件斷開連接的回調。(好奇怪,Apple為什麼單單只弄這麼一個方法?)
識別硬件
好了,我們知道硬件連接進行了,那怎麼知道是不是我們的硬件呢?
蘋果公司將這個能識別硬件身份的東東叫做「協議」。本質上就是一個字符串,一個由反向域名組成的字符串,例如om.apple.myProtocol。
而這個協議(字符串)的定義,是由硬件的生產廠商定義的,所以App開發人員,要和廠商溝通拿到這部分的資料。
所以我們要做幾件事件:
導入框架(這個不用說了吧~)#import
在Info.plist中,增加UISupportedExternalAccessoryProtocols這個key,然後值賦為協議名稱(就是那個反向域名字符串)。(其實是一個array,所以這裡可以支持多個協議,不分順序)
在硬件已經連接的回調中,遍歷所有已經連接的設備,根據協議名稱找到自己的硬件(實現上述代碼的searchOurAccessory方法):
// 從已經連接的外設中查找我們的設備(根據協議名稱來查找) - (void)searchOurAccessory { NSMutableString *info = [[NSMutableString alloc] init]; // search our device for (EAAccessory *accessory in [EAAccessoryManager sharedAccessoryManager].connectedAccessories) { if ([kSPKLightingHeadphoneProtocolString isEqualToString:[accessory.protocolStrings firstObject]] == YES) { // 硬件的協議字符串和硬件廠商提供的一致,這個就是我們要找的設備了! // log:可以打印一下該硬件的相關資訊 for (NSString *proStr in accessory.protocolStrings) { [info appendFormat:@"protocolString = %@\n", proStr]; } [info appendFormat:@"\n"]; [info appendFormat:@"manufacturer = %@\n", accessory.manufacturer]; [info appendFormat:@"name = %@\n", accessory.name]; [info appendFormat:@"modelNumber = %@\n", accessory.modelNumber]; [info appendFormat:@"serialNumber = %@\n", accessory.serialNumber]; [info appendFormat:@"firmwareRevision = %@\n", accessory.firmwareRevision]; [info appendFormat:@"hardwareRevision = %@\n", accessory.hardwareRevision]; // Log... } } }
另外,監視硬件連接的通告Block回調,NSNotification * _Nonnull note這個參數,其實是包含了EAAccessory對象,我們也可以直接通過EAAccessoryKey這個key拿到EAAccessory對象,再對比協議字符串是否相同,從而直接拿到已經連接的硬件,無須遍歷connectedAccessories數組。
傳輸數據(指令)
創建EASession、打開輸入、輸出通道
App和外圍設備通訊、數據傳輸,靠的是NSInputStream和NSOutputStream對象,而這兩個對象是EASession的兩個屬性。所以我們要創建EASession對象,謂曰:打開傳輸通道()。
遵守NSStreamDelegate協議,類似:@interface YourClassName()
創建EASession並打開輸入、輸出通道,類似如下代碼:
- (BOOL)openSession { // 根據已經連接的EAAccessory對象和這個協議(反向域名字符串)來創建EASession對象,並打開輸入、輸出通道 self.session = [[EASession alloc] initWithAccessory:self.accessory forProtocol: kSPKLightingHeadphoneProtocolString]; if(self.session != nil) { // open input stream self.session.inputStream.delegate = self; [self.session.inputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [self.session.inputStream open]; // open output stream self.session.outputStream.delegate = self; [self.session.outputStream scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [self.session.outputStream open]; } else { NSLog(@"Failed to create session"); } return (nil != self.session); }
到此為止,就完整創建了一個包含accessory對象、並已經可以進行數據發送和接收的EASession對象了。
stream:handleEvent:回調:
不過,雖然數據傳輸通道已經打開了,但是怎麼發送、接收數據呢?或者說,怎麼知道什麼時候可以發送數據,什麼時候要接收數據?
注意我們剛剛遵守了NSStreamDelegate協議,這裡就是利用delegate回調來監聽input stream和output stream的數據。
// delegate回調的方法 - (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode { switch (eventCode) { case NSStreamEventNone: break; case NSStreamEventOpenCompleted: break; case NSStreamEventHasBytesAvailable: //NSLog(@"Input stream is ready"); // 接收到硬件數據了,根據指令定義對數據進行解析。 [self readFromDevice]; break; case NSStreamEventHasSpaceAvailable: //NSLog(@"Output stream is ready"); // 可以發送數據給硬件了 [self writeToDevice]; break; case NSStreamEventErrorOccurred: break; case NSStreamEventEndEncountered: break; default: break; } }
HasBytesAvailable:表示stream中有數據需要讀取(硬件發送了數據給App)
HasSpaceAvailable:表示stream中可以接收數據的寫入(App發送了數據給硬件)——當然,不是每次都需要等到這個回調執行,App才能發送數據給硬件,你可以判斷stream的hasBytesAvailable屬性,如果為Yes,照樣可以直接發送數據給硬件。類似如下:
BOOL isAvailable = self.session.outputStream.hasSpaceAvailable; if (isAvailable == YES) { [self writeToDevice]; }
發送數據、接收數據的具體方法:
發送數據:
outputStream的write:maxLength:方法,類似如下:
[self.session.outputStream write:[self.writeData bytes] maxLength:self.writeDataLen];
接收數據:
inputStream的read:maxLength:方法,類似如下:
[self.session.inputStream read:buffer maxLength:SPK_INPUT_DATA_BUFFER_LEN];
到此,我們用ExternalAccessory框架,進行了從識別硬件連接、獲取硬件、打開傳輸通道、發送數據、接收數據的完整過程。
調試、Debug
我們開發的是一個Lightning接口設備的App,當手機連接硬件時,就沒辦法連接電腦進行調試,當手機連接電腦時,就沒辦法連接硬件進行測試。所以整個開發調試、Debug無從下手。網站上咨詢了蘋果,也在StackOverflow上提問,都沒有得到解決方案。
後來我就腦洞大開,把需要打印的日志收集起來,通過一個TextView,顯示到App上做調試用(如下圖)。也算是一個權宜之計,誰有更好的辦法麼~
將Log轉移到App界面上進行Debug
如有謬誤,敬請斧正。