文章目录
  1. 1. 需求来源
  2. 2. 需求分析
  3. 3. 代码实现与问题解决
    1. 3.1. CoreBluetooth框架介绍
    2. 3.2. 技术实现
  4. 4. 细节注意问题总结
  5. 5. 资源推荐

titleImage

首先简要介绍本文内容(看前最好对iOS蓝牙开发做一定了解):

  • 第一部分:蓝牙打印电子面单的需求来源。
  • 第二部分:蓝牙打印电子面单的需求分析。
  • 第三部分:技术实现(大体内容),遇到的问题,以及解决方案。
  • 第四部分:需要注意的细节总结。
  • 第五部分:资源推荐。

需求来源

蓝牙打印电子面单的需求来源于面向快递员用户的”中邮速递易快递员App(以下简称快递员App)”开发线,快递员可以通过该App实现向中邮速递易相关智能设备投件,以及从中取件的交互流程。

而打印电子面单的需求来源于取件流程,取件流程需要快递员从智能设备中取出用户所投快件,并生成相应面单(手填面单,站点机打面单,便携机打面单),并将面单贴到快件上,完成标记发货流程。然而手填面单效率极低,并且出错率很高;而站点机打面单也存在需要额外标记措施,快件送回站点过程中易混乱,并且效率也比较低等等问题;所以,现在主流快递公司很多采用了蓝牙便携设备打印电子面单的方案,可以方便快递员实现现场发货操作,极大提高了取件效率,同时也大大降低了人工操作的出错率。

而本次的需求就是:实现快递员App支持多家快递公司、多种型号蓝牙便携打印设备的电子面单打印功能。

需求分析

经过技术调研,发现移动端Android和iOS两边的技术实现不尽相同,Android那边采用的是比较通用的端口输入输出流的方式,和其他的硬件操作比较类似,并且普适了各种设备;而iOS这边技术更多元化,但是从技术实用和更新角度来看,采用BLE4.0的CoreBluetooth框架更合适。

而后台系统,则采用了快递鸟的API通用接口,获取各家快递公司的面单单号支持,然后将订单数据绑定到对应的面单号上,生成特有的模板。

考虑到Android和iOS系统不同的实现方式,为了实现统一和复用,最后跟后台人员数次会议商议后敲定了如下整体方案:蓝牙打印电子面单采用CPCL指令集下发的模式,先由App端开发人员根据实际面单格式编写(对应公司+对应设备型号)CPCL指令集,然后交由后台配置数据,再统一管理、下发给移动端,移动端再将模板下发便携蓝牙打印设备,打印出电子面单,快递员再将面单贴到快件上完成整个打印电子面单、标记、发货流程。但是仍然存在不使用电子面单的快递公司无法覆盖到的问题,所以这里在实现蓝牙打印电子面单的同时,保留了手写面单绑定的入口,这样就可以全面的满足快递员取件+发货的需求了。

代码实现与问题解决

本篇主要提供iOS端的技术实现方案。

CoreBluetooth框架介绍

iOS端的CoreBluetooth库,解决了大部分跟硬件直接交互的问题,给开发人员提供了友好便捷的集成方式。CoreBluetooth框架主要分为以下两种模式:

  • CBCentralMannager中心模式 :以手机(app)作为中心,连接其他外设。(本次主要使用的方式)
  • CBPeripheralManager外设模式:以手机作为外设,连接其他中心设备。

本次需求的场景,使用到的是CBCentralManager中心模式。CBCentralManager中心模式将蓝牙设备硬件构建成如下几个层级结构:

<font size=2><font color=red>CBPerpheral外设</font>==(包含多个)==><font color=red>CBService服务</font>==(包含多个)==><font color=red>CBCharacteristic特征</font>==(包含多个)==><font color=red>Description描述</font></font>

外设是CoreBluetooth对外部设备的抽象对象,外设具有name等属性,一个外设可以包含多种服务,服务具有一个唯一的标识量:UUID,服务集合中的每个服务都提供不同的服务内容,比方说提供设备信息、提供写入数据接口等,对于硬件开发商来说可以自己定制自己的服务,不过一般都有默认的服务提供;一个服务可以包含多个特征值,每个特征值也具有自己的唯一标识量:UUID,每个特征值提供具体的比方写入、读取、广播等可操作行为,是一般蓝牙开发操作的最小单位;一个特征值包含多个描述

