iOS开发之蓝牙/Socket链接小票打印机(二)

前言

上一篇主要介绍了部分ESC/POS指令集,包括一些常用的排版指令,打印位图指令等。另外,还介绍了将图片转换成点阵图的方法。在这篇文章中,将主要介绍通过蓝牙和Socket连接打印机,发送打印指令相关知识。这里将用到CoreBluetooth.frameworkCocoaAsyncSocket

蓝牙链接小票打印机

简介

蓝牙是一种支持设备间短距离通讯的无线电技术。iOS系统中,有四个框架支持蓝牙链接:

  • GameKit.framework: 只能用于iOS设备之间的连接,多用于蓝牙对战的游戏,iOS7开始已过期;
  • MultipeerConnectivity.framework:只能用于iOS设备之间的连接,从iOS7开始引入,主要用于替代GameKit
  • ExternalAccessory.framework:可用于第三方蓝牙设备交互,但是蓝牙设备必须经过苹果MFi认证;
  • CoreBluetooth.framework:目前最iOS平台最流行的框架,并且设备不需要MFi认证,手机至少4S以上,第三方设备必须支持蓝牙4.0;这里介绍的链接打印机就是使用此框架,因此开始前要确保打印机是支持蓝牙4.0的;

CoreBluetooth框架有两个核心概念,central(中心)和 peripheral(外设),它们分别有自己对应的API;这里显然是手机作为central,蓝牙打印机作为peripheral;

步骤

1.初始化中心设备管理

self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];

2. 确认蓝牙状态

设置代理后,会回调此方法,确认蓝牙状态,当状态为CBCentralManagerStatePoweredOn才能去扫描设备,蓝牙状态变化时,也会回调此方法

- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    NSString * state = nil;
    
    switch ([central state])
    {
        case CBCentralManagerStateUnsupported:
            state = @"The platform/hardware doesn't support Bluetooth Low Energy.";
            break;
        case CBCentralManagerStateUnauthorized:
            state = @"The app is not authorized to use Bluetooth Low Energy.";
            break;
        case CBCentralManagerStatePoweredOff:
            state = @"Bluetooth is currently powered off.";
            break;
        case CBCentralManagerStatePoweredOn:
            state = @"work";
            break;
        case CBCentralManagerStateUnknown:
        default:
            ;
    }
    
    NSLog(@"Central manager state: %@", state);
}

3. 扫描外设

调用此方法开始扫描外设

注意:第一个参数指定一个CBUUID对象数组,每个对象表示外围设备正在通告的服务的通用唯一标识符(UUID)。此时,仅返回公布这些服务的外设。当参数为nil,则返回所有已发现的外设,而不管其支持的服务是什么。

[self.centralManager scanForPeripheralsWithServices:nil options:nil];

当扫描到4.0外设后会回调此方法,这里包含设备的相关信息,如名称、UUID、信号强度等;

/*
 扫描,发现设备后会调用
 */
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    NSString *str = [NSString stringWithFormat:@"----------------发现蓝牙外设: peripheral: %@ rssi: %@, UUID:  advertisementData: %@ ", peripheral, RSSI,  advertisementData];
    NSLog(@"%@",str);
    if (![self.peripherals containsObject:peripheral]) {
        [self.peripherals addObject:peripheral];
    }
}

4. 选择外设进行连接

调用此方法连接外设
[self.centralManager connectPeripheral:peripheral options:nil];

注意:第一个参数是要连接的外设。第二个参数options是可选的NSDictionary,系统定义了一下三个键,它们的值都是NSNumber (Boolean);默认为NO。当设置为YES,则应用进入后台或者被挂起后,系统会用Alert通知蓝牙外设的状态变化,效果是这样

锁屏

未锁屏

CBConnectPeripheralOptionNotifyOnConnectionKey;连接时Alert显示
CBConnectPeripheralOptionNotifyOnDisconnectionKey;断开时Alert显示
CBConnectPeripheralOptionNotifyOnNotificationKey;接收到外设通知时Alert显示
    [self.centralManager connectPeripheral:peripheral  options:@{
                                                                 CBConnectPeripheralOptionNotifyOnConnectionKey : @YES,
                                                                 CBConnectPeripheralOptionNotifyOnDisconnectionKey : @YES,
                                                                 CBConnectPeripheralOptionNotifyOnNotificationKey : @YES
                                                                 }];

连接成功或失败,都有对应的回调方法

