如何在应用中实现多个 CBPeripherals

How to implement multiple CBPeripherals in app

我正在编写一个适用于蓝牙设备的应用程序。起初,只有一个设备,所有应用程序逻辑都是围绕它严格构建的(ViewController 和模型)。但是,设备列表会增长,我需要实现良好的代码可扩展性。事实上,所有设备都遵循一个协议,即所有设备的服务和特性都是相同的,只是一些设备支持某些东西,而其他设备则不支持。我立即意识到为每个设备描述一个单独的模型将是一个非常麻烦的决定,因为会有很多重复的代码。起初,我创建了一个抽象设备classBasePeripheral,它描述了基本的东西(连接、断开、扫描服务和特性),然后继承它具体的设备,为它们实现相应的协议。总的来说,这样的解决方案使任务更容易一些,但我仍然确信这远非理想。我没有足够的经验,因此我想得到提示,在哪个方向为我工作。起初在我看来,解决方案可能是使用泛型,但很难立即理解它们对我的目的的工作。还有一个识别设备类型的问题,目前这是通过笨拙的 Switch-Case 设计完成的,我对它的使用不是很满意,例如,识别 50 种不同的设备。我想知道我应该学习哪些东西来实现我的目标(如果有代码示例就更好了)

protocol ColorPeripheral {
    func writeColor(_ color: Int)
}

protocol AnimationPeriheral {
    func writeAnimation(_ animation: Int)
    func writeAnimationSpeed(_ speed: Int)
}

protocol DistancePeripheral {
    // No write properties. Only readable
}

protocol IndicationPeripheral {
    // No write properties. Only readable
}

protocol PeripheralDataDelegate {
    func getColor(_ color: Any)
    func getAnimation(_ animation: Any)
    func getAnimationSpeed(_ speed: Any)
    func getDistance(_ distance: Any)
    func getLedState(_ state: Any)
}

class BasePeripheral: NSObject,
                      CBCentralManagerDelegate,
                      CBPeripheralDelegate {
    
    let centralManager: CBCentralManager
    let basePeripheral: CBPeripheral
    let advertisingData: [String : Any]
    public private(set) var advertisedName: String = "Unknown Device".localized
    public private(set) var RSSI          : NSNumber
    public var type : BasePeripheral.Type?
    public var services: [CBUUID]?
    public var delegate: PeripheralDataDelegate?
    
    init(withPeripheral peripheral: CBPeripheral,
         advertisementData advertisementDictionary: [String : Any],
         andRSSI currentRSSI: NSNumber,
         using manager: CBCentralManager) {
        self.centralManager = manager
        self.basePeripheral = peripheral
        self.advertisingData = advertisementDictionary
        self.RSSI = currentRSSI
        super.init()
        self.advertisedName = getAdvertisedName(advertisementDictionary)
        self.type = getAdvertisedService(advertisementDictionary)
        self.basePeripheral.delegate = self
    }
    
    private func getAdvertisedName(_ advertisementDictionary: [String : Any]) -> String {
        var advertisedName: String
        if let name = advertisementDictionary[CBAdvertisementDataLocalNameKey] as? String {
            advertisedName = name
        } else {
            advertisedName = "Unknown Device".localized
        }
        return advertisedName
    }
        
    // Getting device type
    private func getAdvertisedService(_ advertisementDictionary: [String : Any]) -> BasePeripheral.Type? {
        if let advUUID = advertisementDictionary[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
            for uuid in advUUID {
                // Getting type from [CBUUID:BasePeripheral.Type] dictionary
                if let service = UUIDs.advServices[uuid] {
                    return service
                }
            }
        }
        return nil
    }
    
    func connect() {
        centralManager.delegate = self
        centralManager.connect(basePeripheral, options: nil)
        print("Connecting to \(advertisedName)")
    }
    
    func disconnect() {
        centralManager.cancelPeripheralConnection(basePeripheral)
        print("Disconnecting from \(advertisedName)")
    }
    
    func didDiscoverService(_ service:CBService) {}
    
    func didDiscoverCharacteristic(_ characteristic: CBCharacteristic) {}
    
    func didReceiveData(from characteristic: CBCharacteristic) {}
    
    // MARK: - CBCentralManagerDelegate
    
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state != .poweredOn {
            print("Central Manager state changed to \(central.state)")
        }
    }
    
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("Connected to \(self.advertisedName )")
        basePeripheral.discoverServices(services)
    }
    
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        print("Disconnected from \(self.advertisedName ) - \(String(describing: error?.localizedDescription))")
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        if let services = peripheral.services {
            for service in services {
                didDiscoverService(service)
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        if let characteristics = service.characteristics {
            for characteristic in characteristics {
                didDiscoverCharacteristic(characteristic)
            }
        }
    }
    
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        didReceiveData(from: characteristic)
    }

}

class FirstPeripheral: BasePeripheral, ColorPeripheral, AnimationPeriheral {
    
    private var colorCharacteristic         : CBCharacteristic?
    private var animationCharacteristic     : CBCharacteristic?
    private var animationSpeedCharacteristic: CBCharacteristic?

    init(superPeripheral: BasePeripheral) {
        super.init(withPeripheral:    superPeripheral.basePeripheral,
                   advertisementData: superPeripheral.advertisingData,
                   andRSSI:           superPeripheral.RSSI,
                   using:             superPeripheral.centralManager)
        super.services = [UUIDs.COLOR_SERVICE_UUID,
                          UUIDs.ANIMATION_SERVICE_UUID]
    }
    
    // ColorPeripheral protocol
    func writeColor(_ color: Int) {
        if let characteristic = colorCharacteristic {
            var value = UInt8(color)
            let data = Data(bytes: &value, count: 1)
            basePeripheral.writeValue(data, for: characteristic, type: .withoutResponse)
        }
    }
    
    // AnimationPeripheral protocol
    func writeAnimation(_ animation: Int) {
        if let characteristic = animationCharacteristic {
            var value = UInt8(animation)
            let data = Data(bytes: &value, count: 1)
            basePeripheral.writeValue(data, for: characteristic, type: .withoutResponse)
        }
    }
    
    func writeAnimationSpeed(_ speed: Int) {
        if let characteristic = animationSpeedCharacteristic {
            var value = UInt8(speed)
            let data = Data(bytes: &value, count: 1)
            basePeripheral.writeValue(data, for: characteristic, type: .withoutResponse)
        }
    }
    
    override func didReceiveData(from characteristic: CBCharacteristic) {
        switch characteristic {
        case colorCharacteristic:
            delegate?.getColor(characteristic.value!)
            break
        case animationCharacteristic:
            delegate?.getAnimation(characteristic.value!)
            break
        case animationSpeedCharacteristic:
            delegate?.getAnimationSpeed(characteristic.value!)
            break
        default:
            print("Unknown value changed")
        }
    }
    
    override func didDiscoverService(_ service: CBService) {
        switch service.uuid {
        case UUIDs.COLOR_SERVICE_UUID:
            // Discover color characteristics
            break
        case UUIDs.ANIMATION_SERVICE_UUID:
            // Discover animation characteristics
            break
        default:
            print("Unknown service found - \(service.uuid)")
        }
    }
    
    override func didDiscoverCharacteristic(_ characteristic: CBCharacteristic) {
        switch characteristic.uuid {
        case UUIDs.COLOR_PRIMARY_CHARACTERISTIC_UUID:
            colorCharacteristic = characteristic
            break
        case UUIDs.ANIMATION_MODE_CHARACTERISTIC_UUID:
            animationCharacteristic = characteristic
            break
        case UUIDs.ANIMATION_ON_SPEED_CHARACTERISTIC_UUID:
            animationSpeedCharacteristic = characteristic
            break
        default:
            print("Unknown characteristic found \(characteristic.uuid)")
        }
    }
    
}

class SecondPeripheral: BasePeripheral, ColorPeripheral, DistancePeripheral, IndicationPeripheral {
    
    private var colorCharacteristic      : CBCharacteristic?
    private var distanceCharacteristic   : CBCharacteristic?
    private var indicationCharacteristic : CBCharacteristic?
        
