Move upserts in UpdateCoreData to the MeshPackets actor

This commit is contained in:
Jake-B 2026-01-23 11:53:04 -05:00
parent 140c1ab734
commit 82640216ac
15 changed files with 1937 additions and 1699 deletions

View file

@ -166,7 +166,7 @@ extension AccessoryManager {
// Update local database with the new node info
// FUTURE: after https://github.com/meshtastic/firmware/pull/8495 is merged, `favorite: true` becomes `favorite: (connectedDeviceRole != DeviceRoles.clientBase)`
upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true, context: context)
await MeshPackets.shared.upsertNodeInfoPacket(packet: nodeMeshPacket, favorite: true)
}
} catch {
Logger.data.error("Failed to decode contact data: \(error.localizedDescription, privacy: .public)")
@ -856,7 +856,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num)
return Int64(meshPacket.id)
@ -912,7 +912,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -995,7 +995,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1049,7 +1049,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1079,7 +1079,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved PAX Counter Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1109,7 +1109,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1140,7 +1140,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1170,7 +1170,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1200,7 +1200,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1379,7 +1379,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1615,7 +1615,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context)
try await MeshPackets.shared.upsertPositionConfigPacket(config: config, nodeNum: toUser.num)
return Int64(meshPacket.id)
}
@ -1669,7 +1669,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertPowerConfigPacket(config: config, nodeNum: toUser.num)
return Int64(meshPacket.id)
}
@ -1725,7 +1725,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertNetworkConfigPacket(config: config, nodeNum: toUser.num)
return Int64(meshPacket.id)
}
@ -1756,7 +1756,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertSecurityConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertSecurityConfigPacket(config: config, nodeNum: toUser.num)
return Int64(meshPacket.id)
}
@ -1890,7 +1890,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
await MeshPackets.shared.upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey)
return Int64(meshPacket.id)
}
@ -1920,7 +1920,7 @@ extension AccessoryManager {
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
await MeshPackets.shared.upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
@ -1973,7 +1973,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
await MeshPackets.shared.upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey)
return Int64(meshPacket.id)
}
@ -2052,7 +2052,7 @@ extension AccessoryManager {
let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "Unknown".localized)"
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
await MeshPackets.shared.upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey)
return Int64(meshPacket.id)
}
@ -2081,7 +2081,7 @@ extension AccessoryManager {
try await sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription)
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
await MeshPackets.shared.upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey)
return Int64(meshPacket.id)
}

View file

