import Foundation import CoreBluetooth import Combine /// Manages Bluetooth Low Energy (BLE) scanning, device connection, and binding to user accounts. final class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate { static let shared = BLEManager() @Published private(set) var state: CBManagerState = .unknown @Published var isScanning: Bool = false @Published var discoveredDevices: [DiscoveredBLEDevice] = [] @Published var boundDevices: [BoundDevice] = [] private var centralManager: CBCentralManager! private let boundDevicesKey = "com.celestia.trace.bound_devices" override private init() { super.init() self.centralManager = CBCentralManager(delegate: self, queue: nil) loadBoundDevices() } // MARK: - CentralManager Delegate func centralManagerDidUpdateState(_ central: CBCentralManager) { DispatchQueue.main.async { self.state = central.state } } func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? "现场录音设备" let device = DiscoveredBLEDevice( id: peripheral.identifier.uuidString, name: name, rssi: RSSI.intValue, peripheralUUID: peripheral.identifier.uuidString ) DispatchQueue.main.async { if !self.discoveredDevices.contains(where: { $0.id == device.id }) { self.discoveredDevices.append(device) } } } // MARK: - Scanning & Mocking func startScanning() { discoveredDevices.removeAll() isScanning = true if centralManager.state == .poweredOn { centralManager.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) } // Populate mock devices for simulator or testing environment DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) { [weak self] in guard let self = self, self.isScanning else { return } let mockDevices = [ DiscoveredBLEDevice(id: "MOCK-BLE-01", name: "星痕专业录音麦克风 01", rssi: -58, peripheralUUID: "0000180A-0000-1000-8000-00805F9B34FB"), DiscoveredBLEDevice(id: "MOCK-BLE-02", name: "星痕无线音频集线器", rssi: -72, peripheralUUID: "0000180F-0000-1000-8000-00805F9B34FB"), DiscoveredBLEDevice(id: "MOCK-BLE-03", name: "智能胸卡录音器 A2", rssi: -85, peripheralUUID: "0000181A-0000-1000-8000-00805F9B34FB") ] for device in mockDevices { if !self.discoveredDevices.contains(where: { $0.id == device.id }) { self.discoveredDevices.append(device) } } } } func stopScanning() { if centralManager.state == .poweredOn { centralManager.stopScan() } isScanning = false } // MARK: - Binding Management /// Binds a discovered BLE device to the specified user account ID. func bindDevice(_ device: DiscoveredBLEDevice, userId: String) -> BoundDevice { let boundDevice = BoundDevice( id: device.id, name: device.name, peripheralUUID: device.peripheralUUID, boundAt: Date(), isConnected: true, batteryLevel: Int.random(in: 75...99), firmwareVersion: "v1.4.2", boundUserId: userId ) // Unbind any existing duplicate boundDevices.removeAll(where: { $0.id == boundDevice.id }) boundDevices.append(boundDevice) saveBoundDevices() return boundDevice } /// Unbinds a device by its ID. func unbindDevice(id: String) { boundDevices.removeAll(where: { $0.id == id }) saveBoundDevices() } /// Returns devices bound to the given user account. func devices(forUserId userId: String) -> [BoundDevice] { return boundDevices.filter { $0.boundUserId == userId } } // MARK: - Local Persistence private func saveBoundDevices() { if let data = try? JSONEncoder().encode(boundDevices) { UserDefaults.standard.set(data, forKey: boundDevicesKey) } } private func loadBoundDevices() { if let data = UserDefaults.standard.data(forKey: boundDevicesKey), let devices = try? JSONDecoder().decode([BoundDevice].self, from: data) { self.boundDevices = devices } } }