payfrit-beacon-ios/PayfritBeacon/BLEBeaconScanner.swift
John Pinkyfloyd 5283d2d265 Fix DX-Smart provisioning protocol and add debug logging
Fix critical packet format bugs matching SDK: frame select/type/trigger/disable
commands now send empty data, RSSI@1m corrected to -59 dBm. Add DebugLog,
read-config mode, service point list, and dev scheme.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:01:12 -08:00

142 lines
4.9 KiB
Swift

import Foundation
import CoreBluetooth
/// Beacon type detected by service UUID or name
enum BeaconType: String {
case kbeacon = "KBeacon"
case dxsmart = "DX-Smart"
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 {
return 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 1 second (beacons advertise every ~200ms so 1s is plenty)
scanTimer?.invalidate()
scanTimer = Timer.scheduledTimer(withTimeInterval: 1.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 > -70 && rssiValue < 0 else { return } // Only show nearby devices
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
// Best-effort type hint from advertised services (informational only)
var beaconType: BeaconType = .unknown
if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] {
if serviceUUIDs.contains(BLEBeaconScanner.KBEACON_SERVICE) {
beaconType = .dxsmart
} else if serviceUUIDs.contains(BLEBeaconScanner.BLUECHARM_SERVICE) {
beaconType = .bluecharm
}
}
// 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 }
}
}