import Foundation import CoreBluetooth /// Provisioner for KBeacon / KBPro hardware /// Protocol: FFE0 service, FFE1 write, FFE2 notify /// Auth via CMD_AUTH (0x01), config via CMD_WRITE_PARAMS (0x03), save via CMD_SAVE (0x04) final class KBeaconProvisioner: NSObject, BeaconProvisioner { // MARK: - Protocol Commands private enum CMD: UInt8 { case auth = 0x01 case readParams = 0x02 case writeParams = 0x03 case save = 0x04 } // MARK: - Parameter IDs private enum ParamID: UInt8 { case uuid = 0x10 case major = 0x11 case minor = 0x12 case txPower = 0x13 case advInterval = 0x14 } // MARK: - Known passwords (tried in order, matching Android) private static let passwords: [Data] = [ "kd1234".data(using: .utf8)!, Data(repeating: 0, count: 16), Data([0x31,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x30,0x31,0x32,0x33,0x34,0x35,0x36]), "0000000000000000".data(using: .utf8)!, "1234567890123456".data(using: .utf8)! ] // MARK: - State private let peripheral: CBPeripheral private let centralManager: CBCentralManager private var writeChar: CBCharacteristic? private var notifyChar: CBCharacteristic? private var connectionContinuation: CheckedContinuation? private var serviceContinuation: CheckedContinuation? private var writeContinuation: CheckedContinuation? private(set) var isConnected = false var diagnosticLog: ProvisionLog? var bleManager: BLEManager? // MARK: - Init init(peripheral: CBPeripheral, centralManager: CBCentralManager) { self.peripheral = peripheral self.centralManager = centralManager super.init() self.peripheral.delegate = self } // MARK: - BeaconProvisioner func connect() async throws { // Connect with retry for attempt in 1...GATTConstants.maxRetries { do { try await connectOnce() try await discoverServices() try await authenticate() isConnected = true return } catch { disconnect() if attempt < GATTConstants.maxRetries { try await Task.sleep(nanoseconds: UInt64(GATTConstants.retryDelay * 1_000_000_000)) } else { throw error } } } } func writeConfig(_ config: BeaconConfig) async throws { guard isConnected, let writeChar else { throw ProvisionError.notConnected } // Build parameter payload var params = Data() // UUID (16 bytes) params.append(ParamID.uuid.rawValue) let uuidBytes = config.uuid.hexToBytes params.append(contentsOf: uuidBytes) // Major (2 bytes BE) params.append(ParamID.major.rawValue) params.append(UInt8(config.major >> 8)) params.append(UInt8(config.major & 0xFF)) // Minor (2 bytes BE) params.append(ParamID.minor.rawValue) params.append(UInt8(config.minor >> 8)) params.append(UInt8(config.minor & 0xFF)) // TX Power params.append(ParamID.txPower.rawValue) params.append(config.txPower) // Adv Interval params.append(ParamID.advInterval.rawValue) params.append(UInt8(config.advInterval >> 8)) params.append(UInt8(config.advInterval & 0xFF)) // Send CMD_WRITE_PARAMS let writeCmd = Data([CMD.writeParams.rawValue]) + params let writeResp = try await sendCommand(writeCmd) guard writeResp.first == CMD.writeParams.rawValue else { throw ProvisionError.writeFailed("Unexpected write response") } // Send CMD_SAVE to flash let saveResp = try await sendCommand(Data([CMD.save.rawValue])) guard saveResp.first == CMD.save.rawValue else { throw ProvisionError.saveFailed } } func disconnect() { if peripheral.state == .connected || peripheral.state == .connecting { centralManager.cancelPeripheralConnection(peripheral) } isConnected = false } // MARK: - Private: Connection private func connectOnce() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in connectionContinuation = cont // Register for connection callbacks via BLEManager (the CBCentralManagerDelegate) bleManager?.onPeripheralConnected = { [weak self] connectedPeripheral in guard connectedPeripheral.identifier == self?.peripheral.identifier else { return } if let c = self?.connectionContinuation { self?.connectionContinuation = nil c.resume() } } bleManager?.onPeripheralFailedToConnect = { [weak self] failedPeripheral, error in guard failedPeripheral.identifier == self?.peripheral.identifier else { return } if let c = self?.connectionContinuation { self?.connectionContinuation = nil c.resume(throwing: error ?? ProvisionError.connectionTimeout) } } centralManager.connect(peripheral, options: nil) // Timeout DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.connectionTimeout) { [weak self] in if let c = self?.connectionContinuation { self?.connectionContinuation = nil c.resume(throwing: ProvisionError.connectionTimeout) } } } } private func discoverServices() async throws { try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in serviceContinuation = cont peripheral.discoverServices([GATTConstants.ffe0Service]) DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.operationTimeout) { [weak self] in if let c = self?.serviceContinuation { self?.serviceContinuation = nil c.resume(throwing: ProvisionError.serviceDiscoveryTimeout) } } } } private func authenticate() async throws { for password in Self.passwords { let cmd = Data([CMD.auth.rawValue]) + password do { let resp = try await sendCommand(cmd) if resp.first == CMD.auth.rawValue && resp.count > 1 && resp[1] == 0x00 { return // Auth success } } catch { continue } } throw ProvisionError.authFailed } private func sendCommand(_ data: Data) async throws -> Data { guard let writeChar else { throw ProvisionError.notConnected } return try await withCheckedThrowingContinuation { (cont: CheckedContinuation) in writeContinuation = cont peripheral.writeValue(data, for: writeChar, type: .withResponse) DispatchQueue.main.asyncAfter(deadline: .now() + GATTConstants.operationTimeout) { [weak self] in if let c = self?.writeContinuation { self?.writeContinuation = nil c.resume(throwing: ProvisionError.operationTimeout) } } } } } // MARK: - CBPeripheralDelegate extension KBeaconProvisioner: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error { serviceContinuation?.resume(throwing: error) serviceContinuation = nil return } guard let service = peripheral.services?.first(where: { $0.uuid == GATTConstants.ffe0Service }) else { serviceContinuation?.resume(throwing: ProvisionError.serviceNotFound) serviceContinuation = nil return } peripheral.discoverCharacteristics([GATTConstants.ffe1Char, GATTConstants.ffe2Char], for: service) } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error { serviceContinuation?.resume(throwing: error) serviceContinuation = nil return } for char in service.characteristics ?? [] { switch char.uuid { case GATTConstants.ffe1Char: writeChar = char case GATTConstants.ffe2Char: notifyChar = char peripheral.setNotifyValue(true, for: char) default: break } } if writeChar != nil { serviceContinuation?.resume() serviceContinuation = nil } else { serviceContinuation?.resume(throwing: ProvisionError.characteristicNotFound) serviceContinuation = nil } } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { guard characteristic.uuid == GATTConstants.ffe2Char, let data = characteristic.value else { return } if let cont = writeContinuation { writeContinuation = nil cont.resume(returning: data) } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { // Write acknowledgment — actual response comes via notify on FFE2 if let error { if let cont = writeContinuation { writeContinuation = nil cont.resume(throwing: error) } } } }