From 7128aface42fea9e0e1314cd310548c8cbe6940d Mon Sep 17 00:00:00 2001 From: Brent Petit Date: Sat, 31 May 2025 20:42:09 -0500 Subject: [PATCH 1/6] App option for purging nodes not heard in certain amount of time --- Localizable.xcstrings | 34 +++++++++++++++++++++ Meshtastic/Helpers/BLEManager.swift | 20 +++++++----- Meshtastic/Helpers/MeshPackets.swift | 10 ++++-- Meshtastic/Persistence/UpdateCoreData.swift | 32 +++++++++++++++++++ Meshtastic/Views/Settings/AppSettings.swift | 32 +++++++++++++++++++ 5 files changed, 118 insertions(+), 10 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index eec7ee0d..c0599a50 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1378,6 +1378,12 @@ } } } + }, + "0" : { + + }, + "1" : { + }, "1 byte" : { "localizations" : { @@ -1652,6 +1658,9 @@ } } } + }, + "180" : { + }, "256 bit" : { "localizations" : { @@ -2459,6 +2468,28 @@ } } }, + "After %lld Days" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "After %lld Day" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "After %lld Days" + } + } + } + } + } + } + }, "After config values save the node will reboot." : { "localizations" : { "de" : { @@ -6379,6 +6410,9 @@ } } } + }, + "Clear Stale Nodes" : { + }, "Client" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 050958e0..d76ffcfa 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -36,6 +36,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? + var maintenenceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false @@ -52,6 +53,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let FROMNUM_UUID = CBUUID(string: "0xED9DA18C-A800-4F66-A670-AA7547E34453") let LEGACY_LOGRADIO_UUID = CBUUID(string: "0x6C6FD238-78FA-436B-AACF-15C5BE1EF2E2") let LOGRADIO_UUID = CBUUID(string: "0x5a3d6e49-06e6-4423-9944-e9de8cdf9547") + @AppStorage("purgeStaleNodeDays") var purgeStaleNodeDays: Double = 0 // MARK: init private override init() { @@ -68,13 +70,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } private init(appState: AppState, context: NSManagedObjectContext) { - self.appState = appState - self.context = context - self.lastConnectionError = "" - self.connectedVersion = "0.0.0" - super.init() - centralManager = CBCentralManager(delegate: self, queue: nil) - mqttManager.delegate = self + self.appState = appState + self.context = context + self.lastConnectionError = "" + self.connectedVersion = "0.0.0" + super.init() + centralManager = CBCentralManager(delegate: self, queue: nil) + mqttManager.delegate = self + // Run clearStaleNodes every 10 minutes + maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 600, repeats: true, block: { _ in + clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + }) } // MARK: Scanning for BLE Devices diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index fe290aa0..6149b582 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -292,9 +292,13 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) } - - newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + if nodeInfo.lastHeard > 0 { + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() + } newNode.snr = nodeInfo.snr if nodeInfo.hasUser { diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 53da7355..ac81a25f 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,38 @@ import CoreData import MeshtasticProtobufs import OSLog +public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) { + var nodeExpireTime: TimeInterval { + return TimeInterval(-nodeExpireDays * 86400) + } + + if nodeExpireDays == 0 { + // Purge Disabled + Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") + return + } + let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + fetchRequest.predicate = NSPredicate(format: "lastHeard < %@ and favorite == false and ignored == false", + NSDate(timeIntervalSinceNow: nodeExpireTime)) + + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + batchDeleteRequest.resultType = .resultTypeCount + + do { + Logger.data.info("💾 [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days") + if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult { + try context.save() + let deletedNodes = batchDeleteResult.result as? Int ?? 0 + Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + } else { + Logger.data.error("💥 [NodeInfoEntity] bad delete results") + } + } catch { + context.rollback() + Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") + } +} + public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index e16fb31b..c64e049c 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -11,6 +11,8 @@ struct AppSettings: View { @State var totalDownloadedTileSize = "" @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false + @State private var purgeStaleNodes: Bool = false + @AppStorage("purgeStaleNodeDays") private var purgeStaleNodeDays: Double = 0 @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true @AppStorage("enableAdministration") private var enableAdministration: Bool = false var body: some View { @@ -40,6 +42,36 @@ struct AppSettings: View { } } Section(header: Text("App Data")) { + Toggle(isOn: $purgeStaleNodes ) { + Label { + Text("Clear Stale Nodes") + } icon: { + Image(systemName: "list.bullet.circle") + } + } + .onFirstAppear { + purgeStaleNodes = purgeStaleNodeDays > 0 + Logger.services.info("ℹ️ Purge Stale Nodes toggle initialized to \(purgeStaleNodes)") + } + .onChange(of: purgeStaleNodes) { _, newValue in + purgeStaleNodeDays = purgeStaleNodeDays > 0 ? purgeStaleNodeDays : 7 + purgeStaleNodeDays = newValue ? purgeStaleNodeDays : 0 + Logger.services.info("ℹ️ Purge Stale Nodes changed to \(purgeStaleNodeDays)") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + + .listRowSeparator(purgeStaleNodes ? .hidden : .visible) + if purgeStaleNodes { + VStack(alignment: .leading) { + Text(String(localized: "After \(Int(purgeStaleNodeDays)) Days")) + Slider(value: $purgeStaleNodeDays, in: 1...180, step: 1) { + } minimumValueLabel: { + Text("1") + } maximumValueLabel: { + Text("180") + } + } + } Button { isPresentingCoreDataResetConfirm = true } label: { From 6ed183d155b49b0877215118f6c9c616913ef18b Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 15:40:19 -0700 Subject: [PATCH 2/6] Make clear function return a boolean so wantconfig can be called if connected, don't purge PKI nodes faster than 7 days to ensure proper reporting of key mismatch errors. --- Meshtastic/Helpers/BLEManager.swift | 8 ++++++-- Meshtastic/Persistence/UpdateCoreData.swift | 16 +++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 9b044fcf..e422a13f 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -88,8 +88,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate centralManager = CBCentralManager(delegate: self, queue: nil) mqttManager.delegate = self // Run clearStaleNodes every 10 minutes - maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 600, repeats: true, block: { _ in - clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: { _ in + let result = clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) + // If you are connected and the clear worked, pull nodes back from the node in case we have deleted anything from that app that is in the device nodedb + if result && self.isSubscribed { + self.sendWantConfig() + } }) } diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 8bc5b226..a203964f 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,20 +8,22 @@ import CoreData import MeshtasticProtobufs import OSLog -public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) { +public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { var nodeExpireTime: TimeInterval { return TimeInterval(-nodeExpireDays * 86400) } + var nodePKIExpireTime: TimeInterval { + return TimeInterval(-7 * 86400) + } if nodeExpireDays == 0 { // Purge Disabled Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") - return + return false } let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") - fetchRequest.predicate = NSPredicate(format: "lastHeard < %@ and favorite == false and ignored == false", - NSDate(timeIntervalSinceNow: nodeExpireTime)) - + fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))", + NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime)) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) batchDeleteRequest.resultType = .resultTypeCount @@ -31,6 +33,9 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext try context.save() let deletedNodes = batchDeleteResult.result as? Int ?? 0 Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes") + if deletedNodes > 0 { + return true + } } else { Logger.data.error("💥 [NodeInfoEntity] bad delete results") } @@ -38,6 +43,7 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext context.rollback() Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") } + return false } public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool { From 0f21ea9599bf8bc143253fbe2f115b29d5f178e4 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:06:02 -0700 Subject: [PATCH 3/6] Minimum expiry for PKI nodes --- Meshtastic/Persistence/UpdateCoreData.swift | 2 +- Meshtastic/Views/Settings/AppSettings.swift | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index a203964f..6d085b21 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -13,7 +13,7 @@ public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext return TimeInterval(-nodeExpireDays * 86400) } var nodePKIExpireTime: TimeInterval { - return TimeInterval(-7 * 86400) + return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) } if nodeExpireDays == 0 { diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index c64e049c..6c6cd44d 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -6,6 +6,7 @@ import MapKit import OSLog struct AppSettings: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @State var totalDownloadedTileSize = "" @@ -71,6 +72,9 @@ struct AppSettings: View { Text("180") } } + Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) } Button { isPresentingCoreDataResetConfirm = true From ada2d0cfaaa3eb0f2fd0084adfb5678ea821929d Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:07:47 -0700 Subject: [PATCH 4/6] Update purge node informational text --- Meshtastic/Views/Settings/AppSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index 6c6cd44d..7bf044e6 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -72,7 +72,7 @@ struct AppSettings: View { Text("180") } } - Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer.") + Text("Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database.") .foregroundStyle(.secondary) .font(idiom == .phone ? .caption : .callout) } From 2c68b4e8d2a0823467effac61438e3468e94f1bf Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:12:44 -0700 Subject: [PATCH 5/6] Update Meshtastic/Helpers/BLEManager.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Meshtastic/Helpers/BLEManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index e422a13f..51b01ba7 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -37,7 +37,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? - var maintenenceTimer: Timer? + var maintenanceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false From 67a2b0631fcce0a1389d9254caf6e20264fafd02 Mon Sep 17 00:00:00 2001 From: Garth Vander Houwen Date: Thu, 19 Jun 2025 16:14:09 -0700 Subject: [PATCH 6/6] Bump timer to every hour, fix spelling typo --- Localizable.xcstrings | 3 +++ Meshtastic/Helpers/BLEManager.swift | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Localizable.xcstrings b/Localizable.xcstrings index ee4749a8..99f6dd10 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -19830,6 +19830,9 @@ } } } + }, + "Nodes without PKI keys are cleared from the app database on the schedule set by the user, nodes with PKI keys are cleared only if the interval is set to 7 days or longer. This feature only purges nodes from the app that are not stored in the device node database." : { + }, "None" : { "localizations" : { diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 51b01ba7..06033468 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -87,8 +87,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate super.init() centralManager = CBCentralManager(delegate: self, queue: nil) mqttManager.delegate = self - // Run clearStaleNodes every 10 minutes - maintenenceTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true, block: { _ in + // Run clearStaleNodes every hour + maintenanceTimer = Timer.scheduledTimer(withTimeInterval: 3600, repeats: true, block: { _ in let result = clearStaleNodes(nodeExpireDays: Int(self.purgeStaleNodeDays), context: self.context) // If you are connected and the clear worked, pull nodes back from the node in case we have deleted anything from that app that is in the device nodedb if result && self.isSubscribed {