错误log:
As: Error Domain=CBATTErrorDomain Code=3 “Writing is not permitted.” UserInfo={NSLocalizedDescription=Writing is not permitted.}

// 这里的type类型有两种 CBCharacteristicWriteWithResponse CBCharacteristicWriteWithoutResponse,它的属性枚举可以组合
[self.selectedPeripheral writeValue:sendData forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];
iOS API:
typedef NS_ENUM(NSInteger, CBCharacteristicWriteType) {
CBCharacteristicWriteWithResponse = 0,
CBCharacteristicWriteWithoutResponse,
};
如果设置为WithResponse,则可以写成功一次,如果为WithoutResponse,则一次也不能写成功。
Ios上BLE开发总结:

蓝牙传输所用的框架是<CoreBluetooth/CoreBluetooth.h>

蓝牙连接需要中心管理者和外部设备,我们所做的开发基本是围绕中心管理来的;
蓝牙设备发过来的每个数据包,为了保证数据在传输的时候没有丢失,一般需要包头,包尾,校验和
有很多蓝牙协议很复杂,需要把数据转化成二进制进行转化解析,对于高字节,低字节,小端模式,大端模式,符号位,位运算这些基本概念需要了解清楚

1.关于Mac地址的获取

自iOS7之后,苹果不支持获取Mac地址,只能用UUID来标识设备,要注意的是同一个设备在不同手机上显示的UUID不相同,但有的设备可以通过 “180A”这个服务来发现特征,再来读取 “2A23”这个特征值,可以获得Mac地址。如果你的蓝牙设备不支持这样获取,你可以跟硬件工程师沟通,来获得Mac地址,添加一个获取地址命令或者增加一个含地址的特征值都可以很容易的获取。上面获取地址的前提都是需要先建立连接,如果一定要在扫描的时候获得Mac地址,让硬件工程师把数据写入广播包里,看是否可行;

2.蓝牙连接流程
建立中心设备管理者
扫描外设
连接外设
扫描外设中的服务
扫描外设中的特征
订阅或读取特征值
获取外设中的数据

建立中心设备管理者

// 创建之后会马上检查蓝牙的状态,nil默认为主线程
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]

蓝牙线程没必要去开异步线程,在主线程消耗不了什么性能

扫描外设

// 蓝牙状态发生改变,这个方法一定要实现
– (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
// 蓝牙状态可用
if (central.state == CBCentralManagerStatePoweredOn) {

// 如果蓝牙支持后台模式,一定要指定服务,否则在后台断开连接不上,如果不支持,可设为nil, option里的CBCentralManagerScanOptionAllowDuplicatesKey默认为NO, 如果设置为YES,允许搜索到重名,会很耗电
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:nil];
}
}
连接外设

/**
* 发现设备
* @param peripheral 设备
* @param advertisementData 广播内容
* @param RSSI 信号强度
*/
– (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
// 判断是否是你需要连接的设备
if ([peripheral.name isEqualToString:kPeripheralName]) {
peripheral.delegate = self;
// 一定要记得把外设保存起来
self.selectedPeripheral = peripheral;
// 开始连接设备
[self.centralManager connectPeripheral:self.selectedPeripheral options:nil];
}
}
扫描外设中的服务

/**
* 已经连接上设备
*/
– (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
// 停止扫描
[self.centralManager stopScan];
// 发现服务
[self.selectedPeripheral discoverServices:nil];
}
扫描外设中的特征

/**
* 已经发现服务
*/
– (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
for (CBService *service in peripheral.services) {
if ([service.UUID isEqual:[CBUUID UUIDWithString:kServiceUUID]]) {
// 根据你要的那个服务去发现特性
[self.selectedPeripheral discoverCharacteristics:nil forService:service];
}

// 这里我是根据 180A 用来获取Mac地址,没什么实际作用,可删掉
if ([service.UUID isEqual:[CBUUID UUIDWithString:@”180A”]]) {
[self.selectedPeripheral discoverCharacteristics:nil forService:service];
}
}
}
订阅或读取特征值

/**
* 已经发现特性
*/
– (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
for (CBCharacteristic *characteristic in service.characteristics) {
if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@”2A23″]]) {
// 这里是读取Mac地址, 可不要, 数据固定, 用readValueForCharacteristic, 不用setNotifyValue:setNotifyValue
[self.selectedPeripheral readValueForCharacteristic:characteristic];
}

if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:kCharacteristicUUID]]) {
// 订阅特性,当数据频繁改变时,一般用它, 不用readValueForCharacteristic
[peripheral setNotifyValue:YES forCharacteristic:characteristic];

// 获取电池电量
unsigned char send[4] = {0x5d, 0x08, 0x01, 0x3b};
NSData *sendData = [NSData dataWithBytes:send length:4];

// 这里的type类型有两种 CBCharacteristicWriteWithResponse CBCharacteristicWriteWithoutResponse,它的属性枚举可以组合
[self.selectedPeripheral writeValue:sendData forCharacteristic:characteristic type:CBCharacteristicWriteWithoutResponse];

/*
characteristic 属性
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
};
*/

NSLog(@”%@”,characteristic);
// 打印结果为 <CBCharacteristic: 0x1702a2a00, UUID = FFF6, properties = 0x16, value = (null), notifying = NO>

//  我的结果 为 0x16  (0x08 & 0x16)结果不成立, (0x04 & 0x16)结果成立,那写入类型就是 CBCharacteristicPropertyWriteWithoutResponse
}
}
}
获取外设中的数据

/**
* 数据更新的回调
*/
– (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
// 这里收到的数据都是16进制,有两种转换,一种就直接转字符串,另一种是转byte数组,看用哪种方便

// 直接转字符串
NSString *orStr = characteristic.value.description;
NSString *str = [orStr substringWithRange:NSMakeRange(1, orStr.length – 2)];
NSString *dataStr = [str stringByReplacingOccurrencesOfString:@” ” withString:@””];
NSLog(@”dataStr = %@”,dataStr);

// 转Byte数组
Byte *byte = (Byte *)characteristic.value.bytes;

//_______________________________________________________________________________________________________________
// 解析你的协议,附几个解协议或许能用到的函数
}
设备连接断开

– (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
// 让它自动重连
[self.centralManager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:kServiceUUID]] options:nil];
}
这是系统代理方法,如果要主动断开需要调用  – (void)cancelPeripheralConnection:(CBPeripheral *)peripheral; 这个方法

写入数据成功的回调

– (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
// 读取数据
[self.selectedPeripheral readValueForCharacteristic:characteristic];
}
如果类型是CBCharacteristicWriteWithoutResponse,不会走这个方法;

需要注意的地方:
1、代理方法- (void)centralManagerDidUpdateState:(CBCentralManager *)central;一定要调用,否则会报错,这个方法只要设置中心设备的代理之后,就一定会走,我们*开始的扫描外设应放在这个方法里;
2、如果蓝牙支持要支持后台模式,只需要去把蓝牙后台模式打开

记住只要勾选Uses Bluetooth LE accessories就行了,别勾选Acts As a Bluetooth LE accessory,除非你把你的手机当做外部设备使用;