Meshtastic-Apple/Meshtastic/Helpers/MeshPackets.swift
Garth Vander Houwen 026bb80fba
Transports Interface to Support TCP for all Platforms and Serial on Mac (#1341)
* Initial implementation of transports

* Initial LogRadio implementation

* Fixes for Settings view (caused by debug commenting)

* Refinement of the object and actor model

* Connect view text and tab updates

* Fix mac catalyst and tests

* Warning and logging clean-up

* In progress commit

* Serial Transport and Reconnect draft work

* Serial transport and reconnection draft work

* Quick fix for BLE - still more work to do

* interim commit

* More in progress changes

* Minor improvements

* Pretty good initial implementation

* Bump version beyond the app store

* Fix for disconnection swipeAction

* Tweaks to TCPConnection implementation

* Retry for NONCE_ONLY_DB

* Revert json string change

* Simplified some of the API + "Anti-discovery"

* Tweaks for devices leaving the discovery process

* Bump version

* iOS26 Tweaks

* Tweaks and bug fixes

* Add link with slash sf symbol

* update symbol image on connect view

* BLE disconnect handling

* Log privacy attributes

* Onboarding and minor fixes.

* change database to nodes, add emoji to tcp logs

* Error handling improvements

* More logging emojis

* Suppressed unnecessary errors on disconnect

* Heartbeat emoji

* Add bluetooth symbol

* add privacy attributes to [TCP] logs, add custom bluetooth logo

* Improve routing logs

* Emoji for connect logs

* Heartbeat emoji

* Add CBCentralManagerScanOptionAllowDuplicatesKey options to central for bluetooth

* fix nav errors by switching from observableobject to state

* Update connection indicator icon

* fix for BLE disconnects

* Connection process fixes

* More fixes/tweaks to connection process

* Strict concurrency

* Fix some warnings, remove wifi warning

* delete stale keys

* interim commit

* Update privacy for log, fix wrong space

* fix a couple of linting items

* Switch to targeted

* interim commit

* BLE Signal strenth on connect view

* Remove BLE RSSI from long press menu

* Modem lights

* minor spacing tweak

* Additional BLE logging and a scanning fix.

* Discovery and BLE RSSI improvements

* Background suspension

* Update isConnected to enable UI during db load

* update protobufs

* Replace config if statements with switches, Fix unknown module config logging, make dark mode modem circle stroke color white so they are visible

* Additional logging cleanup

* hast

* Set unmessagable to true if the longname has the unmessagable emoji

* Connect error handling improvements

* Admin popup list icon and activity lights updates

* Revert use of .toolbar back to .navigationBarItems

* More public logging

* Better BLE error handling

* Node DB progress meter

* minor tweak to activity light interaction timing

* Fix comment linting, remove stale keys

* Remove stale keys

* Easy linting fixes

* Two more simple linting fixes

* clean up meshtasticapp

* More public logging

* Replay config

* Logging

* Fix for unselected node on Settings

* Tweak to progress meter based on device idiom

* Update protos

* Session replay redaction of messages

* Serial fix for old devices, and a let statement

* Mask text too

* Fix typo

* BLE poweredOff is now an auto-reconnectable error

* Update logging

* Fix for peerRemovedPairingInformation

* Logging for BLE peripheral:didUpdateValueFor errors.

* Fix for inconsistent swipe disconnect behavior

* periperal:didUpdateValueFor error handling

* Fix for BLEConnection continuation guarding

* BLEConnection actor deadlock on disconnect

* Heartbeat nonce

* Fix for swipe disconnect and task cancellation

* Fix for swipe actions not honoring .disabled()

* Tell BLETransport when BLEConnection is cancelled

* Update navigation logging

* Logging updates

* Bump version to 2.7.0

* Organize into folders and heartbeat stuff

* Minor improvements to manual TCP connection

* Auto-connect toggle

* Possible BLE bug, still waiting to see in logs

* Concurrency tweaks

* Concurrency improvements

* requestDeviceMetadata fix. fixes remote admin

* Minor typo fixes

* "All" button for log filters: category and level

* More robust continuation handling for BLE

* @FetchRequest based ChannelMessageList

* Update info.plist and device hardware file

* Move auto connect toggle to app settings and debug mode, tint properly with the accent color

* Add label to auto connect toggle

* Update log for node info received from ourselves over the mesh

* Remove unused scrollViewProxy

* Update Meshtastic/Views/Onboarding/DeviceOnboarding.swift

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

* Update target for connect view

* Properly Set datadog environment

* Comment out ble manager

* Adjust cyclomatic complexity thresholds in .swiftlint.yml

* Linting fixes, delete ble manager

* Make session replay debug only

---------

Co-authored-by: jake-b <jake-b@users.noreply.github.com>
Co-authored-by: jake <jake@jakes-Mac-mini.local>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-27 08:09:02 -07:00

1200 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
}
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) {
let ringtone = adminMessage.getRingtoneResponse
upsertRtttlConfigPacket(ringtone: 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.current = telemetryMessage.environmentMetrics.hasCurrent.then(telemetryMessage.environmentMetrics.current)
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.hasCh1Current.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) {
// 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 {
// 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.")
}
}