Merge pull request #1282 from bjpetit/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 13:15:09 -07:00 committed by GitHub
commit 74e169a7f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 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" : {

View file

@ -37,6 +37,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
@ -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,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

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

View file

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