/*
 连接失败后回调
 */
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%@",error);
}
/*
 连接成功后回调
 */
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
    peripheral.delegate = self;//设置代理
    [central stopScan];//停止扫描外设
    [peripheral discoverServices:nil];//寻找外设内所包含的服务
}

5. 扫描外设中的服务和特征

连接成功后设置代理peripheral.delegate = self,调用[peripheral discoverServices:nil];寻找外设内的服务。这里的参数是一个存放CBUUID对象的数组,用于发现特定的服务。当传nil时,表示发现外设内所有的服务。发现服务后系统会回调下面的方法:

/*
 扫描到服务后回调
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
    if (error)
    {
        NSLog(@"Discovered services for %@ with error: %@", peripheral.name, [error localizedDescription]);
        return;
    }
    for (CBService* service in  peripheral.services) {
        NSLog(@"扫描到的serviceUUID:%@",service.UUID);
        //扫描特征
        [peripheral discoverCharacteristics:nil forService:service];
    }
}

发现服务后,调用[peripheral discoverCharacteristics:nil forService:service];去发现服务中包含的特征。和上面几个方法一样,第一个参数用于发现指定的特征。为nil时,表示发现服务的所有特征。

/*
 扫描到特性后回调
 */
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{

    if (error)
    {
        NSLog(@"Discovered characteristics for %@ with error: %@", service.UUID, [error localizedDescription]);
        return;
    }
    
    for (CBCharacteristic * cha in service.characteristics)
    {
        CBCharacteristicProperties p = cha.properties;
        if (p & CBCharacteristicPropertyBroadcast) {//广播特征
            
        }
        if (p & CBCharacteristicPropertyRead) {//读取特征
            self.characteristicRead = cha;
        }
        if (p & CBCharacteristicPropertyWriteWithoutResponse) {//无反馈写入特征

        }
        if (p & CBCharacteristicPropertyWrite) {//有反馈写入特征
            self.peripheral = peripheral;
            self.characteristicInfo = cha;
        }
        if (p & CBCharacteristicPropertyNotify) {//通知特征             
                self.characteristicNotify = cha;
                [self.peripheral setNotifyValue:YES forCharacteristic:self.characteristicNotify];
            NSLog(@"characteristic uuid:%@  value:%@",cha.UUID,cha.value);
            
        }
    }
    
}

当扫描到写入特征时,保存,用于写入数据。

6. 写入数据

写入数据,我们只需要调用方法

[self.peripheral writeValue:subData forCharacteristic:self.characteristicInfo type:CBCharacteristicWriteWithResponse];

这里的self.peripheral就是连接的外设,self.characteristicInfo就是之前保存的写入特征;这里最好使用CBCharacteristicPropertyWrite特征,并且type选择CBCharacteristicWriteWithResponse。当写入数据成功后,系统会通过下面这个方法通知我们:

-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"====error%@",error);
    }else{
        NSLog(@"====写入成功  %@", characteristic);
    }
    
}

由于蓝牙设备每次可写入的数据量是有限制的,因此,我们需要将之前拼接的打印数据进行拆分,分批发送给打印机

- (void)printLongData:(NSData *)printContent{
    NSUInteger cellMin;
    NSUInteger cellLen;
    //数据长度
    NSUInteger strLength = [printContent length];
    if (strLength < 1) {
        return;
    }
    //MAX_CHARACTERISTIC_VALUE_SIZE = 120
    NSUInteger cellCount = (strLength % MAX_CHARACTERISTIC_VALUE_SIZE) ? (strLength/MAX_CHARACTERISTIC_VALUE_SIZE + 1):(strLength/MAX_CHARACTERISTIC_VALUE_SIZE);
    for (int i = 0; i < cellCount; i++) {
        cellMin = i*MAX_CHARACTERISTIC_VALUE_SIZE;
        if (cellMin + MAX_CHARACTERISTIC_VALUE_SIZE > strLength) {
            cellLen = strLength-cellMin;
        }
        else {
            cellLen = MAX_CHARACTERISTIC_VALUE_SIZE;
        }
        NSRange rang = NSMakeRange(cellMin, cellLen);
        //        截取打印数据
        NSData *subData = [printContent subdataWithRange:rang];
        //循环写入数据
        [self.peripheral writeValue:subData forCharacteristic:self.characteristicInfo type:CBCharacteristicWriteWithResponse];
    }
}

