diff --git a/Apps/MetaWear/Info.plist b/Apps/MetaWear/Info.plist index 81a814c..7b27f50 100644 --- a/Apps/MetaWear/Info.plist +++ b/Apps/MetaWear/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 10.0 CFBundleVersion - 2 + 3 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/Apps/MetaWear/MetaWearApp.xcodeproj/project.pbxproj b/Apps/MetaWear/MetaWearApp.xcodeproj/project.pbxproj index 81eae59..72a740e 100644 --- a/Apps/MetaWear/MetaWearApp.xcodeproj/project.pbxproj +++ b/Apps/MetaWear/MetaWearApp.xcodeproj/project.pbxproj @@ -412,7 +412,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = MetaWear.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -449,7 +449,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = MetaWear.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; @@ -484,7 +484,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; @@ -506,7 +506,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; @@ -527,7 +527,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -547,7 +547,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = S2273LZ6GL; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; diff --git a/Sources/MetaWear/MetaWearScanner.swift b/Sources/MetaWear/MetaWearScanner.swift index f796ddb..e804899 100644 --- a/Sources/MetaWear/MetaWearScanner.swift +++ b/Sources/MetaWear/MetaWearScanner.swift @@ -121,15 +121,34 @@ public final class MetaWearScanner { for await result in stream { let id = result.identifier let name = result.name ?? "" + // The scan runs with allow-duplicates (presence and RSSI must + // keep updating), so advertisements arrive several times per + // second per board. Guard every write on actual change — + // @Observable treats same-value dictionary writes as + // mutations, and unguarded writes would invalidate observers + // at advertising rate. + // // Record every advertisement's name so renamed devices remain // observable even when they no longer match the MetaWear prefix. - self.advertisedNames[id] = name - self.advertisementRSSI[id] = result.rssi - self.advertisementLastSeen[id] = .now + if self.advertisedNames[id] != name { + self.advertisedNames[id] = name + } + if self.advertisementRSSI[id] != result.rssi { + self.advertisementRSSI[id] = result.rssi + } + // 1 s granularity is plenty for the UI's freshness window. + if let seen = self.advertisementLastSeen[id] { + if Date.now.timeIntervalSince(seen) >= 1 { + self.advertisementLastSeen[id] = .now + } + } else { + self.advertisementLastSeen[id] = .now + } // Record manufacturer data when present — lets tests verify a // board has flipped into iBeacon mode by inspecting the Apple // company-ID payload broadcast on air. - if let mfg = result.manufacturerData { + if let mfg = result.manufacturerData, + self.advertisementManufacturerData[id] != mfg { self.advertisementManufacturerData[id] = mfg // Boards configured to broadcast their MAC (company // 0x626D) identify themselves here without a connection. diff --git a/Sources/MetaWear/Transport/MWCentralManager.swift b/Sources/MetaWear/Transport/MWCentralManager.swift index edb83af..69e28c7 100644 --- a/Sources/MetaWear/Transport/MWCentralManager.swift +++ b/Sources/MetaWear/Transport/MWCentralManager.swift @@ -115,7 +115,14 @@ actor MWCentralManager: NSObject { return } mwLog("[BLE] beginScan: starting scan") - central.scanForPeripherals(withServices: services, options: nil) + central.scanForPeripherals( + withServices: services, + // Without allow-duplicates, iOS delivers each peripheral ONCE per + // scan session - advertisementLastSeen would freeze at discovery + // and presence/RSSI could never update. Scans here are foreground + // + user-visible, the sanctioned use for this energy-costly flag. + options: [CBCentralManagerScanOptionAllowDuplicatesKey: true] + ) } private func endScan() { @@ -185,7 +192,14 @@ extension MWCentralManager: CBCentralManagerDelegate { if state == .poweredOn, scanContinuation != nil { // A scan was requested before BT was ready — start it now. mwLog("[BLE] handleStateUpdate: BT now poweredOn, starting deferred scan") - central.scanForPeripherals(withServices: pendingScanServices, options: nil) + central.scanForPeripherals( + withServices: pendingScanServices, + // Without allow-duplicates, iOS delivers each peripheral ONCE per + // scan session - advertisementLastSeen would freeze at discovery + // and presence/RSSI could never update. Scans here are foreground + // + user-visible, the sanctioned use for this energy-costly flag. + options: [CBCentralManagerScanOptionAllowDuplicatesKey: true] + ) } }