iOS ExternalAccessory框架初探

2017年8月9日更新:

关于调式,debug,Xcode 9 可以进行无线调试了,当你的硬件占用了Lightning口,手机也可以无线连接Xcode调试,具体操作不做赘述,可参考Xcode9下iOS11适配注意事项及无线部署调试


这个框架能做什么

顾名思义: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开发人员,要和厂商沟通拿到这部分的资料。

所以我们要做几件事件:

  • 0、导入框架(这个不用说了吧~)#import <ExternalAccessory/ExternalAccessory.h>
  • 1、在Info.plist中,增加UISupportedExternalAccessoryProtocols这个key,然后值赋为协议名称(就是那个反向域名字符串)。(其实是一个array,所以这里可以支持多个协议,不分顺序)
  • 2、在硬件已经连接的回调中,遍历所有已经连接的设备,根据协议名称找到自己的硬件(实现上述代码的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对象,谓曰:打开传输通道()。

  • 0、遵守NSStreamDelegate协议,类似:@interface YourClassName()<NSStreamDelegate>,用于后面拿到相关回调。
  • 1、创建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

如有谬误,敬请斧正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容

  • 在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像、声音文件等其他资源。共...
    wo不懂阅读 1,393评论 3 3
  • 在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,以及图像、声音文件等其他资源。共...
    ch123阅读 1,743评论 0 1
  • 6转载-->>IOS框架和服务 在iOS中框架是一个目录,包含了共享资源库,用于访问该资源库中储存的代码的头文件,...
    李小六_阅读 3,587评论 2 24
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,270评论 0 6
  • 自律的人看得到美好的未来! 最美的年龄时间会记录。
    九重艳阳阅读 183评论 0 1