Showing Posts From

蓝牙

iOS蓝牙知识快速入门(详尽版)

以前写过几篇蓝牙相关的文章,但是没有涉及扫描、收发指令这些基础功能的实现。所以打算写一篇尽可能详尽的蓝牙知识汇总,一方面给有需要的同学看,一方面是对自己学习蓝牙的一个总结。 这篇文章的目的:教你实现设备的扫描,连接,数据收发,蓝牙数据解析。如果在实现上面任一功能遇到问题时,欢迎留下你的问题,我将进行补充,对于说法有误的地方也请老司机予以指正。目录0、思维导图 > 1、苹果对蓝牙设备有什么要求 > 2、操作蓝牙设备使用什么库 > 3、如何扫描 > 4、如何连接 > 5、如何发送数据和接收数据 > 6、如何解析数据 > 7、扩展思维导图第一次做图,大家凑合着看哈。这张是我总结的蓝牙知识的结构图,下面的内容将围绕这些东西展开进行。这张是蓝牙连接发送数据的流程图,下文进入coding阶段的讲解顺序,大家先有个大概印象,等阅读完本文再回来看这张图将理解的更深一些。 苹果对蓝牙设备有什么要求 BLE:bluetouch low energy,蓝牙4.0设备因为低功耗,所有也叫作BLE。苹果在iphone4s及之后的手机型号开始支持蓝牙4.0,这也是最常见的蓝牙设备。低于蓝牙4.0协议的设备需要进行MFI认证,关于MFI认证的申请工作可以看这里:关于MFI认证你所必须要知道的事情 在进行操作蓝牙设备前,我们先下载一个蓝牙工具LightBlue,它可以辅助我们的开发,在进行蓝牙开发之前建议先熟悉一下LightBlue这个工具。 操作蓝牙设备使用什么库 苹果自身有一个操作蓝牙的库CoreBluetooth.framework,这个是大多数人员进行蓝牙开发的首选框架,除此之外目前github还有一个比较流行的对原生框架进行封装的三方库BabyBluetooth,它的机制是将CoreBluetooth中众多的delegate写成了block方法,有兴趣的同学可以了解下。下面主要介绍的是原生蓝牙库的知识。 中心和外围设备如图所示,电脑、Pad、手机作为中心,心跳监听器作为外设,这种中心外设模式是最常见的。简单理解就是,发起连接的是中心设备(Central),被连接的是外围设备(Peripheral),对应传统的客户机-服务器体系结构。Central能够扫描侦听到,正在播放广告包的外设。 服务与特征 外设可以包含一个或多个服务(CBService),服务是用于实现装置的功能或特征数据相关联的行为集合。 而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节,外设,服务,特征对应的数据结构如下所示如何扫描蓝牙 在进行扫描之前我们需要,首先新建一个类作为蓝牙类,例如FYBleManager,写成单例,作为处理蓝牙操作的管理类。引入头文件#import <CoreBluetooth/CoreBluetooth.h> CBCentralManager是蓝牙中心的管理类,控制着蓝牙的扫描,连接,蓝牙状态的改变。 1、初始化 dispatch_queue_t centralQueue = dispatch_queue_create(“centralQueue",DISPATCH_QUEUE_SERIAL);     NSDictionary *dic = @{CBCentralManagerOptionShowPowerAlertKey : YES, CBCentralManagerOptionRestoreIdentifierKey : @"unique identifier" }; self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:dic];CBCentralManagerOptionShowPowerAlertKey对应的BOOL值,当设为YES时,表示CentralManager初始化时,如果蓝牙没有打开,将弹出Alert提示框 CBCentralManagerOptionRestoreIdentifierKey对应的是一个唯一标识的字符串,用于蓝牙进程被杀掉恢复连接时用的。 2、扫描 //不重复扫描已发现设备        NSDictionary *option = @{CBCentralManagerScanOptionAllowDuplicatesKey : [NSNumber numberWithBool:NO],CBCentralManagerOptionShowPowerAlertKey:YES};        [self.centralManager scanForPeripheralsWithServices:nil options:option]; - (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;扫面方法,serviceUUIDs用于第一步的筛选,扫描此UUID的设备 options有两个常用参数:CBCentralManagerScanOptionAllowDuplicatesKey设置为NO表示不重复扫瞄已发现设备,为YES就是允许。CBCentralManagerOptionShowPowerAlertKey设置为YES就是在蓝牙未打开的时候显示弹框 3、CBCentralManagerDelegate代理方法 在初始化的时候我们调用了代理,在CoreBluetooth中有两个代理,CBCentralManagerDelegate CBPeripheralDelegateiOS的命名很友好,我们通过名字就能看出,上面那个是关于中心设备的代理方法,下面是关于外设的代理方法。我们这里先研究CBCentralManagerDelegate中的代理方法 - (void)centralManagerDidUpdateState:(CBCentralManager *)central;这个方法标了@required是必须添加的,我们在self.centralManager初始换之后会调用这个方法,回调蓝牙的状态。状态有以下几种: typedef NS_ENUM(NSInteger, CBCentralManagerState{ CBCentralManagerStateUnknown = CBManagerStateUnknown,//未知状态 CBCentralManagerStateResetting = CBManagerStateResetting,//重启状态 CBCentralManagerStateUnsupported = CBManagerStateUnsupported,//不支持 CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,//未授权 CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,//蓝牙未开启 CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,//蓝牙启 } NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead”);该枚举在iOS10之后已经废除了,系统推荐使用CBManagerState,类型都是对应的 typedef NS_ENUM(NSInteger, CBManagerState{ CBManagerStateUnknown = 0, CBManagerStateResetting, CBManagerStateUnsupported, CBManagerStateUnauthorized, CBManagerStatePoweredOff, CBManagerStatePoweredOn, } NS_ENUM_AVAILABLE(NA, 10_0);- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;peripheral是外设类 advertisementData是广播的值,一般携带设备名,serviceUUIDs等信息 RSSI绝对值越大,表示信号越差,设备离的越远。如果想装换成百分比强度,(RSSI+100)/100,(这是一个约数,蓝牙信号值并不一定是-100 - 0的值,但近似可以如此表示) - (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;在蓝牙于后台被杀掉时,重连之后会首先调用此方法,可以获取蓝牙恢复时的各种状态 如何连接 在扫面的代理方法中,我们连接外设名是MI的蓝牙设备 - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{    NSLog(@"advertisementData:%@,RSSI:%@",advertisementData,RSSI);       if([peripheral.name isEqualToString:@"MI"]){        [self.centralManager connectPeripheral:peripheral options:nil];//发起连接的命令          self.peripheral = peripheral;     } }连接的状态 对应另外的CBCentralManagerDelegate代理方法 连接成功的回调 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;连接失败的回调 - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;连接断开的回调 - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;连接成功之后并没有结束,还记得CBPeripheral中的CBService和CBService中的CBCharacteristic吗,对数据的读写是由CBCharacteristic控制的。我们先用lightblue连接小米手环为例,来看一下,手环内部的数据是不是我们说的那样。其中ADVERTISEMENT DATA显示的就是广播信息。iOS蓝牙无法直接获取设备蓝牙MAC地址,可以将MAC地址放到这里广播出来FEEO是ServiceUUIDs,里面的FF01、FF02是CBCharacteristic的UUID Properties是特征的属性,可以看出FF01具有读的权限,FF02具有读写的权限。特征拥有的权限类别有如下几种: 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};如何发送并接收数据 通过上面的步骤我们发现CBCentralManagerDelegate提供了蓝牙状态监测、扫描、连接的代理方法,但是CBPeripheralDelegate的代理方法却还没使用。别急,马上就要用到了,通过名称判断这个代理的作用,肯定是跟Peripheral有关,我们进入系统API,看它的代理方法都有什么,因为这里的代理方法较多,我就挑选几个常用的拿出来说明一下。 1、代理方法 //发现服务的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error; //发现特征的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error; //读数据的回调 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error; //是否写入成功的回调   - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;2、步骤 通过这几个方法我们构建一个流程:连接成功->获取指定的服务->获取指定的特征->订阅指定特征值->通过具有写权限的特征值写数据->在didUpdateValueForCharacteristic回调中读取蓝牙反馈值 解释一下订阅特征值:特征值具有Notify权限才可以进行订阅,订阅之后该特征值的value发生变化才会回调didUpdateValueForCharacteristic 3、实现上面流程的实例代码 //连接成功 - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{     //连接成功之后寻找服务,传nil会寻找所有服务     [peripheral discoverServices:nil]; }//发现服务的回调 - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{    if (!error) {        for (CBService *service in peripheral.services) {            NSLog(@"serviceUUID:%@", service.UUID.UUIDString);            if ([service.UUID.UUIDString isEqualToString:ST_SERVICE_UUID]) {            //发现特定服务的特征值                [service.peripheral discoverCharacteristics:nil forService:service];            }        }    } }//发现characteristics,由发现服务调用(上一步),获取读和写的characteristics - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {    for (CBCharacteristic *characteristic in service.characteristics) {        //有时读写的操作是由一个characteristic完成        if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]) {    self.read = characteristic;            [self.peripheral setNotifyValue:YES forCharacteristic:self.read];        } else if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_WRITE]) {        self.write = characteristic;        }    } }//是否写入成功的代理 - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{    if (error) {        NSLog(@"===写入错误:%@",error);    }else{        NSLog(@"===写入成功");    } }//数据接收 - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {        if([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]){         //获取订阅特征回复的数据         NSData *value = characteristic.value;         NSLog(@"蓝牙回复:%@",value);     } }比如我们要获取蓝牙电量,由硬件文档查询得知该指令是**0x1B9901**,那么获取电量的方法就可以写成 - (void)getBattery{ Byte value[3]={0}; value[0]=x1B; value[1]=x99; value[2]=x01; NSData * data = [NSData dataWithBytes:&value length:sizeof(value)]; //发送数据 [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse]; }如果写入成功,我们将会在didUpdateValueForCharacteristic方法中获取蓝牙回复的信息。 如何解析蓝牙数据 如果你顺利完成了上一步的操作,并且看到了蓝牙返回的数据,那么恭喜你,蓝牙的常用操作你已经了解大半了。因为蓝牙的任务大部分就是围绕发送指令,获取指令,将蓝牙数据呈现给用户。上一步我们已经获取了蓝牙指令,但是获取的却是0x567b0629这样的数据,这是什么意思呢。这时我们参考硬件文档,看到这样一段:那么我们就可以得出设备电量是 60%。 对数据解析的流程就是:判断校验和是否正确,是不是一条正确的数据->该条数据是不是我们需要的电量数据,即首字节为0x567b->根据定义规则解析电量,传给view显示。其中第一步校验数据,视情况而定,也有不需要的情况。 扩展 iOS蓝牙中的进制转换 蓝牙固件升级 nRF芯片设备DFU升级

