iOS 低功耗蓝牙(BLE)的IoT应用之 - 温度检测仪(上)

背景知识

蓝牙技术最初由爱立信(也就是多年前手机做得最丑最奇葩的公司,最终被用户以脚投票踢出市场)创制。技术始于爱立信公司的1994方案,它是研究在移动电话和其他配件间进行低功耗、低成本无线通信连接的方法。发明者希望为设备间的通讯创造一组统一规则(标准化协议),以解决用户间互不兼容的移动电子设备。1997年前爱立信公司以此概念接触了移动设备制造商,讨论其项目合作发展,结果获得支持。

1998年5月20日,索尼爱立信、国际商业机器、诺基亚等业界龙头创立“特别兴趣小组”(Special Interest Group,SIG),即[蓝牙技术联盟的前身,目标是开发一个成本低、效益高、可以在短距离范围内随意无线连接的蓝牙技术标准。

这项无线技术的名称取自古代丹麦维京国王Harald Blåtand的名字,他以统一了因宗教战争和领土争议而分裂的挪威与丹麦而闻名于世,而这个名字的英文便是Harald Bluetooth。

1998年时蓝牙推出0.7规格,支持Baseband与LMP(Link Manager Protocol)通讯协定两部分。1999年推出先后0.8版,0.9版、1.0 Draft版,1.0a版、1.0B版。1.0 Draft版,完成SDP(Service Discovery Protocol)协定、TCS(Telephony Control Specification)协定。1999年7月26日正式公布1.0版,确定使用2.4GHz频谱,最高资料传输速度1Mbps,同时开始了大规模宣传。和当时流行的红外线技术相比,蓝牙有着更高的传输速度,而且不需要像红外线那样进行接口对接口的连接,所有蓝牙设备基本上只要在有效通讯范围内使用,就可以进行随时连接。

当1.0规格推出以后,蓝牙并未立即受到广泛的应用,除了当时对应蓝牙功能的电子设备种类少,蓝牙装置也十分昂贵。2001年的1.1版正式列入IEEE标准,Bluetooth 1.1即为IEEE 802.15.1。同年,SIG成员公司超过2000家。过了几年之后,采用蓝牙技术的电子装置如雨后春笋般增加,售价也大幅下降。为了扩宽蓝牙的应用层面和传输速度,SIG先后推出了1.2、2.0版,以及其他附加新功能,例如EDR(Enhanced Data Rate,配合2.0的技术标准,将最大传输速度提高到3Mbps)、A2DP(Advanced Audio Distribution Profile,一个控音轨分配技术,主要应用于立体声耳机、AVRCP(A/V Remote Control Profile)等。Bluetooth 2.0将传输率提升至2Mbps、3Mbps,远大于1.x版的1Mbps(实际约723.2kbps)。

蓝牙从1.0到2.0其速度与传输距离一直是其硬伤,使用体验极差文件型的数据传输几乎等于不可用,所以我一直对蓝牙设备不感冒。直至遇到BLE的出现也就是低功耗蓝牙,来看看4.0之后的蓝牙有何技术特性吧:

蓝牙4.0

蓝牙4.0是Bluetooth SIG于2010年7月7日推出的新的规范。其最重要的特性是支持省电

  • Bluetooth 4.0,协议组成和当前主流的Bluetooth h2.x+EDR、还未普及的Bluetooth h3.0+HS不同,Bluetooth 4.0是Bluetooth从诞生至今唯一的一个综合协议规范,
    还提出了“低功耗蓝牙”、“传统蓝牙”和“高速蓝牙”三种模式。
  • 其中:高速蓝牙主攻数据交换与传输;传统蓝牙则以信息沟通、设备连接为重点;蓝牙低功耗顾名思义,以不需占用太多带宽的设备连接为主。前身其实是NOKIA开发的Wibree技术,本是作为一项专为移动设备开发的极低功耗的移动无线通信技术,在被SIG接纳并规范化之后重命名为Bluetooth Low Energy(后简称低功耗蓝牙)。这三种协议规范还能够互相组合搭配、从而实现更广泛的应用模式,此外,Bluetooth 4.0还把蓝牙的传输距离提升到100米以上(低功耗模式条件下)。
  • 分Single mode与Dual mode。
  • Single mode只能与BT4.0互相传输无法向下兼容(与3.0/2.1/2.0无法相通);Dual mode可以向下兼容可与BT4.0传输也可以跟3.0/2.1/2.0传输
  • 超低的峰值、平均和待机模式功耗,覆盖范围增强,最大范围可超过100米。
  • 速度:支持1Mbps数据传输率下的超短数据包,最少8个八组位,最多27个。所有连接都使用蓝牙2.1加入的减速呼吸模式(sniff subrating)来达到超低工作循环。
  • 跳频:使用所有蓝牙规范版本通用的自适应跳频,最大程度地减少和其他2.4 GHz ISM频段无线技术的串扰。
  • 主控制:可以休眠更长时间,只在需要执行动作的时候才唤醒。
  • 延迟:最短可在3毫秒内完成连接设置并开始传输数据。
  • 健壮性:所有数据包都使用24-bit CRC校验,确保最大程度抵御干扰。
  • 安全:使用AES-128 CCM加密算法进行数据包加密和认证。
  • 拓扑:每个数据包的每次接收都使用32位寻址,理论上可连接数十亿设备;针对一对一连接最优化,并支持星形拓扑的一对多连接;使用快速连接和断开,数据可以在网状拓扑内转移而无需维持复杂的网状网络。

蓝牙4.1

蓝牙4.1是蓝牙技术联盟于2013年底推出的新的规范,其目的是为了让 Bluetooth Smart 技术最终成为物联网(Internet of Things)发展的核心动力。
此版本为蓝牙4.0的软件更新版本,搭载蓝牙4.0设备的终端可通过软件更新获得此版本。

对于开发人员而言,该更新是蓝牙技术发展史上一项重要的进步。该更新提供了更高的灵活性和掌控度,让开发人员能创造更具创新并催化物联网(IOT)发展的产品。
支持多设备连接。

  • 智能连接:增加设置设备间连接频率的支持。制造商可以对设备设置连接进行设置,使得设备可以更加智能的控制设备电量。

蓝牙4.2

蓝牙4.2是蓝牙技术联盟于2014年12月推出的新的规范。

蓝牙5

蓝牙5在2016年6月被宣布。在有效传输距离上将是4.2LE版本的4倍(理论上可达300米),传输速度将是4.2LE版本的2倍(速度上限为24Mbps)。蓝牙5.0还支持室内定位导航功能(结合WiFi可以实现精度小于1米的室内定位),允许无需配对接受信标的数据(比如广告、Beacon、位置信息等,传输率提高了8倍),针对物联网进行了很多底层优化。

看完这些特性除了不能上网以外其它的无线能力几乎都能秒杀WIFI了。

蓝牙常见名称和缩写

在进入IOS之前我们得深入到的理论领域,了解蓝牙4.0中的一些基本的术语:

  • MFI:make for ipad, iphone, itouch 专们为苹果设备制作的设备
  • BLE:buletouch low energy,蓝牙4.0设备因为低耗电,所以也叫做BLE
  • peripheral:外设,被连接的设备
  • central:中心,发起连接的时central
  • service:服务,每个外设会有很多服务,每个服务中包含很多字段,这些字段的权限一般分为 读read,写write,通知notiy几种,就是我们连接设备后具体需要操作的内容。
  • characteristic:特征 每个设备会提供服务和特征,类似于服务端的api,但是机构不同。
  • Description:每个characteristic可以对应一个或多个Description用户描述characteristic的信息或属性
    外设、服务、特征间的关系

外设、服务、特征间的关系

下图你在Apple的开发者网站上也能找到:

外设、服务、特征间的关系

蓝牙中心模式流程

  1. 建立中心角色
  2. 扫描外设(discover)
  3. 连接外设(connect)
  4. 扫描外设中的服务和特征(discover)
    4.1 获取外设的services
    4.2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值
  5. 与外设做数据交互(explore and interact)
  6. 订阅Characteristic的通知
  7. 断开连接(disconnect)

蓝牙设备工作的状态

  • 准备(standby)
  • 广播(advertising)
  • 监听扫描(Scanning
  • 发起连接(Initiating)
  • 已连接(Connected)

蓝牙和版本的使用限制

  • 蓝牙2.0:越狱设备
  • 蓝牙4.0:IOS6 以上
  • MFI认证设备(Make For ipod/ipad/iphone):无限制

iOS 的实现

先创建一个 Swift 的工程,然后引入CoreBluetooth ,它就是iOS中提供蓝牙通信的核心库。然后ViewController必须实现 CBCentralManagerDelegate,CBPeripheralDelegate 两个接口:

import CoreBluetooth
import UIKit

class ViewController: UIViewController, CBCentralManagerDelegate,CBPeripheralDelegate {
        override func viewDidLoad() {
            super.viewDidLoad()
        }
} 

1.) 建立中心角色

然后要定义一个管理中心的对象变量,它就是蓝牙控制的主入口,然后在viewDidLoad()中实例化它:

class ViewController: UIViewController, CBCentralManagerDelegate,CBPeripheralDelegate {
    var manager : CBCentralManager!        
    override func viewDidLoad() {
            super.viewDidLoad()
            manager = CBCentralManager.init(delegate: self, queue: DispatchQueue.main)
        }
} 

2.) 扫描外设

管理中心一但被初始化后就会检查当前手机上的蓝牙状态,并调用 centralManagerDidUpdateState 方法,假设完整初始化后就马上进行设备扫描,那就要在centralManagerDidUpdateState中进行,在 viewDidLoad下方加入以下的代码:

 func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .unknown:
            print("未知的蓝牙设备")
        case .resetting:
            print("蓝牙正在被重置中")
        case .unsupported:
            print("不支持蓝牙服务")
        case .unauthorized:
            print("蓝牙服务未被授权")
        case .poweredOff:
            print("蓝牙服务已关闭")
        case .poweredOn:
            print("蓝牙已启动,开始扫描...")
            startScan()
        }
    }

 /// 扫描蓝牙设备
  func startScan() {
        manager.scanForPeripherals(withServices: nil, options: nil)
  }

