CoreData Writes wrapped in an Actor (#1569)

* MeshPackets.swift into an actor and make async

* Move upserts in UpdateCoreData to the MeshPackets actor

* Update Meshtastic/Views/Settings/AppSettings.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Meshtastic/Persistence/UpdateCoreData.swift

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Jake-B <jake-b@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
jake-b 2026-01-30 15:06:32 -05:00 committed by GitHub
parent 09dc74ceee
commit 4c370e5bee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 2975 additions and 2688 deletions

View file

@ -61,7 +61,7 @@ extension AccessoryManager {
self.updateState(.communicating)
self.connectionEventTask = Task {
for await event in eventStream {
self.didReceive(event)
await self.didReceive(event)
}
Logger.transport.info("[Accessory] Event stream closed")
}

View file

@ -65,7 +65,7 @@ extension AccessoryManager {
Logger.services.error("⚠️ Client Notification: \(clientNotification.message, privacy: .public)")
}
func handleMyInfo(_ myNodeInfo: MyNodeInfo) {
func handleMyInfo(_ myNodeInfo: MyNodeInfo) async {
// TODO: this works for connections like BLE that have a uniqueId, but what about ones like serial?
guard let connectedDeviceId = activeConnection?.device.id.uuidString else {
Logger.services.error("⚠️ Failed to decode MyInfo, no connected device ID")
@ -75,7 +75,8 @@ extension AccessoryManager {
updateDevice(key: \.num, value: Int64(myNodeInfo.myNodeNum))
if let myInfo = myInfoPacket(myInfo: myNodeInfo, peripheralId: connectedDeviceId, context: context) {
if let myInfoId = await MeshPackets.shared.myInfoPacket(myInfo: myNodeInfo, peripheralId: connectedDeviceId),
let myInfo = try? context.existingObject(with: myInfoId) as? MyInfoEntity {
if let bleName = myInfo.bleName {
updateDevice(key: \.name, value: bleName)
updateDevice(key: \.longName, value: bleName)
@ -97,7 +98,7 @@ extension AccessoryManager {
initializeTAKBridge()
}
func handleNodeInfo(_ nodeInfo: NodeInfo) {
func handleNodeInfo(_ nodeInfo: NodeInfo) async {
if let continuation = self.firstDatabaseNodeInfoContinuation {
continuation.resume()
self.firstDatabaseNodeInfoContinuation = nil
@ -109,10 +110,13 @@ extension AccessoryManager {
}
// Check if we're in database retrieval mode to defer saves for performance
let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false }
// Commented out: No need to defer save when nodeInfoPacket is now happening off the main thread
// let isRetrievingDatabase = if case .retrievingDatabase = self.state { true } else { false }
// TODO: nodeInfoPacket's channel: parameter is not used
if let nodeInfo = nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, context: context, deferSave: isRetrievingDatabase) {
// deferSave hard coded: No need to defer save when nodeInfoPacket is now happening off the main thread
if let nodeInfoId = await MeshPackets.shared.nodeInfoPacket(nodeInfo: nodeInfo, channel: 0, deferSave: false),
let nodeInfo = try? context.existingObject(with: nodeInfoId) as? NodeInfoEntity {
if let activeDevice = activeConnection?.device, activeDevice.num == nodeInfo.num {
if let user = nodeInfo.user {
updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?")
@ -138,24 +142,24 @@ extension AccessoryManager {
}
func handleChannel(_ channel: Channel) {
func handleChannel(_ channel: Channel) async {
guard let deviceNum = activeConnection?.device.num else {
Logger.data.error("Attempt to process channel information when no connected device.")
return
}
channelPacket(channel: channel, fromNum: Int64(truncatingIfNeeded: deviceNum), context: context)
await MeshPackets.shared.channelPacket(channel: channel, fromNum: Int64(truncatingIfNeeded: deviceNum))
}
func handleConfig(_ config: Config) {
func handleConfig(_ config: Config) async {
guard let device = activeConnection?.device, let deviceNum = device.num, let longName = device.longName else {
Logger.data.error("Attempt to process channel information when no connected device.")
return
}
// Local config parses out the variants. Should we do that here maybe?
localConfig(config: config, context: context, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName)
await MeshPackets.shared.localConfig(config: config, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName)
// Handle Timezone
if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
@ -169,12 +173,12 @@ extension AccessoryManager {
}
}
func handleModuleConfig(_ moduleConfigPacket: ModuleConfig) {
func handleModuleConfig(_ moduleConfigPacket: ModuleConfig) async {
guard let device = activeConnection?.device, let deviceNum = device.num, let longName = device.longName else {
Logger.services.error("Attempt to process channel information when no connected device.")
return
}
moduleConfig(config: moduleConfigPacket, context: context, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName)
await MeshPackets.shared.moduleConfig(config: moduleConfigPacket, nodeNum: Int64(truncatingIfNeeded: deviceNum), nodeLongName: longName)
// Get Canned Message Message List if the Module is Canned Messages
if moduleConfigPacket.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfigPacket.cannedMessage) {
try? getCannedMessageModuleMessages(destNum: deviceNum, wantResponse: true)
@ -185,7 +189,7 @@ extension AccessoryManager {
}
}
func handleDeviceMetadata(_ metadata: DeviceMetadata) {
func handleDeviceMetadata(_ metadata: DeviceMetadata) async {
// Note: moved firmware version check to be inline with connection process
guard let device = activeConnection?.device, let deviceNum = device.num else {
Logger.services.error("Attempt to process device metadata information when no connected device.")
@ -196,7 +200,7 @@ extension AccessoryManager {
updateDevice(key: \.firmwareVersion, value: metadata.firmwareVersion)
deviceMetadataPacket(metadata: metadata, fromNum: deviceNum, context: context)
await MeshPackets.shared.deviceMetadataPacket(metadata: metadata, fromNum: deviceNum)
}
internal func tryClearExistingChannels() {
@ -227,17 +231,16 @@ extension AccessoryManager {
}
func handleTextMessageAppPacket(_ packet: MeshPacket) {
func handleTextMessageAppPacket(_ packet: MeshPacket) async {
guard let device = activeConnection?.device, let deviceNum = device.num else {
Logger.services.error("Attempt to handle text message when no connected device.")
return
}
textMessageAppPacket(
await MeshPackets.shared.textMessageAppPacket(
packet: packet,
wantRangeTestPackets: wantRangeTestPackets,
connectedNode: deviceNum,
context: context,
appState: appState
)
@ -322,25 +325,27 @@ extension AccessoryManager {
case .UNRECOGNIZED:
Logger.mesh.info("\("📮 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")")
case .routerTextDirect:
Logger.mesh.info("\("💬 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")")
textMessageAppPacket(
packet: packet,
wantRangeTestPackets: false,
connectedNode: connectedNodeNum,
storeForward: true,
context: context,
appState: appState
)
Task {
Logger.mesh.info("\("💬 Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")")
await MeshPackets.shared.textMessageAppPacket(
packet: packet,
wantRangeTestPackets: false,
connectedNode: connectedNodeNum,
storeForward: true,
appState: appState
)
}
case .routerTextBroadcast:
Logger.mesh.info("\("✉️ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")")
textMessageAppPacket(
packet: packet,
wantRangeTestPackets: false,
connectedNode: connectedNodeNum,
storeForward: true,
context: context,
appState: appState
)
Task {
Logger.mesh.info("\("✉️ Store and Forward \(storeAndForwardMessage.rr) message received from \(packet.from.toHex())")")
await MeshPackets.shared.textMessageAppPacket(
packet: packet,
wantRangeTestPackets: false,
connectedNode: connectedNodeNum,
storeForward: true,
appState: appState
)
}
}
}
}

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)")
@ -864,7 +864,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)
@ -920,7 +920,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)
}
@ -1003,7 +1003,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)
}
@ -1057,7 +1057,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)
}
@ -1087,7 +1087,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)
}
@ -1117,7 +1117,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)
}
@ -1148,7 +1148,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)
}
@ -1178,7 +1178,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)
}
@ -1208,7 +1208,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)
}
@ -1387,7 +1387,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)
}
@ -1623,7 +1623,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)
}
@ -1677,7 +1677,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)
}
@ -1733,7 +1733,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)
}
@ -1764,7 +1764,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)
}
@ -1898,7 +1898,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)
}
@ -1928,7 +1928,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)
}
@ -1981,7 +1981,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)
}
@ -2060,7 +2060,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)
}
@ -2089,7 +2089,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