nRF芯片设备DFU升级(适配Xcode10.2.1)

这里主要参考这个项目:iOS-nRF-Toolbox,它是Nordic公司开发的测试工程,包含一整套nRF设备的测试解决方案。项目是用Swift写的,不过之前还是有OC版本的,但是后来由于一些**(不可描述的问题),才变成了现在的纯Swift版本。对于使用Swift开发的人员,直接仿照Demo操作即可。如果你是用Swift开发的,那下面的内容你可以不用看了。接下来我就讲一下针对OC引用DFU升级的操作步骤和我遇到的问题。 代码研究 nRF-Toolbox项目包含BGM,HRM,HTM,DFU等多个模块,我们今天只关注其中的DFU升级模块。打开项目,在对应的NORDFUViewController.swift中我们能够看到有三个引用库 import UIKit,import CoreBluetooth,import iOSDFULibrary,这里的iOSDFULibrary就是DFU升级的库,也是解决DFU升级最重要的组件。我们只要把这个库集成到我们的项目中,就能够完成nRF设备的DFU升级了。 集成步骤 有两种方案集成:通过cocoapods集成 编译出framework然后把库导入项目第一种方案是作者推荐的,但是我试了很久,引入DFULibrary会出现头文件找不到等一系列问题,无奈只能放弃,如果有人通过这种方式成功,还望告知。下面讲的是通过第二种方案的集成。 第一步:导出iOSDFULibrary 这一步是最关键也是最容易出问题的,这个库也是由Swift写成的,我们将这个库clone到本地,然后选择iOSDFULibrary进行编译最后生成两个framework:iOSDFULibrary.framework Zip.framework这时库内的代码已经变成了我们熟悉的OC语言。理论上这个库应该是没问题的了,但是事实还是有问题的,见issues#39。作者给出的解决方法是:1、On your mac please install carthage (instructions) 2、Create a file named cartfile anywhere on your computer 3、add the following content to the file:github "NordicSemiconductor/IOS-Pods-DFU-Library" ~> 2.1.2 github "marmelroy/Zip" ~> 0.61、Open a new terminal and cd to the directory where the file is 2、Enter the command carthage update --platform iOS 3、Carthage will now take care of building your frameworks, the produced .framework files will be found in a newly created directory called Carthage/Build/iOS,copy over iOSDFULibrary.framework and Zip.framework to your project and you are good to go.carthage是一种和cocoapods相似的的类库管理工具,如果不会使用的话可以参照Demo,将framework文件导入到自己的项目。 第二步、导入framework Target->General直接拖入项目默认只会导入到Linked Frameworks and Libraries,我们还需要在Embeded Binaries中引入。 第三步、使用iOSDFULibrary //create a DFUFirmware object using a NSURL to a Distribution Packer(ZIP) DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url];// or //Use the DFUServiceInitializer to initialize the DFU process. DFUServiceInitiator *initiator = [[DFUServiceInitiator alloc] initWithCentralManager: centralManager target:selectedPeripheral]; [initiator withFirmware:selectedFirmware]; // Optional: // initiator.forceDfu = YES/NO; // default NO // initiator.packetReceiptNotificationParameter = N; // default is 12 initiator.logger = self; // - to get log info initiator.delegate = self; // - to be informed about current state and errors initiator.progressDelegate = self; // - to show progress bar // initiator.peripheralSelector = ... // the default selector is usedDFUServiceController *controller = [initiator start];库中有三个代理方法DFUProgressDelegate,DFUServiceDelegate,LoggerDelegate,它们的作用分别为监视DFU升级进度,DFU升级及蓝牙连接状态,打印状态日志。 常见问题 1、selectedFirmware返回nil DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url];需要在General的Embeded Binaries选项卡里导入那Zip.framework和iOSDFULibrary.framework 2、崩溃报错 dyld: Library not loaded: @rpath/libswiftCore.dylibReferenced from: /private/var/containers/Bundle/Application/02516D79-BB30-4278-81B8-3F86BF2AE2A7/XingtelBLE.app/Frameworks/iOSDFULibrary.framework/iOSDFULibraryReason: image not found需要改两个地方如果不起作用,将Runpath Search Paths的选项内容删掉再重新添加一遍即可 3、打包上架时报ERROR IT MS-90087等问题 问题描述: ERROR ITMS-90087: "Unsupported Architectures. The executable for ***.app/Frameworks/SDK.framework contains unsupported architectures '[x86_64, i386]'." ERROR ITMS-90362: "Invalid Info.plist value. The value for the key 'MinimumOSVersion' in bundle ***.app/Frameworks/SDK.framework is invalid. The minimum value is 8.0" ERROR ITMS-90209: "Invalid Segment Alignment. The app binary at '***.app/Frameworks/SDK.framework/SDK' does not have proper segment alignment. Try rebuilding the app with the latest Xcode version." ERROR ITMS-90125: "The binary is invalid. The encryption info in the LC_ENCRYPTION_INFO load command is either missing or invalid, or the binary is already encrypted. This binary does not seem to have been built with Apple's linker."解决方法,添加Run Script PhaseShell脚本内容填写如下内容,再次编译即可 APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"# This script loops through the frameworks embedded in the application and # removes unused architectures. find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK do FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable) FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME" echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"EXTRACTED_ARCHS=()for ARCH in $ARCHS do echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME" lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH" EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH") doneecho "Merging extracted architectures: ${ARCHS}" lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}" rm "${EXTRACTED_ARCHS[@]}"echo "Replacing original executable with thinned version" rm "$FRAMEWORK_EXECUTABLE_PATH" mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"done完整OC项目 这个是对应Swift版本用OC写的完整项目,应该是OC停止维护之前的版本。会有一些bug。在将DFUFramework更新之后,我把它搬到了我的github上,有需要的同学可以下载研究:OC-nRFTool-box。以下为更新内容,时间:2017.12.26 收到很多关于无法适配Xcode9.2的反馈,因为最近比较忙没时间处理,不好意思啦,今天抽出时间来把代码更新了一下。 Xcode9.2 出现的问题1、dyld: Library not loaded: @rpath/libswiftCore.dylib Referenced from: /private/var/containers/Bundle/Application/02516D79-BB30-4278-81B8-3F86BF2AE2A7/XingtelBLE.app/Frameworks/iOSDFULibrary.framework/iOSDFULibrary Reason: image not found 2、DFUFirmware *selectedFirmware = [[DFUFirmware alloc] initWithUrlToZipFile:url]; 返回为空或者崩溃问题我的测试结果是更新iOSDFULibrary. framework和Zip.framework可以解决以上问题。 解决方案 Carthage 因为这两个库都是通过Swift维护的,所以更新framework最好还是要用适用Swift的方式,包括以后的更新也一样。所以我推荐用Carthage更新这俩库,下面是使用Carthage的简单介绍,详细的可以看这里Carthage的安装和使用。 另外OC-nRFTool-box也已经更新,里面的Framework可以直接拿来用。 1、安装brew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"2、brew更新 $ brew update3、安装Carthage $ brew install carthage4、使用Carthage $ cd ~/路径/项目文件夹 /**进入项目文件夹下*/ $ touch Cartfile /**创建carthage文件*/ $ open Cartfile /**打开carthage文件*/ # /**输入以下内容*/ $ github "NordicSemiconductor/IOS-Pods-DFU-Library" ~> 4.1 $ github "marmelroy/Zip" ~> 1.15、运行Carthage $ carthage update --platform iOS /**编译出iOS版本*/6、更新framework $ cd Carthage/Build/iOS /**framework输出位置,将老的framework替换掉*/注意 nRF Toolbox项目方法变更 [initiator withFirmwareFile:selectedFirmware];/** 旧版本方法 */ initiator = [initiator withFirmware:selectedFirmware];/** 新版本方法 */更新:2019-7-14 针对之前常出的这种问题: dyld: Library not loaded: @rpath/libswiftCore.dylib Referenced from: /private/var/containers/Bundle/Application/CDB2F4ED-C49C-4303-BE1F-5D9D990380F3/nRF Toolbox.app/Frameworks/Zip.framework/Zip Reason: image not found均是由Swift库版本不一致引起的,iOSDFULibrary目前已经支持到Swift 5,所以我们应该升级一下版本。为了方便使用,我将Carthage集成到了项目里,如果以后需要再升级,更新Cartfile文件里的版本号,执行更新命令: $ carthage update --platform iOS如果你想要将DFU的framework集成到你自己的项目里,可以在Carthage/Build/iOS/中找到iOSDFULibrary.framework, ZIPFoundation.framework 将其拖到项目中即可。 上线注意事项(由@jianxiong1997提供)需要删除ZIPFoundation.framework中的libswiftRemoteMirror.dylib Frameworksgithub项目同步更新。