注:只有返回.poweredOn状态下才能进行蓝牙设备的扫描。

scanForPeripherals方法就是对可发现的蓝牙设备进行扫描,输入的参数可以作一些过滤条件,我这里是不加任何的过滤条件无差别化地扫描。

3.) 连接外设

scanForPeripherals方法被调用后,CoreBluetooth就会调用centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber),所以我们就得在这个方法内将我们的目标蓝牙设备找出来,然后用一个变量Hold住它,否则这个外设就会被释放掉,然后你就会在XCode中得到一条提示信息说你没有Hold住你需要的设备变量了。

// 定义一个变量来Hold住目标设备
var connectedPeripheral : CBPeripheral!

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
            if (peripheral.name=="BT05") {
                manager.connect(peripheral, options:nil)
                // 找到的设备必须持有它,否则CBCentralManager中也不会保存peripheral,那么CBPeripheralDelegate中的方法也不会被调用!
                connectedPeripheral = peripheral
                print("正在连接:\(peripheral.name)...")
            }
}

由于我不知道我的设备的ServiceUUID是什么,为了方便我编码的需要所以我只找一个名为"BT05"的设备名称(这是我蓝牙模块的默认名字)找到之后就马上调用connect进行连接,注意:当中心对象不停止扫描(manager.stopScan())以上的方法就会不断地被调用

