前言
在重庆这一年,公司产品主要产品是智能锁,兼带接入一些其余的智能家居产品,lifeSmart,物联等。几乎是跟蓝牙打了一年的交道,不写点什么实在说不过去,也必须写点什么证明曾经来过,走你。
什么是库?
库是共享程序代码的方式,一般分为静态库和动态库。程序运行分为三个步骤:编译,链接,执行。编译作用是将原代码(程序员手写代码)翻译成目标代码(机器的二进制代码),会生成目标文件(有多少个实现文件就会生成多少个目标文件)。链接就是将各个有关联的目标文件和库文件链接,是由机器完成的,在此过程中经常出现linker错误。
1,静态库与动态库
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
2,iOS静态库形式和动态库形式:
静态库:.a和.framework
动态库:.dylib和.framework
3,ramework静态库和动态库的区分:
系统的.framework是动态库,我们自己建立的.framework是静态库
4,.a和.framwork的区别:
.a是一个纯二进制文件,.framework中除了有二进制文件外还有资源文件。
.a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。
.a + .h + sourceFile = .framework
蓝牙开发基础操作
1,建立中心管理者
//引用一下库的头文件 #import <CoreBluetooth/CoreBluetooth.h>
//遵循<CBCentralManagerDelegate,CBPeripheralDelegate>两个代理
CBCentralManager *manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];
2,扫描外设(discover)
调用1方法后,系统会自动检测手机蓝牙状态,并进入此方法。当发现蓝牙状态是开启状态,你就可以进行下一步扫描外设操作了,如果为关闭状态,系统会自动弹出让用户去设置蓝牙,这个不需要我们开发者关心。
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
if (central.state==CBCentralManagerStatePoweredOn) {
//NSLog(@"蓝牙已就绪,开始扫描外设");
//开始扫描
[self.manager scanForPeripheralsWithServices:nil options:nil];
}else{
//NSLog(@"请检查蓝牙状态");
}
}
3,扫描到外设,判断是否为目标设备,之后进行连接操作
//扫描到设备会进入方法
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
if ([peripheral.name isEqualToString:@""]) {
self.thePerpher = peripheral;
//停止扫描
[central stopScan];
//连接设备
[central connectPeripheral:peripheral options:nil];
}
}
这里有一个值得注意的地方,安卓是可以直接扫到蓝牙设备的MAC地址然后进行连接的,但是自从iOS7以后就无法从API直接获取设备的MAC地址,所以绝大多数做法通常是判断设备名字peripheral.name,像我们公司的锁,蓝牙名称一般都是dyBKMWWtwR这种,所以做法是截取头两位,判断是否含有“dy”字段。
4,连接设备成功,开始扫描服务
//连接到外设成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
//这里遵循的代理是CBPeripheralDelegate,后续的扫描服务回掉,扫描特征回掉,都是此代理里面的回掉方法
[peripheral setDelegate:self];
//扫描服务
[peripheral discoverServices:nil];
}
//连接外设失败
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
NSLog(@"连接到外设 失败!%@ %@",[peripheral name],[error localizedDescription]);
}
4,扫描到服务(Services),开始扫描特征(Characteristics)
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
if (error) {
NSLog(@"扫描外设服务出错:%@-> %@", peripheral.name, [error localizedDescription]);
return;
}
//开始扫描特征
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:nil forService:service];
}
}
5, 扫描到特征
//扫描到特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
if (error) {
NSLog(@"扫描特征失败!");
return;
}
//获取Characteristic的值
for (CBCharacteristic *characteristic in service.characteristics){
if ([characteristic.UUID.UUIDString isEqualToString:@"DE4001"]) {
//外设订阅特征的通知,否则无法收到外设返回给手机的数据
[self.thePerpher setNotifyValue:YES forCharacteristic:characteristic];
}else if ([characteristic.UUID.UUIDString isEqualToString:@"DE4002"]) {
//CBCharacteristic 全局对象
self.theSakeCC = characteristic;
}else if ([characteristic.UUID.UUIDStringisEqualToString:@"DE4003"]) {
//CBCharacteristic 全局对象
self.encryptSakeCC = characteristic;
}
}
}
这里是一个很重要的地方,特征值一般是硬件定义好的,比如说我这里,DE4001是回调数据read特征值(只能用来读数据)。DE4002是加密透传传输Write特征值,DE4003是不加密透传传输Write特征值,只能用来写数据。
6,给硬件发送数据
[self.thePerpher writeValue:sendData forCharacteristic:self.theSakeCC type:CBCharacteristicWriteWithoutResponse];
这里要注意在实际开发过程中,根据公司的通信协议,选择5中保存的加密或者不加密特征值发送数据。
7,扫描到具体的值(硬件给App回掉数据的地方)
//扫描到具体的值
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error {
if (error) {
NSLog(@"扫描特征值失败:%@-> %@",peripheral.name, [error localizedDescription]);
return;
}
//正常情况下characteristic.value需要进行解密操作操作之后才能拿来判断,实际开发过程中根据协议来处理
if (characteristic.value) {
NSString *withoutSpaceStr = [[self dataToHexStringWithData:characteristic.value] substringToIndex:4];
if ([withoutSpaceStr isEqualToString:@"0100"] ) {
//请输入管理员密码
}
if ([withoutSpaceStr isEqualToString:@"1101"] ) {
//管理员密码错误
}
if ([withoutSpaceStr isEqualToString:@"0100"] ) {
//开锁失败,时间误差不在允许的范围内
}
}
}
9,断开连接(disconnect)
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
断开与硬件的连接调用此方法,但是调用此方法后并不会立马断开,只是APP层面断开了连接,物理层并没有完全断开,大约5s后才能完全断开,所以如果业务需求上有需要立马断开的,建议除了调用此方法前,同时发送一个跟硬件约定的断开命令,如果是封装的蓝牙单例类,再释放此单例。
蓝牙通讯常见加密方式
蓝牙开发过程中遇到的问题汇总
1,扫描设备时根据peripheral.name判断目标设备,同一设备不同状态下蓝牙名称跟更新不及时,会出现缓存问题
比如我们的锁,普通状态是dyBKMWWtwR,处于等待添加配对状态时dyBKMWWtwA,解决办法取广播中的蓝牙名称,实时更新不会存在缓存问题
[advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
2,无论怎么调试,确认发送数据加密,格式等都没问题,但writeValue之后都收不到消息
我遇到这个问题的原因是跟writeValue的type有关,type有两个值,CBCharacteristicWriteWithoutResponse和CBCharacteristicWriteWithResponse,这两个值,都可以收到回调消息,之前遇到一个坑,用的是WithResponse这个type,不管怎么调试,都收不到硬件返回的消息,安卓却可以,一度开始怀疑人生,百思不得其解之后,尝试着换成了WithoutResponse,秒收!在此大胆的猜想应该是硬件那边的某些设置问题。
3,拼装的NSData数据MD5之后,经常出现同一数据结果不一样
常用的md5算法如下所示,但是当我加密一长串字节数组,比如<0213 2123 2123 1020 2123 0002>时,结果会发生变化,根本原因是[data bytes]返回的结果不一样。 解决办法,CC_MD5方法第二个参数不用original_str的结果,直接用传入的字节数组data长度。将CC_MD5方法替换成这个CC_MD5(input, (uint)data.length, result)即可,至于为什么[data bytes]返回的结果会不一样,一阵google之后尚未找到合理的解释,有知道的同学可以科普一下。
+ (NSString*)getMD5WithData:(NSData *)data {
const char* original_str = (const char *)[data bytes];
unsigned char digist[CC_MD5_DIGEST_LENGTH];
CC_MD5(original_str, (uint)strlen(original_str), digist);
NSMutableString* outPutStr = [NSMutableString stringWithCapacity:0];
for(int i =0; i<CC_MD5_DIGEST_LENGTH;i++){
[outPutStr appendFormat:@"%02x",digist[i]];
}
return [outPutStr lowercaseString];
}
网友评论