Meshtastic-Apple/Meshtastic/Helpers/MeshPackets.swift
2025-09-27 09:14:07 -07:00

1201 lines
58 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// MeshPackets.swift
// Meshtastic Apple
//
// Created by Garth Vander Houwen on 5/27/22.
//
import Foundation
import CoreData
import MeshtasticProtobufs
import SwiftUI
import RegexBuilder
import OSLog
#if canImport(ActivityKit)
import ActivityKit
#endif
// Simple extension to concisely pass values through a has_XXX boolean check
fileprivate extension Bool {
func then<T>(_ value: T) -> T? {
self ? value : nil
}
}
func generateMessageMarkdown (message: String) -> String {
if !message.isEmoji() {
let types: NSTextCheckingResult.CheckingType = [.address, .link, .phoneNumber]
guard let detector = try? NSDataDetector(types: types.rawValue) else {
return message
}
let matches = detector.matches(in: message, options: [], range: NSRange(location: 0, length: message.utf16.count))
var messageWithMarkdown = message
if matches.count > 0 {
for match in matches {
guard let range = Range(match.range, in: message) else { continue }
if match.resultType == .address {
let address = message[range]
let urlEncodedAddress = address.addingPercentEncoding(withAllowedCharacters: .alphanumerics)
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: address, with: "[\(address)](http://maps.apple.com/?address=\(urlEncodedAddress ?? ""))")
} else if match.resultType == .phoneNumber {
let phone = messageWithMarkdown[range]
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: phone, with: "[\(phone)](tel:\(phone))")
} else if match.resultType == .link {
let start = match.range.lowerBound
let stop = match.range.upperBound
let url = message[start ..< stop]
let absoluteUrl = match.url?.absoluteString ?? ""
let markdownUrl = "[\(url)](\(absoluteUrl))"
messageWithMarkdown = messageWithMarkdown.replacingOccurrences(of: url, with: markdownUrl)
}
}
}
return messageWithMarkdown
}
return message
}
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
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:
#if DEBUG
Logger.services.error("⁉️ Unknown Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
#endif
}
}
func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
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:
#if DEBUG
Logger.services.error("⁉️ Unknown Module Config variant UNHANDLED \(config.payloadVariant.debugDescription, privacy: .public)")
#endif
}
}
func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? {
let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum))
Logger.mesh.info(" \(logString, privacy: .public)")
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(myInfo.myNodeNum))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
// Not Found Insert
if fetchedMyInfo.isEmpty {
let myInfoEntity = MyInfoEntity(context: context)
myInfoEntity.peripheralId = peripheralId
myInfoEntity.myNodeNum = Int64(myInfo.myNodeNum)
myInfoEntity.rebootCount = Int32(myInfo.rebootCount)
myInfoEntity.deviceId = myInfo.deviceID
do {
try context.save()
Logger.data.info("💾 Saved a new myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)")
return myInfoEntity
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Inserting New Core Data MyInfoEntity: \(nsError, privacy: .public)")
}
} else {
fetchedMyInfo[0].peripheralId = peripheralId
fetchedMyInfo[0].myNodeNum = Int64(myInfo.myNodeNum)
fetchedMyInfo[0].rebootCount = Int32(myInfo.rebootCount)
do {
try context.save()
Logger.data.info("💾 Updated myInfo for node: \(myInfo.myNodeNum.toHex(), privacy: .public)")
return fetchedMyInfo[0]
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Updating Core Data MyInfoEntity: \(nsError, privacy: .public)")
}
}
} catch {
Logger.data.error("💥 Fetch MyInfo Error")
}
return nil
}
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)")
let fetchedMyInfoRequest = MyInfoEntity.fetchRequest()
fetchedMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", fromNum)
do {
let fetchedMyInfo = try context.fetch(fetchedMyInfoRequest)
if fetchedMyInfo.count == 1 {
let newChannel = ChannelEntity(context: context)
newChannel.id = Int32(channel.index)
newChannel.index = Int32(channel.index)
newChannel.uplinkEnabled = channel.settings.uplinkEnabled
newChannel.downlinkEnabled = channel.settings.downlinkEnabled
newChannel.name = channel.settings.name
newChannel.role = Int32(channel.role.rawValue)
newChannel.psk = channel.settings.psk
if channel.settings.hasModuleSettings {
newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision)
newChannel.mute = channel.settings.moduleSettings.isClientMuted
}
guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else {
return
}
if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity {
let index = mutableChannels.index(of: oldChannel as Any)
mutableChannels.replaceObject(at: index, with: newChannel)
} else {
mutableChannels.add(newChannel)
}
fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet
context.refresh(newChannel, mergeChanges: true)
do {
try context.save()
} catch {
Logger.data.error("💥 Failed to save channel: \(error.localizedDescription, privacy: .public)")
}
Logger.data.info("💾 Updated MyInfo channel \(channel.index, privacy: .public) from Channel App Packet For: \(fetchedMyInfo[0].myNodeNum, privacy: .public)")
} else if channel.role.rawValue > 0 {
Logger.data.error("💥Trying to save a channel to a MyInfo that does not exist: \(fromNum.toHex(), privacy: .public)")
}
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)")
}
}
}
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)")
let fetchedNodeRequest = NodeInfoEntity.fetchRequest()
fetchedNodeRequest.predicate = NSPredicate(format: "num == %lld", fromNum)
do {
let fetchedNode = try context.fetch(fetchedNodeRequest)
let newMetadata = DeviceMetadataEntity(context: context)
newMetadata.time = Date()
newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion)
newMetadata.canShutdown = metadata.canShutdown
newMetadata.hasWifi = metadata.hasWifi_p
newMetadata.hasBluetooth = metadata.hasBluetooth_p
newMetadata.hasEthernet = metadata.hasEthernet_p
newMetadata.role = Int32(metadata.role.rawValue)
newMetadata.positionFlags = Int32(metadata.positionFlags)
newMetadata.excludedModules = Int32(metadata.excludedModules)
// Swift does strings weird, this does work to get the version without the github hash
let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".")
var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))]
version = version.dropLast()
newMetadata.firmwareVersion = String(version)
if fetchedNode.count > 0 {
fetchedNode[0].metadata = newMetadata
} else {
if fromNum > 0 {
let newNode = createNodeInfo(num: Int64(fromNum), context: context)
newNode.metadata = newMetadata
}
}
if sessionPasskey?.count != 0 {
fetchedNode[0].sessionPasskey = sessionPasskey
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
}
do {
try context.save()
} catch {
Logger.data.error("💥 Failed to save device metadata: \(error.localizedDescription, privacy: .public)")
}
Logger.data.info("💾 Updated Device Metadata from Admin App Packet For: \(fromNum.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving MyInfo Channel from ADMIN_APP \(nsError, privacy: .public)")
}
}
}
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, String(nodeInfo.num))
Logger.mesh.info("📟 \(logString, privacy: .public)")
guard nodeInfo.num > 0 else { return nil }
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
// Not Found Insert
if fetchedNode.isEmpty && nodeInfo.num > 0 {
let newNode = NodeInfoEntity(context: context)
newNode.id = Int64(nodeInfo.num)
newNode.num = Int64(nodeInfo.num)
newNode.channel = Int32(nodeInfo.channel)
newNode.favorite = nodeInfo.isFavorite
newNode.ignored = nodeInfo.isIgnored
newNode.hopsAway = Int32(nodeInfo.hopsAway)
if nodeInfo.hasDeviceMetrics {
let telemetry = TelemetryEntity(context: context)
telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel)
telemetry.voltage = nodeInfo.deviceMetrics.voltage
telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization
telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx
var newTelemetries = [TelemetryEntity]()
newTelemetries.append(telemetry)
newNode.telemetries? = NSOrderedSet(array: newTelemetries)
}
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 {
let newUser = UserEntity(context: context)
newUser.userId = nodeInfo.num.toHex()
newUser.num = Int64(nodeInfo.num)
newUser.longName = nodeInfo.user.longName
newUser.shortName = nodeInfo.user.shortName
newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
newUser.hwModelId = Int32(nodeInfo.user.hwModel.rawValue)
Task {
Api().loadDeviceHardwareData { (hw) in
let dh = hw.first(where: { $0.hwModel == newUser.hwModelId })
newUser.hwDisplayName = dh?.displayName
}
}
newUser.isLicensed = nodeInfo.user.isLicensed
newUser.role = Int32(nodeInfo.user.role.rawValue)
if !nodeInfo.user.publicKey.isEmpty {
newUser.pkiEncrypted = true
newUser.publicKey = nodeInfo.user.publicKey
}
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
if nodeInfo.user.hasIsUnmessagable {
newUser.unmessagable = nodeInfo.user.isUnmessagable
} else {
let roles = [2, 4, 5, 6, 7, 10, 11]
let containsRole = roles.contains(Int(newUser.role))
if containsRole {
newUser.unmessagable = true
} else {
newUser.unmessagable = false
}}
newNode.user = newUser
} else if nodeInfo.num > Constants.minimumNodeNum {
do {
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
newNode.user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
let position = PositionEntity(context: context)
position.latest = true
position.seqNo = Int32(nodeInfo.position.seqNumber)
position.latitudeI = nodeInfo.position.latitudeI
position.longitudeI = nodeInfo.position.longitudeI
position.altitude = nodeInfo.position.altitude
position.satsInView = Int32(nodeInfo.position.satsInView)
position.speed = Int32(nodeInfo.position.groundSpeed)
position.heading = Int32(nodeInfo.position.groundTrack)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
var newPostions = [PositionEntity]()
newPostions.append(position)
newNode.positions? = NSOrderedSet(array: newPostions)
}
// Look for a MyInfo
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if fetchedMyInfo.count > 0 {
newNode.myInfo = fetchedMyInfo[0]
}
do {
try context.save()
Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num), privacy: .public)")
return newNode
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)")
}
} catch {
Logger.data.error("Fetch MyInfo Error")
}
} else if nodeInfo.num > 0 {
fetchedNode[0].id = Int64(nodeInfo.num)
fetchedNode[0].num = Int64(nodeInfo.num)
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard)))
fetchedNode[0].snr = nodeInfo.snr
fetchedNode[0].channel = Int32(nodeInfo.channel)
fetchedNode[0].favorite = nodeInfo.isFavorite
fetchedNode[0].ignored = nodeInfo.isIgnored
fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway)
if nodeInfo.hasUser {
if fetchedNode[0].user == nil {
fetchedNode[0].user = UserEntity(context: context)
}
// Set the public key for a user if it is empty, don't update
if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty {
fetchedNode[0].user?.pkiEncrypted = true
fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey
}
fetchedNode[0].user?.userId = nodeInfo.num.toHex()
fetchedNode[0].user?.num = Int64(nodeInfo.num)
fetchedNode[0].user?.numString = String(nodeInfo.num)
fetchedNode[0].user?.longName = nodeInfo.user.longName
fetchedNode[0].user?.shortName = nodeInfo.user.shortName
fetchedNode[0].user?.isLicensed = nodeInfo.user.isLicensed
fetchedNode[0].user?.role = Int32(nodeInfo.user.role.rawValue)
fetchedNode[0].user?.hwModel = String(describing: nodeInfo.user.hwModel).uppercased()
fetchedNode[0].user?.hwModelId = Int32(nodeInfo.user.hwModel.rawValue)
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
if nodeInfo.user.hasIsUnmessagable {
fetchedNode[0].user?.unmessagable = nodeInfo.user.isUnmessagable
} else {
let roles = [-1, 2, 4, 5, 6, 7, 10, 11]
let containsRole = roles.contains(Int(fetchedNode[0].user?.role ?? -1))
if containsRole {
fetchedNode[0].user?.unmessagable = true
} else {
fetchedNode[0].user?.unmessagable = false
}
}
Task {
Api().loadDeviceHardwareData { (hw) in
let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user!.hwModelId })
fetchedNode[0].user?.hwDisplayName = dh?.displayName
}
}
} else {
if fetchedNode[0].user == nil && nodeInfo.num > Constants.minimumNodeNum {
do {
let newUser = try createUser(num: Int64(nodeInfo.num), context: context)
fetchedNode[0].user = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node (Invalid Input) from node number: \(nodeInfo.num, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(nodeInfo.num, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
}
if nodeInfo.hasDeviceMetrics {
let newTelemetry = TelemetryEntity(context: context)
newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel)
newTelemetry.voltage = nodeInfo.deviceMetrics.voltage
newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization
newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx
guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else {
return nil
}
mutableTelemetries.add(newTelemetry)
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
}
if nodeInfo.hasPosition {
if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) {
let position = PositionEntity(context: context)
position.latitudeI = nodeInfo.position.latitudeI
position.longitudeI = nodeInfo.position.longitudeI
position.altitude = nodeInfo.position.altitude
position.satsInView = Int32(nodeInfo.position.satsInView)
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time)))
guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else {
return nil
}
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
}
}
// Look for a MyInfo
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if fetchedMyInfo.count > 0 {
fetchedNode[0].myInfo = fetchedMyInfo[0]
}
do {
try context.save()
Logger.data.info("💾 [NodeInfo] saved for \(nodeInfo.num.toHex(), privacy: .public)")
return fetchedNode[0]
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Saving Core Data NodeInfoEntity: \(nsError, privacy: .public)")
}
} catch {
Logger.data.error("💥 Fetch MyInfo Error")
}
}
} catch {
Logger.data.error("💥 Fetch NodeInfoEntity Error")
}
return nil
}
func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
if let adminMessage = try? AdminMessage(serializedBytes: packet.decoded.payload) {
if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getCannedMessageModuleMessagesResponse(adminMessage.getCannedMessageModuleMessagesResponse) {
if let cmmc = try? CannedMessageModuleConfig(serializedBytes: packet.decoded.payload) {
let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex())
Logger.mesh.info("🥫 \(logString, privacy: .public)")
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeRequest)
if fetchedNode.count == 1 {
let messages = String(cmmc.textFormatString())
.replacingOccurrences(of: "11: ", with: "")
.replacingOccurrences(of: "\"", with: "")
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n").first ?? ""
fetchedNode[0].cannedMessageConfig?.messages = messages
do {
try context.save()
Logger.data.info("💾 Updated Canned Messages Messages For: \(fetchedNode.first?.num.toHex() ?? "Unknown".localized, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)")
}
}
} catch {
Logger.data.error("💥 Error Deserializing ADMIN_APP packet.")
}
}
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) {
channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context)
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) {
deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
} 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)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
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)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
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)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
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)
} else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) {
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)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
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)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.externalNotification(moduleConfig.externalNotification) {
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)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.rangeTest(moduleConfig.rangeTest) {
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)
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.storeForward(moduleConfig.storeForward) {
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)
}
} 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)
}
} else {
Logger.mesh.error("🕸️ MESH PACKET received Admin App UNHANDLED \((try? packet.decoded.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
// Save an ack for the admin message log for each admin message response received as we stopped sending acks if there is also a response to reduce airtime.
adminResponseAck(packet: packet, context: context)
}
}
func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
let fetchedAdminMessageRequest = MessageEntity.fetchRequest()
fetchedAdminMessageRequest.predicate = NSPredicate(format: "messageId == %lld", packet.decoded.requestID)
do {
let fetchedMessage = try context.fetch(fetchedAdminMessageRequest)
if fetchedMessage.count > 0 {
fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970)
fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue)
fetchedMessage[0].receivedACK = true
fetchedMessage[0].realACK = true
fetchedMessage[0].ackSNR = packet.rxSnr
if fetchedMessage[0].fromUser != nil {
fetchedMessage[0].fromUser?.objectWillChange.send()
}
do {
try context.save()
} catch {
Logger.data.error("Failed to save admin message response as an ack: \(error.localizedDescription, privacy: .public)")
}
}
} catch {
Logger.data.error("Failed to fetch admin message by requestID: \(error.localizedDescription, privacy: .public)")
}
}
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from))
Logger.mesh.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
if let paxMessage = try? Paxcount(serializedBytes: packet.decoded.payload) {
let newPax = PaxCounterEntity(context: context)
newPax.ble = Int32(truncatingIfNeeded: paxMessage.ble)
newPax.wifi = Int32(truncatingIfNeeded: paxMessage.wifi)
newPax.uptime = Int32(truncatingIfNeeded: paxMessage.uptime)
newPax.time = Date()
if fetchedNode.count > 0 {
guard let mutablePax = fetchedNode[0].pax!.mutableCopy() as? NSMutableOrderedSet else {
return
}
mutablePax.add(newPax)
fetchedNode[0].pax = mutablePax
do {
try context.save()
} catch {
Logger.data.error("Failed to save pax: \(error.localizedDescription, privacy: .public)")
}
} else {
Logger.data.info("Node Info Not Found")
}
}
} catch {
}
}
func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSManagedObjectContext) {
if let routingMessage = try? Routing(serializedBytes: packet.decoded.payload) {
let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue)
let routingErrorString = routingError?.display ?? "Unknown".localized
let logString = String.localizedStringWithFormat("Routing received for RequestID: %@ Ack Status: %@".localized, String(packet.decoded.requestID), routingErrorString)
Logger.mesh.info("🕸️ \(logString, privacy: .public)")
let fetchMessageRequest = MessageEntity.fetchRequest()
fetchMessageRequest.predicate = NSPredicate(format: "messageId == %lld", Int64(packet.decoded.requestID))
do {
let fetchedMessage = try context.fetch(fetchMessageRequest)
if fetchedMessage.count > 0 {
if fetchedMessage[0].toUser != nil {
// Real ACK from DM Recipient
if packet.to != packet.from {
fetchedMessage[0].realACK = true
}
}
fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue)
if routingMessage.errorReason == Routing.Error.none {
fetchedMessage[0].receivedACK = true
}
fetchedMessage[0].ackSNR = packet.rxSnr
if packet.rxTime > 0 {
fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime)
} else {
fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970)
}
if fetchedMessage[0].toUser != nil {
fetchedMessage[0].toUser!.objectWillChange.send()
} else {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", connectedNodeNum)
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if fetchedMyInfo.count > 0 {
for ch in fetchedMyInfo[0].channels!.array as? [ChannelEntity] ?? [] where ch.index == packet.channel {
ch.objectWillChange.send()
}
}
} catch { }
}
} else {
return
}
try context.save()
Logger.data.info("💾 ACK Saved for Message: \(packet.decoded.requestID, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving ACK for message: \(packet.id, privacy: .public) Error: \(nsError, privacy: .public)")
}
}
}
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
Task { @MainActor in
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
/// Other unhandled telemetry packets
return
}
let telemetry = TelemetryEntity(context: context)
let fetchNodeTelemetryRequest = NodeInfoEntity.fetchRequest()
fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeTelemetryRequest)
if fetchedNode.count == 1 {
/// Currently only Device Metrics and Environment Telemetry are supported in the app
if telemetryMessage.variant == Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) {
// Device Metrics
Logger.data.info("📈 [Telemetry] Device Metrics Received for Node: \(packet.from.toHex(), privacy: .public)")
telemetry.airUtilTx = telemetryMessage.deviceMetrics.hasAirUtilTx.then(telemetryMessage.deviceMetrics.airUtilTx)
telemetry.channelUtilization = telemetryMessage.deviceMetrics.hasChannelUtilization.then(telemetryMessage.deviceMetrics.channelUtilization)
telemetry.batteryLevel = telemetryMessage.deviceMetrics.hasBatteryLevel.then(Int32(telemetryMessage.deviceMetrics.batteryLevel))
telemetry.voltage = telemetryMessage.deviceMetrics.hasVoltage.then(telemetryMessage.deviceMetrics.voltage)
telemetry.uptimeSeconds = telemetryMessage.deviceMetrics.hasUptimeSeconds.then(Int32(telemetryMessage.deviceMetrics.uptimeSeconds))
telemetry.metricsType = 0
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx, privacy: .public) for Node: \(packet.from.toHex(), privacy: .public)")
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) {
// Environment Metrics
Logger.data.info("📈 [Telemetry] Environment Metrics Received for Node: \(packet.from.toHex(), privacy: .public)")
telemetry.barometricPressure = telemetryMessage.environmentMetrics.hasBarometricPressure.then(telemetryMessage.environmentMetrics.barometricPressure)
telemetry.iaq = telemetryMessage.environmentMetrics.hasIaq.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.iaq))
telemetry.gasResistance = telemetryMessage.environmentMetrics.hasGasResistance.then(telemetryMessage.environmentMetrics.gasResistance)
telemetry.relativeHumidity = telemetryMessage.environmentMetrics.hasRelativeHumidity.then(telemetryMessage.environmentMetrics.relativeHumidity)
telemetry.temperature = telemetryMessage.environmentMetrics.hasTemperature.then(telemetryMessage.environmentMetrics.temperature)
telemetry.current = telemetryMessage.environmentMetrics.hasCurrent.then(telemetryMessage.environmentMetrics.current)
telemetry.voltage = telemetryMessage.environmentMetrics.hasVoltage.then(telemetryMessage.environmentMetrics.voltage)
telemetry.weight = telemetryMessage.environmentMetrics.hasWeight.then(telemetryMessage.environmentMetrics.weight)
telemetry.distance = telemetryMessage.environmentMetrics.hasDistance.then(telemetryMessage.environmentMetrics.distance)
telemetry.windSpeed = telemetryMessage.environmentMetrics.hasWindSpeed.then(telemetryMessage.environmentMetrics.windSpeed)
telemetry.windGust = telemetryMessage.environmentMetrics.hasWindGust.then(telemetryMessage.environmentMetrics.windGust)
telemetry.windLull = telemetryMessage.environmentMetrics.hasWindLull.then(telemetryMessage.environmentMetrics.windLull)
telemetry.windDirection = telemetryMessage.environmentMetrics.hasWindDirection.then(Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection))
telemetry.irLux = telemetryMessage.environmentMetrics.hasIrLux.then(telemetryMessage.environmentMetrics.irLux)
telemetry.lux = telemetryMessage.environmentMetrics.hasLux.then(telemetryMessage.environmentMetrics.lux)
telemetry.whiteLux = telemetryMessage.environmentMetrics.hasWhiteLux.then(telemetryMessage.environmentMetrics.whiteLux)
telemetry.uvLux = telemetryMessage.environmentMetrics.hasUvLux.then(telemetryMessage.environmentMetrics.uvLux)
telemetry.radiation = telemetryMessage.environmentMetrics.hasRadiation.then(telemetryMessage.environmentMetrics.radiation)
telemetry.rainfall1H = telemetryMessage.environmentMetrics.hasRainfall1H.then(telemetryMessage.environmentMetrics.rainfall1H)
telemetry.rainfall24H = telemetryMessage.environmentMetrics.hasRainfall24H.then(telemetryMessage.environmentMetrics.rainfall24H)
telemetry.soilTemperature = telemetryMessage.environmentMetrics.hasSoilTemperature.then(telemetryMessage.environmentMetrics.soilTemperature)
telemetry.soilMoisture = telemetryMessage.environmentMetrics.hasSoilMoisture.then(telemetryMessage.environmentMetrics.soilMoisture)
telemetry.metricsType = 1
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
// Local Stats for Live activity
telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds)
telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization
telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx
telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx)
telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx)
telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad)
telemetry.numRxDupe = Int32(truncatingIfNeeded: telemetryMessage.localStats.numRxDupe)
telemetry.numTxRelay = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelay)
telemetry.numTxRelayCanceled = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTxRelayCanceled)
telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes)
telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes)
telemetry.metricsType = 4
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)")
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
Logger.data.info("📈 [Telemetry] Power Metrics Received for Node: \(packet.from.toHex(), privacy: .public)")
telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.hasCh1Voltage.then(telemetryMessage.powerMetrics.ch1Voltage)
telemetry.powerCh1Current = telemetryMessage.powerMetrics.hasCh1Current.then(telemetryMessage.powerMetrics.ch1Current)
telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.hasCh2Voltage.then(telemetryMessage.powerMetrics.ch2Voltage)
telemetry.powerCh2Current = telemetryMessage.powerMetrics.hasCh2Current.then(telemetryMessage.powerMetrics.ch2Current)
telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.hasCh3Voltage.then(telemetryMessage.powerMetrics.ch3Voltage)
telemetry.powerCh3Current = telemetryMessage.powerMetrics.hasCh3Current.then(telemetryMessage.powerMetrics.ch3Current)
telemetry.metricsType = 2
}
telemetry.snr = packet.rxSnr
telemetry.rssi = packet.rxRssi
telemetry.time = Date(timeIntervalSince1970: TimeInterval(Int64(truncatingIfNeeded: telemetryMessage.time)))
guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else {
return
}
mutableTelemetries.add(telemetry)
if packet.rxTime > 0 {
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime))
} else {
fetchedNode[0].lastHeard = Date()
}
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
}
try context.save()
Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)")
if telemetry.metricsType == 0 {
// Connected Device Metrics
// ------------------------
// 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()
}
}
} else if telemetry.metricsType == 4 {
// Update our live activity if there is one running, not available on mac
#if !targetEnvironment(macCatalyst)
#if canImport(ActivityKit)
let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
let date = Date.now...fifteenMinutesLater
let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: telemetry.uptimeSeconds.map { UInt32($0) },
channelUtilization: telemetry.channelUtilization,
airtime: telemetry.airUtilTx,
sentPackets: UInt32(telemetry.numPacketsTx),
receivedPackets: UInt32(telemetry.numPacketsRx),
badReceivedPackets: UInt32(telemetry.numPacketsRxBad),
dupeReceivedPackets: UInt32(telemetry.numRxDupe),
packetsSentRelay: UInt32(telemetry.numTxRelay),
packetsCanceledRelay: UInt32(telemetry.numTxRelayCanceled),
nodesOnline: UInt32(telemetry.numOnlineNodes),
totalNodes: UInt32(telemetry.numTotalNodes),
timerRange: date)
let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default)
let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil)
let meshActivity = Activity<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })
if meshActivity != nil {
Task {
await meshActivity?.update(updatedContent, alertConfiguration: alertConfiguration)
// await meshActivity?.update(updatedContent)
Logger.services.debug("Updated live activity.")
}
}
#endif
#endif
}
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 Error Saving Telemetry for Node \(packet.from, privacy: .public) Error: \(nsError, privacy: .public)")
}
} else {
Logger.data.error("💥 Error Fetching NodeInfoEntity for Node \(packet.from.toHex(), privacy: .public)")
}
}
}
func textMessageAppPacket(
packet: MeshPacket,
wantRangeTestPackets: Bool,
critical: Bool = false,
connectedNode: Int64,
storeForward: Bool = false,
context: NSManagedObjectContext,
appState: AppState?
) {
var messageText = String(bytes: packet.decoded.payload, encoding: .utf8)
let rangeRef = Reference(Int.self)
let rangeTestRegex = Regex {
"seq "
TryCapture(as: rangeRef) {
OneOrMore(.digit)
} transform: { match in
Int(match)
}
}
let rangeTest = messageText?.contains(rangeTestRegex) ?? false && messageText?.starts(with: "seq ") ?? false
if !wantRangeTestPackets && rangeTest {
return
}
var storeForwardBroadcast = false
if storeForward {
if let storeAndForwardMessage = try? StoreAndForward(serializedBytes: packet.decoded.payload) {
messageText = String(bytes: storeAndForwardMessage.text, encoding: .utf8)
if storeAndForwardMessage.rr == .routerTextBroadcast {
storeForwardBroadcast = true
}
}
}
if messageText?.count ?? 0 > 0 {
Logger.mesh.info("💬 \("Message received from the text message app.".localized, privacy: .public)")
let messageUsers = UserEntity.fetchRequest()
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
do {
let fetchedUsers = try context.fetch(messageUsers)
let newMessage = MessageEntity(context: context)
newMessage.messageId = Int64(packet.id)
if packet.rxTime > 0 {
newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime)
} else {
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
}
newMessage.receivedACK = false
newMessage.snr = packet.rxSnr
newMessage.rssi = packet.rxRssi
newMessage.isEmoji = packet.decoded.emoji == 1
newMessage.channel = Int32(packet.channel)
newMessage.portNum = Int32(packet.decoded.portnum.rawValue)
if packet.decoded.portnum == PortNum.detectionSensorApp {
if !UserDefaults.enableDetectionNotifications {
newMessage.read = true
}
}
if packet.decoded.replyID > 0 {
newMessage.replyID = Int64(packet.decoded.replyID)
}
// Updated logic for handling toUser
if fetchedUsers.first(where: { $0.num == packet.to }) != nil && packet.to != Constants.maximumNodeNum {
if !storeForwardBroadcast {
newMessage.toUser = fetchedUsers.first(where: { $0.num == packet.to })
} else if storeForwardBroadcast {
// For S&F broadcast messages, treat as a channel message (not a DM)
newMessage.toUser = nil
} else {
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.to), context: context)
newMessage.toUser = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.to, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.to, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
}
if fetchedUsers.first(where: { $0.num == packet.from }) != nil {
newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from })
/// Set the public key for the message
if newMessage.fromUser?.pkiEncrypted ?? false && packet.pkiEncrypted {
newMessage.pkiEncrypted = true
newMessage.publicKey = packet.publicKey
}
/// Check for key mismatch
if let nodeKey = newMessage.fromUser?.publicKey {
if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty {
if nodeKey != newMessage.publicKey {
newMessage.fromUser?.keyMatch = false
newMessage.fromUser?.newPublicKey = newMessage.publicKey
let nodeKey = String(nodeKey.base64EncodedString()).prefix(8)
let messageKey = String(newMessage.publicKey?.base64EncodedString() ?? "No Key").prefix(8)
Logger.data.error("🔑 Key mismatch original key: \(nodeKey, privacy: .public) . . . new key: \(messageKey, privacy: .public) . . .")
}
}
} else if packet.pkiEncrypted {
/// We have no key, set it if it is not empty
if !packet.publicKey.isEmpty {
newMessage.fromUser?.pkiEncrypted = true
newMessage.fromUser?.publicKey = packet.publicKey
}
}
} else {
/// Make a new from user if they are unknown
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
newMessage.fromUser = newUser
} catch CoreDataError.invalidInput(let message) {
Logger.data.error("Error Creating a new Core Data UserEntity (Invalid Input) from node number: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
if packet.rxTime > 0 {
newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
} else {
newMessage.fromUser?.userNode?.lastHeard = Date()
}
newMessage.messagePayload = messageText
newMessage.messagePayloadMarkdown = generateMessageMarkdown(message: messageText!)
if packet.to != Constants.maximumNodeNum && newMessage.fromUser != nil {
newMessage.fromUser?.lastMessage = Date()
}
var messageSaved = false
do {
try context.save()
Logger.data.info("💾 Saved a new message for \(newMessage.messageId, privacy: .public)")
messageSaved = true
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Failed to save new MessageEntity \(nsError, privacy: .public)")
}
// Send notifications if the message saved properly to core data
if messageSaved {
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
return
}
if newMessage.fromUser != nil && newMessage.toUser != nil {
// Set Unread Message Indicators
if packet.to == connectedNode {
appState?.unreadDirectMessages = newMessage.toUser?.unreadMessages ?? 0
}
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)")
}
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedNode))
do {
let fetchedMyInfo = try context.fetch(fetchMyInfoRequest)
if !fetchedMyInfo.isEmpty {
appState?.unreadChannelMessages = fetchedMyInfo[0].unreadMessages
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)")
}
}
}
} catch {
// Handle error
}
}
}
} catch {
Logger.data.error("Fetch Message To and From Users Error")
}
}
}
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from))
Logger.mesh.info("📍 \(logString, privacy: .public)")
do {
if let waypointMessage = try? Waypoint(serializedBytes: packet.decoded.payload) {
// Fetch waypoint by waypointMessage.id, not packet.id
let fetchWaypointRequest = WaypointEntity.fetchRequest()
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id))
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
// Fetch the node info to get the short name
var nodeShortName: String = "?"
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
fetchNodeRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeRequest)
if let node = fetchedNode.first, let user = node.user {
nodeShortName = user.shortName ?? node.user?.userId ?? String(packet.from.toHex())
}
} catch {
Logger.data.error("Failed to fetch NodeInfoEntity for node \(packet.from.toHex(), privacy: .public): \(error)")
}
if fetchedWaypoint.isEmpty {
// Create a new waypoint
let waypoint = WaypointEntity(context: context)
waypoint.id = Int64(waypointMessage.id) // Use waypointMessage.id
waypoint.name = waypointMessage.name
waypoint.longDescription = waypointMessage.description_p
waypoint.latitudeI = waypointMessage.latitudeI
waypoint.longitudeI = waypointMessage.longitudeI
waypoint.icon = Int64(waypointMessage.icon)
waypoint.locked = Int64(waypointMessage.lockedTo)
if waypointMessage.expire >= 1 {
waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
} else {
waypoint.expire = nil
}
waypoint.created = Date()
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()
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
}
} else {
// Update existing waypoint
let existingWaypoint = fetchedWaypoint[0]
if existingWaypoint.locked == 0 || existingWaypoint.locked == packet.from {
let currentTime = Int64(Date().timeIntervalSince1970)
if waypointMessage.expire > 0 && waypointMessage.expire <= currentTime {
context.delete(existingWaypoint)
do {
try context.save()
Logger.data.info("💾 Deleted a waypoint")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
}
} else {
existingWaypoint.name = waypointMessage.name
existingWaypoint.longDescription = waypointMessage.description_p
existingWaypoint.latitudeI = waypointMessage.latitudeI
existingWaypoint.longitudeI = waypointMessage.longitudeI
existingWaypoint.icon = Int64(waypointMessage.icon)
existingWaypoint.locked = Int64(waypointMessage.lockedTo)
if waypointMessage.expire >= 1 {
existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
} else {
existingWaypoint.expire = nil
}
existingWaypoint.lastUpdated = Date()
do {
try context.save()
Logger.data.info("💾 Updated Node Waypoint App Packet For: \(existingWaypoint.id, privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("Error Saving WaypointEntity from WAYPOINT_APP \(nsError, privacy: .public)")
}
}
}
}
}
} catch {
Logger.mesh.error("Error Deserializing WAYPOINT_APP packet.")
}
}