一个主设备最多能连7个外设,每个外设最多只能给一个主设备连接,连接成功,失败,断开会进入各自的委托

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral)
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?)
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?)

下文就会补充这几个委托方法的实现

4.1) 获取外设的服务

当连接成功后 centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) 就会被调用,在这里我们会设置找到的外设对象的委托(self),通常这个方法内会做另一种筛选那就是服务特征,通俗点说就是这个蓝牙设备可以提供些啥服务,例如写入,读取,或者其它什么的一些动作。

    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral){        
        print("正在查找Service")
        // 外设寻找目标服务
        peripheral.discoverServices(nil)
        peripheral.delegate = self
        self.title = peripheral.name
        // 停止扫描
        manager.stopScan()
    }

4.2) 获取外设特征

discoverServices(nil)传入了nil 那么就啥服务信息都直接获取,然后对服务进行发现,这个过程和前文中的设备发现非常的像

    // 这个服务地址可以从设备中查到的
    let ServiceUUID =  "FFE0"
    //扫描到Services
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){
        if (error != nil){
            print("查找 services 时 \(peripheral.name) 报错 \(error?.localizedDescription)")
        }
        
        for service in peripheral.services! {
            // 需要连接的 CBCharacteristic 的 UUID
            if service.uuid.uuidString == ServiceUUID {
                peripheral.discoverCharacteristics(nil, for: service)
            }
        }
    }

