import Foundation import CoreBluetooth #if canImport(JL_BLEKit) import JL_BLEKit #endif /// Connect / disconnect via JL_BLEMultiple. Mirrors `feature/ConnectFeature.kt`. /// /// Connection state events are published as `connectionState` payloads with /// the same numeric coding as the Dart `ConnectionStateEvent` constants /// (0=disconnect, 1=ok, 2=connecting). RCSP-init success surfaces as a /// separate `rcspInit` event when the JieLi SDK confirms `cmdTargetFeatureResult` /// returns successfully — same flow Android publishes. public class ConnectFeature: NSObject { weak var server: JieliHomeServer? init(server: JieliHomeServer) { self.server = server } /// Cached id → entity for lookup by Dart-side `address` (peripheral UUID). private var entityCache: [String: AnyObject] = [:] private var connectedId: String? private var connectedName: String? func attach() { #if canImport(JL_BLEKit) // Hook into the JieLi notifications. The SDK posts named NSNotifications // (kJL_BLE_M_ENTITY_CONNECTED / kJL_BLE_M_ENTITY_DISCONNECTED). let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(onConnected(_:)), name: NSNotification.Name(kJL_BLE_M_ENTITY_CONNECTED), object: nil) nc.addObserver(self, selector: #selector(onDisconnected(_:)), name: NSNotification.Name(kJL_BLE_M_ENTITY_DISCONNECTED), object: nil) nc.addObserver(self, selector: #selector(onAdapterOff(_:)), name: NSNotification.Name(kJL_BLE_M_OFF), object: nil) nc.addObserver(self, selector: #selector(onAdapterOn(_:)), name: NSNotification.Name(kJL_BLE_M_ON), object: nil) #endif } func connect( bleAddress: String, edrAddress: String?, deviceType: Int, connectWay: Int ) throws { server?.dispatcher.send([ "type": "connectionState", "address": bleAddress, "state": 2, // connecting ]) #if canImport(JL_BLEKit) guard let server = server else { throw PluginError("CONNECT_FAILED", "server not initialized") } let mult = server.bleMultiple // Build / lookup entity. JL_BLEMultiple keeps a `blePeripheralArr` with // discovered entities — we resolve by UUID, matching the iOS demo. let entity: JL_EntityM if let cached = entityCache[bleAddress] as? JL_EntityM { entity = cached } else if let made = mult.makeEntity(withUUID: bleAddress) { entity = made entityCache[bleAddress] = entity } else { throw PluginError("CONNECT_FAILED", "remote device not found (UUID=\(bleAddress))") } mult.connectEntity(entity) { [weak self] status in guard let self = self else { return } switch status { case .paired: self.connectedId = bleAddress self.connectedName = entity.mPeripheral.name // RCSP target-feature query — same role as Android's connect-success // path that issues `cmdTargetFeatureResult` after handshake. entity.mCmdManager.cmdTargetFeatureResult { _, _, _ in self.server?.dispatcher.send([ "type": "connectionState", "address": bleAddress, "state": 1, // ok / link connected ]) self.server?.dispatcher.send([ "type": "rcspInit", "address": bleAddress, "code": 0, ]) } case .connecting: break case .disconnectOk: self.server?.dispatcher.send([ "type": "connectionState", "address": bleAddress, "state": 0, ]) default: // Any other terminal failure → expose as 0/disconnect with a code. self.server?.dispatcher.send([ "type": "connectionState", "address": bleAddress, "state": 0, ]) } } #else throw PluginError("CONNECT_FAILED", "JL_BLEKit framework not linked") #endif } func disconnect(address: String) throws { #if canImport(JL_BLEKit) guard let server = server else { return } let mult = server.bleMultiple if let entity = entityCache[address] as? JL_EntityM { mult.disconnectEntity(entity) { _ in } } server.dispatcher.send([ "type": "connectionState", "address": address, "state": 0, ]) if connectedId == address { connectedId = nil connectedName = nil } #endif } func isConnected(address: String) -> Bool { return connectedId == address } func connectedDeviceInfo() -> [String: Any?]? { guard let id = connectedId else { return nil } return ["address": id, "name": connectedName ?? ""] } // MARK: - Internal helpers used by sibling features func currentEntity() -> AnyObject? { guard let id = connectedId else { return nil } return entityCache[id] } // MARK: - Notifications @objc private func onConnected(_ note: Notification) { // The SDK posts the CBPeripheral as the object on Custom-BLE flow. The // BleMultiple flow publishes via the `paired` callback above, so the only // job here is to ensure consistency for native subscribers. if let cbp = note.object as? CBPeripheral { connectedId = cbp.identifier.uuidString connectedName = cbp.name } } @objc private func onDisconnected(_ note: Notification) { if let cbp = note.object as? CBPeripheral { let id = cbp.identifier.uuidString server?.dispatcher.send([ "type": "connectionState", "address": id, "state": 0, ]) if connectedId == id { connectedId = nil; connectedName = nil } } } @objc private func onAdapterOff(_ note: Notification) { server?.dispatcher.send(["type": "adapterStatus", "enabled": false, "hasBle": true]) } @objc private func onAdapterOn(_ note: Notification) { server?.dispatcher.send(["type": "adapterStatus", "enabled": true, "hasBle": true]) } }