    init(superPeripheral: BasePeripheral) {
        super.init(withPeripheral:    superPeripheral.basePeripheral,
                   advertisementData: superPeripheral.advertisingData,
                   andRSSI:           superPeripheral.RSSI,
                   using:             superPeripheral.centralManager)
        super.services = [UUIDs.COLOR_SERVICE_UUID,
                          UUIDs.DISTANCE_SERVICE_UUID,
                          UUIDs.INDICATION_SERVICE_UUID]
    }
    
    // ColorPeripheral protocol
    func writeColor(_ color: Int) {
        if let characteristic = colorCharacteristic {
            var value = UInt8(color)
            let data = Data(bytes: &value, count: 1)
            basePeripheral.writeValue(data, for: characteristic, type: .withoutResponse)
        }
    }
    
    override func didReceiveData(from characteristic: CBCharacteristic) {
        switch characteristic {
        case colorCharacteristic:
            delegate?.getColor(characteristic.value!)
            break
        case distanceCharacteristic:
            delegate?.getDistance(characteristic.value!)
            break
        case indicationCharacteristic:
            delegate?.getLedState(characteristic.value!)
            break
        default:
            print("Unknown value changed")
        }
    }
    
    override func didDiscoverService(_ service: CBService) {
        switch service.uuid {
        case UUIDs.COLOR_SERVICE_UUID:
            // Discover color characteristics
            break
        case UUIDs.DISTANCE_SERVICE_UUID:
            // Discover distance characteristics
            break
        case UUIDs.INDICATION_SERVICE_UUID:
            // Discover indication characteristics
        default:
            print("Unknown service found - \(service.uuid)")
        }
    }
    
    override func didDiscoverCharacteristic(_ characteristic: CBCharacteristic) {
        switch characteristic.uuid {
        case UUIDs.COLOR_PRIMARY_CHARACTERISTIC_UUID:
            colorCharacteristic = characteristic
            break
        case UUIDs.DISTANCE_CHARACTERISTIC_UUID:
            distanceCharacteristic = characteristic
            break
        case UUIDs.INDICATION_CHARACTERISTIC_UUID:
            indicationCharacteristic = characteristic
        default:
            print("Unknown characteristic found \(characteristic.uuid)")
        }
    }
    
}

这是非常具体的情况,但进入协议的方向可能是一个很好的方法。但是你必须大量使用协议扩展。举个简单的例子:

您应该将最基本的结构声明为协议。正如您所说,它们都连接到蓝牙,并且它们都具有您希望使用的相同特性。所以:

protocol BasicPeripheralInterface {
    
    var peripheral: CBPeripheral { get }
    var firstCharacteristic: CBCharacteristic { get }
    var secondCharacteristic: CBCharacteristic { get }
    
}

现在您希望在其上公开一些接口,这些接口可能会根据设备类型而改变。例如,让我们给它发送一些颜色:

protocol ColorPeripheralInterface {
    func setColor(_ color: UIColor)
}

到目前为止没有什么大不了的。但是现在我们可以像这样通过扩展协议来连接两者:

extension ColorPeripheralInterface where Self:BasicPeripheralInterface {
    
    func setColor(_ color: UIColor) {
        peripheral.writeValue(color.toData(), for: firstCharacteristic, type: .withoutResponse)
    }
    
}

通过这样做,我们是说每个 BasicPeripheralInterfaceColorPeripheralInterface 的对象都已经具有扩展中描述的逻辑。所以要创建这样一个对象,我们会做类似

的事情
class MyDevice: BasicPeripheralInterface, ColorPeripheralInterface {
    
    let peripheral: CBPeripheral
    let firstCharacteristic: CBCharacteristic
    let secondCharacteristic: CBCharacteristic
    
    init(peripheral: CBPeripheral, firstCharacteristic: CBCharacteristic, secondCharacteristic: CBCharacteristic) {
        self.peripheral = peripheral
        self.firstCharacteristic = firstCharacteristic
        self.secondCharacteristic = secondCharacteristic
    }
    
}

