背景知识
蓝牙技术最初由爱立信(也就是多年前手机做得最丑最奇葩的公司,最终被用户以脚投票踢出市场)创制。技术始于爱立信公司的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的开发者网站上也能找到:
蓝牙中心模式流程
- 建立中心角色
- 扫描外设(discover)
- 连接外设(connect)
- 扫描外设中的服务和特征(discover)
4.1 获取外设的services
4.2 获取外设的Characteristics,获取Characteristics的值,获取Characteristics的Descriptor和Descriptor的值 - 与外设做数据交互(explore and interact)
- 订阅Characteristic的通知
- 断开连接(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 一端关于硬件部分与固件部分的制作。