CoreBluetooth框架秉承了Objective-C语言编写的系统框架的一贯风格,采用了协议代理的开发方式。主要遵循两个协议CBCentralManagerDelegate和CBPeripheralDelegate。CBCentralManagerDelegate主要包含了中心对象连接外设时的状态获取回调方法;而CBPeripheralDelegate则主要包含了外设对象的服务、特征值等等的发现回调方法。

而CoreBluetooth框架的中心模式的开发规则如下:

  1. 创建CBCentralManager实例,配置代理控制器,然后调用扫描外设方法:-(void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *,id> *)options;

  2. 在代理控制器中,实现扫描到外设的代理方法: -(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI; ,在该方法中可以获取到扫描到的外设列表,用来进行相应的展示或者操作。

  3. 在扫描到的外设列表中,选择需要的外设,调用CBCentralManager的连接外设方法: - (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;

  4. 在代理控制器中,实现连接到外设的代理方法: - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;,在该方法中,继续调用发现服务的方法:- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;

  5. 在代理控制器中,实现发现外设服务的代理方法: - (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;,在该方法中,继续调用发现特征值的方法:- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;

  6. 在代理控制器中,实现发现特征值的代理方法: - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error,在该方法中就可以获取到对应的特征值,具体的写入和读取操作,就来源于这些特征值。

  7. 在第6步中获取到的特征值中,筛选出需要的特征值,进行相应的写入、读取、注册广播操作。

技术实现

因为考虑到蓝牙设备的全局唯一性和通用性,所以在项目中建立了一个管理单例PrintManager用来管理蓝牙连接和操作相关的内容。而管理类作为CBCenterManager代理,处理大多数跟系统蓝牙沟通的问题:

系统代理方法

然后配置一套自身的协议,只对外公开一些必要的业务相关的操作即可:
管理类公开方法

通过两层协议(PrintManager遵守系统协议、业务层遵守PrintManager协议)的方式,将与系统蓝牙框架交互的处理跟与业务层交互的处理分离开了,并将项目中使用蓝牙的操作分为了如下几个阶段:

  • 搜索外设阶段

这里在系统蓝牙硬件搜索到外设的时候,CBCenterManager的回调方法每次都是扫描到外设就会回调,包含可能已经扫描到过的外设,所以这里需要进行排除重复处理。

这里有个小处理,就是业务里面有一个需求,链接过某个设备之后,如果手机打开了蓝牙,那就自动连接之前的设备。所以这里添加了自动连接的属性配置,如果打开自动链接,会在搜索阶段从本地缓存取出之前最后一次链接过的外设的identifier,然后对比扫描到的外设,如果符合,则会自动调用连接方法,从而实现“自动连接”

这里还有个小技巧,就是连接方法,系统提供了根据对应服务UUID集合扫描含有特定服务的外设的方法,根据业务需求,可以将特定的服务UUID集合传入给扫描方法,这样可以排除很多具有蓝牙但是不为打印业务所用的设备。

而在对应的系统回调方法里,还可以获取到外设的广播信息,以及外设的信号强度信息,在有需要的项目里可以使用,本项目未使用到,就不多做解释了。

最后PrintManager通过协议,将扫描到的设备信息,回调给业务层使用。

  • 链接外设阶段

PrintManager自动连接,或者业务层主动调用PrintManager的链接方法,主动链接某个外设后,会有链接成功和连接失败的回调方法,如果主动调用了断开连接的方法,还有断开连接回调的方法。

这里在连接成功后,对连上的外设的identifier进行了本地缓存处理,以供下次自动链接时使用,同时通过PrintManager协议方法,将获取到的状态信息回调给业务层处理,然后自动调用了系统发现服务的方法。

而在连接失败的方法里面也进行了同样的回调处理,将重连或者其他操作的权限,交给了业务层。

  • 发现服务阶段

发现服务阶段,同样的会有系统回调方法,操作和上面的连接回调方法基本类似,PrintManager对发现的服务将继续进行发现特征值操作,PrintManager同样的会将该步骤回调给业务层,业务层将对发现服务的外设进行相应的处理。

这里有个小问题,因为我们是便携蓝牙打印机,大部分厂商的机型都提供了一个通用UUID类似FF00或者49535343-FE7D-4AE5-8FA9-9FAFD205E455的带有可写入特征值的服务,所以只需要发现对应UUID服务下的特征值并写入模板就可以了。这里还有一个除杂的操作比较有意思,就是在发现的各种蓝牙设备里面,只有蓝牙打印设备才具有某个特殊的服务UUID,所以这里还在业务层进行了除杂操作,就是将不符合UUID的外设,在连接之前就排除掉了,这样省去了连接到其他类型设备的冗余操作。

  • 发现特征值阶段

这里有个小技巧,涉及到我们业务上的一些有意思的东西,因为我们采用了CPCL指令集下发的形式,并且便携蓝牙打印机的型号比较多,需要定位到某一个型号的打印机,并且对应到是哪个快递公司的,才能唯一确定下发的模板。而通过对各个型号的打印机提供的服务的整理分析,大部分打印机都会有一个通用UUID的服务里提供了通用的写入特征值,可以写入对应的模板,但是有部分打印机的通用服务下的通用特征值并不能支持,所以这里需要根据相应的机型进行判断处理,将对应的写入特征值缓存到本地,特殊机型使用特殊的特征值写入。

还有一个问题,因为服务下的写入特征值是不同属性的,分为如下两种:CBCharacteristicWriteWithResponseCBCharacteristicWriteWithoutResponse,写入带回调和写入不带回调,而取用不同的属性的特征值,将直接影响到数据的写入操作,所以使用的时候要根据相应的业务进行特殊处理,这里我们项目中取用了带有写入回调属性的特征值。

  • 写数据阶段

首先最需要注意的就是,不管之前连接的设备是什么状态的,都要在写入数据之前进行判断,当前的设备是不是还在连接开启状态,只有连接开启状态的设备才能进行写入操作。

这里还有几个比较有意思的小事情。一个是关于写入类型的:因为系统给特征值的写入方法如下:- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;,这里需要注意type必须传入之前提到过的枚举类型,如果是带有回调的类型,可以在PrintManager里面实现相应的回调方法,并在相应的方法里面进行一些业务操作。

另一个是关于下发数据类型的:需要注意的是数据下发是以NSData类型下发的,而我们项目采用的是CPCL指令集下发的方案,移动端从后台直接获取的是一段文本,这里需要将文本类型转换成NSData类型,并且要注意使用相应的编码规范,这里我们使用kCFStringEncodingGB_18030_2000

还有一个是数据下发分段的:这个就比较有意思了,是在同一套代码,大部分机型正常打印,部分机型没有反应的情况下发现的这个问题,检查并多次调试了代码,发现仍然无法打印,所以就联系了打印机硬件厂商的技术人员,通过沟通和调试,发现了这个问题所在:因为打印机厂商的硬件系统都是自己烧的单片,部分硬件厂商对单次传入打印机的数据量做了限制,所以我们项目中将模板一次性下发的时候,部分机子处理不了,就将相应的数据忽略掉了,造成下发无效果的情况。所以,这里更新了下发方案:在选用特征值类型的时候就取用之前提到的带有回调类型的特征值,然后定义了一个全局参数,限制每次下发的数据量,将整体模板根据限制进行分片,在每次下发之后,监听下发数据回调方法,如果下发成功,则继续下发下一片数据,直到最后一片数据下发完成。然后修改代码,测试各种机型,全部通过~

而在解决数据分段的问题的同时,某个机型出现过本地模板下发机子正常打印,后台获取的模板下发机子无反应的奇怪情况。通过和硬件厂商技术人员的多次远程会议沟通,定位到了两个比较有意思的专业性问题:CPCL指令集格式和系统文本编码问题:因为CPCL指令对我们的移动端开发人员来说不是常用语言,需求排期内研究语法加写出可以使用的模板是可以的,但是有些CPCL本身的细节问题,还是有所欠缺。在硬件厂商技术人员的协助下,发现了原来我们的后台同事在移动端同事编码好的指令集中绑定数据的时候,因为后台系统本身的问题,会略微改动指令集的编码格式,涉及到Windows系统的文本BOM头问题,打印机识别不了;同时指令集有固定的格式,数据最后需要以命令PRINT+\r\n(回车符)结束,用以通知打印机数据发送完毕,可以打印。然而后台小伙伴处理过后的模板,因为编码的问题只剩下\n,打印机并不认同,和Android的小伙伴儿沟通过后,发现Android端存在同样的问题,所以最后和后台小伙伴、Android小伙伴一起商议决定,后台处理掉关于BOM的问题,下发纯文本文档模板,而两个移动端负责判断特殊机型,在模板最后拼接PIRNT+\r\n后,再编码下发给打印机。最后测试通过,完美解决了这个奇葩的问题~

  • 断开外设阶段

最后,发现、连接、下发数据打印,都正常完成了,一定不要忘记了,在业务层不再需要蓝牙打印功能的时候,将蓝牙连接断开,并且停止发现外设的操作,一方面是出于技术考虑的角度,虽然BLE4.0是低功耗的,但是不使用的时候还是要关闭,避免App对系统资源的一直占用;另一方面,出于业务角度考虑,不使用的时候关闭蓝牙,可以节省电量,降低手机发热,给App的用户更好的使用体验。

因为项目方案是采用的单例,在关闭扫描外设和断开外设连接的时候,需要注意涉及到的业务层是不是都进行了避险处理,并且PrintManager采用了协议代理+Block回调的方式处理跟业务层的交互,所以一定要注意Block和Delegate的判空处理,避免出现野指针崩溃的问题。

细节注意问题总结

最后总结了一下本次方案开发过程中遇到的或者需要注意的一些问题,以及建议解决方法:

  • 因为CoreBluetooth框架是硬件回调系统,存在不确定性,所以不管是系统蓝牙的哪个阶段的回调,都要留心蓝牙状态、Error处理。

  • 因为使用了单例的方式管理了蓝牙相关开发,所以享受便利性的同时,一定要注意谨慎使用Delegate和Block,使用时一定要加判空,防止野指针奔溃发生。

  • 扫描外设阶段,系统框架存在蓝牙状态不稳定的情况,在UpdateState的系统回调里面,一定要判断当前硬件状态,如果状态更新为可用,再继续进行相应的处理。

  • 扫描外设阶段的去重处理。如果业务中需要将扫描到的外设显示到列表,不去重就会发现列表里边出现一堆重复外设信息,可以通过Service或者Characteristic来判断是否为业务所需外设。

  • 数据下发这块,不单单只有我们采用的CPCL指令下发的模式,还有很多其他方式,单条十六进制指令下发、ESC指令下发、EPL指令下发、ZPL指令下发等等,大部分都类似,有兴趣的小伙伴儿可以研究一下其他方式。

  • 数据下发的分段问题,不同机型可能存在不同的单次数据受用量,上边已经详细描述过了,这里就不赘述了,还是要注意这个点。

  • 最后就是指令的格式问题,不同的指令格式一定要注意,避免因为指令本身的问题造成不必要的麻烦。

资源推荐

先推荐一个iOS端蓝牙开发的调试利器:LightBlue。手机安装LightBlue,打开后可以自动扫描范围内支持的蓝牙设备,并且可以清晰地看到对应设备的名称、信号强度、服务、特征值等等信息,是iOS端蓝牙开发调试不可或缺的工具。

然后推荐一下个人封装的iOS蓝牙开发管理类(标题图片所示),现已开放到Github上以供大家参考:ZJCBluetoothManager。当前还是比较简单的一个初期框架,后期会持续不定时更新优化,希望能帮助到正在项目中使用到蓝牙的各位小伙伴。当然如果各位小伙伴在iOS蓝牙开发过程中遇到了什么困惑的问题,也欢迎提出来一起讨论解决!

祝各位好!

以上

文章目录
  1. 1. 需求来源
  2. 2. 需求分析
  3. 3. 代码实现与问题解决
    1. 3.1. CoreBluetooth框架介绍
    2. 3.2. 技术实现
  4. 4. 细节注意问题总结
  5. 5. 资源推荐