蓝牙使用
距离上次博客更新已经过去了好几个月 这段时间一直在忙公司项目的重构和整体UI重做 一直抽不出时间来对做的东西做一个归纳和整理 前几天项目成功上线了 现在对这段时间项目重构中遇到的问题和使用的技术做一些简单的整理 首先介绍项目中用的的占比较重的东西 蓝牙的使用及蓝牙交互逻辑的优化
iOS 蓝牙简介
- 蓝牙模式简介
蓝牙开发分为两种模式,中心模式(central),和外设模式(peripheral)。一般来讲,我们需要在软件内连接硬件,通过连接硬件给硬件发送指令以完成一些动作的蓝牙开发都是基于中心模式(central)模式的开发,也就是说我们开发的app是中心,我们要连接的硬件是外设。如果需要其他设备连接手机蓝牙,并对手机进行一些操作,那就是基于外设模式(peripheral)的开发。 本次我们主要介绍的就是中心模式的蓝牙开发 - 设备简介
- 中心设备(CBCentralManager):iOS系统的手机等设备
- 外围设备(CBPeripheral):手环等第三方设备
- 蓝牙数据传输简介
将外围设备(车辆)的数据传送给中心设备(手机)时, 数据是经过两层包装的
第一层是 Service(服务) , 可以是一个或多个, 比如车辆数据(服务)
第二层是 Characteristic(特征) , 他提供了更多关于Service(服务)的数据, 例如车辆数据(服务)中包含了两个数据, 分别是里程数据和续航数据, 这两个就是车辆数据(服务)的具体数据(特征) - 具体操作简介
读(read) , 写(write) , 订阅(notify)
我们的目的是读取设备中的数据(read) , 或者给设备写入一定的数据(write)。有时候我们还想设备的数据变化的时候不需要我们手动去读取这个值,需要设备自动通知我们它的值变化了,值是多少。把值告诉app,这个时候就需要订阅这个特征了(notify)
具体使用步骤
- 数据读写步骤
- 创建中心设备(CBCentralManager)
- 中心设备开始扫描(scanForPeripherals)
- 扫描到外围设备之后, 自动调用中心设备的代理方法(didDiscoverPeripheral)
- 如果设备过多, 可以将扫描到的外围设备添加到数组
- 开始连接, 从数组中过滤出自己想要的设备, 进行连接(connectPeripheral)
- 连接上之后, 自动调用中心设备的代理方法(didConnectPeripheral), 在代理中, 进行查找外围设备的服务(peripheral.discoverServices)
- 查找到服务之后, 自动调用外围设备的代理(didDiscoverServices), 可通过UUID,查找具体的服务,查找服务(discoverCharacteristics)
- 查找到特征之后, 自动调用外围设备的代理(didDiscoverCharacteristics), 通过UUID找到自己想要的特征, 读取特征(readValueForCharacteristic)
- 读取到特征之后, 自动调用外设的代理方法(didUpdateValueForCharacteristic),在这里打印或者解析自己想要的特征值.
代码拆解实现
//创建中心设备(CBCentralManager)
import Foundation
import CoreBluetooth
//用于看发送数据是否成功!
class LLBlueTooth:NSObject {
//单例对象
internal static let instance = LLBlueTooth()
//中心对象
var central : CBCentralManager?
//中心扫描到的设备都可以保存起来,
//扫描到新设备后可以通过通知的方式发送出去,连接设备界面可以接收通知,实时刷新设备列表
var deviceList: NSMutableArray?
// 当前连接的设备
var peripheral:CBPeripheral!
//发送数据特征(连接到设备之后可以把需要用到的特征保存起来,方便使用)
var sendCharacteristic:CBCharacteristic?
override init() {
super.init()
self.central = CBCentralManager.init(delegate:self, queue:nil, options:[CBCentralManagerOptionShowPowerAlertKey:false])
self.deviceList = NSMutableArray()
}
// MARK: 扫描设备的方法
func scanForPeripheralsWithServices(_ serviceUUIDS:[CBUUID]?, options:[String: AnyObject]?){
self.central?.scanForPeripherals(withServices: serviceUUIDS, options: options)
}
// MARK: 停止扫描
func stopScan() {
self.central?.stopScan()
}
// MARK: 写数据
func writeToPeripheral(_ data: Data) {
peripheral.writeValue(data , for: sendCharacteristic!, type: CBCharacteristicWriteType.withResponse)
}
// MARK: 连接某个设备的方法
/*
* 设备有几个状态
@available(iOS 7.0, *)
public enum CBPeripheralState : Int {
case disconnected
case connecting
case connected
@available(iOS 9.0, *)
case disconnecting
}
*/
func requestConnectPeripheral(_ model:CBPeripheral) {
if (model.state != CBPeripheralState.connected) {
central?.connect(model , options: nil)
}
}
}
//MARK: -- 中心管理器的代理
extension LLBlueTooth : CBCentralManagerDelegate{
// MARK: 检查运行这个App的设备是不是支持BLE。
func centralManagerDidUpdateState(_ central: CBCentralManager){
if #available(iOS 10.0, *) {
switch central.state {
case CBManagerState.poweredOn:
print("蓝牙打开")
case CBManagerState.unauthorized:
print("没有蓝牙功能")
case CBManagerState.poweredOff:
print("蓝牙关闭")
default:
print("未知状态")
}
}
// 手机蓝牙状态发生变化,可以发送通知出去。提示用户
}
// 开始扫描之后会扫描到蓝牙设备,扫描到之后走到这个代理方法
// MARK: 中心管理器扫描到了设备
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// 因为iOS目前不提供蓝牙设备的UUID的获取,所以在这里通过蓝牙名称判断是否是本公司的设备
guard peripheral.name != nil , peripheral.name!.contains("蓝牙名称") else {
return
}
}
// MARK: 连接外设成功,开始发现服务
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral){
// 设置代理
peripheral.delegate = self
// 开始发现服务
peripheral.discoverServices(nil)
// 保存当前连接设备
self.peripheral = peripheral
// 这里可以发通知出去告诉设备连接界面连接成功
}
// MARK: 连接外设失败
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
// 这里可以发通知出去告诉设备连接界面连接失败
}
// MARK: 连接丢失
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
NotificationCenter.default.post(name: Notification.Name(rawValue: "DidDisConnectPeriphernalNotification"), object: nil, userInfo: ["deviceList": self.deviceList as AnyObject])
// 这里可以发通知出去告诉设备连接界面连接丢失
}
}
// 外设的代理
extension LLBlueTooth : CBPeripheralDelegate {
//MARK: - 匹配对应服务UUID
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){
if error != nil {
return
}
for service in peripheral.services! {
peripheral.discoverCharacteristics(nil, for: service )
}
}
//MARK: - 服务下的特征
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?){
if (error != nil){
return
}
for characteristic in service.characteristics! {
switch characteristic.uuid.description {
case "具体特征值":
// 订阅特征值,订阅成功后后续所有的值变化都会自动通知
peripheral.setNotifyValue(true, for: characteristic)
case "******":
// 读区特征值,只能读到一次
peripheral.readValue(for:characteristic)
default:
print("扫描到其他特征")
}
}
}
//MARK: - 特征的订阅状体发生变化
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){
guard error == nil else {
return
}
}
// MARK: - 获取外设发来的数据
// 注意,所有的,不管是 read , notify 的特征的值都是在这里读取
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)-> (){
if(error != nil){
return
}
switch characteristic.uuid.uuidString {
case "***************":
print("接收到了设备的温度特征的值的变化")
default:
print("收到了其他数据特征数据: \(characteristic.uuid.uuidString)")
}
}
//MARK: - 检测中心向外设写数据是否成功
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if(error != nil){
print("发送数据失败!error信息:\(String(describing: error))")
}
}
}
//MARK: - 以上就是蓝牙模块的具体代码 在这里可以使用一些已有封装的第三方蓝牙框架 只要按步骤做就可以
其他相关优化
- 安全相关
因为蓝牙的操作可能会涉及到设备的安全性问题,所以在安全方面需要考量一下,在这里我提供一些我在安全方面所做的具体措施- 在连接到蓝牙设备以后,先与蓝牙设备及服务端做一次三方的安全验证,使用一些加密算法,保证当前是自己的APP对自己设备下发的蓝牙指令
- 在所有的蓝牙操作指令中增加时间戳安全判断,可以跟硬件工程师商讨具体设置的安全超时时长,时间超过预留时间的,不做响应
- 在一些涉及到设备和财产安全的蓝牙操作上,可以加一些与服务端的实时安全校验,保证当前蓝牙指令的安全性
- 性能相关
- 因为受蓝牙信号强度限制,蓝牙操作的灵敏度和响应时间会存在一定问题,所以要求硬件工程师对蓝牙设备的型号强度进行定制
- 在进行蓝牙设备搜索连接时,iOS没有自带的超时设置,如果不手动停止的话,会不停地进行设备搜索,影响设备性能,所以建议设置一个超时时长,进行一段时间的搜索以后停止对设备的搜索,并提示用户进行重试