iOS蓝牙中的进制转换

最近在忙一个蓝牙项目,在处理蓝牙数据的时候,经常遇到进制之间的转换,蓝牙处理的是16进制(NSData),而我们习惯的计数方式是10进制,为了节省空间,蓝牙也会把16进制(NSData)拆成2进制记录。这里我们研究下如何在他们之间进行转换。 假设我们要向蓝牙发送0x1B9901这条数据 Byte转NSData Byte value[3]={0}; value[0]=0x1B; value[1]=0x99; value[2]=0x01; NSData * data = [NSData dataWithBytes:&value length:sizeof(value)]; //发送数据 [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];优点:这种方法比较简单,没有进行转换,直接一个字节一个字节的拼装好发送出去。缺点:当发送数据比较长时会很麻烦,而且不易更改。NSString转NSData - (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; } //发送数据 [self.peripheral writeValue:[self hexToBytes:@"1B9901"] forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];优点:比较直观,可以一次转换一长条数据,对于一些功能简单的蓝牙程序,这种转换能处理大部分情况。 缺点:只能发送一些固定的指令,不能参与计算。求校验和 接下来探讨下发送的数据需要计算的情况。 最常用的发送数据需要计算的场景是求校验和(CHECKSUM)。这个根据硬件厂商来定,常见的求校验和的规则有:如果发送数据长度为n字节,则CHECKSUM为前n-1字节之和的低字节 CHECKSUM=0x100-CHECKSUM(上一步的校验和)如果我要发送带上校验和的0x1B9901,方法就是: - (NSData *)getCheckSum:(NSString *)byteStr{     int length = (int)byteStr.length/2;     NSData *data = [self hexToBytes:byteStr];     Byte *bytes = (unsigned char *)[data 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);     if (at == 256) {         at = 0;     }     NSString *str = [NSString stringWithFormat:@"%@%@",byteStr,[self ToHex:at]];     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;         }     } //不够一个字节凑0     if(str.length == 1){         return [NSString stringWithFormat:@"0%@",str];     }else{         return str;     } } //发送数据 NSData *data = [self getCheckSum:@"1B9901"];//data=<1b99014b> [self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];拆分数据 这种是比较麻烦的,举个栗子:在传输某条信息时,我想把时间放进去,不能用时间戳,还要节省空间,这样就出现了一种新的方式存储时间。 这里再补充一些C语言知识:一个字节8位(bit) char 1字节 int 4字节 unsigned 2字节 float 4字节存储时间的条件是:只用四个字节(32位) 前5位表示年(从2000年算起),接着4位表示月,接着5位表示日,接着5位表示时,接着6位表示分,接着3位表示星期,剩余4位保留。这样直观的解决办法就是分别取出现在时间的年月日时分星期,先转成2进制,再转成16进制发出去。当然你这么写进去,读的时候就要把16进制数据先转成2进制再转成10进制显示。我们就按这个简单粗暴的思路来,准备工作如下: 10进制转2进制 //  十进制转二进制 - (NSString *)toBinarySystemWithDecimalSystem:(int)num length:(int)length {     int remainder = 0;      //余数     int divisor = 0;        //除数    NSString * prepare = @"";    while (true)     {         remainder = num%2;         divisor = num/2;         num = divisor;         prepare = [prepare stringByAppendingFormat:@"%d",remainder];        if (divisor == 0)         {             break;         }     }     //倒序输出     NSString * result = @"";     for (int i = length -1; i >= 0; i --)     {         if (i <= prepare.length - 1) {             result = [result stringByAppendingFormat:@"%@",                       [prepare substringWithRange:NSMakeRange(i , 1)]];        }else{             result = [result stringByAppendingString:@"0"];        }     }     return result; }2进制转10进制 //  二进制转十进制 - (NSString *)toDecimalWithBinary:(NSString *)binary {     int ll = 0 ;     int  temp = 0 ;     for (int i = 0; i < binary.length; i ++)     {         temp = [[binary substringWithRange:NSMakeRange(i, 1)] intValue];         temp = temp * powf(2, binary.length - i - 1);         ll += temp;     }    NSString * result = [NSString stringWithFormat:@"%d",ll];    return result; } ### 16进制和2进制互转 - (NSString *)getBinaryByhex:(NSString *)hex binary:(NSString *)binary {     NSMutableDictionary  *hexDic = [[NSMutableDictionary alloc] init];     hexDic = [[NSMutableDictionary alloc] initWithCapacity:16];     [hexDic setObject:@"0000" forKey:@"0"];     [hexDic setObject:@"0001" forKey:@"1"];     [hexDic setObject:@"0010" forKey:@"2"];     [hexDic setObject:@"0011" forKey:@"3"];     [hexDic setObject:@"0100" forKey:@"4"];     [hexDic setObject:@"0101" forKey:@"5"];     [hexDic setObject:@"0110" forKey:@"6"];     [hexDic setObject:@"0111" forKey:@"7"];     [hexDic setObject:@"1000" forKey:@"8"];     [hexDic setObject:@"1001" forKey:@"9"];     [hexDic setObject:@"1010" forKey:@"a"];     [hexDic setObject:@"1011" forKey:@"b"];     [hexDic setObject:@"1100" forKey:@"c"];     [hexDic setObject:@"1101" forKey:@"d"];     [hexDic setObject:@"1110" forKey:@"e"];     [hexDic setObject:@"1111" forKey:@"f"];    NSMutableString *binaryString=[[NSMutableString alloc] init];     if (hex.length) {         for (int i=0; i<[hex length]; i++) {             NSRange rage;             rage.length = 1;             rage.location = i;             NSString *key = [hex substringWithRange:rage];             [binaryString appendString:hexDic[key]];         }    }else{         for (int i=0; i<binary.length; i+=4) {             NSString *subStr = [binary substringWithRange:NSMakeRange(i, 4)];             int index = 0;             for (NSString *str in hexDic.allValues) {                 index ++;                 if ([subStr isEqualToString:str]) {                     [binaryString appendString:hexDic.allKeys[index-1]];                     break;                 }             }         }     }     return binaryString; }有了这几种转换函数,完成上面的功能就容易多了,具体怎么操作这里就不写一一出来了。但总感觉怪怪的,这么一个小功能怎么要写这么一大堆代码,当然还可以用C语言的方法去解决。这里主要是为了展示iOS中数据如何转换,C语言的实现方法这里就不写了,有兴趣的同学可以研究下。 附带两个函数 int转NSData - (NSData *) setId:(int)Id { //用4个字节接收     Byte bytes[4];     bytes[0] = (Byte)(Id>>24);     bytes[1] = (Byte)(Id>>16);     bytes[2] = (Byte)(Id>>8);     bytes[3] = (Byte)(Id);     NSData *data = [NSData dataWithBytes:bytes length:4]; }NSData转int 接受到的数据0x00000a0122 //4字节表示的int NSData *intData = [data subdataWithRange:NSMakeRange(2, 4)];     int value = CFSwapInt32BigToHost(*(int*)([intData bytes]));//655650 //2字节表示的int NSData *intData = [data subdataWithRange:NSMakeRange(4, 2)];     int value = CFSwapInt16BigToHost(*(int*)([intData bytes]));//290 //1字节表示的int char *bs = (unsigned char *)[[data subdataWithRange:NSMakeRange(5, 1) ] bytes];     int value = *bs;//34 ------------------------ //补充内容,因为没有三个字节转int的方法,这里补充一个通用方法 - (unsigned)parseIntFromData:(NSData *)data{    NSString *dataDescription = [data description];     NSString *dataAsString = [dataDescription substringWithRange:NSMakeRange(1, [dataDescription length]-2)];    unsigned intData = 0;     NSScanner *scanner = [NSScanner scannerWithString:dataAsString];     [scanner scanHexInt:&intData];     return intData; }这两个转换在某些场景下使用频率也是挺高的,蓝牙里面的数据转换基本也就这么多了,希望能够帮助大家。 更多关于字节编码的问题,大家可以点这里:传送门 扩展 基于CoreBluetooth4.0框架的连接BLE4.0的Demo:你不点一下吗

