You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
5.8 KiB

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.
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])
}
}