Merge pull request #1283 from meshtastic/purge_stale_nodes

App option for purging nodes not heard in certain amount of time
This commit is contained in:
Garth Vander Houwen 2025-06-19 16:26:28 -07:00 committed by GitHub
commit 8119c0d5bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 135 additions and 10 deletions

View file

@ -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" : {

View file

@ -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

View file

@ -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 {

View file

@ -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<NSFetchRequestResult>(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()

View file

@ -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: {