(更新) 这两天在网上又翻找了一下,发现已经有位大神对蓝牙打印机这一块操作作了详细的讲解,大家可以移步iOS Bluetooth 打印小票(二)进行学习。
最近由于项目需要,了解了下蓝牙相关的知识,实现了通过iPhone来操作蓝牙热敏打印机打印小票的功能,在此记录总结。
背景
网上已经有很多关于蓝牙知识的介绍,此处不做详述,可以参考这篇文章iOS蓝牙知识快速入门,这里做一个说明,对于目前大多数已经支持到4.0版本的蓝牙设备,苹果本身已经提供了对应的框架 CoreBluetooth.framework
, 对于4.0以下的蓝牙设备,连接起来比较麻烦,不过网上也有不少解决方法,故此篇文章只是对于蓝牙4.0设备连接的操作说明。
蓝牙
苹果对于蓝牙设备分为2种角色,一种是中心设备Central
,另一种是外围设备Peripheral
,这两种角色决定了谁来发起连接,谁是被动连接,同一种设备,在不同场合下扮演的角色可能不相同,比如iPhone,在本文中所扮演的就是中心设备角色,对于被操作的打印机,就是外设;而在文件传输中,iPhone所扮演的角色可以是中心设备,也可以是外围设备,这取决于具体场合。
那么对于一个完整的蓝牙连接流程,可以分为下面几大步骤。
1. 扫描
开发之前提一句,蓝牙功能一般是抽离出来做成一个单独的工具类,方便在其他地方使用,最好的就是写成一个单例,方便全局调用,毕竟蓝牙这东西初始化也就那么一次,其他的时候基本都是连接->传输数据这么些操作,状态啥的也都是能通过代理来获取的。
-
初始化蓝牙模块
引入代理协议,用于接收响应蓝牙的代理事件,如状态变更,连接成功/失败,数据读写等:
#import
@interface MHBLEManager () @property (strong, nonatomic) CBCentralManager *centralManager;/**<蓝牙中心管理器*/ @property (strong, nonatomic) NSMutableArray *listPerpheralsArray;/**<搜索到的蓝牙设备列表*/ @property (strong, nonatomic) CBPeripheral *connectedPeripheral;/**< 当前连接的外设*/ @property (strong, nonatomic) CBCharacteristic *character;/**<连接的外设对应需要操作的特征码*/ @end 接着在初始化方法中创建
CBCentralManager
,此处可以作为单例来处理,一般只用初始化一次:- (instancetype)init { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [super init]; self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; }); return instance; }
当初始化完成后,蓝牙所有的状态变化都会触发
centralManagerDidUpdateState:
代理方法,从此处获取蓝牙状态变更,进行后续操作:- (void)centralManagerDidUpdateState:(CBCentralManager *)central { switch (central.state) { case CBCentralManagerStatePoweredOn: //蓝牙已打开 break; case CBCentralManagerStatePoweredOff: //蓝牙未打开 break; case CBCentralManagerStateUnsupported: //SDK不支持 break; case CBCentralManagerStateUnauthorized: //程序未授权,目前版本暂未涉及未授权状态 break; case CBCentralManagerStateResetting: //蓝牙连接暂时丢失 break; case CBCentralManagerStateUnknown: //蓝牙未知状态 break; } }
ok,到此为止,蓝牙管理器已经初始化完毕,并且系统蓝牙状态切换,我们也能通过代理来获取到了,下一步就是对周边蓝牙进行搜索了。
-
扫描蓝牙
通过蓝牙管理器来调用搜索方法,需要注意的是,一旦开始搜索蓝牙设备,将会持续进行搜索,除非通过
[_centralManager stop]
方法来停止搜索操作:- (void)startScanPeripherals { //开始搜索蓝牙设备,此方法一旦运行将会一直搜索,除非通过[_centralManager stop]方法来停止 [_centralManager scanForPeripheralsWithServices:nil options:nil]; }
搜索过程中,如果发现了蓝牙设备,将会在
centralManager: didDiscoverPeripheral: advertisementData: RSSI:
代理中获取搜索到的设备信息:- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary
*)advertisementData RSSI:(NSNumber *)RSSI { if (peripheral.name.length <= 0) { return ;//忽略没有名称的蓝牙设备 } if (_listPerpheralsArray.count == 0) { //设备列表为空,则新增设备到数组中 [_listPerpheralsArray addObject:peripheral]; } else { //设备列表有值,则需判断是否已经存在此设备信息 BOOL isExist = NO; for (int i = 0; i < _listPerpheralsArray.count; i++) { CBPeripheral *per = [_listPerpheralsArray objectAtIndex:i]; if ([per.identifier.UUIDString isEqualToString:peripheral.identifier.UUIDString]) { isExist = YES; //此设备已存在列表中,则做覆盖操作 [_listPerpheralsArray replaceObjectAtIndex:i withObject:peripheral]; } } if (!isExist) { //无此设备信息,则做添加操作 [_listPerpheralsArray addObject:peripheral]; } } } 由于此代理方法不具备筛选重复搜索到的蓝牙设备操作,所以此代理触发的频率很频繁,如果不做限制,那么机器必将卡死(亲测...),所以在此方法中增加了设备去重的判断,同时也做了筛选掉无名设备操作,最后拿到的数组可以直接显示在界面上展示给用户了。
一般来说,此处代理需要加一个数据传值的操作,可以是通知、delegate、block或者target-action方式,用于实时绑定最新的设备数组数据到界面列表中。
到此蓝牙的初始化部分已经完成,拿到具体的设备数据后,下面就可以定向连接目标设备了。
2. 连接
连接分为3个部分:连接指定的蓝牙设备、获取到指定设备的服务列表
、针对不同的服务获取特征值列表
。
-
连接蓝牙
首先需要选择一个指定的蓝牙设备,此处直接从_listPerpheralsArray
中选择一个设备,然后通过connectPeripheral: options:
方法连接设备,并在centralManager: didConnectPeripheral:
代理中处理结果://简历连接 - (void)connectPeripheral:(CBPeripheral *)peripheral { [_centralManager connectPeripheral:peripheral options:nil]; } //连接建立成功后代理 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { //记录当前连接的外设 _connectedPeripheral = peripheral; //可以在此处做停止搜索的操作,如果有必要的话 [_centralManager stopScan]; //代理设置,用于后续的服务码和特征码的获取 peripheral.delegate = self; [peripheral discoverServices:nil];//获取此设备的服务码列表 }
-
获取蓝牙设备服务(services)与特征(characteristic)
其实到了这一步,通过连接成功的代理方法,记录已连接上的设备,已经是初步连接成功了,后续只需要对设备进行
服务码
和特征码
的获取操作即可://发现服务代理 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error { if (error) { return; } for (CBService *service in peripheral.services) { //遍历设备中所有的服务码,对其进行特征码获取操作 /**注意!!如果蓝牙设备作用目的明确,可以在此处做一个判断,来限制所需服务码和对应特征码的获取操作*/ [peripheral discoverCharacteristics:nil forService:service]; } } //发现特征代理 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error { if (error) { return; } for (CBCharacteristic *character in service.characteristics) { CBCharacteristicProperties properties = character.properties; if (properties & CBCharacteristicPropertyWrite) { //此处判断是否为可写特征,如果为可写特征记录下来 _character = character; break; } } }
通过以上两个代理方法可以拿到该蓝牙设备的所有的特征码,特征码就是后续中心设备用来与蓝牙外设之间的数据传输操作。不过一般来说,一台蓝牙设备的服务码有很多个,对应的特征码也有很多,其实完全可以依据蓝牙外设的工作性质来限定其服务码与特征码的搜索,达到精准获取。比如本文主要的蓝牙对象 -- 小票打印机,其主要的特征码就是能用来进行数据写入和读取的,那么网上搜一搜也能很容易的获取到,基本都差不多,那么在搜索服务码与特征码的时候就能进一步筛选控制了。
3. 数据传输
蓝牙的中心设备与外围设备数据传输有多种方式:读取、写入、通知,针对蓝牙打印机,我们这里主要使用的是写入方法,将封装好的数据通过中心设备发送到外围设备上,并且指定特征码进行打印操作:
//发送打印数据以及对应的操作特征码
-(void)sendPrintData:(NSData *)printData
{
[_connectedPeripheral writeValue:printData forCharacteristic:_character type:CBCharacteristicWriteWithoutResponse];
}
此处printData
是已经封装完毕的打印数据,下面会说到如何封装此数据,另外对于参数CBCharacteristicWriteWithResponse
指的是打印结果需要代理来返回,如果不需要返回结果,还有一个值可以设置:CBCharacteristicWriteWithoutResponse
。
结束
到此为止,蓝牙搜索、连接、数据传输已经完成,所有的代理只说了成功部分,失败的代理大家可以进入到CBCentralManagerDelegate
与CBPeripheralDelegate
协议里面进行查看。
打印
蓝牙配对部分完成了,接下来才是重头戏:如何对连接的蓝牙外设进行数据传输,如何操作对应的功能,如何封装等待传输的数据?下面一一来说明。
指令
先从数据说起,一般需要我们发送给外围蓝牙设备的数据,都是以字节形式传输的,那么在传输数据之前,有个更重要的事情需要完成,就是数据封装,此处所对应的蓝牙外设前面已经提到了,蓝牙小票打印机,具体的型号是佳博热敏票据打印机KS-2160II
,其使用的是常见的ESC/POS指令集,其他类型的打印机需要对照具体的指令说明书来对数据进行封装设置,大体上只要能看懂具体指令对应的操作功能,基本都是相通的。
-
ESC/POS打印指令集
WPSON StandardCode for Printer 是EPSON公司自己制定的针式打印机的标准化指令集,现在已成为针式打印机控制语言事实上的工业标准。ESC/POS打印命令集是ESC打印控制命令的简化版本,现在大多数票据打印都采用ESC/POS指令集。其显著特征是:其中很大一部分指令都是以ESC控制符开始的一串代码。
-
介绍
一般来说,打印机接收指的令支持三种格式:ASCII、十进制、十六进制,一般我们选用十六进制来设置操作数据,更方便的去进行字节上的转换。另外需要注意的一点是,发送数据时,打印机会有单次发送数据的长度限制,超过限制后,部分打印机出来的数据就是乱码,一般来说需要对数据进行分段传输,需要设置一个合适的值,具体需要查看打印机的指令手册,此处我们设置为20,基本可以兼容大部分的打印机数据传输了。
-
理解
那么,如何读懂指令数据?
上图是一个让打印机走纸n行的操作指令,可以看到格式中,十六进制码一行,给出了2个固定数值,还有一个变量
n
:前2个固定数值代表的是机器的操作指令,后一个变量由用户来控制,指的是具体要移动的行数。这么一看是不是很好理解?没错,其实能看懂这一种操作指令的设置方式,其他的基本都是触类旁通的了。
再来看一组复杂点的二维码设置操作:
这是二维码数据设置的其中一步
存储数据到符合存储区
,乍一看,好长的数值...还有这么多变量参数,其实仔细看他给出的格式、范围说明以及描述,不难理解。先确定数值设置格式:{0x1D,0x28,0x6B,pL,pH,0x31,0x50,0x30}
,格式出来了,可以看到,需要解决的2个变量是pL
与pH
,再看范围说明最后一行k = (pL+pHx256)-3
,此处k为数据的长度(dk),换算出来:k+3 = pL+pHx256; ---> 假设 x = k+3 x = pL + pHx256; ---> 此处可以看出 pL其实是 x/pH 的余数,那么就有了: pL = x%pH; ---> 由于pH未知,同理可以用256来替换 pL = x%256; ---> 由于前面所示,x/pH = 256...pL,可得出x/pH的整数部分为256,那么换算一下: pH = x/256
换算过程不算复杂,需要稍微思考下,稍微绕一点的地方在于需要能看出来
x = pL+pH*256
这个公式其实就是个模运算的反过程,可以理解为:x/256 = pH,余pL
。最后把x的值带入pL和pH的公式中得到:
pL = (k+3)%256
和pH = (k+3)/256
,k的值前面说了,为二维码数据的长度值,那么用代码来表示这个公式如下:NSStringEncoding enc = CFStringConvertEncodingToNSStringEncoding(kCFStringEncodingGB_18030_2000);//设置编码格式 NSData *infoData = [data dataUsingEncoding:enc];//将二维码字符数据转成字节码 NSInteger x = infoData.length + 3; NSInteger pL = x % 256; NSInteger pH = x / 256;
结合数据格式
{0x1D,0x28,0x6B,pL,pH,0x31,0x50,0x30}
,就得出了具体的字节码:NSMutableData *printerData = [[NSMutableData alloc] init]; Byte dataBytes [] = {0x1D,0x28,0x6B,pL,pH,0x31,0x50,0x30}; [printerData appendBytes:dataBytes length:sizeof(dataBytes)];//将操作码转字节码后添加到待发送数据对象中 [printerData appendData:infoData];//添加二维码字符对应的字节码到待发送数据对象中
以上为一个操作指令的数据转换过程,实际中的指令设置,是需要根据具体的数据排版来设置不同的操作指令,比如,初始化清除打印缓冲区(重要)、对其方式、字体大小、行距设置等等。
示例
此处不做更多的示例了,有另外一位大神已经做了详细的指令操作说明,如果需要更多的示例讲解,可以移步到 iOS开发之蓝牙/Socket链接小票打印机(一)。