5) 与外设做数据交互

这样我们实现的CBPeripheralDelegate委托接口中的peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)将会被调用,用于读取蓝牙传过来的数据。

   // 同理我们要Hold住特征变量,否则会被释放掉
    var savedCharacteristic : CBCharacteristic!
    var lastString : NSString!

   // Interface Build 中的标签对象,用来显示从传感器读取的室温
   @IBOutlet weak var �lbTemperature: UILabel!
    //获取的charateristic的值,处理收接到的数据
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        // 好了,charcteristic.value 就是从蓝牙设备传过来的原数据内容,我们可以在这里进行处理
        let resultStr = NSString(data: characteristic.value!, encoding: String.Encoding.utf8.rawValue)
        print(resultStr)
        
        // 将温度显示到标签中
        lbTemperature.text = resultStr
        
        if lastString == resultStr {
            return;
        }

        self.savedCharacteristic = characteristic
    }

6) 订阅 Characteristic 的通知

由于BLE4.0的"减弱式呼吸"工作特性我们需要对外设置发出的通知进行订阅,如数据发生改变中心可以第一时间获知。

    /// 扫描到 characteristic 时
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error? {
        if error != nil {
            print("查找 characteristics 时 \(peripheral.name) 报错 \(error?.localizedDescription)")
        }
        
        //获取Characteristic的值,读到数据会进入方法:
        for characteristic in service.characteristics! {
            peripheral.readValue(for: characteristic)
            //设置 characteristic 的 notifying 属性 为 true , 表示接受广播
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }

7) 断开连接

其实到此这个App就完成了,以下的这些代码是对相关的错误处理进行补全:

    /// 连接到Peripherals-失败
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?){
        print("连接到名字为 \(peripheral.name) 的设备失败,原因是 \(error?.localizedDescription)")
        
    }
    
    /// 断开
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?){
        print("连接到名字为 \(peripheral.name) 的设备断开,原因是 \(error?.localizedDescription)"
        let alertView = UIAlertController.init(title: "抱歉", message: "蓝牙设备\(peripheral.name)连接断开,请重新扫描设备连接", preferredStyle: UIAlertControllerStyle.alert)
        alertView.show(self, sender: nil)
        
    }

   func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?){
        if error != nil{
            print("写入 characteristics 时 \(peripheral.name) 报错 \(error?.localizedDescription)")
        }
        // 这里可以处理向设备写入数据时的回调
    }

在下一篇中将会介绍蓝牙设备与Arduino 一端关于硬件部分与固件部分的制作。

你可能感兴趣的:(移动开发,嵌入式,swift)