这里的MAX_CHARACTERISTIC_VALUE_SIZE是个宏定义,表示每次发送的数据长度,经笔者测试,当MAX_CHARACTERISTIC_VALUE_SIZE = 20时,打印文字是正常速度。但打印图片的速度非常慢,应该在硬件允许的范围内,每次发尽量多的数据。不同品牌型号的打印机,这个参数是不同的,笔者的蓝牙打印机该值最多到140。超出后会出现无法打印问题。最后笔者将该值定为MAX_CHARACTERISTIC_VALUE_SIZE = 120,测试了公司几台打印机都没有问题。

另外iOS9以后增加了方法maximumWriteValueLengthForType:可以获取写入特诊的最大写入数据量,但经笔者测试,对于部分打印机(比如我们公司的)是不准确的,因此,不要太依赖此方法,最好还是自己取一个合适的值。

注意:每个打印机都有一个缓冲区,缓冲区的大小视品牌型号有所不同。打印机的打印速度有限,如果我们瞬间发送大量的数据给打印机,会造成打印机缓冲区满。缓冲区满后,如继续写入,可能会出现数据丢失,打印乱码。

Socket链接小票打印机

简介

这里使用CocoaAsyncSocket开源框架,与打印机进行Socket连接。CocoaAsyncSocket中主要包含两个类:

  • GCDAsyncSocket:用GCD搭建的基于TCP/IP协议的socket网络库;
  • GCDAsyncUdpSocket:用GCD搭建的基于UDP/IP协议的socket网络库。

这里我们只用到GCDAsyncSocket,因此只需要将GCDAsyncSocket.hGCDAsyncSocket.m两个文件导入项目。

注意:手机和打印机必须在同一局域网下,设置到打印机的host和port。

步骤

1、遵循GCDAsyncSocketDelegate协议

@interface MNSocketManager()<GCDAsyncSocketDelegate>

2、声明属性

@property (nonatomic, strong) GCDAsyncSocket *asyncSocket;

3、初始化GCDAsyncSocket对象

self.asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()];

4、连接打印机

NSError *error = nil;
[self.asyncSocket connectToHost:host onPort:port withTimeout:timeout error:&error];

连接成功后会通过代理回调

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port {
    
}

5、发送数据给打印机

Timeout为负,表示不设置超时时间。这里的data就是上一篇中拼接的打印数据。

[self.asyncSocket writeData:data withTimeout:-1 tag:0];

写入完成后回调

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag {
    NSLog(@"写入完成");
}

6、断开连接

断开连接有以下几种方法

[self.asyncSocket disconnect];
[self.asyncSocket disconnectAfterReading];
[self.asyncSocket disconnectAfterWriting];
[self.asyncSocket disconnectAfterReadingAndWriting];

连接断开后回调

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err {
    NSLog(@"连接断开");

}

7、读取数据

读取到数据会回调

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    NSLog(@"读取完成");
}

网口打印机一般都支持状态查询,查询指令如下:


打印机状态查询指令

可以通过上一篇介绍指令拼接方法,查询打印机的状态。

总结

本篇只是简单介绍了,通过蓝牙和Socket连接打印机的方法。虽然可以初步完成连接和打印,但是,在真正的项目中使用还是远远不够的。这里还有很多情况需要考虑,比如连接断开、打印机异常、打印机缓冲区满、打印机缺纸等。我们可以针对自身的业务情况,进行相应的处理。

参考

Core Bluetooth Programming Guide

Getting the pixel data from a CGImage object

Core Bluetooth Programming Guide

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

推荐阅读更多精彩内容

  • 蓝牙简介 蓝牙( Bluetooth® ):是一种无线技术标准,可实现固定设备、移动设备和楼宇个人域网之间的短距离...
    Chefil阅读 2,041评论 2 19
  • 原文:http://www.myexception.cn/operating-system/2052286.htm...
    KYM1988阅读 1,945评论 2 2
  • 在写这个博客之前,空余时间抽看了近一个月的文档和Demo,系统给的解释很详细,接口也比较实用,唯独有一点,对于设备...
    木易林1阅读 3,358评论 3 4
  • 最近竞品公司出了一个接入蓝牙打印机的功能,作为竞争对手公司肯定不能少所以就给我分了任务,搞定蓝牙打印机 首先介绍一...
    呆北默阅读 3,271评论 12 10
  • 像灯光一样 总有一天我会完成我的梦想 眼神不要那么凶 这样不美 要 散发
    治愈的卡其色亮亮虾阅读 170评论 0 0