@ -197,7 +197,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
return
}
_ = clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays), context: self.context)
_ = await MeshPackets.shared.clearStaleNodes(nodeExpireDays: Int(UserDefaults.purgeStaleNodeDays))
try await withTaskCancellationHandler {
var toRadio: ToRadio = ToRadio()
@ -497,7 +497,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
case .packet(let packet):
// All received packets get passed through updateAnyPacketFrom to update lastHeard, rxSnr, etc. (like firmware's NodeDB::updateFrom).
if let connectedNodeNum = self.activeDeviceNum {
updateAnyPacketFrom(packet: packet, activeDeviceNum: connectedNodeNum, context: context)
await MeshPackets.shared.updateAnyPacketFrom(packet: packet, activeDeviceNum: connectedNodeNum)
} else {
Logger.mesh.error("🕸️ Unable to determine connectedNodeNum for updateAnyPacketFrom. Skipping.")
}
@ -510,7 +510,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
case .remoteHardwareApp:
Logger.mesh.info("🕸️ MESH PACKET received for Remote Hardware App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .positionApp:
upsertPositionPacket(packet: packet, context: context)
await MeshPackets.shared.upsertPositionPacket(packet: packet)
case .waypointApp:
await MeshPackets.shared.waypointPacket(packet: packet)
case .nodeinfoApp:
@ -519,7 +519,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
return
}
if packet.from != connectedNodeNum {
upsertNodeInfoPacket(packet: packet, context: context)
await MeshPackets.shared.upsertNodeInfoPacket(packet: packet)
} else {
Logger.mesh.error("🕸️ Received a node info packet from ourselves over the mesh. Dropping.")
}

View file

@ -2,6 +2,7 @@ import Foundation
import SwiftUI
import OSLog
@MainActor
class LocalNotificationManager {
var notifications = [Notification]()
@ -10,20 +11,23 @@ class LocalNotificationManager {
let replyInputAction = UNTextInputNotificationAction(identifier: "messageNotification.replyInputAction", title: "Reply".localized, options: [])
// Step 1 Request Permissions for notifications
private func requestAuthorization() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted == true && error == nil {
self.scheduleNotifications()
private func requestAuthorization() async {
do {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound])
if granted {
self.scheduleNotifications()
}
} catch {
Logger.services.error("Error requesting notification authorization: \(error.localizedDescription, privacy: .public)")
}
}
func schedule() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
Task { @MainActor in
let settings = await UNUserNotificationCenter.current().notificationSettings()
switch settings.authorizationStatus {
case .notDetermined:
self.requestAuthorization()
await self.requestAuthorization()
case .authorized, .provisional:
self.scheduleNotifications()
default:
@ -97,7 +101,7 @@ class LocalNotificationManager {
for notification in notifications {
if let userInfo = notification.content.userInfo["messageId"] as? Int64, userInfo == messageId {
Logger.services.debug("Cancelling notification with id: \(notification.identifier)")
center.removePendingNotificationRequests(withIdentifiers: [notification.identifier])
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [notification.identifier])
}
}
}

View file

@ -60,69 +60,63 @@ actor MeshPackets {
// Create an actor-level background context
// We keep this alive so sequential writes happen on the same context (efficient)
private lazy var backgroundContext: NSManagedObjectContext = {
lazy var backgroundContext: NSManagedObjectContext = {
let ctx = PersistenceController.shared.container.newBackgroundContext()
ctx.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy // Handle conflicts automatically
return ctx
}()
func localConfig (config: Config, nodeNum: Int64, nodeLongName: String) async {
let context = self.backgroundContext
await context.perform { [weak self] in
switch config.payloadVariant {
case .bluetooth:
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context)
case .device:
upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum, context: context)
case .display:
upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum, context: context)
case .lora:
upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum, context: context)
case .network:
upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum, context: context)
case .position:
upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context)
case .power:
upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context)
case .security:
upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context)
default:
switch config.payloadVariant {
case .bluetooth:
await self.upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum)
case .device:
await self.upsertDeviceConfigPacket(config: config.device, nodeNum: nodeNum)
case .display:
await self.upsertDisplayConfigPacket(config: config.display, nodeNum: nodeNum)
case .lora:
await self.upsertLoRaConfigPacket(config: config.lora, nodeNum: nodeNum)
case .network:
await self.upsertNetworkConfigPacket(config: config.network, nodeNum: nodeNum)
case .position:
await self.upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum)
case .power:
await self.upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum)
case .security:
await self.upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum)
default:
#if DEBUG
Logger.services.error("⁉️ Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
Logger.services.error("⁉️ Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
#endif
}
}
}
func moduleConfig (config: ModuleConfig, nodeNum: Int64, nodeLongName: String) async {
let context = self.backgroundContext
await context.perform {
switch config.payloadVariant {
case .ambientLighting:
upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context)
case .cannedMessage:
upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context)
case .detectionSensor:
upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context)
case .externalNotification:
upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum, context: context)
case .mqtt:
upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum, context: context)
case .paxcounter:
upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum, context: context)
case .rangeTest:
upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum, context: context)
case .serial:
upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum, context: context)
case .telemetry:
upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum, context: context)
case .storeForward:
upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum, context: context)
default:
switch config.payloadVariant {
case .ambientLighting:
await self.upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum)
case .cannedMessage:
await self.upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum)
case .detectionSensor:
await self.upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum)
case .externalNotification:
await self.upsertExternalNotificationModuleConfigPacket(config: config.externalNotification, nodeNum: nodeNum)
case .mqtt:
await self.upsertMqttModuleConfigPacket(config: config.mqtt, nodeNum: nodeNum)
case .paxcounter:
await self.upsertPaxCounterModuleConfigPacket(config: config.paxcounter, nodeNum: nodeNum)
case .rangeTest:
await self.upsertRangeTestModuleConfigPacket(config: config.rangeTest, nodeNum: nodeNum)
case .serial:
await self.upsertSerialModuleConfigPacket(config: config.serial, nodeNum: nodeNum)
case .telemetry:
await self.upsertTelemetryModuleConfigPacket(config: config.telemetry, nodeNum: nodeNum)
case .storeForward:
await self.upsertStoreForwardModuleConfigPacket(config: config.storeForward, nodeNum: nodeNum)
default:
#if DEBUG
Logger.services.error("⁉️ Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
Logger.services.error("⁉️ Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
#endif
}
}
}
@ -178,12 +172,13 @@ actor MeshPackets {
}
func channelPacket (channel: Channel, fromNum: Int64) async {
await backgroundContext.perform {
self.channelPacket(channel: channel, fromNum: fromNum, context: self.backgroundContext)
let context = self.backgroundContext
await context.perform {
self.channelPacket(channel: channel, fromNum: fromNum, context: context)
}
}
private func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) {
nonisolated private func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectContext) {
if channel.isInitialized && channel.hasSettings && channel.role != Channel.Role.disabled {
let logString = String.localizedStringWithFormat("mesh.log.channel.received %d %@".localized, channel.index, String(fromNum))
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
@ -235,12 +230,13 @@ actor MeshPackets {
}
func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data()) async {
await self.backgroundContext.perform {
self.deviceMetadataPacket(metadata: metadata, fromNum: fromNum, sessionPasskey: sessionPasskey, context: self.backgroundContext)
let context = self.backgroundContext
await context.perform {
self.deviceMetadataPacket(metadata: metadata, fromNum: fromNum, sessionPasskey: sessionPasskey, context: context)
}
}
private func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
nonisolated private func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
if metadata.isInitialized {
let logString = String.localizedStringWithFormat("Device Metadata received from: %@".localized, fromNum.toHex())
Logger.mesh.info("🏷️ \(logString, privacy: .public)")
@ -595,46 +591,46 @@ actor MeshPackets {
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) {
let config = adminMessage.getConfigResponse
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
MeshPackets.shared.upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
MeshPackets.shared.upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) {
upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) {
upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) {
upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
self.upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) {
let moduleConfig = adminMessage.getModuleConfigResponse
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) {
upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context)
self.upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context)
self.upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) {
upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)
self.upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) {
upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context)
self.upsertExternalNotificationModuleConfigPacket(config: moduleConfig.externalNotification, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.mqtt(moduleConfig.mqtt) {
upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context)
self.upsertMqttModuleConfigPacket(config: moduleConfig.mqtt, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) {
upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context)
self.upsertRangeTestModuleConfigPacket(config: moduleConfig.rangeTest, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.serial(moduleConfig.serial) {
upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context)
self.upsertSerialModuleConfigPacket(config: moduleConfig.serial, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) {
upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context)
self.upsertStoreForwardModuleConfigPacket(config: moduleConfig.storeForward, nodeNum: Int64(packet.from), context: context)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.telemetry(moduleConfig.telemetry) {
upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context)
self.upsertTelemetryModuleConfigPacket(config: moduleConfig.telemetry, nodeNum: Int64(packet.from), context: context)
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getRingtoneResponse(adminMessage.getRingtoneResponse) {
if let rt = try? RTTTLConfig(serializedBytes: packet.decoded.payload) {
upsertRtttlConfigPacket(ringtone: rt.ringtone, nodeNum: Int64(packet.from), context: context)
self.upsertRtttlConfigPacket(ringtone: rt.ringtone, nodeNum: Int64(packet.from), context: context)
}
} else {
Logger.mesh.error("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
@ -645,7 +641,7 @@ actor MeshPackets {
}
}
private func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
nonisolated private func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
let fetchedAdminMessageRequest = MessageEntity.fetchRequest()
fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID)
do {
@ -879,19 +875,21 @@ actor MeshPackets {
// Low Battery notification
if connectedNode == Int64(packet.from) {
let batteryLevel = telemetry.batteryLevel ?? 0
if UserDefaults.lowBatteryNotifications && batteryLevel > 0 && batteryLevel < 4 {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(UUID().uuidString)"),
title: "Critically Low Battery!",
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
content: "Time to charge your radio, there is \(telemetry.batteryLevel?.formatted(.number) ?? Constants.nilValueIndicator)% battery remaining.",
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
)
]
manager.schedule()
Task {@MainActor in
if UserDefaults.lowBatteryNotifications && batteryLevel > 0 && batteryLevel < 4 {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(UUID().uuidString)"),
title: "Critically Low Battery!",
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
content: "Time to charge your radio, there is \(telemetry.batteryLevel?.formatted(.number) ?? Constants.nilValueIndicator)% battery remaining.",
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
)
]
manager.schedule()
}
}
}
} else if telemetry.metricsType == 4 {
@ -1098,23 +1096,26 @@ actor MeshPackets {
}
if !(newMessage.fromUser?.mute ?? false) && newMessage.isEmoji == false {
// Create an iOS Notification for the received DM message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(packet.from),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
Task {@MainActor in
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(packet.from),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
@ -1122,30 +1123,32 @@ actor MeshPackets {
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if !fetchedMyInfo.isEmpty {
appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: context)
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications && newMessage.isEmoji == false {
// Create an iOS Notification for the received channel message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
Task {@MainActor in
appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages(context: context)
for channel in (fetchedMyInfo[0].channels?.array ?? []) as? [ChannelEntity] ?? [] {
if channel.index == newMessage.channel {
context.refresh(channel, mergeChanges: true)
}
if channel.index == newMessage.channel && !channel.mute && UserDefaults.channelMessageNotifications && newMessage.isEmoji == false {
// Create an iOS Notification for the received channel message
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: ("notification.id.\(newMessage.messageId)"),
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
content: messageText!,
target: "messages",
path: "meshtastic:///messages?channelId=\(newMessage.channel)&messageId=\(newMessage.isEmoji ? newMessage.replyID : newMessage.messageId)",
messageId: newMessage.messageId,
channel: newMessage.channel,
userNum: Int64(newMessage.fromUser?.userId ?? "0"),
critical: critical
)
]
manager.schedule()
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
}
}
}
}
@ -1205,22 +1208,25 @@ actor MeshPackets {
do {
try context.save()
Logger.data.info("💾 Added Node Waypoint App Packet For: \(waypoint.id, privacy: .public)")
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
let longitude = Double(waypoint.longitudeI) / 1e7
manager.notifications = [
Notification(
id: ("notification.id.\(waypoint.id)"),
title: "New Waypoint From \(nodeShortName)",
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
target: "map",
path: "meshtastic:///map?waypointid=\(waypoint.id)"
)
]
Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id, privacy: .public)")
manager.schedule()
Task { @MainActor in
let manager = LocalNotificationManager()
let icon = String(UnicodeScalar(Int(waypoint.icon)) ?? "📍")
let latitude = Double(waypoint.latitudeI) / 1e7
let longitude = Double(waypoint.longitudeI) / 1e7
manager.notifications = [
Notification(
id: ("notification.id.\(waypoint.id)"),
title: "New Waypoint From \(nodeShortName)",
subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")",
content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")",
target: "map",
path: "meshtastic:///map?waypointid=\(waypoint.id)"
)
]
Logger.data.debug("meshtastic:///map?waypointid=\(waypoint.id, privacy: .public)")
manager.schedule()
}
} catch {
context.rollback()
let nsError = error as NSError

File diff suppressed because it is too large Load diff

View file

@ -511,23 +511,23 @@ struct ManualConnectionMenu: View {
})
}.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
Button("Connect to new radio?", role: .destructive) {
if let device = deviceForManualConnection {
UserDefaults.preferredPeripheralId = device.id.uuidString
UserDefaults.preferredPeripheralNum = 0
if accessoryManager.allowDisconnect {
Task { try await accessoryManager.disconnect() }
}
clearCoreDataDatabase(context: context, includeRoutes: false)
clearNotifications()
Task {
try await selectedTransport?.transport.manuallyConnect(toDevice: device)
}
// Clean up just in case
deviceForManualConnection = nil
}
}
}
Task {
if let device = deviceForManualConnection {
UserDefaults.preferredPeripheralId = device.id.uuidString
UserDefaults.preferredPeripheralNum = 0
if accessoryManager.allowDisconnect {
try await accessoryManager.disconnect()
}
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
clearNotifications()
try await selectedTransport?.transport.manuallyConnect(toDevice: device)
// Clean up just in case
deviceForManualConnection = nil
}
}
}
}
}
}
@ -593,15 +593,17 @@ struct DeviceConnectRow: View {
}.padding([.bottom, .top])
.confirmationDialog("Connecting to a new radio will clear all app data on the phone.", isPresented: $presentingSwitchPreferredPeripheral, titleVisibility: .visible) {
Button("Connect to new radio?", role: .destructive) {
UserDefaults.preferredPeripheralId = device.id.uuidString
UserDefaults.preferredPeripheralNum = 0
if accessoryManager.allowDisconnect {
Task { try await accessoryManager.disconnect() }
}
clearCoreDataDatabase(context: context, includeRoutes: false)
clearNotifications()
Task {
UserDefaults.preferredPeripheralId = device.id.uuidString
UserDefaults.preferredPeripheralNum = 0
if accessoryManager.allowDisconnect {
try await accessoryManager.disconnect()
}
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: false)
clearNotifications()
try await accessoryManager.connect(to: device)
}
}
}

