diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 0a5a5162..99f6dd10 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1378,6 +1378,12 @@ } } } + }, + "0" : { + + }, + "1" : { + }, "1 byte" : { "localizations" : { @@ -1655,6 +1661,9 @@ } } } + }, + "180" : { + }, "256 bit" : { "localizations" : { @@ -2481,6 +2490,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" : { @@ -6354,6 +6385,9 @@ } } } + }, + "Clear Stale Nodes" : { + }, "Client" : { "localizations" : { @@ -19796,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 930c8fb9..06033468 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -37,6 +37,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? + var maintenanceTimer: Timer? let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false var wantStoreAndForwardPackets = false @@ -53,6 +54,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 let NONCE_ONLY_CONFIG = 69420 let NONCE_ONLY_DB = 69421 @@ -78,13 +80,21 @@ 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 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 { + self.sendWantConfig() + } + }) } // MARK: Scanning for BLE Devices diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 1e97611b..89d79f60 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -289,9 +289,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 78b14ef7..6d085b21 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -8,6 +8,44 @@ import CoreData import MeshtasticProtobufs import OSLog +public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool { + var nodeExpireTime: TimeInterval { + return TimeInterval(-nodeExpireDays * 86400) + } + var nodePKIExpireTime: TimeInterval { + return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400) + } + + if nodeExpireDays == 0 { + // Purge Disabled + Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes") + return false + } + let fetchRequest = NSFetchRequest(entityName: "NodeInfoEntity") + 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 + + 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") + if deletedNodes > 0 { + return true + } + } else { + Logger.data.error("💥 [NodeInfoEntity] bad delete results") + } + } catch { + context.rollback() + Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes") + } + return false +} + 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..7bf044e6 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -6,11 +6,14 @@ 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 = "" @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 +43,39 @@ 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") + } + } + 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) + } Button { isPresentingCoreDataResetConfirm = true } label: {