@ -198,7 +198,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()
@ -370,13 +370,13 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
}
}
func didReceive(_ event: ConnectionEvent) {
func didReceive(_ event: ConnectionEvent) async {
packetsReceived += 1
switch event {
case .data(let fromRadio):
// Logger.transport.info(" [Accessory] didReceive: \(fromRadio.payloadVariant.debugDescription)")
self.processFromRadio(fromRadio)
await self.processFromRadio(fromRadio)
Task {
await self.heartbeatResponseTimer?.cancel(withReason: "Data packet received")
await self.heartbeatTimer?.reset(delay: .seconds(15.0))
@ -484,7 +484,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
}
}
private func processFromRadio(_ decodedInfo: FromRadio) {
private func processFromRadio(_ decodedInfo: FromRadio) async {
switch decodedInfo.payloadVariant {
case .mqttClientProxyMessage(let mqttClientProxyMessage):
handleMqttClientProxyMessage(mqttClientProxyMessage)
@ -493,12 +493,12 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
handleClientNotification(clientNotification)
case .myInfo(let myNodeInfo):
handleMyInfo(myNodeInfo)
await handleMyInfo(myNodeInfo)
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.")
}
@ -507,20 +507,20 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
if case let .decoded(data) = packet.payloadVariant {
switch data.portnum {
case .textMessageApp, .detectionSensorApp, .alertApp:
handleTextMessageAppPacket(packet)
await handleTextMessageAppPacket(packet)
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:
waypointPacket(packet: packet, context: context)
await MeshPackets.shared.waypointPacket(packet: packet)
case .nodeinfoApp:
guard let connectedNodeNum = self.activeDeviceNum else {
Logger.mesh.error("🕸️ Unable to determine connectedNodeNum for node info upsert.")
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.")
}
@ -529,16 +529,16 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
Logger.mesh.error("🕸️ No active connection. Unable to determine connectedNodeNum for routingPacket.")
return
}
routingPacket(packet: packet, connectedNodeNum: deviceNum, context: context)
await MeshPackets.shared.routingPacket(packet: packet, connectedNodeNum: deviceNum)
case .adminApp:
adminAppPacket(packet: packet, context: context)
await MeshPackets.shared.adminAppPacket(packet: packet)
case .replyApp:
Logger.mesh.info("🕸️ MESH PACKET received for Reply App handling as a text message")
guard let deviceNum = activeConnection?.device.num else {
Logger.mesh.error("🕸️ No active connection. Unable to determine connectedNodeNum for replyApp.")
return
}
textMessageAppPacket(packet: packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: deviceNum, context: context, appState: appState)
await MeshPackets.shared.textMessageAppPacket(packet: packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: deviceNum, appState: appState)
case .ipTunnelApp:
Logger.mesh.info("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED")
case .serialApp:
@ -555,11 +555,10 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
return
}
if wantRangeTestPackets {
textMessageAppPacket(
await MeshPackets.shared.textMessageAppPacket(
packet: packet,
wantRangeTestPackets: true,
connectedNode: deviceNum,
context: context,
appState: appState
)
} else {
@ -570,7 +569,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
Logger.mesh.error("🕸️ No active connection. Unable to determine connectedNodeNum for telemetryApp.")
return
}
telemetryPacket(packet: packet, connectedNode: deviceNum, context: context)
await MeshPackets.shared.telemetryPacket(packet: packet, connectedNode: deviceNum)
case .textMessageCompressedApp:
Logger.mesh.info("🕸️ MESH PACKET received for Text Message Compressed App UNHANDLED")
case .zpsApp:
@ -592,7 +591,7 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
Logger.mesh.info("🕸️ MESH PACKET received for Neighbor Info App UNHANDLED \((try? neighborInfo.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
case .paxcounterApp:
paxCounterPacket(packet: decodedInfo.packet, context: context)
await MeshPackets.shared.paxCounterPacket(packet: decodedInfo.packet)
case .mapReportApp:
Logger.mesh.info("🕸️ MESH PACKET received Map Report App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
case .UNRECOGNIZED:
@ -615,19 +614,19 @@ class AccessoryManager: ObservableObject, MqttClientProxyManagerDelegate {
}
case .nodeInfo(let nodeInfo):
handleNodeInfo(nodeInfo)
await handleNodeInfo(nodeInfo)
case .channel(let channel):
handleChannel(channel)
await handleChannel(channel)
case .config(let config):
handleConfig(config)
await handleConfig(config)
case .moduleConfig(let moduleConfig):
handleModuleConfig(moduleConfig)
await handleModuleConfig(moduleConfig)
case .metadata(let metadata):
handleDeviceMetadata(metadata)
await handleDeviceMetadata(metadata)
case .deviceuiConfig:
#if DEBUG

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])
}
}
}

File diff suppressed because it is too large Load diff

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, context: context)
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")