Meshtastic-Apple/Meshtastic/Persistence/UpdateCoreData.swift

1578 lines
73 KiB
Swift
Raw Normal View History

//
// UpdateCoreData.swift
// Meshtastic
//
// Copyright(c) Garth Vander Houwen 10/3/22.
import CoreData
import MeshtasticProtobufs
2024-06-03 02:17:55 -07:00
import OSLog
public func clearStaleNodes(nodeExpireDays: Int, context: NSManagedObjectContext) -> Bool {
var nodeExpireTime: TimeInterval {
return TimeInterval(-nodeExpireDays * 86400)
}
var nodePKIExpireTime: TimeInterval {
2025-06-19 16:06:02 -07:00
return TimeInterval((nodeExpireDays < 7 ? -7 : -nodeExpireDays) * 86400)
}
if nodeExpireDays == 0 {
// Purge Disabled
Logger.data.info("💾 [NodeInfoEntity] Skip clearing stale nodes")
return false
}
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "NodeInfoEntity")
fetchRequest.predicate = NSPredicate(format: "favorite == false AND ignored == false AND ((user.pkiEncrypted == NO AND lastHeard < %@) OR (user.pkiEncrypted == YES AND lastHeard < %@))",
NSDate(timeIntervalSinceNow: nodeExpireTime), NSDate(timeIntervalSinceNow: nodePKIExpireTime))
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDeleteRequest.resultType = .resultTypeCount
do {
Logger.data.info("💾 [NodeInfoEntity] Clearing nodes older than \(nodeExpireDays) days")
if let batchDeleteResult = try context.execute(batchDeleteRequest) as? NSBatchDeleteResult {
try context.save()
let deletedNodes = batchDeleteResult.result as? Int ?? 0
Logger.data.info("💾 [NodeInfoEntity] Cleared \(deletedNodes) stale nodes")
if deletedNodes > 0 {
return true
}
} else {
Logger.data.error("💥 [NodeInfoEntity] bad delete results")
}
} catch {
context.rollback()
Logger.data.error("💥 [NodeInfoEntity] Error deleting stale nodes")
}
return false
}
public func clearPax(destNum: Int64, context: NSManagedObjectContext) -> Bool {
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
let newPax = [PaxCounterLog]()
fetchedNode[0].pax? = NSOrderedSet(array: newPax)
do {
try context.save()
return true
} catch {
context.rollback()
return false
}
} catch {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NodeInfoEntity] fetch data error")
return false
}
}
public func clearPositions(destNum: Int64, context: NSManagedObjectContext) -> Bool {
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum))
2023-03-06 10:33:18 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
let newPostions = [PositionEntity]()
fetchedNode[0].positions? = NSOrderedSet(array: newPostions)
do {
try context.save()
return true
2023-03-06 10:33:18 -08:00
} catch {
context.rollback()
return false
}
} catch {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NodeInfoEntity] fetch data error")
return false
}
}
public func clearTelemetry(destNum: Int64, metricsType: Int32, context: NSManagedObjectContext) -> Bool {
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(destNum))
2023-03-06 10:33:18 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
let emptyTelemetry = [TelemetryEntity]()
fetchedNode[0].telemetries? = NSOrderedSet(array: emptyTelemetry)
do {
try context.save()
return true
2023-03-06 10:33:18 -08:00
} catch {
context.rollback()
return false
}
} catch {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NodeInfoEntity] fetch data error")
return false
}
}
2022-12-17 23:53:06 -08:00
public func deleteChannelMessages(channel: ChannelEntity, context: NSManagedObjectContext) {
2022-11-24 23:25:44 -08:00
do {
let objects = channel.allPrivateMessages
2023-02-01 09:19:45 -08:00
for object in objects {
context.delete(object)
}
2022-11-24 23:25:44 -08:00
try context.save()
} catch let error as NSError {
2025-03-31 22:06:00 -07:00
Logger.data.error("\(error.localizedDescription, privacy: .public)")
2022-11-24 23:25:44 -08:00
}
}
2022-11-25 00:22:51 -08:00
public func deleteUserMessages(user: UserEntity, context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2022-11-25 00:22:51 -08:00
do {
let objects = user.messageList
2023-02-01 09:19:45 -08:00
for object in objects {
context.delete(object)
}
2022-11-25 00:22:51 -08:00
try context.save()
} catch let error as NSError {
2025-03-31 22:06:00 -07:00
Logger.data.error("\(error.localizedDescription, privacy: .public)")
2022-11-25 00:22:51 -08:00
}
}
public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes: Bool) {
2023-03-06 10:33:18 -08:00
let persistenceController = PersistenceController.shared.container
for i in 0...persistenceController.managedObjectModel.entities.count-1 {
let entity = persistenceController.managedObjectModel.entities[i]
let query = NSFetchRequest<NSFetchRequestResult>(entityName: entity.name!)
var deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
let entityName = entity.name ?? "UNK"
if includeRoutes {
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
} else if !includeRoutes {
if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) {
deleteRequest = NSBatchDeleteRequest(fetchRequest: query)
}
}
2022-11-24 23:25:44 -08:00
do {
try context.executeAndMergeChanges(using: deleteRequest)
} catch {
2025-03-31 22:06:00 -07:00
Logger.data.error("\(error.localizedDescription, privacy: .public)")
2022-11-25 00:26:13 -08:00
}
}
}
2023-01-20 19:14:49 -08:00
2.7.6 Working Changes (#1479) * Bump version * Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> * Explicitly set unmessagable, seems unnessary * Add back missing mesh map features * Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Hide route lines filter from mesh map * Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> * Fix bad merge * Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays * upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) * R1 NEO * Neo * Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove bad if * Git rid of extra environment variable * Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) * NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) * Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) * Fix interval drop down formatter * Clean up channel qr code functionality. * perferredPeripheralId fix * Set opt in * Retry once 5 second timer. dont throw the error * Queue for peripherals * Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix * Don't favorite client base * Update device hardware * Prevent nil environment metrics * Bump datadog sdk * fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config * Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) * `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text * Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove discovery queue * revert problematic retry functionalliy * format file * Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update protobufs to 2.7.1 * Add long-turbo preset * Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add new device images * Remove print statement, get rid of cut and paste error --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jake-b <1012393+jake-b@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Co-authored-by: Radio <35003866+radiolee@users.noreply.github.com> Co-authored-by: Jason Houk <dubsectordevelopment@gmail.com>
2025-12-21 12:15:01 -08:00
func updateAnyPacketFrom (packet: MeshPacket, activeDeviceNum: Int64, context: NSManagedObjectContext) {
// Update NodeInfoEntity for any packet received. This mirrors the firmware's NodeDB::updateFrom, which sniffs ALL received packets and updates the radio's nodeDB with packet.from's:
// - last_heard (from rxTime)
// - snr
// - via_mqtt
// - hops_away
// However, unlike the firmware, this function will NOT create a new NodeInfoEntity if we don't have it already. We'll leave that to the existing code paths.
// We do NOT update fetchedNode[0].channel, because we may hear a node over multiple channels, and only some packet types should update what we consider the node's channel to be. (Example: primary private channel, secondary public channel. A text message on the secondary public channel should NOT change fetchedNode[0].channel.)
guard packet.from > 0 else { return }
guard packet.from != activeDeviceNum else { return } // Ignore if packet is from our own node
let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
do {
let fetchedNode = try context.fetch(fetchNodeInfoAppRequest)
if fetchedNode.count >= 1 {
fetchedNode[0].id = Int64(packet.from)
fetchedNode[0].num = Int64(packet.from)
if packet.rxTime > 0 {
fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard from rxTime=\(packet.rxTime)")
} else {
fetchedNode[0].lastHeard = Date()
Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) lastHeard to now (rxTime==0)")
}
fetchedNode[0].snr = packet.rxSnr
fetchedNode[0].rssi = packet.rxRssi
fetchedNode[0].viaMqtt = packet.viaMqtt
if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(packet.from.toHex(), privacy: .public) hopsAway=\(fetchedNode[0].hopsAway)")
}
do {
try context.save()
Logger.data.info("💾 [updateAnyPacketFrom] Updating node \(fetchedNode[0].num.toHex(), privacy: .public) snr=\(fetchedNode[0].snr), rssi=\(fetchedNode[0].rssi) from packet \(packet.id.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
Logger.data.error("💥 [updateAnyPacketFrom] Error Saving node \(fetchedNode[0].num.toHex(), privacy: .public) from packet \(packet.id.toHex(), privacy: .public) \(nsError, privacy: .public)")
}
}
} catch {
Logger.data.error("💥 [updateAnyPacketFrom] fetch data error")
}
}
func upsertNodeInfoPacket (packet: MeshPacket, favorite: Bool = false, context: NSManagedObjectContext) {
2023-03-14 12:44:10 -07:00
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
let logString = String.localizedStringWithFormat("[NodeInfo] received for: %@".localized, packet.from.toHex())
2025-03-31 22:06:00 -07:00
Logger.mesh.info("📟 \(logString, privacy: .public)")
2023-03-14 12:44:10 -07:00
guard packet.from > 0 else { return }
2023-03-14 12:44:10 -07:00
let fetchNodeInfoAppRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoAppRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
2023-03-14 12:44:10 -07:00
do {
2023-03-14 12:44:10 -07:00
let fetchedNode = try context.fetch(fetchNodeInfoAppRequest)
if fetchedNode.count == 0 {
// Not Found Insert
let newNode = NodeInfoEntity(context: context)
newNode.id = Int64(packet.from)
newNode.num = Int64(packet.from)
newNode.favorite = favorite
if packet.rxTime > 0 {
2024-07-09 20:47:26 -05:00
newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime)))
} else {
newNode.firstHeard = Date()
newNode.lastHeard = Date()
2024-07-09 20:47:26 -05:00
}
newNode.snr = packet.rxSnr
2023-05-04 22:20:22 -07:00
newNode.rssi = packet.rxRssi
newNode.viaMqtt = packet.viaMqtt
2024-07-17 00:20:35 -07:00
if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum {
newNode.channel = Int32(packet.channel)
}
2024-10-05 15:50:57 -07:00
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
if nodeInfoMessage.hasHopsAway {
newNode.hopsAway = Int32(nodeInfoMessage.hopsAway)
}
2024-03-26 09:59:07 -07:00
newNode.favorite = nodeInfoMessage.isFavorite
2023-04-02 15:00:15 -07:00
}
2024-10-05 15:50:57 -07:00
if let newUserMessage = try? User(serializedBytes: packet.decoded.payload) {
if newUserMessage.id.isEmpty {
if packet.from > Constants.minimumNodeNum {
2025-06-15 09:47:27 -07:00
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), 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: \(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)")
}
}
} else {
let newUser = UserEntity(context: context)
newUser.userId = newNode.num.toHex()
newUser.num = Int64(packet.from)
newUser.longName = newUserMessage.longName
newUser.shortName = newUserMessage.shortName
2023-12-20 10:24:01 -08:00
newUser.role = Int32(newUserMessage.role.rawValue)
newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased()
2024-08-06 14:53:32 -07:00
newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue)
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
2025-05-15 07:44:12 -07:00
if newUserMessage.hasIsUnmessagable {
newUser.unmessagable = newUserMessage.isUnmessagable
} else {
let roles = [2, 4, 5, 6, 7, 10, 11]
let containsRole = roles.contains(Int(newUser.role))
if containsRole {
2025-05-15 07:44:12 -07:00
newUser.unmessagable = true
} else {
newUser.unmessagable = false
}
}
2024-09-10 15:05:13 -07:00
if !newUserMessage.publicKey.isEmpty {
newUser.pkiEncrypted = true
newUser.publicKey = newUserMessage.publicKey
}
2024-08-06 14:53:32 -07:00
Task {
Api().loadDeviceHardwareData { (hw) in
let dh = hw.first(where: { $0.hwModel == newUser.hwModelId })
newUser.hwDisplayName = dh?.displayName
}
}
newNode.user = newUser
if UserDefaults.newNodeNotifications {
let manager = LocalNotificationManager()
manager.notifications = [
Notification(
id: (UUID().uuidString),
title: "New Node".localized,
2025-04-27 16:19:10 -07:00
subtitle: "\(newUser.longName ?? "Unknown".localized)",
content: "New Node has been discovered".localized,
target: "nodes",
path: "meshtastic:///nodes?nodenum=\(newUser.num)"
)
]
manager.schedule()
}
}
} else {
if packet.from > Constants.minimumNodeNum {
2025-06-15 09:47:27 -07:00
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), context: context)
if !packet.publicKey.isEmpty {
newNode.user?.pkiEncrypted = true
newNode.user?.publicKey = packet.publicKey
}
newNode.user = 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)")
2024-09-10 15:05:13 -07:00
}
}
}
2025-06-15 14:14:18 -07:00
// User is messed up and has failed to create at least once, if this fails bail out
if newNode.user == nil && packet.from > Constants.minimumNodeNum {
2025-06-15 09:47:27 -07:00
do {
let newUser = try createUser(num: Int64(packet.from), 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: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
2025-06-15 14:14:18 -07:00
context.rollback()
2025-06-15 09:47:27 -07:00
return
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
2025-06-15 14:14:18 -07:00
context.rollback()
2025-06-15 09:47:27 -07:00
return
}
}
let myInfoEntity = MyInfoEntity(context: context)
myInfoEntity.myNodeNum = Int64(packet.from)
myInfoEntity.rebootCount = 0
newNode.myInfo = myInfoEntity
do {
try context.save()
2024-09-10 15:05:13 -07:00
Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)")
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [MyInfoEntity] Saved a new myInfo for node number: \(packet.from.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)")
}
} else {
// Update an existing node
2024-07-17 00:20:35 -07:00
if packet.to == Constants.maximumNodeNum || packet.to == UserDefaults.preferredPeripheralNum {
fetchedNode[0].channel = Int32(packet.channel)
}
2023-03-14 12:44:10 -07:00
2024-10-05 15:50:57 -07:00
if let nodeInfoMessage = try? NodeInfo(serializedBytes: packet.decoded.payload) {
2024-03-23 09:01:44 -07:00
fetchedNode[0].hopsAway = Int32(nodeInfoMessage.hopsAway)
2024-03-26 09:59:07 -07:00
fetchedNode[0].favorite = nodeInfoMessage.isFavorite
if nodeInfoMessage.hasDeviceMetrics {
let telemetry = TelemetryEntity(context: context)
telemetry.batteryLevel = Int32(nodeInfoMessage.deviceMetrics.batteryLevel)
telemetry.voltage = nodeInfoMessage.deviceMetrics.voltage
telemetry.channelUtilization = nodeInfoMessage.deviceMetrics.channelUtilization
telemetry.airUtilTx = nodeInfoMessage.deviceMetrics.airUtilTx
var newTelemetries = [TelemetryEntity]()
newTelemetries.append(telemetry)
fetchedNode[0].telemetries? = NSOrderedSet(array: newTelemetries)
}
if nodeInfoMessage.hasUser {
fetchedNode[0].user?.userId = nodeInfoMessage.num.toHex()
fetchedNode[0].user?.num = Int64(nodeInfoMessage.num)
fetchedNode[0].user?.longName = nodeInfoMessage.user.longName
fetchedNode[0].user?.shortName = nodeInfoMessage.user.shortName
fetchedNode[0].user?.role = Int32(nodeInfoMessage.user.role.rawValue)
fetchedNode[0].user?.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
fetchedNode[0].user?.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue)
/// For nodes that have the optional isUnmessagable boolean use that, otherwise excluded roles that are unmessagable by default
2025-05-15 07:44:12 -07:00
if nodeInfoMessage.user.hasIsUnmessagable {
2025-06-10 20:41:08 -07:00
fetchedNode[0].user?.unmessagable = nodeInfoMessage.user.isUnmessagable
2025-05-15 07:44:12 -07:00
} 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
2025-05-15 07:44:12 -07:00
} else {
fetchedNode[0].user?.unmessagable = false
2025-05-15 07:44:12 -07:00
}
}
2024-09-10 15:05:13 -07:00
if !nodeInfoMessage.user.publicKey.isEmpty {
fetchedNode[0].user?.pkiEncrypted = true
fetchedNode[0].user?.publicKey = nodeInfoMessage.user.publicKey
2024-09-05 10:39:54 -07:00
}
2024-08-06 14:53:32 -07:00
Task {
Api().loadDeviceHardwareData { (hw) in
2024-08-06 15:26:16 -07:00
let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 })
fetchedNode[0].user?.hwDisplayName = dh?.displayName
2024-08-06 14:53:32 -07:00
}
}
}
2024-03-23 09:01:44 -07:00
} else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart {
fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit)
}
if fetchedNode[0].user == nil {
2025-06-15 09:47:27 -07:00
do {
let newUser = try createUser(num: Int64(truncatingIfNeeded: packet.from), 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: \(packet.from, privacy: .public) Error: \(message, privacy: .public)")
} catch {
Logger.data.error("Error Creating a new Core Data UserEntity on an existing node from node number: \(packet.from, privacy: .public) Error: \(error.localizedDescription, privacy: .public)")
}
}
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [NodeInfoEntity] Updated from Node Info App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NodeInfoEntity] Error Saving from NODEINFO_APP \(nsError, privacy: .public)")
}
}
} catch {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NodeInfoEntity] fetch data error for NODEINFO_APP")
}
}
func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
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
let logString = String.localizedStringWithFormat("[Position] received from node: %@".localized, String(packet.from))
2025-03-31 22:06:00 -07:00
Logger.mesh.info("📍 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodePositionRequest = NodeInfoEntity.fetchRequest()
fetchNodePositionRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
2023-03-06 10:33:18 -08:00
do {
2023-03-06 10:33:18 -08:00
2024-10-05 15:50:57 -07:00
if let positionMessage = try? Position(serializedBytes: packet.decoded.payload) {
2023-03-06 10:33:18 -08:00
2023-10-03 16:47:36 -07:00
/// Don't save empty position packets from null island or apple park
if (positionMessage.longitudeI != 0 && positionMessage.latitudeI != 0) && (positionMessage.latitudeI != 373346000 && positionMessage.longitudeI != -1220090000) {
let fetchedNode = try context.fetch(fetchNodePositionRequest)
if fetchedNode.count == 1 {
2023-03-06 10:33:18 -08:00
// Unset the current latest position for this node
let fetchCurrentLatestPositionsRequest = PositionEntity.fetchRequest()
fetchCurrentLatestPositionsRequest.predicate = NSPredicate(format: "nodePosition.num == %lld && latest = true", Int64(packet.from))
2023-03-14 12:44:10 -07:00
let fetchedPositions = try context.fetch(fetchCurrentLatestPositionsRequest)
if fetchedPositions.count > 0 {
for position in fetchedPositions {
position.latest = false
}
}
let position = PositionEntity(context: context)
position.latest = true
position.snr = packet.rxSnr
2023-05-04 22:20:22 -07:00
position.rssi = packet.rxRssi
position.seqNo = Int32(positionMessage.seqNumber)
position.latitudeI = positionMessage.latitudeI
position.longitudeI = positionMessage.longitudeI
position.altitude = positionMessage.altitude
position.satsInView = Int32(positionMessage.satsInView)
position.speed = Int32(positionMessage.groundSpeed)
2024-05-29 12:52:35 -07:00
let heading = Int32(positionMessage.groundTrack)
// Throw out bad haeadings from the device
if heading >= 0 && heading <= 360 {
position.heading = Int32(positionMessage.groundTrack)
}
position.precisionBits = Int32(positionMessage.precisionBits)
if positionMessage.timestamp != 0 {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.timestamp)))
} else {
position.time = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time)))
}
guard let mutablePositions = fetchedNode[0].positions?.mutableCopy() as? NSMutableOrderedSet else {
2023-03-06 15:30:10 -08:00
return
}
/// Don't save nearly the same position over and over. If the next position is less than 10 meters from the new position, delete the previous position and save the new one.
2024-03-23 09:01:44 -07:00
if mutablePositions.count > 0 && (position.precisionBits == 32 || position.precisionBits == 0) {
if let mostRecent = mutablePositions.lastObject as? PositionEntity, mostRecent.coordinate.distance(from: position.coordinate) < 9.0 {
mutablePositions.remove(mostRecent)
}
} else if mutablePositions.count > 0 {
/// Don't store any history for reduced accuracy positions, we will just show a circle
mutablePositions.removeAllObjects()
}
mutablePositions.add(position)
2.7.6 Working Changes (#1479) * Bump version * Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> * Explicitly set unmessagable, seems unnessary * Add back missing mesh map features * Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Hide route lines filter from mesh map * Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> * Fix bad merge * Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays * upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) * R1 NEO * Neo * Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove bad if * Git rid of extra environment variable * Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) * NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) * Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) * Fix interval drop down formatter * Clean up channel qr code functionality. * perferredPeripheralId fix * Set opt in * Retry once 5 second timer. dont throw the error * Queue for peripherals * Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix * Don't favorite client base * Update device hardware * Prevent nil environment metrics * Bump datadog sdk * fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config * Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) * `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text * Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove discovery queue * revert problematic retry functionalliy * format file * Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update protobufs to 2.7.1 * Add long-turbo preset * Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add new device images * Remove print statement, get rid of cut and paste error --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jake-b <1012393+jake-b@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Co-authored-by: Radio <35003866+radiolee@users.noreply.github.com> Co-authored-by: Jason Houk <dubsectordevelopment@gmail.com>
2025-12-21 12:15:01 -08:00
fetchedNode[0].channel = Int32(packet.channel)
fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet
2023-03-06 10:33:18 -08:00
do {
try context.save()
Logger.data.info("💾 [Position] Saved from Position App Packet For: \(fetchedNode[0].num.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 Error Saving NodeInfoEntity from POSITION_APP \(nsError, privacy: .public)")
}
}
} else {
Logger.data.error("💥 Empty POSITION_APP Packet: \((try? packet.jsonString()) ?? "JSON Decode Failure", privacy: .public)")
}
}
} catch {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 Error Deserializing POSITION_APP packet.")
}
}
2024-08-18 08:36:30 -07:00
func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 15:18:36 -07:00
let logString = String.localizedStringWithFormat("Bluetooth config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.mesh.info("📶 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-23 17:56:04 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-23 17:56:04 -08:00
// Found a node, save Device Config
if !fetchedNode.isEmpty {
if fetchedNode[0].bluetoothConfig == nil {
let newBluetoothConfig = BluetoothConfigEntity(context: context)
2023-01-31 22:08:03 -08:00
newBluetoothConfig.enabled = config.enabled
newBluetoothConfig.mode = Int32(config.mode.rawValue)
newBluetoothConfig.fixedPin = Int32(config.fixedPin)
2023-01-23 17:56:04 -08:00
fetchedNode[0].bluetoothConfig = newBluetoothConfig
} else {
2023-01-31 22:08:03 -08:00
fetchedNode[0].bluetoothConfig?.enabled = config.enabled
fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue)
fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin)
2023-01-23 17:56:04 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-23 17:56:04 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-01-23 17:56:04 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [BluetoothConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [BluetoothConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Bluetooth Config")
2023-01-23 17:56:04 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [BluetoothConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 15:18:36 -07:00
let logString = String.localizedStringWithFormat("Device config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.mesh.info("📟 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-23 17:56:04 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-23 17:56:04 -08:00
// Found a node, save Device Config
if !fetchedNode.isEmpty {
if fetchedNode[0].deviceConfig == nil {
let newDeviceConfig = DeviceConfigEntity(context: context)
2023-01-31 22:08:03 -08:00
newDeviceConfig.role = Int32(config.role.rawValue)
newDeviceConfig.buttonGpio = Int32(config.buttonGpio)
newDeviceConfig.buzzerGpio = Int32(config.buzzerGpio)
newDeviceConfig.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
2024-02-07 15:42:20 -08:00
newDeviceConfig.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
2023-04-09 23:04:11 -07:00
newDeviceConfig.doubleTapAsButtonPress = config.doubleTapAsButtonPress
2024-10-05 13:52:38 -07:00
newDeviceConfig.tripleClickAsAdHocPing = !config.disableTripleClick
2024-04-26 18:06:23 -07:00
newDeviceConfig.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
2023-05-13 20:50:20 -07:00
newDeviceConfig.isManaged = config.isManaged
2025-07-10 10:23:06 -07:00
newDeviceConfig.tzdef = config.tzdef
2023-01-23 17:56:04 -08:00
fetchedNode[0].deviceConfig = newDeviceConfig
} else {
2023-01-31 22:08:03 -08:00
fetchedNode[0].deviceConfig?.role = Int32(config.role.rawValue)
fetchedNode[0].deviceConfig?.buttonGpio = Int32(config.buttonGpio)
fetchedNode[0].deviceConfig?.buzzerGpio = Int32(config.buzzerGpio)
fetchedNode[0].deviceConfig?.rebroadcastMode = Int32(config.rebroadcastMode.rawValue)
2024-02-07 15:42:20 -08:00
fetchedNode[0].deviceConfig?.nodeInfoBroadcastSecs = Int32(truncating: config.nodeInfoBroadcastSecs as NSNumber)
2023-05-13 20:50:20 -07:00
fetchedNode[0].deviceConfig?.doubleTapAsButtonPress = config.doubleTapAsButtonPress
2024-10-05 13:52:38 -07:00
fetchedNode[0].deviceConfig?.tripleClickAsAdHocPing = !config.disableTripleClick
2024-04-26 18:06:23 -07:00
fetchedNode[0].deviceConfig?.ledHeartbeatEnabled = !config.ledHeartbeatDisabled
2023-05-13 20:50:20 -07:00
fetchedNode[0].deviceConfig?.isManaged = config.isManaged
2025-07-10 10:23:06 -07:00
fetchedNode[0].deviceConfig?.tzdef = config.tzdef
2023-01-23 17:56:04 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-23 17:56:04 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)")
2023-01-23 17:56:04 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DeviceConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DeviceConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 20:34:25 -07:00
let logString = String.localizedStringWithFormat("Display config received: %@".localized, nodeNum.toHex())
2025-03-31 22:06:00 -07:00
Logger.data.info("🖥️ \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-23 17:56:04 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-23 17:56:04 -08:00
// Found a node, save Device Config
if !fetchedNode.isEmpty {
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
if fetchedNode[0].displayConfig == nil {
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
let newDisplayConfig = DisplayConfigEntity(context: context)
newDisplayConfig.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs)
newDisplayConfig.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs)
2023-01-31 22:08:03 -08:00
newDisplayConfig.compassNorthTop = config.compassNorthTop
newDisplayConfig.flipScreen = config.flipScreen
newDisplayConfig.oledType = Int32(config.oled.rawValue)
newDisplayConfig.displayMode = Int32(config.displaymode.rawValue)
2023-11-26 12:54:45 -08:00
newDisplayConfig.units = Int32(config.units.rawValue)
newDisplayConfig.headingBold = config.headingBold
2025-06-15 20:04:21 -07:00
newDisplayConfig.use12HClock = config.use12HClock
2023-01-23 17:56:04 -08:00
fetchedNode[0].displayConfig = newDisplayConfig
} else {
fetchedNode[0].displayConfig?.screenOnSeconds = Int32(truncatingIfNeeded: config.screenOnSecs)
fetchedNode[0].displayConfig?.screenCarouselInterval = Int32(truncatingIfNeeded: config.autoScreenCarouselSecs)
2023-01-31 22:08:03 -08:00
fetchedNode[0].displayConfig?.compassNorthTop = config.compassNorthTop
fetchedNode[0].displayConfig?.flipScreen = config.flipScreen
fetchedNode[0].displayConfig?.oledType = Int32(config.oled.rawValue)
fetchedNode[0].displayConfig?.displayMode = Int32(config.displaymode.rawValue)
2023-11-26 12:54:45 -08:00
fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue)
fetchedNode[0].displayConfig?.headingBold = config.headingBold
2025-07-19 23:25:43 -07:00
fetchedNode[0].displayConfig?.use12HClock = config.use12HClock
2023-01-23 17:56:04 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-23 17:56:04 -08:00
do {
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [DisplayConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DisplayConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
} else {
2025-03-31 22:06:00 -07:00
Logger.data.error("💥 [DisplayConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Display Config")
2023-01-23 17:56:04 -08:00
}
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DisplayConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("LoRa config received: %@".localized, nodeNum.toHex())
2025-03-31 22:06:00 -07:00
Logger.data.info("📻 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-20 19:14:49 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", nodeNum)
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-20 19:14:49 -08:00
// Found a node, save LoRa Config
2023-01-23 17:56:04 -08:00
if fetchedNode.count > 0 {
2023-01-20 19:14:49 -08:00
if fetchedNode[0].loRaConfig == nil {
2023-01-23 17:56:04 -08:00
// No lora config for node, save a new lora config
2023-01-20 19:14:49 -08:00
let newLoRaConfig = LoRaConfigEntity(context: context)
2023-01-31 10:50:17 -08:00
newLoRaConfig.regionCode = Int32(config.region.rawValue)
newLoRaConfig.usePreset = config.usePreset
newLoRaConfig.modemPreset = Int32(config.modemPreset.rawValue)
newLoRaConfig.bandwidth = Int32(config.bandwidth)
newLoRaConfig.spreadFactor = Int32(config.spreadFactor)
newLoRaConfig.codingRate = Int32(config.codingRate)
newLoRaConfig.frequencyOffset = config.frequencyOffset
2023-02-06 19:06:15 -08:00
newLoRaConfig.overrideFrequency = config.overrideFrequency
newLoRaConfig.overrideDutyCycle = config.overrideDutyCycle
2023-01-31 10:50:17 -08:00
newLoRaConfig.hopLimit = Int32(config.hopLimit)
newLoRaConfig.txPower = Int32(config.txPower)
newLoRaConfig.txEnabled = config.txEnabled
newLoRaConfig.channelNum = Int32(config.channelNum)
newLoRaConfig.sx126xRxBoostedGain = config.sx126XRxBoostedGain
newLoRaConfig.ignoreMqtt = config.ignoreMqtt
2024-09-12 13:52:47 -07:00
newLoRaConfig.okToMqtt = config.configOkToMqtt
2023-01-20 19:14:49 -08:00
fetchedNode[0].loRaConfig = newLoRaConfig
} else {
2023-01-31 10:50:17 -08:00
fetchedNode[0].loRaConfig?.regionCode = Int32(config.region.rawValue)
fetchedNode[0].loRaConfig?.usePreset = config.usePreset
fetchedNode[0].loRaConfig?.modemPreset = Int32(config.modemPreset.rawValue)
fetchedNode[0].loRaConfig?.bandwidth = Int32(config.bandwidth)
fetchedNode[0].loRaConfig?.spreadFactor = Int32(config.spreadFactor)
fetchedNode[0].loRaConfig?.codingRate = Int32(config.codingRate)
fetchedNode[0].loRaConfig?.frequencyOffset = config.frequencyOffset
2023-02-06 19:06:15 -08:00
fetchedNode[0].loRaConfig?.overrideFrequency = config.overrideFrequency
fetchedNode[0].loRaConfig?.overrideDutyCycle = config.overrideDutyCycle
2023-01-31 10:50:17 -08:00
fetchedNode[0].loRaConfig?.hopLimit = Int32(config.hopLimit)
fetchedNode[0].loRaConfig?.txPower = Int32(config.txPower)
fetchedNode[0].loRaConfig?.txEnabled = config.txEnabled
fetchedNode[0].loRaConfig?.channelNum = Int32(config.channelNum)
fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain
fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt
2024-09-12 13:52:47 -07:00
fetchedNode[0].loRaConfig?.okToMqtt = config.configOkToMqtt
fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain
2023-01-20 19:14:49 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-20 19:14:49 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-01-20 19:14:49 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [LoRaConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-20 19:14:49 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [LoRaConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Lora Config")
2023-01-20 19:14:49 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [LoRaConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-20 19:14:49 -08:00
}
}
2023-01-23 17:56:04 -08:00
2024-08-18 08:36:30 -07:00
func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("Network config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🌐 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-23 17:56:04 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-23 17:56:04 -08:00
// Found a node, save WiFi Config
if !fetchedNode.isEmpty {
if fetchedNode[0].networkConfig == nil {
let newNetworkConfig = NetworkConfigEntity(context: context)
2023-01-31 22:20:16 -08:00
newNetworkConfig.wifiEnabled = config.wifiEnabled
newNetworkConfig.wifiSsid = config.wifiSsid
newNetworkConfig.wifiPsk = config.wifiPsk
newNetworkConfig.ethEnabled = config.ethEnabled
newNetworkConfig.enabledProtocols = Int32(config.enabledProtocols)
2023-01-23 17:56:04 -08:00
fetchedNode[0].networkConfig = newNetworkConfig
} else {
2023-01-31 22:20:16 -08:00
fetchedNode[0].networkConfig?.ethEnabled = config.ethEnabled
fetchedNode[0].networkConfig?.wifiEnabled = config.wifiEnabled
fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid
fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk
fetchedNode[0].networkConfig?.enabledProtocols = Int32(config.enabledProtocols)
2023-01-23 17:56:04 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-23 17:56:04 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)")
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NetworkConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NetworkConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Network Config")
2023-01-23 17:56:04 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [NetworkConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🗺️ \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-01-23 17:56:04 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-01-23 17:56:04 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-01-23 17:56:04 -08:00
// Found a node, save LoRa Config
if !fetchedNode.isEmpty {
if fetchedNode[0].positionConfig == nil {
let newPositionConfig = PositionConfigEntity(context: context)
2023-01-31 22:20:16 -08:00
newPositionConfig.smartPositionEnabled = config.positionBroadcastSmartEnabled
newPositionConfig.deviceGpsEnabled = config.gpsEnabled
2025-07-12 23:01:45 -07:00
newPositionConfig.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue)
newPositionConfig.rxGpio = Int32(truncatingIfNeeded: config.rxGpio)
newPositionConfig.txGpio = Int32(truncatingIfNeeded: config.txGpio)
newPositionConfig.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio)
2023-01-31 22:20:16 -08:00
newPositionConfig.fixedPosition = config.fixedPosition
newPositionConfig.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs)
2025-07-12 23:01:45 -07:00
newPositionConfig.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs)
newPositionConfig.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance)
newPositionConfig.positionFlags = Int32(truncatingIfNeeded: config.positionFlags)
2023-12-22 19:08:32 -08:00
newPositionConfig.gpsAttemptTime = 900
2025-07-12 23:01:45 -07:00
newPositionConfig.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval)
2023-01-23 17:56:04 -08:00
fetchedNode[0].positionConfig = newPositionConfig
} else {
2023-01-31 22:20:16 -08:00
fetchedNode[0].positionConfig?.smartPositionEnabled = config.positionBroadcastSmartEnabled
fetchedNode[0].positionConfig?.deviceGpsEnabled = config.gpsEnabled
2025-07-12 23:01:45 -07:00
fetchedNode[0].positionConfig?.gpsMode = Int32(truncatingIfNeeded: config.gpsMode.rawValue)
fetchedNode[0].positionConfig?.rxGpio = Int32(truncatingIfNeeded: config.rxGpio)
fetchedNode[0].positionConfig?.txGpio = Int32(truncatingIfNeeded: config.txGpio)
fetchedNode[0].positionConfig?.gpsEnGpio = Int32(truncatingIfNeeded: config.gpsEnGpio)
2023-01-31 22:20:16 -08:00
fetchedNode[0].positionConfig?.fixedPosition = config.fixedPosition
2024-02-07 15:42:20 -08:00
fetchedNode[0].positionConfig?.positionBroadcastSeconds = Int32(truncatingIfNeeded: config.positionBroadcastSecs)
2025-07-12 23:01:45 -07:00
fetchedNode[0].positionConfig?.broadcastSmartMinimumIntervalSecs = Int32(truncatingIfNeeded: config.broadcastSmartMinimumIntervalSecs)
fetchedNode[0].positionConfig?.broadcastSmartMinimumDistance = Int32(truncatingIfNeeded: config.broadcastSmartMinimumDistance)
2023-12-22 19:08:32 -08:00
fetchedNode[0].positionConfig?.gpsAttemptTime = 900
2025-07-12 23:01:45 -07:00
fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(truncatingIfNeeded: config.gpsUpdateInterval)
fetchedNode[0].positionConfig?.positionFlags = Int32(truncatingIfNeeded: config.positionFlags)
2023-01-23 17:56:04 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-01-23 17:56:04 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-01-23 17:56:04 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PositionConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PositionConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Position Config")
2023-01-23 17:56:04 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PositionConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-01-23 17:56:04 -08:00
}
}
2023-02-01 09:19:45 -08:00
2024-08-18 08:36:30 -07:00
func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("Power config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🗺️ \(logString, privacy: .public)")
2024-02-19 21:30:19 -07:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2024-02-19 21:30:19 -07:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2024-02-19 21:30:19 -07:00
// Found a node, save Power Config
if !fetchedNode.isEmpty {
if fetchedNode[0].powerConfig == nil {
let newPowerConfig = PowerConfigEntity(context: context)
newPowerConfig.adcMultiplierOverride = config.adcMultiplierOverride
newPowerConfig.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress)
newPowerConfig.isPowerSaving = config.isPowerSaving
newPowerConfig.lsSecs = Int32(truncatingIfNeeded: config.lsSecs)
newPowerConfig.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs)
newPowerConfig.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs)
newPowerConfig.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs)
2024-02-19 21:30:19 -07:00
fetchedNode[0].powerConfig = newPowerConfig
} else {
fetchedNode[0].powerConfig?.adcMultiplierOverride = config.adcMultiplierOverride
fetchedNode[0].powerConfig?.deviceBatteryInaAddress = Int32(config.deviceBatteryInaAddress)
fetchedNode[0].powerConfig?.isPowerSaving = config.isPowerSaving
fetchedNode[0].powerConfig?.lsSecs = Int32(truncatingIfNeeded: config.lsSecs)
fetchedNode[0].powerConfig?.minWakeSecs = Int32(truncatingIfNeeded: config.minWakeSecs)
fetchedNode[0].powerConfig?.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs)
fetchedNode[0].powerConfig?.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs)
2024-02-19 21:30:19 -07:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2024-02-19 21:30:19 -07:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)")
2024-02-19 21:30:19 -07:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PowerConfigEntity] Error Updating Core Data PowerConfigEntity: \(nsError, privacy: .public)")
2024-02-19 21:30:19 -07:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PowerConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Power Config")
2024-02-19 21:30:19 -07:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PowerConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2024-02-19 21:30:19 -07:00
}
}
2024-08-18 08:36:30 -07:00
func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2024-08-08 07:33:31 -07:00
let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🛡️ \(logString, privacy: .public)")
2024-08-08 07:33:31 -07:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
// Found a node, save Security Config
if !fetchedNode.isEmpty {
if fetchedNode[0].securityConfig == nil {
let newSecurityConfig = SecurityConfigEntity(context: context)
newSecurityConfig.publicKey = config.publicKey
newSecurityConfig.privateKey = config.privateKey
2024-09-12 11:25:51 -07:00
if config.adminKey.count > 0 {
newSecurityConfig.adminKey = config.adminKey[0]
}
2024-08-08 07:33:31 -07:00
newSecurityConfig.isManaged = config.isManaged
newSecurityConfig.serialEnabled = config.serialEnabled
2024-08-08 10:39:45 -07:00
newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled
newSecurityConfig.adminChannelEnabled = config.adminChannelEnabled
2024-08-11 00:08:43 -07:00
fetchedNode[0].securityConfig = newSecurityConfig
2024-08-08 07:33:31 -07:00
} else {
fetchedNode[0].securityConfig?.publicKey = config.publicKey
fetchedNode[0].securityConfig?.privateKey = config.privateKey
2024-09-12 11:25:51 -07:00
if config.adminKey.count > 0 {
fetchedNode[0].securityConfig?.adminKey = config.adminKey[0]
2024-11-14 08:11:49 -08:00
if config.adminKey.count > 1 {
2024-12-13 10:23:19 -08:00
fetchedNode[0].securityConfig?.adminKey2 = config.adminKey[1]
}
if config.adminKey.count > 2 {
2024-12-13 10:23:19 -08:00
fetchedNode[0].securityConfig?.adminKey3 = config.adminKey[2]
2024-11-14 08:11:49 -08:00
}
2024-09-12 11:25:51 -07:00
}
2024-08-08 07:33:31 -07:00
fetchedNode[0].securityConfig?.isManaged = config.isManaged
fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled
2024-08-08 10:39:45 -07:00
fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled
fetchedNode[0].securityConfig?.adminChannelEnabled = config.adminChannelEnabled
2024-08-08 07:33:31 -07:00
}
2024-08-19 12:30:26 -07:00
if sessionPasskey?.count != 0 {
2024-08-18 08:36:30 -07:00
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2024-08-08 07:33:31 -07:00
do {
try context.save()
2024-08-09 21:12:27 -07:00
Logger.data.info("💾 [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)")
2024-08-08 07:33:31 -07:00
} catch {
context.rollback()
let nsError = error as NSError
2024-08-09 21:12:27 -07:00
Logger.data.error("💥 [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2024-08-08 07:33:31 -07:00
}
} else {
2024-08-09 21:12:27 -07:00
Logger.data.error("💥 [SecurityConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Security Config")
2024-08-08 07:33:31 -07:00
}
} catch {
let nsError = error as NSError
2024-08-09 21:12:27 -07:00
Logger.data.error("💥 [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2024-08-08 07:33:31 -07:00
}
}
2024-08-18 08:36:30 -07:00
func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2025-05-08 14:42:11 -07:00
let logString = String.localizedStringWithFormat("Ambient Lighting module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🏮 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
// Found a node, save Ambient Lighting Config
if !fetchedNode.isEmpty {
if fetchedNode[0].cannedMessageConfig == nil {
let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context)
newAmbientLightingConfig.ledState = config.ledState
newAmbientLightingConfig.current = Int32(config.current)
newAmbientLightingConfig.red = Int32(config.red)
newAmbientLightingConfig.green = Int32(config.green)
newAmbientLightingConfig.blue = Int32(config.blue)
fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig
} else {
if fetchedNode[0].ambientLightingConfig == nil {
fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context)
}
fetchedNode[0].ambientLightingConfig?.ledState = config.ledState
fetchedNode[0].ambientLightingConfig?.current = Int32(config.current)
fetchedNode[0].ambientLightingConfig?.red = Int32(config.red)
fetchedNode[0].ambientLightingConfig?.green = Int32(config.green)
fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue)
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [AmbientLightingConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [AmbientLightingConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Ambient Lighting Module Config")
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [AmbientLightingConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
}
}
2024-08-18 08:36:30 -07:00
func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 20:34:25 -07:00
let logString = String.localizedStringWithFormat("Canned Message module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🥫 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-02-01 09:19:45 -08:00
// Found a node, save Canned Message Config
if !fetchedNode.isEmpty {
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
if fetchedNode[0].cannedMessageConfig == nil {
let newCannedMessageConfig = CannedMessageConfigEntity(context: context)
newCannedMessageConfig.enabled = config.enabled
newCannedMessageConfig.sendBell = config.sendBell
newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled
newCannedMessageConfig.updown1Enabled = config.updown1Enabled
newCannedMessageConfig.inputbrokerPinA = Int32(config.inputbrokerPinA)
newCannedMessageConfig.inputbrokerPinB = Int32(config.inputbrokerPinB)
newCannedMessageConfig.inputbrokerPinPress = Int32(config.inputbrokerPinPress)
newCannedMessageConfig.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue)
newCannedMessageConfig.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue)
newCannedMessageConfig.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue)
fetchedNode[0].cannedMessageConfig = newCannedMessageConfig
} else {
fetchedNode[0].cannedMessageConfig?.enabled = config.enabled
fetchedNode[0].cannedMessageConfig?.sendBell = config.sendBell
fetchedNode[0].cannedMessageConfig?.rotary1Enabled = config.rotary1Enabled
fetchedNode[0].cannedMessageConfig?.updown1Enabled = config.updown1Enabled
fetchedNode[0].cannedMessageConfig?.inputbrokerPinA = Int32(config.inputbrokerPinA)
fetchedNode[0].cannedMessageConfig?.inputbrokerPinB = Int32(config.inputbrokerPinB)
fetchedNode[0].cannedMessageConfig?.inputbrokerPinPress = Int32(config.inputbrokerPinPress)
fetchedNode[0].cannedMessageConfig?.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue)
fetchedNode[0].cannedMessageConfig?.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue)
fetchedNode[0].cannedMessageConfig?.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue)
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [CannedMessageConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [CannedMessageConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Canned Message Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [CannedMessageConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-08-26 20:45:15 -07:00
2025-05-08 20:34:25 -07:00
let logString = String.localizedStringWithFormat("Detection Sensor module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🕵️ \(logString, privacy: .public)")
2023-08-26 20:45:15 -07:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-08-26 20:45:15 -07:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-08-26 20:45:15 -07:00
// Found a node, save Detection Sensor Config
if !fetchedNode.isEmpty {
if fetchedNode[0].detectionSensorConfig == nil {
let newConfig = DetectionSensorConfigEntity(context: context)
newConfig.enabled = config.enabled
newConfig.sendBell = config.sendBell
newConfig.name = config.name
newConfig.monitorPin = Int32(config.monitorPin)
2024-10-11 18:18:45 -07:00
newConfig.triggerType = Int32(config.detectionTriggerType.rawValue)
2023-08-26 20:45:15 -07:00
newConfig.usePullup = config.usePullup
newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs)
newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs)
2023-08-26 20:45:15 -07:00
fetchedNode[0].detectionSensorConfig = newConfig
} else {
fetchedNode[0].detectionSensorConfig?.enabled = config.enabled
fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell
fetchedNode[0].detectionSensorConfig?.name = config.name
fetchedNode[0].detectionSensorConfig?.monitorPin = Int32(config.monitorPin)
fetchedNode[0].detectionSensorConfig?.usePullup = config.usePullup
2024-10-11 18:18:45 -07:00
fetchedNode[0].detectionSensorConfig?.triggerType = Int32(config.detectionTriggerType.rawValue)
fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs)
fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs)
2023-08-26 20:45:15 -07:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-08-26 20:45:15 -07:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-08-26 20:45:15 -07:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DetectionSensorConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)")
2023-08-26 20:45:15 -07:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DetectionSensorConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Detection Sensor Module Config")
2023-08-26 20:45:15 -07:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [DetectionSensorConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-08-26 20:45:15 -07:00
}
}
2024-08-18 08:36:30 -07:00
func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 20:34:25 -07:00
let logString = String.localizedStringWithFormat("External Notification module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("📣 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-02-01 09:19:45 -08:00
// Found a node, save External Notificaitone Config
if !fetchedNode.isEmpty {
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
if fetchedNode[0].externalNotificationConfig == nil {
let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context)
newExternalNotificationConfig.enabled = config.enabled
newExternalNotificationConfig.usePWM = config.usePwm
newExternalNotificationConfig.alertBell = config.alertBell
newExternalNotificationConfig.alertBellBuzzer = config.alertBellBuzzer
newExternalNotificationConfig.alertBellVibra = config.alertBellVibra
newExternalNotificationConfig.alertMessage = config.alertMessage
newExternalNotificationConfig.alertMessageBuzzer = config.alertMessageBuzzer
newExternalNotificationConfig.alertMessageVibra = config.alertMessageVibra
newExternalNotificationConfig.active = config.active
newExternalNotificationConfig.output = Int32(config.output)
newExternalNotificationConfig.outputBuzzer = Int32(config.outputBuzzer)
newExternalNotificationConfig.outputVibra = Int32(config.outputVibra)
newExternalNotificationConfig.outputMilliseconds = Int32(config.outputMs)
newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout)
2023-12-20 10:24:01 -08:00
newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer
2023-02-01 09:19:45 -08:00
fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig
} else {
fetchedNode[0].externalNotificationConfig?.enabled = config.enabled
fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm
fetchedNode[0].externalNotificationConfig?.alertBell = config.alertBell
fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.alertBellBuzzer
fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.alertBellVibra
fetchedNode[0].externalNotificationConfig?.alertMessage = config.alertMessage
fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.alertMessageBuzzer
fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.alertMessageVibra
fetchedNode[0].externalNotificationConfig?.active = config.active
fetchedNode[0].externalNotificationConfig?.output = Int32(config.output)
fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.outputBuzzer)
fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.outputVibra)
fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.outputMs)
fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout)
2023-12-20 10:24:01 -08:00
fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer
2023-02-01 09:19:45 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [ExternalNotificationConfigEntity] Error Updating Core Data : \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [ExternalNotificationConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save External Notification Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [ExternalNotificationConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2024-02-25 11:24:01 -08:00
2025-05-05 08:23:15 -07:00
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🧑‍🤝‍🧑 \(logString, privacy: .public)")
2024-02-25 11:24:01 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2024-02-25 11:24:01 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2024-02-25 11:24:01 -08:00
// Found a node, save PAX Counter Config
if !fetchedNode.isEmpty {
if fetchedNode[0].paxCounterConfig == nil {
let newPaxCounterConfig = PaxCounterConfigEntity(context: context)
newPaxCounterConfig.enabled = config.enabled
2024-06-02 09:45:56 -07:00
newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval)
2024-02-25 11:24:01 -08:00
fetchedNode[0].paxCounterConfig = newPaxCounterConfig
} else {
fetchedNode[0].paxCounterConfig?.enabled = config.enabled
2024-06-02 09:45:56 -07:00
fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval)
2024-02-25 11:24:01 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2024-02-25 11:24:01 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)")
2024-02-25 11:24:01 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PaxCounterConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2024-02-25 11:24:01 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PaxCounterConfigEntity] No Nodes found in local database matching node number \(nodeNum.toHex(), privacy: .public) unable to save PAX Counter Module Config")
2024-02-25 11:24:01 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [PaxCounterConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2024-02-25 11:24:01 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-25 14:30:18 -07:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("RTTTL Ringtone config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("⛰️ \(logString, privacy: .public)")
2023-03-25 14:30:18 -07:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-03-25 14:30:18 -07:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-03-25 14:30:18 -07:00
// Found a node, save RTTTL Config
if !fetchedNode.isEmpty {
if fetchedNode[0].rtttlConfig == nil {
let newRtttlConfig = RTTTLConfigEntity(context: context)
2023-03-25 22:14:39 -07:00
newRtttlConfig.ringtone = ringtone
2023-03-25 14:30:18 -07:00
fetchedNode[0].rtttlConfig = newRtttlConfig
} else {
2023-03-25 22:14:39 -07:00
fetchedNode[0].rtttlConfig?.ringtone = ringtone
2023-03-25 14:30:18 -07:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-03-25 14:30:18 -07:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-03-25 14:30:18 -07:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RtttlConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-03-25 14:30:18 -07:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RtttlConfigEntity] No nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save RTTTL Ringtone Config")
2023-03-25 14:30:18 -07:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RtttlConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-03-25 14:30:18 -07:00
}
}
2024-08-18 08:36:30 -07:00
func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("MQTT module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🌉 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-02-01 09:19:45 -08:00
// Found a node, save MQTT Config
if !fetchedNode.isEmpty {
if fetchedNode[0].mqttConfig == nil {
let newMQTTConfig = MQTTConfigEntity(context: context)
newMQTTConfig.enabled = config.enabled
2023-08-06 17:41:46 -07:00
newMQTTConfig.proxyToClientEnabled = config.proxyToClientEnabled
2023-02-01 09:19:45 -08:00
newMQTTConfig.address = config.address
newMQTTConfig.username = config.username
2023-02-01 09:19:45 -08:00
newMQTTConfig.password = config.password
2023-05-04 21:49:35 -07:00
newMQTTConfig.root = config.root
2023-02-01 09:19:45 -08:00
newMQTTConfig.encryptionEnabled = config.encryptionEnabled
newMQTTConfig.jsonEnabled = config.jsonEnabled
2023-05-04 21:49:35 -07:00
newMQTTConfig.tlsEnabled = config.tlsEnabled
2024-03-26 13:26:23 -07:00
newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled
2025-05-30 12:37:14 -07:00
newMQTTConfig.mapReportingShouldReportLocation = config.mapReportSettings.shouldReportLocation
2024-03-26 13:26:23 -07:00
newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision)
newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs)
2023-02-01 09:19:45 -08:00
fetchedNode[0].mqttConfig = newMQTTConfig
} else {
fetchedNode[0].mqttConfig?.enabled = config.enabled
2023-08-06 17:41:46 -07:00
fetchedNode[0].mqttConfig?.proxyToClientEnabled = config.proxyToClientEnabled
2023-02-01 09:19:45 -08:00
fetchedNode[0].mqttConfig?.address = config.address
fetchedNode[0].mqttConfig?.username = config.username
2023-02-01 09:19:45 -08:00
fetchedNode[0].mqttConfig?.password = config.password
2023-05-04 21:49:35 -07:00
fetchedNode[0].mqttConfig?.root = config.root
2023-02-01 09:19:45 -08:00
fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled
fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled
2023-05-04 21:49:35 -07:00
fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled
2024-03-26 13:26:23 -07:00
fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled
fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision)
fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs)
2023-02-01 09:19:45 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [MQTTConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [MQTTConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save MQTT Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [MQTTConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("Range Test module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("⛰️ \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-02-01 09:19:45 -08:00
// Found a node, save Device Config
if !fetchedNode.isEmpty {
if fetchedNode[0].rangeTestConfig == nil {
let newRangeTestConfig = RangeTestConfigEntity(context: context)
newRangeTestConfig.sender = Int32(config.sender)
newRangeTestConfig.enabled = config.enabled
newRangeTestConfig.save = config.save
fetchedNode[0].rangeTestConfig = newRangeTestConfig
} else {
fetchedNode[0].rangeTestConfig?.sender = Int32(config.sender)
fetchedNode[0].rangeTestConfig?.enabled = config.enabled
fetchedNode[0].rangeTestConfig?.save = config.save
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RangeTestConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RangeTestConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Range Test Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [RangeTestConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-03-06 10:33:18 -08:00
2025-05-08 22:50:44 -07:00
let logString = String.localizedStringWithFormat("Serial module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("🤖 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-02-01 09:19:45 -08:00
// Found a node, save Device Config
if !fetchedNode.isEmpty {
if fetchedNode[0].serialConfig == nil {
let newSerialConfig = SerialConfigEntity(context: context)
newSerialConfig.enabled = config.enabled
newSerialConfig.echo = config.echo
newSerialConfig.rxd = Int32(config.rxd)
newSerialConfig.txd = Int32(config.txd)
newSerialConfig.baudRate = Int32(config.baud.rawValue)
newSerialConfig.timeout = Int32(config.timeout)
newSerialConfig.mode = Int32(config.mode.rawValue)
fetchedNode[0].serialConfig = newSerialConfig
} else {
fetchedNode[0].serialConfig?.enabled = config.enabled
fetchedNode[0].serialConfig?.echo = config.echo
fetchedNode[0].serialConfig?.rxd = Int32(config.rxd)
fetchedNode[0].serialConfig?.txd = Int32(config.txd)
fetchedNode[0].serialConfig?.baudRate = Int32(config.baud.rawValue)
fetchedNode[0].serialConfig?.timeout = Int32(config.timeout)
fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue)
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
context.rollback()
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [SerialConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [SerialConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Serial Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [SerialConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2023-02-01 09:19:45 -08:00
2025-05-08 20:34:25 -07:00
let logString = String.localizedStringWithFormat("Store & Forward module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("📬 \(logString, privacy: .public)")
2023-03-06 10:33:18 -08:00
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
2023-02-01 09:19:45 -08:00
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
2023-03-06 10:33:18 -08:00
2023-02-01 09:19:45 -08:00
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-08-26 20:45:15 -07:00
// Found a node, save Store & Forward Sensor Config
2023-02-01 09:19:45 -08:00
if !fetchedNode.isEmpty {
2023-08-26 20:45:15 -07:00
if fetchedNode[0].storeForwardConfig == nil {
let newConfig = StoreForwardConfigEntity(context: context)
newConfig.enabled = config.enabled
newConfig.heartbeat = config.heartbeat
newConfig.records = Int32(config.records)
newConfig.historyReturnMax = Int32(config.historyReturnMax)
newConfig.historyReturnWindow = Int32(config.historyReturnWindow)
2025-03-31 22:37:51 -07:00
newConfig.isRouter = config.isServer
2023-08-26 20:45:15 -07:00
fetchedNode[0].storeForwardConfig = newConfig
2023-02-01 09:19:45 -08:00
} else {
2023-08-26 20:45:15 -07:00
fetchedNode[0].storeForwardConfig?.enabled = config.enabled
fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat
fetchedNode[0].storeForwardConfig?.records = Int32(config.records)
fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax)
fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow)
2023-02-01 09:19:45 -08:00
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
2023-02-01 09:19:45 -08:00
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)")
2023-02-01 09:19:45 -08:00
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [StoreForwardConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [StoreForwardConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Store & Forward Module Config")
2023-02-01 09:19:45 -08:00
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [StoreForwardConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)")
2023-02-01 09:19:45 -08:00
}
}
2024-08-18 08:36:30 -07:00
func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
2025-05-08 15:18:36 -07:00
let logString = String.localizedStringWithFormat("Telemetry module config received: %@".localized, String(nodeNum))
2025-03-31 22:06:00 -07:00
Logger.data.info("📈 \(logString, privacy: .public)")
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
do {
let fetchedNode = try context.fetch(fetchNodeInfoRequest)
2023-08-26 20:45:15 -07:00
// Found a node, save Telemetry Config
if !fetchedNode.isEmpty {
2023-08-26 20:45:15 -07:00
if fetchedNode[0].telemetryConfig == nil {
let newTelemetryConfig = TelemetryConfigEntity(context: context)
2025-07-12 23:14:08 -07:00
newTelemetryConfig.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval)
2.7.6 Working Changes (#1479) * Bump version * Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> * Explicitly set unmessagable, seems unnessary * Add back missing mesh map features * Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Hide route lines filter from mesh map * Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> * Fix bad merge * Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays * upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) * R1 NEO * Neo * Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove bad if * Git rid of extra environment variable * Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) * NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) * Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) * Fix interval drop down formatter * Clean up channel qr code functionality. * perferredPeripheralId fix * Set opt in * Retry once 5 second timer. dont throw the error * Queue for peripherals * Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix * Don't favorite client base * Update device hardware * Prevent nil environment metrics * Bump datadog sdk * fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config * Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) * `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text * Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove discovery queue * revert problematic retry functionalliy * format file * Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update protobufs to 2.7.1 * Add long-turbo preset * Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add new device images * Remove print statement, get rid of cut and paste error --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jake-b <1012393+jake-b@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Co-authored-by: Radio <35003866+radiolee@users.noreply.github.com> Co-authored-by: Jason Houk <dubsectordevelopment@gmail.com>
2025-12-21 12:15:01 -08:00
newTelemetryConfig.deviceTelemetryEnabled = config.deviceTelemetryEnabled
2025-07-12 23:14:08 -07:00
newTelemetryConfig.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval)
2023-08-26 20:45:15 -07:00
newTelemetryConfig.environmentMeasurementEnabled = config.environmentMeasurementEnabled
newTelemetryConfig.environmentScreenEnabled = config.environmentScreenEnabled
newTelemetryConfig.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit
2024-03-14 00:04:35 -07:00
newTelemetryConfig.powerMeasurementEnabled = config.powerMeasurementEnabled
2025-07-12 23:14:08 -07:00
newTelemetryConfig.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval)
2024-03-14 00:04:35 -07:00
newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled
2023-08-26 20:45:15 -07:00
fetchedNode[0].telemetryConfig = newTelemetryConfig
} else {
2025-07-12 23:14:08 -07:00
fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(truncatingIfNeeded: config.deviceUpdateInterval)
2.7.6 Working Changes (#1479) * Bump version * Message list performance fixes into 2.7.6 (#1475) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text * Use mDNS text records for node name * TCP IP and port on the connection screen * Hide app icon chooser on mac * Infinite loop hang bugfixes and performance improvements for both `UserMessageList` and `ChannelMessageList` (#1465) * 2.7.5 Working Changes (#1460) * Remove extra want config call when adding a contact * App badge and unnecessary notification fixes (#1455) * - Fix for app badge not going to zero if a message arrives while you have that chat open - Fix for push notifications popping up when a message is received while that chat is open * Fix for cancelling notifications, works now. And scroll to bottom of conversation upon new message * Fix: Channels help grammer fix (#1452) * remove outdated TCP not available on Apple devices (#1450) * Update initial onboarding view * remove toggle gating for mac * Update crash reporting opt out in real time * Update onboarding text --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * UserEntity: add mostRecentMessage and unreadMessages with early exit when lastMessage is nil, and fetch 1 row (not N) otherwise * UserList: replace 5 slow calls to user.messageList with new fast calls * NodeList: always put the connected node at the top of list (if it matches the node filters) * ChannelEntity: add faster mostRecentPrivateMessage and unreadMessages which fetch 1 row (not N) * ChannelList: replace 5 calls to channel.allPrivateMessage with new fast calls * Fix incorrect appState.unreadDirectMessages calculations * MyInfoEntity: also fix unreadMessages count here to be fast, and use it for appState.unreadChannelMessages * UserMessageList: use @FetchRequest to prevent the N^2 behavior that was happening in calls to allPrivateMessages * Refactor ChannelEntityExtension and MyInfoEntityExtension to be more similar to UserEntityExtension * Remove SwiftUI-infinite-loop-causing `.id(redrawTapbacksTrigger)` in ChannelMessageList and UserMessageList (duplicate row ids) * MyInfoEntityExtension: exclude emoji tapbacks (which never get marked as read anyway) from unread message count * Add SaveChannelLinkData so MessageText and MeshtasticApp can use .sheet(item: ...) and avoid infinite loop hang due to Binding rebuild * ChannelMessageList and UserMessageList: switch to stable messageId for ForEach SwiftUI row identity * ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification * ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear * ChannelMessageList and UserMessageList: block spurious markMessagesAsRead when this View is not active --------- Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> * message-list-performance: revert scrolling changes (#1472) * Revert e0f0b4a0f749d2e83946f2c1297e5c97c9fdf46e (ChannelMessageList and UserMessageList: scroll to bottom onFirstAppear) * Revert "ChannelMessageList and UserMessageList: debouncedScrollToBottom; keyboardWillShowNotification/keyboardDidShowNotification" This reverts commit ee1a7c44157eb6970ec2111fc1ac4d67a44a8238. --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> * Explicitly set unmessagable, seems unnessary * Add back missing mesh map features * Fix: "Retrieving nodes" significantly slower after reconnect extracted from #1424 (#1477) * Fix: "Retrieving nodes" significantly slower after reconnect (#1424) The node database retrieval was calling context.save() for every single NodeInfo packet received (250 saves for 250 nodes). This caused severe performance degradation on reconnect when CoreData had accumulated state. Root Cause: - nodeInfoPacket() called context.save() immediately for each node - With 250 nodes, this meant 250 individual CoreData save operations - On first connection, CoreData is fresh and fast - On reconnect, CoreData has accumulated change tracking, undo management, and memory pressure, making each save progressively slower - This resulted in 10+ second retrieval times vs 1-2 seconds initially Solution: - Added deferSave parameter to nodeInfoPacket() function - During database retrieval (.retrievingDatabase state), defer all saves - Perform a single batch save when database retrieval completes (when NONCE_ONLY_DB configCompleteID is received) - This reduces 250 saves to 1 save Performance Impact: - Eliminates N individual saves during node database sync - Reduces database retrieval time back to 1-2 seconds on reconnect - Matches first-connection performance consistently Fixes #1424 * Revert *MessageListUnified files --------- Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Hide route lines filter from mesh map * Mesh Map: fuzz imprecise locations so they're distinguishable and clickable at the highest zoom levels (#1478) Co-authored-by: Garth Vander Houwen <garth@meshtastic.com> * Fix bad merge * Update Meshtastic/Extensions/CoreData/UserEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/MyInfoEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Keep list of previous manual connections (#1484) * Keep list of previous manual connections * More descriptive manual connection rows * Merge fixes and new way to show IP on Connect view --------- Co-authored-by: Jake-B <jake-b@users.noreply.github.com> * Show who relayed messages (#1486) * Add identification for node that relayed text messages and add count for ammount of relayers of your message * Ack Relays * upsertPositionPacket: don't use future timestamps to set node's lastHeard (#1488) * R1 NEO * Neo * Update Meshtastic/Views/Settings/AppSettings.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove bad if * Git rid of extra environment variable * Update Meshtastic/Accessory/Transports/TCP/TCPTransport.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * MeshMap performance: quick wins (#1490) * MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame * MeshMapContent: factor out reducedPrecisionMapCircles into a separate function * MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas) * NodeMap performance improvements for high # positions history (#1480) * NodeMapContent: move Route Lines out of ForEach * NodeMapContent: move Convex Hull out of ForEach * NodeMapContent: Replace `position.nodePosition?` with `node` * NodeMapContent: drop unnecessary LazyVStack in showNodeHistory * NodeMapContent: hoist out nodeColorSwift * Move lineCoords, loraCoords calculations within showRouteLines, showConvexHull respectively * Hoist out repeated node.metadata?.positionFlags lookups / PositionFlags creation * NodeMapContent: remove unused @State * NodeMapSwiftUI: add NodeMapContentEquatableWrapper and NodeMapContentSignature to prevent frequent NodeMapContent recomputation and infinite render loops * NodeMapSwiftUI: disable animation during SwiftUI transactions * NodeMapContent: hoist nodeBorderColor and set allowsHitTesting(false) on history point views * NodeMapContent: prerenderHistoryPointCircle and prerenderHistoryPointArrow to avoid thousands of vector draw operations * NodeMapContent: Shared coordinate list for Route Lines and Convex Hull * NodeMapContent.prerenderHistoryPointArrow: add .frame(width: 16, height: 16) * Fix wantRangeTestPackets to correctly follow rangeTestConfig.enabled (#1489) * Fix interval drop down formatter * Clean up channel qr code functionality. * perferredPeripheralId fix * Set opt in * Retry once 5 second timer. dont throw the error * Queue for peripherals * Fix: hoplimit of dms would always fallback to hops away of the node even when configured hops was higher (#1495) * fix hops setting in dms * Fix hops for exchange position * Final fix * Don't favorite client base * Update device hardware * Prevent nil environment metrics * Bump datadog sdk * fix setting device telemetry enabled (#1515) * Update Muzi R1 Neo to actively supported * fix setting device telemetry enabled --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Don't subscribe to mqtt topic if downlink is not on (#1501) * Dont sub if no downlink * moved reload mqtt connect config * Preview enabled in connected devices (#1509) * Update Muzi R1 Neo to actively supported * Preview enabled in connected devices * Fixing indentation * Fixing indentation --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * UpdateCoreData.updateAnyPacketFrom: mirror firmware's lastHeard/snr/rssi/hopsAway update logic from NodeDB::updateFrom (#1492) * `CLIENT_BASE` add-favorite/role-change confirmation dialog (#1493) * FavoriteNodeButton: refactor task out * AccessoryManager.connectedDeviceRole helper * FavoriteNodeButton: show confirmation dialog when a CLIENT_BASE is trying to add a favorite * addContactFromURL: add comment referencing upcoming change in https://github.com/meshtastic/firmware/pull/8495 * DeviceConfig: role picker: show a warning when selecting CLIENT_BASE, similar to warning shown for ROUTER * Adjust device configuration Client Base warning text * Compass view (#1521) * Added compass view * Added Compass View * Node colors in compass * Update Muzi R1 Neo to actively supported * Update PositionPopover.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Meshtastic/Views/Helpers/CompassView.swift Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove discovery queue * revert problematic retry functionalliy * format file * Update & improve zh-Hans translation (#1523) * Update Muzi R1 Neo to actively supported * update & improve zh-Hans translation rt --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Update protobufs to 2.7.1 * Add long-turbo preset * Disable Range Test module when primary channel is public/unsecured (#1512) * Update Muzi R1 Neo to actively supported * Disable Range Test module when primary channel is public/unsecured Updated RangeTestConfig.swift to determine whether the primary channel (index 0) is operating without encryption or with a 1-byte minimal PSK. Disabled Range Test UI controls when on a public/default channel to prevent user interaction. Added safety enforcement in the save operation: Range Test enabled flag is automatically forced to false before sending updates to the device. Introduced a computed property isPrimaryChannelPublic following existing code patterns and security indicators (e.g., hexDescription PSK length). Matches the behavior implemented in the Android client for consistent policy across platforms. --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> * Add new device images * Remove print statement, get rid of cut and paste error --------- Co-authored-by: Gnome Adrift <646322+gnomeadrift@users.noreply.github.com> Co-authored-by: Zain Kergaye <62440012+ZainKergaye@users.noreply.github.com> Co-authored-by: NillRudd <102033730+NillRudd@users.noreply.github.com> Co-authored-by: Jake-B <jake-b@users.noreply.github.com> Co-authored-by: Mike Robbins <mrobbins@alum.mit.edu> Co-authored-by: Martin Bogomolni <martinbogo@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: jake-b <1012393+jake-b@users.noreply.github.com> Co-authored-by: Benjamin Faershtein <119711889+RCGV1@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Charles Pinesky <25388414+Vaidios@users.noreply.github.com> Co-authored-by: Radio <35003866+radiolee@users.noreply.github.com> Co-authored-by: Jason Houk <dubsectordevelopment@gmail.com>
2025-12-21 12:15:01 -08:00
fetchedNode[0].telemetryConfig?.deviceTelemetryEnabled = config.deviceTelemetryEnabled
2025-07-12 23:14:08 -07:00
fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(truncatingIfNeeded: config.environmentUpdateInterval)
2023-08-26 20:45:15 -07:00
fetchedNode[0].telemetryConfig?.environmentMeasurementEnabled = config.environmentMeasurementEnabled
fetchedNode[0].telemetryConfig?.environmentScreenEnabled = config.environmentScreenEnabled
fetchedNode[0].telemetryConfig?.environmentDisplayFahrenheit = config.environmentDisplayFahrenheit
2024-03-14 00:04:35 -07:00
fetchedNode[0].telemetryConfig?.powerMeasurementEnabled = config.powerMeasurementEnabled
2025-07-12 23:14:08 -07:00
fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(truncatingIfNeeded: config.powerUpdateInterval)
2024-03-14 00:04:35 -07:00
fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled
}
2024-08-18 08:36:30 -07:00
if sessionPasskey != nil {
fetchedNode[0].sessionPasskey = sessionPasskey
2024-08-18 08:53:59 -07:00
fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300)
2024-08-18 08:36:30 -07:00
}
do {
try context.save()
2024-06-23 18:25:22 -07:00
Logger.data.info("💾 [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)")
} catch {
context.rollback()
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [TelemetryConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)")
}
} else {
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [TelemetryConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Telemetry Module Config")
}
} catch {
let nsError = error as NSError
2024-06-23 18:25:22 -07:00
Logger.data.error("💥 [TelemetryConfigEntity] Fetching node for core data TelemetryConfigEntity failed: \(nsError, privacy: .public)")
}
}