处理ANCS设备连接绑定问题

ANCS(Apple Notification Center Service,苹果通知中心)的目的是提供给蓝牙外设一种简单、方便的获取iOS设备通知信息的方式。使得蓝牙手环,手表可以接收到来自iPhone的来电、短信及QQ、微信等应用的通知消息。 如果你已经能够连接普通蓝牙,初次面对ANCS设备可能会有以下问题:问题一:遵循ANCS协议的的设备会直接和系统相连,即使杀掉应用,连接还是存在的。而如果蓝牙设备处于连接状态,它不会被扫描到,怎么再次连接呢?在Core Bluetooth framework里提供了两个方法,用于获取已连接的设备 //通过传入的peripherals.identifier返回与系统连接的已知设备数组 - (NSArray<CBPeripheral *> *)retrievePeripheralsWithIdentifiers:(NSArray<NSUUID *> *)identifiers; //通过传入设备的serviceID返回已连接的设备数组 - (NSArray<CBPeripheral *> *)retrieveConnectedPeripheralsWithServices:(NSArray<CBUUID *> *)serviceUUIDs;我们就可以通过这两个方法,获取已连接设备,并建立重连。参考代码: NSArray *peripherals = [central retrieveConnectedPeripheralsWithServices:@[serviceUUID]]; if (peripherals.count > 0) { CBPeripheral *peripheral = [peripherals firstObject]; peripheral.delegate = self; self.peripheral = peripheral;//**关键**需要转存外设值,才能发起连接 [central connectPeripheral:self.peripheral options:nil]; } else { [central scanForPeripheralsWithServices:@[serviceUUID] options:nil]; }根据不同的使用情况,可能会有不同的扫描,连接的逻辑,苹果提供了一个流程图:问题二:有绑定和解除功能,如何处理两者的关系首先我们要知道,不能通过代码,断开ANCS设备与系统之间的连接,那么如果我们想解除设备的绑定,只能控制设备与APP之间的断开。 ###绑定 再回看上面提到的苹果提供的两个获取已连接设备的方法,一个是通过serviceUUID,它可以返回同一类型的设备列表;一个是通过设备UUID,它在一定情况下就是唯一的(如果设备名唯一,这里可以使用设备名),返回的是唯一设备。那么我们就可以利用UUID的唯一性,作为绑定的标示,存到NSUserDefault里面,对于未绑定的设备通过serviceUUID去获取设备列表。参考代码: NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults]; NSString *uuidString = [userDefault objectForKey:RWBLE_BANDIDENTIFI_ID];NSArray *peripherals; if (uuidString) {         //通过uuid获取连接设备     NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];         peripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[uuid]]; }else{         //通过serviceUUID获取连接设备         peripherals = [self.centralManager retrieveConnectedPeripheralsWithServices:@[[CBUUID UUIDWithString:ST_SERVICE_UUID]]]; } /* peripherals connect code */###解绑 不能使ANCS设备与系统连接断开,那么我们就在程序里销毁这个外设对象,这样APP与蓝牙设备的连接通讯就不存在了,造成了一种断开的感觉。参考代码: //解绑设备 - (void)unbindDevice{     [self disconnect];//通知app,设备已经断开    NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];     [userDefault removeObjectForKey:RWBLE_BANDIDENTIFI_ID];//销毁uuid     self.peripheral = nil; }这么写看似已经解决问题了,但是会出现一种情况:解绑了设备,杀掉应用,再次进入设备还是能连上。why?因为虽然没有了UUID,但进入程序会通过serviceUUID再次获取连接。 这时可以在扫面做一个判断,是否刚解绑过设备。可以是个BOOL值,绑定和初始绑定为NO,解绑操作改为YES。如果刚解绑过设备,就直接返回不做后面的扫描操作,这样就解决了上面的问题。这个比较简单,就不列具体代码了。 参考文档: Best Practices for Interacting with a Remote Peripheral Device