View file

@ -160,9 +160,11 @@ struct ChannelList: View {
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteChannelMessages(channel: channelToDeleteMessages!, context: context)
context.refresh(myInfo, mergeChanges: true)
channelToDeleteMessages = nil
Task {
await MeshPackets.shared.deleteChannelMessages(channel: channelToDeleteMessages!)
context.refresh(myInfo, mergeChanges: true)
channelToDeleteMessages = nil
}
} label: {
Text("Delete")
}

View file

@ -224,8 +224,10 @@ fileprivate struct FilteredUserList: View {
titleVisibility: .visible
) {
Button(role: .destructive) {
deleteUserMessages(user: userToDeleteMessages!, context: context)
context.refresh(node!.user!, mergeChanges: true)
Task {
await MeshPackets.shared.deleteUserMessages(user: userToDeleteMessages!)
context.refresh(node!.user!, mergeChanges: true)
}
} label: {
Text("Delete")
}

View file

@ -199,10 +199,12 @@ struct DeviceMetricsLog: View {
titleVisibility: .visible
) {
Button("Delete all device metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Device Metrics Log Failed")
Task {
if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 0) {
Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Device Metrics Log Failed")
}
}
}
}

View file

@ -128,8 +128,10 @@ struct EnvironmentMetricsLog: View {
titleVisibility: .visible
) {
Button("Delete all environment metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 1, context: context) {
Logger.services.error("Clear Environment Metrics Log Failed")
Task {
if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 1) {
Logger.services.error("Clear Environment Metrics Log Failed")
}
}
}
}

