import Foundation import CoreBluetooth /// Result of a provisioning operation enum ProvisioningResult { case success case failure(String) } /// Configuration to write to a beacon struct BeaconConfig { let uuid: String // 32-char hex, no dashes let major: UInt16 let minor: UInt16 let txPower: Int8 // Typically -59 let interval: UInt16 // Advertising interval in ms, typically 350 } /// Handles GATT connection and provisioning of beacons class BeaconProvisioner: NSObject, ObservableObject { // MARK: - BlueCharm GATT Characteristics private static let BLUECHARM_SERVICE = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB") private static let BLUECHARM_PASSWORD_CHAR = CBUUID(string: "0000FFF1-0000-1000-8000-00805F9B34FB") private static let BLUECHARM_UUID_CHAR = CBUUID(string: "0000FFF2-0000-1000-8000-00805F9B34FB") private static let BLUECHARM_MAJOR_CHAR = CBUUID(string: "0000FFF3-0000-1000-8000-00805F9B34FB") private static let BLUECHARM_MINOR_CHAR = CBUUID(string: "0000FFF4-0000-1000-8000-00805F9B34FB") private static let BLUECHARM_TXPOWER_CHAR = CBUUID(string: "0000FFF5-0000-1000-8000-00805F9B34FB") // MARK: - KBeacon GATT (basic - for full support use their SDK) private static let KBEACON_SERVICE = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB") // BlueCharm default passwords to try private static let BLUECHARM_PASSWORDS = ["000000", "FFFF", "123456"] // KBeacon default passwords private static let KBEACON_PASSWORDS = [ "0000000000000000", // 16 zeros "31323334353637383930313233343536" // ASCII "1234567890123456" ] @Published var state: ProvisioningState = .idle @Published var progress: String = "" enum ProvisioningState: Equatable { case idle case connecting case discoveringServices case authenticating case writing case verifying case success case failed(String) } private var centralManager: CBCentralManager! private var peripheral: CBPeripheral? private var beaconType: BeaconType = .unknown private var config: BeaconConfig? private var completion: ((ProvisioningResult) -> Void)? private var configService: CBService? private var characteristics: [CBUUID: CBCharacteristic] = [:] private var passwordIndex = 0 private var writeQueue: [(CBCharacteristic, Data)] = [] override init() { super.init() centralManager = CBCentralManager(delegate: self, queue: .main) } /// Provision a beacon with the given configuration func provision(beacon: DiscoveredBeacon, config: BeaconConfig, completion: @escaping (ProvisioningResult) -> Void) { guard centralManager.state == .poweredOn else { completion(.failure("Bluetooth not available")) return } self.peripheral = beacon.peripheral self.beaconType = beacon.type self.config = config self.completion = completion self.passwordIndex = 0 self.characteristics.removeAll() self.writeQueue.removeAll() state = .connecting progress = "Connecting to \(beacon.displayName)..." centralManager.connect(beacon.peripheral, options: nil) // Timeout after 30 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 30) { [weak self] in if self?.state != .success && self?.state != .idle { self?.fail("Connection timeout") } } } /// Cancel current provisioning func cancel() { if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } cleanup() } private func cleanup() { peripheral = nil config = nil completion = nil configService = nil characteristics.removeAll() writeQueue.removeAll() state = .idle progress = "" } private func fail(_ message: String) { NSLog("BeaconProvisioner: Failed - \(message)") state = .failed(message) if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } completion?(.failure(message)) cleanup() } private func succeed() { NSLog("BeaconProvisioner: Success!") state = .success if let peripheral = peripheral { centralManager.cancelPeripheralConnection(peripheral) } completion?(.success) cleanup() } // MARK: - BlueCharm Provisioning private func provisionBlueCharm() { guard let service = configService else { fail("Config service not found") return } // Discover characteristics peripheral?.discoverCharacteristics([ BeaconProvisioner.BLUECHARM_PASSWORD_CHAR, BeaconProvisioner.BLUECHARM_UUID_CHAR, BeaconProvisioner.BLUECHARM_MAJOR_CHAR, BeaconProvisioner.BLUECHARM_MINOR_CHAR, BeaconProvisioner.BLUECHARM_TXPOWER_CHAR ], for: service) } private func authenticateBlueCharm() { guard let passwordChar = characteristics[BeaconProvisioner.BLUECHARM_PASSWORD_CHAR] else { fail("Password characteristic not found") return } let passwords = BeaconProvisioner.BLUECHARM_PASSWORDS guard passwordIndex < passwords.count else { fail("Authentication failed - tried all passwords") return } state = .authenticating progress = "Authenticating..." let password = passwords[passwordIndex] if let data = password.data(using: .utf8) { NSLog("BeaconProvisioner: Trying BlueCharm password \(passwordIndex + 1)/\(passwords.count)") peripheral?.writeValue(data, for: passwordChar, type: .withResponse) } } private func writeBlueCharmConfig() { guard let config = config else { fail("No config provided") return } state = .writing progress = "Writing configuration..." // Build write queue writeQueue.removeAll() // UUID - 16 bytes, no dashes if let uuidChar = characteristics[BeaconProvisioner.BLUECHARM_UUID_CHAR] { if let uuidData = hexStringToData(config.uuid) { writeQueue.append((uuidChar, uuidData)) } } // Major - 2 bytes big-endian if let majorChar = characteristics[BeaconProvisioner.BLUECHARM_MAJOR_CHAR] { var major = config.major.bigEndian let majorData = Data(bytes: &major, count: 2) writeQueue.append((majorChar, majorData)) } // Minor - 2 bytes big-endian if let minorChar = characteristics[BeaconProvisioner.BLUECHARM_MINOR_CHAR] { var minor = config.minor.bigEndian let minorData = Data(bytes: &minor, count: 2) writeQueue.append((minorChar, minorData)) } // TxPower - 1 byte signed if let txChar = characteristics[BeaconProvisioner.BLUECHARM_TXPOWER_CHAR] { var txPower = config.txPower let txData = Data(bytes: &txPower, count: 1) writeQueue.append((txChar, txData)) } // Start writing processWriteQueue() } private func processWriteQueue() { guard !writeQueue.isEmpty else { // All writes complete progress = "Configuration complete!" succeed() return } let (characteristic, data) = writeQueue.removeFirst() NSLog("BeaconProvisioner: Writing \(data.count) bytes to \(characteristic.uuid)") peripheral?.writeValue(data, for: characteristic, type: .withResponse) } // MARK: - KBeacon Provisioning private func provisionKBeacon() { // KBeacon uses a more complex protocol // For now, we'll just try basic GATT writes // Full support would require their SDK state = .writing progress = "KBeacon requires their SDK for full support.\nUse clipboard to copy config." // For now, just succeed and let user use clipboard DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in self?.fail("KBeacon provisioning requires their SDK. Please use the KBeacon app with the copied config.") } } // MARK: - Helpers private func hexStringToData(_ hex: String) -> Data? { let clean = hex.replacingOccurrences(of: "-", with: "").uppercased() guard clean.count == 32 else { return nil } var data = Data() var index = clean.startIndex while index < clean.endIndex { let nextIndex = clean.index(index, offsetBy: 2) let byteString = String(clean[index..