Merge pull request 'fix: overhaul beacon discovery to find all DX beacons' (#32) from schwifty/fix-beacon-discovery into main
This commit is contained in:
commit
df2a03f15a
1 changed files with 101 additions and 24 deletions
|
|
@ -22,6 +22,10 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
// GATT Service UUIDs
|
// GATT Service UUIDs
|
||||||
static let ffe0Service = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
|
static let ffe0Service = CBUUID(string: "0000FFE0-0000-1000-8000-00805F9B34FB")
|
||||||
static let fff0Service = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB")
|
static let fff0Service = CBUUID(string: "0000FFF0-0000-1000-8000-00805F9B34FB")
|
||||||
|
static let fea0Service = CBUUID(string: "0000FEA0-0000-1000-8000-00805F9B34FB")
|
||||||
|
|
||||||
|
// DX-Smart factory default iBeacon UUID
|
||||||
|
static let dxSmartDefaultUUID = "E2C56DB5-DFFB-48D2-B060-D0F5A71096E0"
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
|
@ -88,10 +92,34 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - iBeacon Manufacturer Data Parsing
|
||||||
|
|
||||||
|
/// Parse iBeacon data from manufacturer data (Apple company ID 0x4C00)
|
||||||
|
/// Returns (uuid, major, minor) if valid iBeacon advertisement found
|
||||||
|
private func parseIBeaconData(_ mfgData: Data) -> (uuid: String, major: UInt16, minor: UInt16)? {
|
||||||
|
// iBeacon format in manufacturer data:
|
||||||
|
// [0x4C 0x00] (Apple company ID) [0x02 0x15] (iBeacon type+length)
|
||||||
|
// [UUID 16 bytes] [Major 2 bytes] [Minor 2 bytes] [TX Power 1 byte]
|
||||||
|
guard mfgData.count >= 25 else { return nil }
|
||||||
|
guard mfgData[0] == 0x4C && mfgData[1] == 0x00 &&
|
||||||
|
mfgData[2] == 0x02 && mfgData[3] == 0x15 else { return nil }
|
||||||
|
|
||||||
|
// Extract UUID (bytes 4-19)
|
||||||
|
let uuidBytes = mfgData.subdata(in: 4..<20)
|
||||||
|
let hex = uuidBytes.map { String(format: "%02X", $0) }.joined()
|
||||||
|
let uuid = "\(hex.prefix(8))-\(hex.dropFirst(8).prefix(4))-\(hex.dropFirst(12).prefix(4))-\(hex.dropFirst(16).prefix(4))-\(hex.dropFirst(20))"
|
||||||
|
|
||||||
|
// Extract Major (bytes 20-21) and Minor (bytes 22-23)
|
||||||
|
let major = UInt16(mfgData[20]) << 8 | UInt16(mfgData[21])
|
||||||
|
let minor = UInt16(mfgData[22]) << 8 | UInt16(mfgData[23])
|
||||||
|
|
||||||
|
return (uuid: uuid, major: major, minor: minor)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Beacon Type Detection (matches Android BeaconScanner.kt)
|
// MARK: - Beacon Type Detection (matches Android BeaconScanner.kt)
|
||||||
// NOTE: Android also detects DX-Smart by MAC OUI prefix 48:87:2D.
|
// NOTE: Android also detects DX-Smart by MAC OUI prefix 48:87:2D.
|
||||||
// CoreBluetooth does not expose raw MAC addresses, so this detection
|
// CoreBluetooth does not expose raw MAC addresses, so we compensate
|
||||||
// path is unavailable on iOS. We rely on service UUID + device name instead.
|
// with broader iBeacon UUID detection and more permissive inclusion.
|
||||||
|
|
||||||
func detectBeaconType(
|
func detectBeaconType(
|
||||||
name: String?,
|
name: String?,
|
||||||
|
|
@ -100,12 +128,19 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
) -> BeaconType {
|
) -> BeaconType {
|
||||||
let deviceName = (name ?? "").lowercased()
|
let deviceName = (name ?? "").lowercased()
|
||||||
|
|
||||||
// 1. Service UUID matching
|
// Parse iBeacon data if available (needed for UUID-based detection)
|
||||||
|
let iBeaconData: (uuid: String, major: UInt16, minor: UInt16)?
|
||||||
|
if let mfgData = manufacturerData {
|
||||||
|
iBeaconData = parseIBeaconData(mfgData)
|
||||||
|
} else {
|
||||||
|
iBeaconData = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Service UUID matching (matches Android lines 122-126)
|
||||||
if let services = serviceUUIDs {
|
if let services = serviceUUIDs {
|
||||||
let serviceStrings = services.map { $0.uuidString.uppercased() }
|
let serviceStrings = services.map { $0.uuidString.uppercased() }
|
||||||
if serviceStrings.contains(where: { $0.hasPrefix("0000FFF0") }) {
|
|
||||||
return .bluecharm
|
// Android: KBeacon uses FFE0 as primary service
|
||||||
}
|
|
||||||
if serviceStrings.contains(where: { $0.hasPrefix("0000FFE0") }) {
|
if serviceStrings.contains(where: { $0.hasPrefix("0000FFE0") }) {
|
||||||
// Could be KBeacon or DXSmart — check name to differentiate
|
// Could be KBeacon or DXSmart — check name to differentiate
|
||||||
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
||||||
|
|
@ -114,9 +149,45 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
return .kbeacon
|
return .kbeacon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Android: DXSmart also uses FFF0 (line 125)
|
||||||
|
// FIXED: Was incorrectly mapping FFF0 → BlueCharm only.
|
||||||
|
// Android maps DXSmartProvisioner.SERVICE_UUID_FFF0 → DXSMART
|
||||||
|
if serviceStrings.contains(where: { $0.hasPrefix("0000FFF0") }) {
|
||||||
|
// Check name patterns to decide: DXSmart or BlueCharm
|
||||||
|
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
||||||
|
deviceName.contains("dx") || deviceName.contains("pddaxlque") ||
|
||||||
|
deviceName.isEmpty {
|
||||||
|
// DX beacons often have no name or DX-prefixed names
|
||||||
|
return .dxsmart
|
||||||
|
}
|
||||||
|
if deviceName.contains("bluecharm") || deviceName.hasPrefix("bc") ||
|
||||||
|
deviceName.hasPrefix("table-") {
|
||||||
|
return .bluecharm
|
||||||
|
}
|
||||||
|
// Default FFF0 to DXSmart (matching Android behavior)
|
||||||
|
return .dxsmart
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Device name patterns
|
// Android: BlueCharm uses FEA0 (line 124)
|
||||||
|
if serviceStrings.contains(where: { $0.hasPrefix("0000FEA0") }) {
|
||||||
|
return .bluecharm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Detect DX-Smart by factory default iBeacon UUID (Android line 130)
|
||||||
|
// This is critical — catches DX beacons that don't advertise service UUIDs
|
||||||
|
if let ibeacon = iBeaconData {
|
||||||
|
if ibeacon.uuid.caseInsensitiveCompare(Self.dxSmartDefaultUUID) == .orderedSame {
|
||||||
|
return .dxsmart
|
||||||
|
}
|
||||||
|
// Check if broadcasting a Payfrit shard UUID (already provisioned DX beacon)
|
||||||
|
if BeaconShardPool.isPayfrit(ibeacon.uuid) {
|
||||||
|
return .dxsmart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Device name patterns (Android lines 131-147)
|
||||||
if deviceName.contains("kbeacon") || deviceName.contains("kbpro") ||
|
if deviceName.contains("kbeacon") || deviceName.contains("kbpro") ||
|
||||||
deviceName.hasPrefix("kb") {
|
deviceName.hasPrefix("kb") {
|
||||||
return .kbeacon
|
return .kbeacon
|
||||||
|
|
@ -126,29 +197,25 @@ final class BLEManager: NSObject, ObservableObject {
|
||||||
return .bluecharm
|
return .bluecharm
|
||||||
}
|
}
|
||||||
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
if deviceName.contains("cp28") || deviceName.contains("cp-28") ||
|
||||||
deviceName.contains("dx-smart") || deviceName.contains("dxsmart") ||
|
deviceName.contains("dx-cp") || deviceName.contains("dx-smart") ||
|
||||||
deviceName.contains("pddaxlque") {
|
deviceName.contains("dxsmart") || deviceName.contains("pddaxlque") {
|
||||||
return .dxsmart
|
return .dxsmart
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Generic beacon patterns
|
// 4. Detect by iBeacon minor in high range (Android line 143)
|
||||||
|
if let ibeacon = iBeaconData, ibeacon.minor > 10000 {
|
||||||
|
return .dxsmart
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Generic beacon patterns (Android lines 145-147)
|
||||||
if deviceName.contains("ibeacon") || deviceName.contains("beacon") ||
|
if deviceName.contains("ibeacon") || deviceName.contains("beacon") ||
|
||||||
deviceName.hasPrefix("ble") {
|
deviceName.hasPrefix("ble") {
|
||||||
return .dxsmart // Default to DXSmart like Android
|
return .dxsmart // Default to DXSmart like Android
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Check manufacturer data for iBeacon advertisement
|
// 6. Any remaining iBeacon advertisement — still a beacon we should show
|
||||||
if let mfgData = manufacturerData, mfgData.count >= 23 {
|
if iBeaconData != nil {
|
||||||
// Apple iBeacon prefix: 0x4C00 0215
|
return .dxsmart
|
||||||
if mfgData.count >= 4 && mfgData[0] == 0x4C && mfgData[1] == 0x00 &&
|
|
||||||
mfgData[2] == 0x02 && mfgData[3] == 0x15 {
|
|
||||||
// Extract minor (bytes 22-23) — high minors suggest DXSmart factory defaults
|
|
||||||
if mfgData.count >= 24 {
|
|
||||||
let minorVal = UInt16(mfgData[22]) << 8 | UInt16(mfgData[23])
|
|
||||||
if minorVal > 10000 { return .dxsmart }
|
|
||||||
}
|
|
||||||
return .kbeacon
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return .unknown
|
return .unknown
|
||||||
|
|
@ -175,11 +242,21 @@ extension BLEManager: CBCentralManagerDelegate {
|
||||||
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
let name = peripheral.name ?? advertisementData[CBAdvertisementDataLocalNameKey] as? String ?? ""
|
||||||
let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
|
let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID]
|
||||||
let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
let mfgData = advertisementData[CBAdvertisementDataManufacturerDataKey] as? Data
|
||||||
|
let isConnectable = advertisementData[CBAdvertisementDataIsConnectable] as? Bool ?? false
|
||||||
|
|
||||||
let type = detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData)
|
let type = detectBeaconType(name: name, serviceUUIDs: serviceUUIDs, manufacturerData: mfgData)
|
||||||
|
|
||||||
// Only show recognized beacons
|
// Match Android behavior (lines 164-169):
|
||||||
guard type != .unknown else { return }
|
// Include devices that have a recognized type, OR
|
||||||
|
// are broadcasting iBeacon data, OR
|
||||||
|
// are connectable with a name (potential configurable beacon)
|
||||||
|
if type == .unknown {
|
||||||
|
let hasName = !name.isEmpty
|
||||||
|
let hasIBeaconData = mfgData.flatMap { parseIBeaconData($0) } != nil
|
||||||
|
if !hasIBeaconData && !(isConnectable && hasName) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let idx = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
if let idx = discoveredBeacons.firstIndex(where: { $0.id == peripheral.identifier }) {
|
||||||
// Update existing
|
// Update existing
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue