- Fixed App Store icon display with ios-marketing idiom - Added iPad orientation support for multitasking - Added UILaunchScreen for iPad requirements - Removed unused BLE permissions and files from build Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
156 lines
5.4 KiB
Swift
156 lines
5.4 KiB
Swift
import Foundation
|
|
import CoreBluetooth
|
|
|
|
/// Beacon type detected by service UUID
|
|
enum BeaconType: String {
|
|
case kbeacon = "KBeacon"
|
|
case bluecharm = "BlueCharm"
|
|
case unknown = "Unknown"
|
|
}
|
|
|
|
/// A discovered BLE beacon that can be provisioned
|
|
struct DiscoveredBeacon: Identifiable {
|
|
let id: UUID // CoreBluetooth peripheral identifier
|
|
let peripheral: CBPeripheral
|
|
let name: String
|
|
let type: BeaconType
|
|
var rssi: Int
|
|
var lastSeen: Date
|
|
|
|
var displayName: String {
|
|
if name.isEmpty || name == "Unknown" {
|
|
return "\(type.rawValue) (\(id.uuidString.prefix(8))...)"
|
|
}
|
|
return name
|
|
}
|
|
}
|
|
|
|
/// Scans for BLE beacons that can be configured (KBeacon and BlueCharm)
|
|
class BLEBeaconScanner: NSObject, ObservableObject {
|
|
|
|
// KBeacon config service
|
|
static let KBEACON_SERVICE = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
|
|
// BlueCharm config service
|
|
static let BLUECHARM_SERVICE = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB")
|
|
|
|
@Published var isScanning = false
|
|
@Published var discoveredBeacons: [DiscoveredBeacon] = []
|
|
@Published var bluetoothState: CBManagerState = .unknown
|
|
|
|
private var centralManager: CBCentralManager!
|
|
private var scanTimer: Timer?
|
|
|
|
override init() {
|
|
super.init()
|
|
centralManager = CBCentralManager(delegate: self, queue: .main)
|
|
}
|
|
|
|
/// Start scanning for configurable beacons
|
|
func startScanning() {
|
|
guard centralManager.state == .poweredOn else {
|
|
NSLog("BLEBeaconScanner: Bluetooth not ready, state=\(centralManager.state.rawValue)")
|
|
return
|
|
}
|
|
|
|
NSLog("BLEBeaconScanner: Starting scan for configurable beacons")
|
|
discoveredBeacons.removeAll()
|
|
isScanning = true
|
|
|
|
// Scan for devices advertising our config services
|
|
// Note: We scan for all devices and filter by service after connection
|
|
// because some beacons don't advertise their config service UUID
|
|
centralManager.scanForPeripherals(
|
|
withServices: nil, // Scan all - we'll filter by name/characteristics
|
|
options: [CBCentralManagerScanOptionAllowDuplicatesKey: true]
|
|
)
|
|
|
|
// Auto-stop after 10 seconds
|
|
scanTimer?.invalidate()
|
|
scanTimer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) { [weak self] _ in
|
|
self?.stopScanning()
|
|
}
|
|
}
|
|
|
|
/// Stop scanning
|
|
func stopScanning() {
|
|
NSLog("BLEBeaconScanner: Stopping scan, found \(discoveredBeacons.count) beacons")
|
|
centralManager.stopScan()
|
|
isScanning = false
|
|
scanTimer?.invalidate()
|
|
scanTimer = nil
|
|
}
|
|
|
|
/// Check if Bluetooth is available
|
|
var isBluetoothReady: Bool {
|
|
centralManager.state == .poweredOn
|
|
}
|
|
}
|
|
|
|
// MARK: - CBCentralManagerDelegate
|
|
|
|
extension BLEBeaconScanner: CBCentralManagerDelegate {
|
|
|
|
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
bluetoothState = central.state
|
|
NSLog("BLEBeaconScanner: Bluetooth state changed to \(central.state.rawValue)")
|
|
|
|
if central.state == .poweredOn && isScanning {
|
|
// Resume scanning if we were trying to scan
|
|
startScanning()
|
|
}
|
|
}
|
|
|
|
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral,
|
|
advertisementData: [String: Any], rssi RSSI: NSNumber) {
|
|
|
|
let rssiValue = RSSI.intValue
|
|
guard rssiValue > -90 && rssiValue < 0 else { return } // Filter weak signals
|
|
|
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
|
|
|
// Determine beacon type from name or advertised services
|
|
var beaconType: BeaconType = .unknown
|
|
|
|
// Check advertised service UUIDs
|
|
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
|
|
if serviceUUIDs.contains(BLEBeaconScanner.KBEACON_SERVICE) {
|
|
beaconType = .kbeacon
|
|
} else if serviceUUIDs.contains(BLEBeaconScanner.BLUECHARM_SERVICE) {
|
|
beaconType = .bluecharm
|
|
}
|
|
}
|
|
|
|
// Also check by name patterns
|
|
if beaconType == .unknown {
|
|
let lowerName = name.lowercased()
|
|
if lowerName.contains("kbeacon") || lowerName.contains("kbpro") || lowerName.hasPrefix("kb") {
|
|
beaconType = .kbeacon
|
|
} else if lowerName.contains("bluecharm") || lowerName.contains("bc") || lowerName.hasPrefix("bc") {
|
|
beaconType = .bluecharm
|
|
}
|
|
}
|
|
|
|
// Only track beacons we can identify
|
|
guard beaconType != .unknown else { return }
|
|
|
|
// Update or add beacon
|
|
if let index = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
|
discoveredBeacons[index].rssi = rssiValue
|
|
discoveredBeacons[index].lastSeen = Date()
|
|
} else {
|
|
let beacon = DiscoveredBeacon(
|
|
id: peripheral.identifier,
|
|
peripheral: peripheral,
|
|
name: name,
|
|
type: beaconType,
|
|
rssi: rssiValue,
|
|
lastSeen: Date()
|
|
)
|
|
discoveredBeacons.append(beacon)
|
|
NSLog("BLEBeaconScanner: Discovered \(beaconType.rawValue) beacon: \(name) RSSI=\(rssiValue)")
|
|
}
|
|
|
|
// Sort by RSSI (strongest first)
|
|
discoveredBeacons.sort { $0.rssi > $1.rssi }
|
|
}
|
|
}
|