此 class 现在实现了 BasicPeripheralInterface 所需的逻辑,但根本没有与 ColorPeripheralInterface 相关的逻辑。不需要实现 setColor 因为这个方法已经在扩展中了。以下代码按预期工作:

func sendColor(toDevice device: MyDevice) {
    device.setColor(.red)
}

现在,当我们添加更多协议和更多设备时,我们会期待这样的事情:

protocol ColorPeripheralInterface {
    func setColor(_ color: UIColor)
}

extension ColorPeripheralInterface where Self:BasicPeripheralInterface {
    
    func setColor(_ color: UIColor) {
        peripheral.writeValue(color.toData(), for: firstCharacteristic, type: .withoutResponse)
    }
    
}

protocol StringPeripheralInterface {
    func setText(_ text: String)
}

extension StringPeripheralInterface where Self:BasicPeripheralInterface {
    
    func setText(_ text: String) {
        peripheral.writeValue(text.data(using: .utf8)!, for: secondCharacteristic, type: .withoutResponse)
    }
    
}

protocol AmountPeripheralInterface {
    func setAmount1(_ value: Int)
    func setAmount2(_ value: Int)
}

extension AmountPeripheralInterface where Self:BasicPeripheralInterface {
    
    func setAmount1(_ value: Int) {
        peripheral.writeValue("\(value)".data(using: .utf8)!, for: firstCharacteristic, type: .withoutResponse)
    }
    func setAmount2(_ value: Int) {
        peripheral.writeValue("\(value)".data(using: .utf8)!, for: secondCharacteristic, type: .withoutResponse)
    }
    
}

现在这只是定义了 3 个扩展了一些愚蠢逻辑的协议。但现在这些协议可以应用于并定义您喜欢的任何设备,并可按您需要的任何组合方式进行定义。首先一个基础 class 是有意义的

class BasicDevice: BasicPeripheralInterface {
    
    let peripheral: CBPeripheral
    let firstCharacteristic: CBCharacteristic
    let secondCharacteristic: CBCharacteristic
    
    init(peripheral: CBPeripheral, firstCharacteristic: CBCharacteristic, secondCharacteristic: CBCharacteristic) {
        self.peripheral = peripheral
        self.firstCharacteristic = firstCharacteristic
        self.secondCharacteristic = secondCharacteristic
    }
    
}

这个就不直接用了。但神奇之处在于声明具体设备:

class StringDevice: BasicDevice, StringPeripheralInterface {  }
class AmountStringDevice: BasicDevice, StringPeripheralInterface, AmountPeripheralInterface {  }
class LatestAllSupportingDevice: BasicDevice, StringPeripheralInterface, AmountPeripheralInterface, ColorPeripheralInterface {  }

所以这些是 3 个 classes,其中每个扩展不同的接口以实现不同的功能,现在可以用作具体设备 classes。他们每个人只持有分配给他们的协议中定义的方法,仅此而已。

这样您就可以轻松定义 50 个设备,对于每个设备,您只需要列出它对应的所有协议,仅此而已。

如果某个设备只有一种方法略有不同,您可以随时覆盖它。例如:

class ThisNewDeviceThatDoesThingsADashDifferently: BasicDevice, StringPeripheralInterface, AmountPeripheralInterface, ColorPeripheralInterface {
    
    func setAmount2(_ value: Int) {
        peripheral.writeValue("2-\(value)".data(using: .utf8)!, for: firstCharacteristic, type: .withoutResponse)
    }
    
}

现在 class 所做的一切与 LatestAllSupportingDevice 完全相同,但覆盖 setAmount2 以做一些不同的事情。

您还应该知道,在涉及 where Self: 时,扩展协议不仅限于协议。您可以轻松地简单地删除 BasicPeripheralInterface 并在任何地方使用 BasicDevice,例如 extension ColorPeripheralInterface where Self:BasicDevice.

使用这种方法可以做很多事情。但最终它可能适合也可能不适合您使用。从技术上讲,这个问题的任何答案都是基于意见的。