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>
142 lines
4.9 KiB
Swift
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 }
|
|
}
|
|
}
|