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]
+ )
}
}