payfrit-beacon-ios/PayfritBeacon/BLEBeaconScanner.swift
John Pinkyfloyd 8c2320da44 Add ios-marketing idiom, iPad orientations, launch screen
- 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>
2026-02-10 19:38:11 -08:00

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 }
}
}