diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index aa5a8a3f..a96e3707 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -1583,7 +1583,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.8; + MARKETING_VERSION = 2.3.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1617,7 +1617,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.8; + MARKETING_VERSION = 2.3.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1690,7 +1690,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.8; + MARKETING_VERSION = 2.3.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1723,7 +1723,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.8; + MARKETING_VERSION = 2.3.9; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 680b88bc..bac4f840 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -14,7 +14,7 @@ extension PositionEntity { static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() - request.fetchLimit = 100 + request.fetchLimit = 1000 request.returnsObjectsAsFaults = false request.includesSubentities = true request.returnsDistinctResults = true diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index ecf4e5e2..e4887c78 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -6,6 +6,7 @@ // import Foundation +import CoreData extension UserEntity { @@ -27,3 +28,16 @@ extension UserEntity { return unreadMessages.count } } + + +public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity { + let newUser = UserEntity(context: context) + newUser.num = Int64(num) + let userId = String(format:"%2X", num) + newUser.userId = "!\(userId)" + let last4 = String(userId.suffix(4)) + newUser.longName = "Meshtastic \(last4)" + newUser.shortName = last4 + newUser.hwModel = "UNSET" + return newUser +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index dd78784e..729a6fa0 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -20,7 +20,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var context: NSManagedObjectContext? static let shared = BLEManager() - //var userSettings: UserSettings? private var centralManager: CBCentralManager! @Published var peripherals: [Peripheral] = [] @Published var connectedPeripheral: Peripheral! @@ -40,7 +39,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var timeoutTimer: Timer? var timeoutTimerCount = 0 var positionTimer: Timer? - var lastPosition: CLLocationCoordinate2D? static let emptyNodeNum: UInt32 = 4294967295 let mqttManager = MqttClientProxyManager.shared var wantRangeTestPackets = false @@ -1002,52 +1000,45 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func getPositionFromPhoneGPS(destNum: Int64) -> Position? { var positionPacket = Position() - do { - if #available(iOS 17.0, macOS 14.0, *) { + if #available(iOS 17.0, macOS 14.0, *) { - if let lastLocation = LocationsHandler.shared.locationsArray.last { - - positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) - positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) - let timestamp = lastLocation.timestamp - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(lastLocation.altitude) - positionPacket.satsInView = UInt32(LocationsHandler.satsInView) - - let currentSpeed = lastLocation.speed - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) - } - let currentHeading = lastLocation.course - if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) - } - } - - } else { - if destNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 { - return nil - } - - positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) - positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) - let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() - positionPacket.time = UInt32(timestamp.timeIntervalSince1970) - positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) - positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) - positionPacket.satsInView = UInt32(LocationHelper.satsInView) - let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 - if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { - positionPacket.groundSpeed = UInt32(currentSpeed * 3.6) - } - let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 - if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) { - positionPacket.groundTrack = UInt32(currentHeading) - } + guard let lastLocation = LocationsHandler.shared.locationsArray.last else { + return nil + } + positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) + positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) + let timestamp = lastLocation.timestamp + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(lastLocation.altitude) + positionPacket.satsInView = UInt32(LocationsHandler.satsInView) + + let currentSpeed = lastLocation.speed + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed) + } + let currentHeading = lastLocation.course + if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) + } + + } else { + + positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7) + positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7) + let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date() + positionPacket.time = UInt32(timestamp.timeIntervalSince1970) + positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970) + positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0) + positionPacket.satsInView = UInt32(LocationHelper.satsInView) + let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0 + if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) { + positionPacket.groundSpeed = UInt32(currentSpeed) + } + let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0 + if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) { + positionPacket.groundTrack = UInt32(currentHeading) } - } catch { - return nil } return positionPacket } @@ -1328,33 +1319,43 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if isConnected { var i: Int32 = 0 + var myInfo: MyInfoEntity // Before we get started delete the existing channels from the myNodeInfo if !addChannels { tryClearExistingChannels() - } else { - // We are trying to add a channel so lets get the last index - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) - do { - let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] - if fetchedMyInfo.count == 1 { - if addChannels { - i = Int32(fetchedMyInfo[0].channels?.count ?? -1) - // Bail out if the index is negative or bigger than our max of 8 - if i < 0 || i > 8 { - return false - } - } - } - } catch { - print("Failed to find a node MyInfo to save these channels to") - } } + let decodedString = base64UrlString.base64urlToBase64() if let decodedData = Data(base64Encoded: decodedString) { do { let channelSet: ChannelSet = try ChannelSet(serializedData: decodedData) for cs in channelSet.settings { + if addChannels { + // We are trying to add a channel so lets get the last index + let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") + fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(connectedPeripheral.num)) + do { + let fetchedMyInfo = try context?.fetch(fetchMyInfoRequest) as? [MyInfoEntity] ?? [] + if fetchedMyInfo.count == 1 { + i = Int32(fetchedMyInfo[0].channels?.count ?? -1) + myInfo = fetchedMyInfo[0] + // Bail out if the index is negative or bigger than our max of 8 + if i < 0 || i > 8 { + return false + } + // Bail out if there are no channels or if the same channel name already exists + guard let mutableChannels = myInfo.channels!.mutableCopy() as? NSMutableOrderedSet else { + return false + } + if mutableChannels.first(where: {($0 as AnyObject).name == cs.name }) is ChannelEntity { + return false + } + } + } catch { + print("Failed to find a node MyInfo to save these channels to") + } + } + var chan = Channel() if i == 0 { chan.role = Channel.Role.primary @@ -1364,6 +1365,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate chan.settings = cs chan.index = i i += 1 + var adminPacket = AdminMessage() adminPacket.setChannel = chan var meshPacket: MeshPacket = MeshPacket() @@ -2623,6 +2625,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate newMessage.fromUser = fromUser newMessage.toUser = toUser + do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f61d1994..78121fb0 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -294,16 +294,8 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) newNode.user = newUser - } else { - let newUser = UserEntity(context: context) - newUser.num = Int64(nodeInfo.num) - newUser.numString = String(nodeInfo.num) - let userId = String(format:"%2X", nodeInfo.num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" + } else if nodeInfo.num > Int16.max { + let newUser = createUser(num: Int64(nodeInfo.num), context: context) newNode.user = newUser } @@ -315,7 +307,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje position.longitudeI = nodeInfo.position.longitudeI position.altitude = nodeInfo.position.altitude position.satsInView = Int32(nodeInfo.position.satsInView) - position.speed = Int32(nodeInfo.position.groundSpeed * UInt32(3.6)) + position.speed = Int32(nodeInfo.position.groundSpeed) position.heading = Int32(nodeInfo.position.groundTrack) position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) var newPostions = [PositionEntity]() @@ -369,15 +361,9 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() } else { - if (fetchedNode[0].user == nil) { - let newUser = UserEntity(context: context) - newUser.num = Int64(nodeInfo.num) - let userId = String(format:"%2X", nodeInfo.num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" + if (fetchedNode[0].user == nil && nodeInfo.num > Int16.max) { + + let newUser = createUser(num: Int64(nodeInfo.num), context: context) fetchedNode[0].user = newUser } } @@ -738,7 +724,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", target: "nodes", - path: "meshtastic://nodes/\(telemetry.nodeTelemetry?.num ?? 0)/devicetelemetrylog" + path: "meshtastic://nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)" ) ] manager.schedule() @@ -871,8 +857,8 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, - target: "message", - path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)" + target: "messages", + path: "meshtastic://messages?userNum=\(newMessage.fromUser?.num ?? 0)&messageId=\(newMessage.messageId)" ) ] manager.schedule() @@ -904,8 +890,8 @@ func textMessageAppPacket(packet: MeshPacket, wantRangeTestPackets: Bool, connec title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText!, - target: "message", - path: "meshtastic://messages/channel/\(newMessage.messageId)") + target: "messages", + path: "meshtastic://messages?channel=\(newMessage.channel)&messageId=\(newMessage.messageId)") ] manager.schedule() print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") @@ -972,9 +958,10 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", target: "map", - path: "meshtastic://open-waypoint?id=\(waypoint.id)" + path: "meshtastic://map?waypontid=\(waypoint.id)" ) ] + print("meshtastic://map?waypontid=\(waypoint.id)") manager.schedule() } catch { context.rollback() diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index a2fde04b..2e319241 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -35,7 +35,7 @@ CFBundleTypeRole - Viewer + Editor CFBundleURLIconFile alpha CFBundleURLName diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents index f7144b48..b9eddf6d 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 36.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index e9de24bc..2a8e37d1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -6,9 +6,10 @@ import CoreData import TipKit #endif +@available(iOS 17.0, *) @main struct MeshtasticAppleApp: App { - + @UIApplicationDelegateAdaptor(MeshtasticAppDelegate.self) var appDelegate let persistenceController = PersistenceController.shared @ObservedObject private var bleManager: BLEManager = BLEManager.shared @@ -28,7 +29,7 @@ struct MeshtasticAppleApp: App { .environmentObject(bleManager) .sheet(isPresented: $saveChannels) { SaveChannelQRCode(channelSetLink: channelSettings ?? "Empty Channel URL", addChannels: addChannels, bleManager: bleManager) - .presentationDetents([.medium, .large]) + .presentationDetents([.large]) .presentationDragIndicator(.visible) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in @@ -62,6 +63,16 @@ struct MeshtasticAppleApp: App { } self.saveChannels = true print("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") + } else if url.absoluteString.lowercased().contains("meshtastic://") { + appState.navigationPath = url.absoluteString + let path = appState.navigationPath ?? "" + if path.starts(with: "meshtastic://map") { + AppState.shared.tabSelection = Tab.map + } else if path.starts(with: "meshtastic://nodes") { + AppState.shared.tabSelection = Tab.nodes + } + + } else { saveChannels = false print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")") @@ -157,6 +168,6 @@ class AppState: ObservableObject { @Published var unreadDirectMessages: Int = 0 @Published var unreadChannelMessages: Int = 0 @Published var firmwareVersion: String = "0.0.0" - @Published var connectedNode: NodeInfoEntity? + //@Published var connectedNode: NodeInfoEntity? @Published var navigationPath: String? } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 94a6df6c..c44a7f7d 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -7,11 +7,10 @@ import SwiftUI -class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { +class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { print("🚀 Meshtstic Apple App launched!") // Default User Default Values - UserDefaults.standard.register(defaults: ["blockRangeTest" : true]) UserDefaults.standard.register(defaults: ["meshMapRecentering" : true]) UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true]) UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true]) @@ -27,19 +26,22 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification } return true } + // Lets us show the notification in the app in the foreground func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.list, .banner, .sound]) } - // This method is called when user clicked on the notification + // This method is called when a user clicks on the notification func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo let targetValue = userInfo["target"] as? String - AppState.shared.navigationPath = userInfo["path"] as? String - print("\(AppState.shared.navigationPath ?? "EMPTY")") + let deepLink = userInfo["path"] as? String + + AppState.shared.navigationPath = deepLink if targetValue == "map" { AppState.shared.tabSelection = Tab.map - } else if targetValue == "message" { + } else if targetValue == "messages" { AppState.shared.tabSelection = Tab.messages - } else if targetValue == "node" { + } else if targetValue == "nodes" { AppState.shared.tabSelection = Tab.nodes } completionHandler() diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index e9114b7d..46e27062 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -120,7 +120,6 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes deleteRequest = NSBatchDeleteRequest(fetchRequest: query) } else if !includeRoutes { if !(entityName.contains("RouteEntity") || entityName.contains("LocationEntity")) { - print(entity.name?.lowercased()) deleteRequest = NSBatchDeleteRequest(fetchRequest: query) } } @@ -161,22 +160,15 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if let nodeInfoMessage = try? NodeInfo(serializedData: packet.decoded.payload) { newNode.hopsAway = Int32(nodeInfoMessage.hopsAway) newNode.favorite = nodeInfoMessage.isFavorite - } else if packet.hopStart != 0 && packet.hopLimit <= packet.hopStart { - newNode.hopsAway = Int32(packet.hopStart - packet.hopLimit) } + if let newUserMessage = try? User(serializedData: packet.decoded.payload) { - if newUserMessage.id.isEmpty { - let newUser = UserEntity(context: context) - newUser.num = Int64(packet.from) - let userId = String(format:"%2X", packet.from) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - newNode.user = newUser - + if newUserMessage.id.isEmpty { + if packet.from > Int16.max { + let newUser = createUser(num: Int64(packet.from), context: context) + newNode.user = newUser + } } else { let newUser = UserEntity(context: context) @@ -197,27 +189,22 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) title: "New Node", subtitle: "\(newUser.longName ?? "unknown".localized)", content: "New Node has been discovered", - target: "nodeInfo", - path: "meshtastic://nodeInfo" + target: "nodes", + path: "meshtastic://nodes?nodenum=\(newUser.num)" ) ] manager.schedule() } } } else { - let newUser = UserEntity(context: context) - newUser.num = Int64(packet.from) - let userId = String(format:"%2X", packet.from) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - newNode.user = newUser + if packet.from > Int16.max { + let newUser = createUser(num: Int64(packet.from), context: context) + fetchedNode[0].user = newUser + } } - if newNode.user == nil { - print("Nil User on nodeinfo") + if newNode.user == nil && packet.from > Int16.max { + newNode.user = createUser(num: Int64(packet.from), context: context) } let myInfoEntity = MyInfoEntity(context: context) @@ -274,14 +261,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if (fetchedNode[0].user == nil) { - let newUser = UserEntity(context: context) - newUser.num = Int64(packet.from) - let userId = String(format:"%2X", packet.from) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" + let newUser = createUser(num: Int64(packet.from), context: context) fetchedNode[0].user! = newUser } do { @@ -338,7 +318,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) position.longitudeI = positionMessage.longitudeI position.altitude = positionMessage.altitude position.satsInView = Int32(positionMessage.satsInView) - position.speed = Int32(positionMessage.groundSpeed * UInt32(3.6)) + position.speed = Int32(positionMessage.groundSpeed) position.heading = Int32(positionMessage.groundTrack) position.precisionBits = Int32(positionMessage.precisionBits) if positionMessage.timestamp != 0 { diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index a3dc0f6d..7d33c743 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -278,6 +278,16 @@ enum HardwareModel: SwiftProtobuf.Enum { /// Adafruit NRF52840 feather express with SX1262, SSD1306 OLED and NEO6M GPS case twcMeshV4 // = 62 + /// + /// NRF52_PROMICRO_DIY + /// Promicro NRF52840 with SX1262/LLCC68, SSD1306 OLED and NEO6M GPS + case nrf52PromicroDiy // = 63 + + /// + /// RadioMaster 900 Bandit Nano, https://www.radiomasterrc.com/products/bandit-nano-expresslrs-rf-module + /// ESP32-D0WDQ6 With SX1276/SKY66122, SSD1306 OLED and No GPS + case radiomaster900BanditNano // = 64 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. @@ -350,6 +360,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case 60: self = .tdLorac case 61: self = .cdebyteEoraS3 case 62: self = .twcMeshV4 + case 63: self = .nrf52PromicroDiy + case 64: self = .radiomaster900BanditNano case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -416,6 +428,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case .tdLorac: return 60 case .cdebyteEoraS3: return 61 case .twcMeshV4: return 62 + case .nrf52PromicroDiy: return 63 + case .radiomaster900BanditNano: return 64 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -487,6 +501,8 @@ extension HardwareModel: CaseIterable { .tdLorac, .cdebyteEoraS3, .twcMeshV4, + .nrf52PromicroDiy, + .radiomaster900BanditNano, .privateHw, ] } @@ -2787,6 +2803,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 60: .same(proto: "TD_LORAC"), 61: .same(proto: "CDEBYTE_EORA_S3"), 62: .same(proto: "TWC_MESH_V4"), + 63: .same(proto: "NRF52_PROMICRO_DIY"), + 64: .same(proto: "RADIOMASTER_900_BANDIT_NANO"), 255: .same(proto: "PRIVATE_HW"), ] } diff --git a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift index f6c28745..465e4e1f 100644 --- a/Meshtastic/Protobufs/meshtastic/module_config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/module_config.pb.swift @@ -618,6 +618,14 @@ struct ModuleConfig { var paxcounterUpdateInterval: UInt32 = 0 + /// + /// WiFi RSSI threshold. Defaults to -80 + var wifiThreshold: Int32 = 0 + + /// + /// BLE RSSI threshold. Defaults to -80 + var bleThreshold: Int32 = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -1921,6 +1929,8 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M static let _protobuf_nameMap: SwiftProtobuf._NameMap = [ 1: .same(proto: "enabled"), 2: .standard(proto: "paxcounter_update_interval"), + 3: .standard(proto: "wifi_threshold"), + 4: .standard(proto: "ble_threshold"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -1931,6 +1941,8 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M switch fieldNumber { case 1: try { try decoder.decodeSingularBoolField(value: &self.enabled) }() case 2: try { try decoder.decodeSingularUInt32Field(value: &self.paxcounterUpdateInterval) }() + case 3: try { try decoder.decodeSingularInt32Field(value: &self.wifiThreshold) }() + case 4: try { try decoder.decodeSingularInt32Field(value: &self.bleThreshold) }() default: break } } @@ -1943,12 +1955,20 @@ extension ModuleConfig.PaxcounterConfig: SwiftProtobuf.Message, SwiftProtobuf._M if self.paxcounterUpdateInterval != 0 { try visitor.visitSingularUInt32Field(value: self.paxcounterUpdateInterval, fieldNumber: 2) } + if self.wifiThreshold != 0 { + try visitor.visitSingularInt32Field(value: self.wifiThreshold, fieldNumber: 3) + } + if self.bleThreshold != 0 { + try visitor.visitSingularInt32Field(value: self.bleThreshold, fieldNumber: 4) + } try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ModuleConfig.PaxcounterConfig, rhs: ModuleConfig.PaxcounterConfig) -> Bool { if lhs.enabled != rhs.enabled {return false} if lhs.paxcounterUpdateInterval != rhs.paxcounterUpdateInterval {return false} + if lhs.wifiThreshold != rhs.wifiThreshold {return false} + if lhs.bleThreshold != rhs.bleThreshold {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift index 7f9d3396..c778bf49 100644 --- a/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/telemetry.pb.swift @@ -92,6 +92,30 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { /// /// RCWL-9620 Doppler Radar Distance Sensor, used for water level detection case rcwl9620 // = 16 + + /// + /// Sensirion High accuracy temperature and humidity + case sht4X // = 17 + + /// + /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. + case veml7700 // = 18 + + /// + /// MLX90632 non-contact IR temperature sensor. + case mlx90632 // = 19 + + /// + /// TI OPT3001 Ambient Light Sensor + case opt3001 // = 20 + + /// + /// Lite On LTR-390UV-01 UV Light Sensor + case ltr390Uv // = 21 + + /// + /// AMS TSL25911FN RGB Light Sensor + case tsl25911Fn // = 22 case UNRECOGNIZED(Int) init() { @@ -117,6 +141,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case 14: self = .ina3221 case 15: self = .bmp085 case 16: self = .rcwl9620 + case 17: self = .sht4X + case 18: self = .veml7700 + case 19: self = .mlx90632 + case 20: self = .opt3001 + case 21: self = .ltr390Uv + case 22: self = .tsl25911Fn default: self = .UNRECOGNIZED(rawValue) } } @@ -140,6 +170,12 @@ enum TelemetrySensorType: SwiftProtobuf.Enum { case .ina3221: return 14 case .bmp085: return 15 case .rcwl9620: return 16 + case .sht4X: return 17 + case .veml7700: return 18 + case .mlx90632: return 19 + case .opt3001: return 20 + case .ltr390Uv: return 21 + case .tsl25911Fn: return 22 case .UNRECOGNIZED(let i): return i } } @@ -168,6 +204,12 @@ extension TelemetrySensorType: CaseIterable { .ina3221, .bmp085, .rcwl9620, + .sht4X, + .veml7700, + .mlx90632, + .opt3001, + .ltr390Uv, + .tsl25911Fn, ] } @@ -245,6 +287,14 @@ struct EnvironmentMetrics { /// RCWL9620 Doppler Radar Distance Sensor, used for water level detection. Float value in mm. var distance: Float = 0 + /// + /// VEML7700 high accuracy ambient light(Lux) digital 16-bit resolution sensor. + var lux: Float = 0 + + /// + /// VEML7700 high accuracy white light(irradiance) not calibrated digital 16-bit resolution sensor. + var whiteLux: Float = 0 + var unknownFields = SwiftProtobuf.UnknownStorage() init() {} @@ -479,6 +529,12 @@ extension TelemetrySensorType: SwiftProtobuf._ProtoNameProviding { 14: .same(proto: "INA3221"), 15: .same(proto: "BMP085"), 16: .same(proto: "RCWL9620"), + 17: .same(proto: "SHT4X"), + 18: .same(proto: "VEML7700"), + 19: .same(proto: "MLX90632"), + 20: .same(proto: "OPT3001"), + 21: .same(proto: "LTR390UV"), + 22: .same(proto: "TSL25911FN"), ] } @@ -549,6 +605,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple 6: .same(proto: "current"), 7: .same(proto: "iaq"), 8: .same(proto: "distance"), + 9: .same(proto: "lux"), + 10: .standard(proto: "white_lux"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -565,6 +623,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple case 6: try { try decoder.decodeSingularFloatField(value: &self.current) }() case 7: try { try decoder.decodeSingularUInt32Field(value: &self.iaq) }() case 8: try { try decoder.decodeSingularFloatField(value: &self.distance) }() + case 9: try { try decoder.decodeSingularFloatField(value: &self.lux) }() + case 10: try { try decoder.decodeSingularFloatField(value: &self.whiteLux) }() default: break } } @@ -595,6 +655,12 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if self.distance != 0 { try visitor.visitSingularFloatField(value: self.distance, fieldNumber: 8) } + if self.lux != 0 { + try visitor.visitSingularFloatField(value: self.lux, fieldNumber: 9) + } + if self.whiteLux != 0 { + try visitor.visitSingularFloatField(value: self.whiteLux, fieldNumber: 10) + } try unknownFields.traverse(visitor: &visitor) } @@ -607,6 +673,8 @@ extension EnvironmentMetrics: SwiftProtobuf.Message, SwiftProtobuf._MessageImple if lhs.current != rhs.current {return false} if lhs.iaq != rhs.iaq {return false} if lhs.distance != rhs.distance {return false} + if lhs.lux != rhs.lux {return false} + if lhs.whiteLux != rhs.whiteLux {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 75e6fb8e..da24c465 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -4,7 +4,9 @@ import SwiftUI +@available(iOS 17.0, *) struct ContentView: View { + @StateObject var appState = AppState.shared var body: some View { TabView(selection: $appState.tabSelection) { @@ -54,14 +56,21 @@ struct ContentView: View { } } } +//#Preview { +// if #available(iOS 17.0, *) { +// // ContentView(deepLinkManager: .init()) +// } else { +// // Fallback on earlier versions +// } +//} -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} +//struct ContentView_Previews: PreviewProvider { +// static var previews: some View { +// ContentView() +// } +//} -enum Tab { +enum Tab: Hashable { case contacts case messages case map diff --git a/Meshtastic/Views/Helpers/CircleText.swift b/Meshtastic/Views/Helpers/CircleText.swift index 0a348349..47b4ebbf 100644 --- a/Meshtastic/Views/Helpers/CircleText.swift +++ b/Meshtastic/Views/Helpers/CircleText.swift @@ -16,12 +16,20 @@ struct CircleText: View { Circle() .fill(color) .frame(width: circleSize, height: circleSize) + #if os(macOS) Text(text) - .textCase(.uppercase) + .frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center) .foregroundColor(color.isLight() ? .black : .white) - .font(.system(size: 8000)) - .minimumScaleFactor(0.001) - .frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) + .font(.system(size: 3000)) + .minimumScaleFactor(0.001) + #else + Text(text) + .frame(width: circleSize * 0.95, height: circleSize * 0.95, alignment: .center) + .foregroundColor(color.isLight() ? .black : .white) + .font(.system(size: 5000)) + .minimumScaleFactor(0.001) + #endif + } .aspectRatio(1, contentMode: .fit) } diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 879a361c..482f538f 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -12,7 +12,7 @@ import TipKit #endif struct Messages: View { - + @StateObject var appState = AppState.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager @@ -66,6 +66,26 @@ struct Messages: View { .navigationTitle("messages") .navigationBarTitleDisplayMode(.large) .navigationBarItems(leading: MeshtasticLogo()) + .onChange(of: (appState.navigationPath)) { newPath in + + if ((newPath?.hasPrefix("meshtastic://messages")) != nil) { + + if let urlComponent = URLComponents(string: newPath ?? "") { + let queryItems = urlComponent.queryItems + let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value + let channel = queryItems?.first(where: { $0.name == "channel" })?.value + + if channel == nil { + print("Channel not found") + } + else { + print("Channel \(channel)") + // selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") }) + // AppState.shared.navigationPath = nil + } + } + } + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 03dcf882..2d13d28f 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -35,6 +35,7 @@ struct UserList: View { private var users: FetchedResults @State var node: NodeInfoEntity? + @State var selectedUserNum: Int64? @State private var userSelection: UserEntity? // Nothing selected by default. @State private var isPresentingDeleteUserMessagesConfirm: Bool = false @@ -203,6 +204,10 @@ struct UserList: View { .onChange(of: distanceFilter) { _ in searchUserList() } + .onChange(of: selectedUserNum) { newUserNum in + userSelection = users.first(where: { $0.num == newUserNum }) + print(userSelection) + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 6a03aba8..4270343c 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -11,6 +11,7 @@ import MapKit @available(iOS 17.0, macOS 14.0, *) struct MeshMapContent: MapContent { + @StateObject var appState = AppState.shared /// Parameters @Binding var showUserLocation: Bool @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @@ -39,13 +40,14 @@ struct MeshMapContent: MapContent { @MapContentBuilder var meshMap: some MapContent { - let lineCoords = Array(positions).compactMap({(position) -> CLLocationCoordinate2D in - return position.nodeCoordinate ?? LocationsHandler.DefaultLocation + let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false } + let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation }) /// Convex Hull if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() + if loraCoords.count > 0 { + let hull = loraCoords.getConvexHull() MapPolygon(coordinates: hull) .stroke(.blue, lineWidth: 3) .foregroundStyle(.indigo.opacity(0.4)) @@ -92,6 +94,7 @@ struct MeshMapContent: MapContent { } } + /// Node History and Route Lines for favorites if position.nodePosition?.favorite ?? false { if showRouteLines { @@ -142,7 +145,7 @@ struct MeshMapContent: MapContent { } } /// Reduced Precision Map Circles - if 11...16 ~= position.precisionBits { + if 10...19 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius : CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift index 83a5ccbc..78d20407 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/NodeMapContent.swift @@ -40,6 +40,7 @@ struct NodeMapContent: MapContent { let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in return position.nodeCoordinate ?? LocationsHandler.DefaultLocation }) + /// Node Color from node.num let nodeColor = UIColor(hex: UInt32(node.num)) @@ -49,7 +50,7 @@ struct NodeMapContent: MapContent { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) /// Reduced Precision Map Circle - if position.latest && 11...16 ~= position.precisionBits { + if position.latest && 10...19 ~= position.precisionBits { let pp = PositionPrecision(rawValue: Int(position.precisionBits)) let radius : CLLocationDistance = pp?.precisionMeters ?? 0 if radius > 0.0 { @@ -58,13 +59,17 @@ struct NodeMapContent: MapContent { .stroke(.white, lineWidth: 2) } } + let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false } + let loraCoords = Array(loraNodes).compactMap({(position) -> CLLocationCoordinate2D in + return position.nodeCoordinate ?? LocationsHandler.DefaultLocation + }) /// Convex Hull if showConvexHull { - if lineCoords.count > 0 { - let hull = lineCoords.getConvexHull() + if loraCoords.count > 0 { + let hull = loraCoords.getConvexHull() MapPolygon(coordinates: hull) - .stroke(Color(nodeColor.darker()), lineWidth: 3) - .foregroundStyle(Color(nodeColor).opacity(0.4)) + .stroke(.blue, lineWidth: 3) + .foregroundStyle(.indigo.opacity(0.4)) } } /// Route Lines diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index cf983edb..bab5aa81 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -87,13 +87,13 @@ struct NodeMapSwiftUI: View { switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) case .hybrid: UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) case .satellite: UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.imagery(elevation: .realistic) + mapStyle = MapStyle.imagery(elevation: .flat) case .offline: return } @@ -118,13 +118,13 @@ struct NodeMapSwiftUI: View { UIApplication.shared.isIdleTimerDisabled = true switch selectedMapLayer { case .standard: - mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) case .hybrid: - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) case .satellite: - mapStyle = MapStyle.imagery(elevation: .realistic) + mapStyle = MapStyle.imagery(elevation: .flat) case .offline: - mapStyle = MapStyle.hybrid(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) } mostRecent = node.positions?.lastObject as? PositionEntity if node.positions?.count ?? 0 > 1 { diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index e4c4886e..d93a6ab2 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -35,7 +35,7 @@ struct NodeDetail: View { Label( title: { Text("\("uptime".localized)") - .font(.title2)+Text(": \(components)") + .font(.title3)+Text(": \(components)") .font(.title3) .foregroundColor(Color.gray) }, diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 5a848908..35b67397 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -35,9 +35,11 @@ struct MeshMap: View { @State var selectedPosition: PositionEntity? @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? + @State var selectedWaypointId: String? @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true + var body: some View { NavigationStack { @@ -106,33 +108,33 @@ struct MeshMap: View { .sheet(isPresented: $isEditingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) } - .onChange(of: (appState.navigationPath)) { newPath in - - if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) { - guard let url = URL(string: appState.navigationPath ?? "NONE") else { - print("Invalid URL") - return - } - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { - print("Invalid URL Components") - return - } - guard let action = components.host, action == "open-waypoint" else { - print("Unknown waypoint URL action") - return - } - guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else { - print("Waypoint id not found") - return - } -// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else { -// print("Waypoint not found") +// .onChange(of: (appState.navigationPath)) { newPath in +// +// if ((newPath?.hasPrefix("meshtastic://open-waypoint")) != nil) { +// guard let url = URL(string: appState.navigationPath ?? "NONE") else { +// print("Invalid URL") +// return +// } +// guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { +// print("Invalid URL Components") // return // } - //showWaypoints = true - //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) - } - } +// guard let action = components.host, action == "open-waypoint" else { +// print("Unknown waypoint URL action") +// return +// } +// guard let waypointId = components.queryItems?.first(where: { $0.name == "id" })?.value else { +// print("Waypoint id not found") +// return +// } +//// guard let waypoint = waypoints.first(where: { $0.id == Int64(waypointId) }) else { +//// print("Waypoint not found") +//// return +//// } +// //showWaypoints = true +// //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) +// } +// } .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { case .standard: @@ -174,6 +176,10 @@ struct MeshMap: View { if self.bleManager.context == nil { self.bleManager.context = context } + + // let wayPointEntity = getWaypoint(id: Int64(deepLinkManager.waypointId) ?? -1, context: context) + //if wayPointEntity.id > 0 { + // position = .camera(MapCamera(centerCoordinate: wayPointEntity.coordinate, distance: 1000, heading: 0, pitch: 60)) switch selectedMapLayer { case .standard: mapStyle = MapStyle.standard(elevation: .realistic, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index 588fec7a..a6e1368d 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -9,6 +9,7 @@ import CoreLocation struct NodeList: View { + @StateObject var appState = AppState.shared @State private var columnVisibility = NavigationSplitViewVisibility.all @State private var selectedNode: NodeInfoEntity? @State private var isPresentingTraceRouteSentAlert = false @@ -44,6 +45,14 @@ struct NodeList: View { var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { +// HStack { +// Button("Open Node") { +// UIApplication +// .shared +// .open(URL(string: "meshtastic://nodes?nodeNum=530606484")!) +// } +// } + let connectedNodeNum = Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) let connectedNode = nodes.first(where: { $0.num == connectedNodeNum }) List(nodes, id: \.self, selection: $selectedNode) { node in @@ -203,6 +212,7 @@ struct NodeList: View { .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) .navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count))) + .listStyle(.plain) .confirmationDialog( @@ -213,12 +223,11 @@ struct NodeList: View { Button("Delete Node") { let deleteNode = getNodeInfo(id: deleteNodeId, context: context) if connectedNode != nil { - - } - if deleteNode != nil { - let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum)) - if !success { - print("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)") + if deleteNode != nil { + let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(connectedNodeNum)) + if !success { + print("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized)") + } } } } @@ -305,6 +314,26 @@ struct NodeList: View { .onChange(of: distanceFilter) { _ in searchNodeList() } + .onChange(of: (appState.navigationPath)) { newPath in + + guard let deepLink = newPath else { + return + } + if deepLink.hasPrefix("meshtastic://nodes") { + + if let urlComponent = URLComponents(string: deepLink) { + let queryItems = urlComponent.queryItems + let nodeNum = queryItems?.first(where: { $0.name == "nodenum" })?.value + if nodeNum == nil { + print("nodeNum not found") + } + else { + selectedNode = nodes.first(where: { $0.num == Int64(nodeNum ?? "-1") }) + AppState.shared.navigationPath = nil + } + } + } + } .onAppear { if self.bleManager.context == nil { self.bleManager.context = context diff --git a/Meshtastic/Views/Settings/Channels/ChannelForm.swift b/Meshtastic/Views/Settings/Channels/ChannelForm.swift index 60e1b2dd..560d1d70 100644 --- a/Meshtastic/Views/Settings/Channels/ChannelForm.swift +++ b/Meshtastic/Views/Settings/Channels/ChannelForm.swift @@ -160,7 +160,7 @@ struct ChannelForm: View { if !preciseLocation { VStack(alignment: .leading) { Label("Approximate Location", systemImage: "location.slash.circle.fill") - Slider(value: $positionPrecision, in: 11...18, step: 1) { + Slider(value: $positionPrecision, in: 10...19, step: 1) { } minimumValueLabel: { Image(systemName: "minus") } maximumValueLabel: { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 1ba854fa..575de199 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -163,7 +163,7 @@ struct PositionConfig: View { .font(.callout) } } - if gpsMode != 1 { + if gpsMode != 1 && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { VStack(alignment: .leading) { Toggle(isOn: $fixedPosition) { Label("Fixed Position", systemImage: "location.square.fill") diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index 28967927..f27b1967 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -12,7 +12,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.3.8" + @State var minimumVersion = "2.3.9" @State var version = "" @State private var currentDevice: DeviceHardware? @State private var latestStable: FirmwareRelease? diff --git a/Meshtastic/Views/Settings/SaveChannelQRCode.swift b/Meshtastic/Views/Settings/SaveChannelQRCode.swift index f9ca2557..7de80622 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -13,6 +13,7 @@ struct SaveChannelQRCode: View { var channelSetLink: String var addChannels: Bool = false var bleManager: BLEManager + @State var showError: Bool = false @State var connectedToDevice = false var body: some View { @@ -20,26 +21,48 @@ struct SaveChannelQRCode: View { Text("\(addChannels ? "Add" : "Replace all") Channels?") .font(.title) Text("These settings will \(addChannels ? "add" : "replace all") channels. The current LoRa Config will be replaced. After everything saves your device will reboot.") + .fixedSize(horizontal: false, vertical: true) .foregroundColor(.gray) .font(.title3) .padding() + if showError { + Text("Channels being added from the QR code did not save. When adding channels the names must be unique.") + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.red) + .font(.callout) + .padding() + } HStack { - - Button { - let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) - if success { - dismiss() + if !showError { + Button { + let success = bleManager.saveChannelSet(base64UrlString: channelSetLink, addChannels: addChannels) + if success { + dismiss() + } else { + showError = true + } + } label: { + Label("save", systemImage: "square.and.arrow.down") } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() + .disabled(!connectedToDevice) + } else { + Button { + dismiss() + } label: { + Label("cancel", systemImage: "xmark") - } label: { - Label("save", systemImage: "square.and.arrow.down") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - .disabled(!connectedToDevice) + #if targetEnvironment(macCatalyst) Button { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index 686af46f..b2e2f1c3 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -278,8 +278,6 @@ struct ShareChannels: View { channelSettings.name = ch.name! channelSettings.psk = ch.psk! channelSettings.id = UInt32(ch.id) - channelSettings.uplinkEnabled = ch.uplinkEnabled - channelSettings.downlinkEnabled = ch.downlinkEnabled channelSet.settings.append(channelSettings) } }