蓝牙固件升级

升级介绍蓝牙固件升级是使用手机给固件进行更新,以达到修复bug,完善功能的作用。升级的大概流程是:首先,当手环的固件需要升级时,由嵌入式开发人员提供新的固件,由服务器管理人员将固件放到服务器上,此时,用户打开手机APP的时候会检测到服务器有更新,请求更新手环固件,确认更新后,手机会从服务器下载固件。下载完毕后,APP会读取固件内容,并根据升级协议将内容传到手环里,完成升级。DFU = Device Firmware Update (设备固件更新) OTA = Over The Air (空中升级)升级流程 各个蓝牙设备不尽相同,以下是我测试设备的升级流程: OTA下载固件 从云端下载的固件为.bin后缀的文件,文件名会有一定的格式,含有固件版本号和文件CRC32校验值。 数据分块 规定一个数据块大小比如2048字节,然后把升级数据进行分块,不够的就剩余多少作为一块。蓝牙一次发送的数据量是有限的,所以每次发送20字节的数据。这个数据要遵循升级数据格式,带指令头和校验和,下载包的数据只是这20字节中的一部分。所有包内数据都携带在每条升级数据指令中。 升级过程连接设备,发送升级请求。 待蓝牙确认之后,开始发送数据头告知蓝牙此次发送的数据量和CRC校验。 开始发送升级数据。(每条数据之间间隔20ms为了蓝牙能够方便处理) 待一个块发送完就发送块结束命令 蓝牙确认发送下一个块,返回错误则终止此次升级 发完所有数据之后发送升级完成 蓝牙确认则升级完成,返回错误则升级失败流程图 ###总结 蓝牙升级最复杂的就在升级过程,大量的数据与蓝牙交互,这时最好记录发送到升级数据的那一部分,可以给用户展示升级的进程。