iOS 蓝牙随笔
最近搞了一段时间的蓝牙,把一些收获体会和大家分享一下,其实网上大神们写的蓝牙相关的都比较实用全面了,我主要是想贴一下我项目里不太一样的地方。
蓝牙的流程什么的在这里我就不赘述了,大家可以自行google。另外给大家推荐一个大牛用block封装的蓝牙---babyBlueTooth,个人感觉还是不错的。言归正传,
首先,需要仔细看看硬件的说明文档(由于本人项目硬件比较坑,文档不详细害的我走了很多的弯路),对蓝牙的操作常用的无非就是read write 和 notify。根据一般步骤:
1,建立中心角色 2,扫描外设(discover)3,连接外设(connect) 4,扫描外设中的服务和特征(discover)(这些都是一般流程,就不再重复了),现在说一下扫描到服务和特征后的一些注意事项,一个设备里的服务和特征往往比较多,大部分情况下我们只是关心其中几个,所以一般会在发现服务和特征的回调里去匹配我们关心那些,这些就要根据硬件的文档具体操作了,例如我的:
for (CBCharacteristic *characteristic in service.characteristics) { NSLog(@"service:%@ 的 Characteristic: %@,characterustics的权限是什么:%lu",service.UUID,characteristic.UUID,characteristic.properties); _characteristic = characteristic; CBUUID *uid = [CBUUID UUIDWithString:@"0xFFF6"]; if ([characteristic.UUID isEqual:uid]) { [peripheral setNotifyValue:YES forCharacteristic:characteristic]; NSLog(@"开始通讯"); [manager stopScan]; //执行write } }
执行操作后就到了 5,外设做数据交互(explore and interact),数据的读分为两种,一种是直接读(reading directly),另外一种是订阅(subscribe)。从名字也能基本理解两者的不同。实际使用中具体用一种要看具体的应用场景以及特征本身的属性。特征有个properties字段(characteristic.properties),它是一个整型值,有如下几个定义:
//打印出 characteristic 的权限,可以看到有很多种,这是一个NS_OPTIONS,就是可以同时用于好几个值,常见的有read,write,notify,indicate,知知道这几个基本就够用了,前连个是读写权限,后两个都是通知,两种不同的通知方式。 /* typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties) { CBCharacteristicPropertyBroadcast = 0x01, CBCharacteristicPropertyRead = 0x02, CBCharacteristicPropertyWriteWithoutResponse = 0x04, CBCharacteristicPropertyWrite = 0x08, CBCharacteristicPropertyNotify = 0x10, CBCharacteristicPropertyIndicate = 0x20, CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40, CBCharacteristicPropertyExtendedProperties = 0x80, CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100, CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200 }; */
注意:这个属性也有可能是叠加的,例如,返回的characteristic.properties = 0x26,即同时有三个属性:CBCharacteristicPropertyWriteWithoutResponse CBCharacteristicPropertyWrite CBCharacteristicPropertyIndicate
我的项目中交互的特征properties的值是0x10,表示你只能用订阅的方式来接收数据。我这里是用订阅的方式,启动订阅的代码如下:
[_peripheral setNotifyValue:YES forCharacteristic:_readCharacteristic];
注意:这句是必须要加的,否则硬件是不会给你返回数据的。
假如propertites是订阅,当设备有数据返回时,同样是通过一个系统回调通知我,就会走如下方法:- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error. 这个方法是你写入数据后就会调用的,前提是设置了代理。我在开发的过程中就遇到了这个问题,我执行write操作后这个方法不走,开始我以为是流程逻辑的问题,后来终于解决了,是因为写入不成功,所以,假如这个方法不走的话原因就一个那就是蓝牙没有收到你发的正确的数据。因为这个函数都是以"did"开头的,函数不用你调用,达到条件后系统后自动调用,没有调用仅是因为没有达到条件,当然,前提是你设置了代理。
到这里数据就能发能收了基本的需求也就OK了。
另外,我还要说一下发送数据的一些小的注意的地方,我觉得我项目中数据还是比较麻烦的,涉及的地方也比较多。蓝牙接收的数据是NSData格式的,返回的也是NSData的,所以一般都要转化成NSData的。下面是几种格式的转化(均为转载,特此申明):
1,字符串转为NSData(非编码):
- (NSData *)convertHexStrToData:(NSString *)str { if (!str || [str length] == 0) { return nil; } NSMutableData *hexData = [[NSMutableData alloc] initWithCapacity:8]; NSRange range; if ([str length] % 2 == 0) { range = NSMakeRange(0, 2); } else { range = NSMakeRange(0, 1); } for (NSInteger i = range.location; i < [str length]; i += 2) { unsigned int anInt; NSString *hexCharStr = [str substringWithRange:range]; NSScanner *scanner = [[NSScanner alloc] initWithString:hexCharStr]; [scanner scanHexInt:&anInt]; NSData *entity = [[NSData alloc] initWithBytes:&anInt length:1]; [hexData appendData:entity]; range.location += range.length; range.length = 2; } // NSLog(@"hexdata: %@", hexData); return hexData; }
例如,NSString *modelStr = @“1311212313”;
NSLog(@“NSData = %@”,[self convertHexStrToData:modelStr]);
打印结果:
NSData = <13112123 13>,length = 5
注意,与下面的方法的对比
-(Byte)strToByte:(NSString *)str { int endMinutes = [str intValue]; Byte endMinuteByte = (Byte)0xff&endMinutes; return endMinuteByte; }
还是上面的字符串,进行下面操作
NSString *modelStr = @"1311212313"; Byte nameUser[10]; for (int i = 0; i<10;i++ ) { NSString *byteStr = [modelStr substringWithRange:NSMakeRange(i, 1)]; nameUser[i] = [self strToByte:byteStr]; } NSData *nameUserData = [NSData dataWithBytes:nameUser length:12]; NSLog(@"NSData = %@ %zd",nameUserData,nameUserData.length);
打印结果:NSData = <01030101 02010203 0103>,length = 10
上面两种情况是转成 相应NSData的方法,可以根据文档提供的数据长度的要求使用。
2,求累加和(校验和)(CHECKSUM)的求法
顾名思义,累加和即是所求数据依次累加的和,一般的规则就是前n-1个字节之和的低字节,CHECKSUM=0x100-CHECKSUM(上一步的校验和),得到的cs字节长度1byte,类型为NSData,具体代码如下,
- (NSData *)getCheckSum:(NSData *)byteStr{ int length = (int)byteStr.length; // NSData *data = [self hexToBytes:byteStr]; Byte *bytes = (unsigned char *)[byteStr bytes]; Byte sum = 0; for (int i = 0; i<length; i++) { sum += bytes[i]; } int sumT = sum; int at = 256 - sumT; printf("校验和:%d\n",at); printf("累加和:%d\n",sumT); if (at == 256) { at = 0; } NSString *str = [NSString stringWithFormat:@"%@",[self ToHex:sumT]]; return [self hexToBytes:str]; } //将十进制转化为十六进制 - (NSString *)ToHex:(int)tmpid { NSString *nLetterValue; NSString *str =@""; int ttmpig; for (int i = 0; i<9; i++) { ttmpig=tmpid%16; tmpid=tmpid/16; switch (ttmpig) { case 10: nLetterValue =@"A";break; case 11: nLetterValue =@"B";break; case 12: nLetterValue =@"C";break; case 13: nLetterValue =@"D";break; case 14: nLetterValue =@"E";break; case 15: nLetterValue =@"F";break; default: nLetterValue = [NSString stringWithFormat:@"%u",ttmpig]; } str = [nLetterValue stringByAppendingString:str]; if (tmpid == 0) { break; } } //NSLog(@"16进制是:%@",str); //不够一个字节凑0 if(str.length == 1){ return [NSString stringWithFormat:@"0%@",str]; }else{ return str; } } - (NSData *)hexToBytes:(NSString *)str { NSMutableData* data = [NSMutableData data]; int idx; for (idx = 0; idx+2 <= str.length; idx+=2) { NSRange range = NSMakeRange(idx, 2); NSString* hexStr = [str substringWithRange:range]; NSScanner* scanner = [NSScanner scannerWithString:hexStr]; unsigned int intValue; [scanner scanHexInt:&intValue]; [data appendBytes:&intValue length:1]; } return data; }
例如,求上节中的data的累加和:
NSData *csData = [self getCheckSum:[self convertHexStrToData:modelStr]];csData即要的结果。
数据类型的转化还有最基本的byte[]的操作,在这里就不说了。
根据BLE4.0协议,蓝牙数据缓存区最大接收20字节,如果数据包过大的话,可以通过拆分成小的数据包来操作,具体的可以通过使用定时器来发送或者直接通过循环发送(在我项目中两种我都试过了,是都可以的,但是还是那句话,还是要具体看文档操作的),但是要注意的是,定时器的精度不是很高,(据说低于50ms就有误差了,本人没有实际测)。