View file

@ -175,10 +175,12 @@ struct PaxCounterLog: View {
titleVisibility: .visible
) {
Button("Delete all pax data?", role: .destructive) {
if clearPax(destNum: node.num, context: context) {
Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)")
} else {
Logger.services.error("Clear Pax Counter Log Failed")
Task {
if await MeshPackets.shared.clearPax(destNum: node.num) {
Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)")
} else {
Logger.services.error("Clear Pax Counter Log Failed")
}
}
}
}

View file

@ -131,10 +131,12 @@ struct PositionLog: View {
titleVisibility: .visible
) {
Button("Delete all positions?", role: .destructive) {
if clearPositions(destNum: node.num, context: context) {
Logger.services.info("Successfully Cleared Position Log")
} else {
Logger.services.error("Clear Position Log Failed")
Task {
if await MeshPackets.shared.clearPositions(destNum: node.num) {
Logger.services.info("Successfully Cleared Position Log")
} else {
Logger.services.error("Clear Position Log Failed")
}
}
}
}

View file

@ -242,10 +242,12 @@ struct PowerMetricsLog: View {
titleVisibility: .visible
) {
Button("Delete Power metrics?", role: .destructive) {
if clearTelemetry(destNum: node.num, metricsType: 2, context: context) {
Logger.data.notice("Cleared Power Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Power Metrics Log Failed")
Task {
if await MeshPackets.shared.clearTelemetry(destNum: node.num, metricsType: 2) {
Logger.data.notice("Cleared Power Metrics for \(node.num, privacy: .public)")
} else {
Logger.data.error("Clear Power Metrics Log Failed")
}
}
}
}

View file

@ -138,30 +138,31 @@ struct AppSettings: View {
Button("Erase all app data?", role: .destructive) {
Task {
try await accessoryManager.disconnect()
}
/// Delete any database backups too
if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum))
do {
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite"))
/// Delete -shm file
/// Delete any database backups too
if var url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
url = url.appendingPathComponent("backup").appendingPathComponent(String(UserDefaults.preferredPeripheralNum))
do {
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal"))
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite"))
/// Delete -shm file
do {
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm"))
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-wal"))
do {
try FileManager.default.removeItem(at: url.appendingPathComponent("Meshtastic.sqlite-shm"))
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)")
}
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-shm file \(error, privacy: .public)")
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)")
}
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite-wal file \(error, privacy: .public)")
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)")
}
} catch {
Logger.services.error("🗄 Error Deleting Meshtastic.sqlite file \(error, privacy: .public)")
}
await MeshPackets.shared.clearCoreDataDatabase(includeRoutes: true)
clearNotifications()
context.refreshAllObjects()
}
clearCoreDataDatabase(context: context, includeRoutes: true)
clearNotifications()
context.refreshAllObjects()
}
}
Button {

View file

@ -175,7 +175,7 @@ struct DeviceConfig: View {
try await accessoryManager.sendNodeDBReset(fromUser: node!.user!, toUser: node!.user!)
try await Task.sleep(for: .seconds(1))
try await accessoryManager.disconnect()
clearCoreDataDatabase(context: context, includeRoutes: false)
await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false)
clearNotifications()
} catch {
Logger.mesh.error("NodeDB Reset Failed")
@ -200,7 +200,7 @@ struct DeviceConfig: View {
try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!)
try await Task.sleep(for: .seconds(1))
try await accessoryManager.disconnect()
clearCoreDataDatabase(context: context, includeRoutes: false)
await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false)
clearNotifications()
} catch {
Logger.mesh.error("Factory Reset Failed")
@ -213,7 +213,7 @@ struct DeviceConfig: View {
try await accessoryManager.sendFactoryReset(fromUser: node!.user!, toUser: node!.user!, resetDevice: true)
try? await Task.sleep(for: .seconds(1))
try await accessoryManager.disconnect()
clearCoreDataDatabase(context: context, includeRoutes: false)
await MeshPackets.shared.clearCoreDataDatabase(context: context, includeRoutes: false)
clearNotifications()
} catch {
Logger.mesh.error("Factory Reset Failed")