swift 蓝牙开发、OTA升级

公司项目需要用到BLE以CBCentralManager的身份和硬件交互,开发过程中解决了一些遇到的问题和一些处理思路,这里简单记录一下。如果有什么问题或写的不对的地方希望大家可以一起讨论。
首先了解一下什么是BLE,蓝牙低能耗(Bluetooth Low Energy,或称Bluetooth LE、BLE,旧商标Bluetooth Smart,蓝牙版本4.0),也称低功耗蓝牙。相较经典蓝牙(蓝牙版本2.0),低功耗蓝牙旨在保持同等通信范围的同时显著降低功耗和成本。


讨论一些要注意的和一些思路

与设备的交互使用的是16进制,所以要对发送的数据进行16进制转换,转换方法放在末尾
连接和操作一个设备就要持有这个设备对象,系统不维护设备对象的内存管理
发送数据异步回调可以封装一个任务机制,发送数据后生成一个任务,在收到想要的数据的时候关闭任务或者等待任务超时关闭任务。
iOS更换手机的时候设备的UUID会改变,如果想换手机后依然可以重连设备,就需要让设备端配合把设备唯一MAC地址放入广播内容中,给设备扩充MAC属性,根据MAC来选择设备进行连接,做到设备MAC和UUID的匹配
本篇只做了简单的功能介绍和使用,OTA部分需要按照实际协议来做。如果大家有遇到问题或者有好的主意可以找我一起讨论,万分荣幸。


iOS对蓝牙库进行了封装,封装在CoreBluetooth库,所以使用时

import CoreBluetooth

接下来是对一些名词的介绍

CBCentralManager - 中心管理者
CBPeripheralManager - 外设管理者
CBPeripheral - 外设对象
CBService - 外设服务
CBCharacteristic - 外设服务的特征

大致结构如下

image.png

注:一个CBPeripheral可以包含多个CBService ,而一个CBService 也可以包含多个CBCharacteristic 。


接下来介绍蓝牙从打开到连接到发送数据到接收数据的一整个流程
1.首先肯定是权限设置,Info.plist里面加入
Privacy - Bluetooth Peripheral Usage Description
2.然后是初始化中心管理者,初始化有三种方式,我使用的默认的初始化方法即

CBCentralManager()//默认主线程,无代理,无options

如果想自己设置线程和其他条件,则可以通过接下来的初始化方法一次性进行设置

//说明:options包含两个key
//CBCentralManagerOptionShowPowerAlertKey
//布尔值,表示的是在central manager初始化时,如果当前蓝牙没打开,是否弹出alert框。
//CBCentralManagerOptionRestoreIdentifierKey
//字符串,一个唯一的标示符,用来蓝牙的恢复连接的。
CBCentralManager.init(delegate:, queue:, options: )

3.判断蓝牙状态,通过CBCentralManager的state来获取

//⚠️iOS10.0之后更新了蓝牙状态枚举的名字,但是枚举类型未变
//10.0之前
CBCentralManagerState
//10.0之后
CBManagerState
//枚举类型如下:
case unknown 
case resetting 
case unsupported
case unauthorized
case poweredOff
case poweredOn

4.如果状态为打开,则可以进行搜索操作

//说明:
//serviceUUIDs为硬件端定好的发送/接受服务,可不填,不填默认搜索全部
//options
scanForPeripherals(withServices serviceUUIDs: [CBUUID]?, options: [String : Any]? = nil)

注:如果连接和操作一个设备就要持有这个设备对象,系统不维护设备对象的内存管理
接下来就是一系列的代理事件了,我会把主要代理按照流程来进行说明,大致流程如下:


搜索-连接-连接成功/失败(设置外设代理,搜索服务)-搜索到服务(搜索特征)-搜索到特征-监听需要的特征(读写、读、写等根据情况来确定)-通过外设读写特征写入指令-收到设备返回信息-断开连接


接下来对每个代理来进行详细介绍
CBCentralManagerDelegate:中心管理者代理,负责搜索,设备状态的一些回调
CBPeripheralDelegate:外设代理,负责对外设的一些操作,特征的订阅,以及设备信息和消息的更新回调
搜索&连接

    /// 搜索到新的外设
    ///
    /// - Parameters:
    ///   - central: 蓝牙中心
    ///   - peripheral: 外设
    ///   - advertisementData: 外设广播内容
    ///   - RSSI: 信号
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber){
          if peripheral.name == "你需要连接的设备名称" {
            //连接指定设备
            central.connect(peripheral, options: nil)
            //持有外设对象,自己管理生命周期
            discoverPeripherals.insert(peripheral)
        }
    }

连接成功&失败

    /// 连接外设成功  
    ///
    /// - Parameters:
    ///   - central: 蓝牙中心
    ///   - peripheral: 外设
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        //设置外设代理以便对外设代理的处理
        peripheral.delegate = self
        //搜索外设的所有服务(可指定服务,默认为搜索全部服务)
        peripheral.discoverServices(nil)
    }
    
    /// 连接外设失败  
    ///
    /// - Parameters:
    ///   - central: 蓝牙中心
    ///   - peripheral: 外设
    ///   - error: 错误
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        //释放持有的外设对象
        discoverPeripherals.remove(peripheral)
    }

搜索到服务

    /// 发现外设的服务
    ///
    /// - Parameters:
    ///   - peripheral: 外设
    ///   - error: 错误
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        peripheral.services?.forEach{
            //搜索所有的设备特征
            peripheral.discoverCharacteristics(nil, for: $0)}
    }

搜索到特征

    /// 发现外设的特征,订阅特征(读、写等)
    ///
    /// - Parameters:
    ///   - peripheral: 外设
    ///   - service: 外设的w服务
    ///   - error: 错误
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        service.characteristics?.forEach {
            //properties
            //订阅自己需要的特征
            if $0.properties == .notify {
                peripheral.setNotifyValue(true, for: $0)
            }
        }
    }

收到外设消息更新

    /// 收到外设发送内容    
    ///
    /// - Parameters:
    ///   - peripheral: 外设
    ///   - characteristic: 特征
    ///   - error: 错误
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
 
    }

断开设备连接

    /// 外设断开连接  
    ///
    /// - Parameters:
    ///   - central: 蓝牙中心
    ///   - peripheral: 外设
    ///   - error: 错误
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        //释放持有的外设对象
        discoverPeripherals.remove(peripheral)
    }

接下来介绍OTA升级
OTA是DFU(Device Firmware Update)的一种类型,准确说,OTA的全称应该是OTA DFU,就是设备固件升级的意思。只不过大家为了方便起见,直接用OTA来指代固件空中升级(有时候大家也将OTA称为FOTA)。
OTA升级并不复杂,只需要按照硬件定制的协议,把数据按照正常的写入方式发送给硬件即可(注意查看硬件是否规定数据的大小端),如果遇到问题可以找我,可以一起讨论。

大端模式:是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。


16进制的转换

16进制类型的字符串[A-F,0-9]和Data之间的转换可以使用下面的方法。如果是包含=之类的可以直接用字符串转换Data即可

extension String {
    ///16进制字符串转Data
    func hexData() -> Data? {
        var data = Data(capacity: count / 2)
        let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
        regex.enumerateMatches(in: self, range: NSMakeRange(0, utf16.count)) { match, flags, stop in
            let byteString = (self as NSString).substring(with: match!.range)
            var num = UInt8(byteString, radix: 16)!
            data.append(&num, count: 1)
        }
        guard data.count > 0 else { return nil }
        return data
    }
    
    func utf8Data()-> Data? {
        return self.data(using: .utf8)
    }
    
}

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

推荐阅读更多精彩内容