diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index bb725735..cb6e47ba 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -334,6 +334,7 @@ DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; + DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 24.xcdatamodel"; sourceTree = ""; }; DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothConfig.swift; sourceTree = ""; }; DDB6ABD828B0A4BA00384BA1 /* BluetoothModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothModes.swift; sourceTree = ""; }; DDB6ABDA28B0AC6000384BA1 /* DistanceText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceText.swift; sourceTree = ""; }; @@ -1491,7 +1492,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.19; + MARKETING_VERSION = 2.2.20; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1525,7 +1526,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.2.19; + MARKETING_VERSION = 2.2.20; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1647,7 +1648,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.19; + MARKETING_VERSION = 2.2.20; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1680,7 +1681,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.2.19; + MARKETING_VERSION = 2.2.20; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1791,6 +1792,7 @@ DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */, DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */, DD295CE92B323ED9002CC4AC /* MeshtasticDataModelV22.xcdatamodel */, DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */, @@ -1815,7 +1817,7 @@ DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */, DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */, ); - currentVersion = DD33DB602B3D1ECC003E1EA0 /* MeshtasticDataModelV 23.xcdatamodel */; + currentVersion = DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */; name = Meshtastic.xcdatamodeld; path = Meshtastic/Meshtastic.xcdatamodeld; sourceTree = ""; diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 26014859..155f19bf 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -215,7 +215,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate id: (peripheral.identifier.uuidString), title: "Radio Disconnected", subtitle: "\(peripheral.name ?? "unknown".localized)", - content: e.localizedDescription + content: e.localizedDescription, + target: "bluetooth", + path: "meshtastic://bluetooth" ) ] manager.schedule() @@ -233,7 +235,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate id: (peripheral.identifier.uuidString), title: "Radio Disconnected", subtitle: "\(peripheral.name ?? "unknown".localized)", - content: e.localizedDescription + content: e.localizedDescription, + target: "bluetooth", + path: "meshtastic://bluetooth" ) ] manager.schedule() @@ -548,17 +552,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate // Channels if decodedInfo.channel.isInitialized && connectedPeripheral != nil { nowKnown = true - channelPacket(channel: decodedInfo.channel, fromNum: connectedPeripheral.num, context: context!) + channelPacket(channel: decodedInfo.channel, fromNum: Int64(truncatingIfNeeded: connectedPeripheral.num), context: context!) } // Config if decodedInfo.config.isInitialized && !invalidVersion && connectedPeripheral != nil { nowKnown = true - localConfig(config: decodedInfo.config, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) + localConfig(config: decodedInfo.config, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) } // Module Config if decodedInfo.moduleConfig.isInitialized && !invalidVersion && self.connectedPeripheral?.num != 0{ nowKnown = true - moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: self.connectedPeripheral.num, nodeLongName: self.connectedPeripheral.longName) + moduleConfig(config: decodedInfo.moduleConfig, context: context!, nodeNum: Int64(truncatingIfNeeded: self.connectedPeripheral.num), nodeLongName: self.connectedPeripheral.longName) if decodedInfo.moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(decodedInfo.moduleConfig.cannedMessage) { if decodedInfo.moduleConfig.cannedMessage.enabled { _ = self.getCannedMessageModuleMessages(destNum: self.connectedPeripheral.num, wantResponse: true) diff --git a/Meshtastic/Helpers/LocalNotificationManager.swift b/Meshtastic/Helpers/LocalNotificationManager.swift index c5efe48e..5ae3df98 100644 --- a/Meshtastic/Helpers/LocalNotificationManager.swift +++ b/Meshtastic/Helpers/LocalNotificationManager.swift @@ -31,15 +31,19 @@ class LocalNotificationManager { // This function iterates over the Notification objects in the notifications array and schedules them for delivery in the future private func scheduleNotifications() { for notification in notifications { - let content = UNMutableNotificationContent() - content.subtitle = notification.subtitle - content.title = notification.title - content.body = notification.content - content.sound = .default - content.interruptionLevel = .timeSensitive + let content = UNMutableNotificationContent() + content.subtitle = notification.subtitle + content.title = notification.title + content.body = notification.content + content.sound = .default + content.interruptionLevel = .timeSensitive + if notification.target != nil { content.userInfo["target"] = notification.target } + if notification.path != nil { + content.userInfo["path"] = notification.path + } let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: notification.id, content: content, trigger: trigger) @@ -69,4 +73,5 @@ struct Notification { var subtitle: String var content: String var target: String? + var path: String? } diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3fab145b..9b720845 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -660,23 +660,37 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage // Connected Device Metrics // ------------------------ // Low Battery notification - if telemetry.batteryLevel > 0 && telemetry.batteryLevel < 5 { - let content = UNMutableNotificationContent() - content.title = "Critically Low Battery!" - content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining." - content.userInfo["target"] = "node" - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - let uuidString = UUID().uuidString - let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) - let notificationCenter = UNUserNotificationCenter.current() - notificationCenter.add(request) { (error) in - if error != nil { - // Handle any errors. - print("Error creating local low battery notification: \(error?.localizedDescription ?? "no description")") - } else { - print("Created local low battery notification.") - } - } + if telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 { + let manager = LocalNotificationManager() + manager.notifications = [ + Notification( + id: ("notification.id.\(UUID().uuidString)"), + title: "Critically Low Battery!", + subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")", + content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.", + target: "nodes", + path: "meshtastic://nodes/\(telemetry.nodeTelemetry?.num ?? 0)/devicetelemetrylog" + ) + ] + manager.schedule() + +// let content = UNMutableNotificationContent() +// content.title = "Critically Low Battery!" +// content.body = "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining." +// content.userInfo["target"] = "node" +// content.userInfo["path"] = "meshtastic://node/\(telemetry.nodeTelemetry?.num ?? 0)" +// let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) +// let uuidString = UUID().uuidString +// let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) +// let notificationCenter = UNUserNotificationCenter.current() +// notificationCenter.add(request) { (error) in +// if error != nil { +// // Handle any errors. +// print("Error creating local low battery notification: \(error?.localizedDescription ?? "no description")") +// } else { +// print("Created local low battery notification.") +// } +// } } // Update our live activity if there is one running, not available on mac iOS >= 16.2 #if !targetEnvironment(macCatalyst) @@ -781,7 +795,8 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText, - target: "message" + target: "message", + path: "meshtastic://open-dm?userid=\(newMessage.fromUser?.num ?? 0)&id=\(newMessage.messageId)" ) ] manager.schedule() @@ -812,7 +827,8 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod title: "\(newMessage.fromUser?.longName ?? "unknown".localized)", subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")", content: messageText, - target: "message") + target: "message", + path: "meshtastic://messages/channel/\(newMessage.messageId)") ] manager.schedule() print("💬 iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized)") @@ -878,7 +894,8 @@ func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) { title: "New Waypoint Received", subtitle: "\(icon) \(waypoint.name ?? "Dropped Pin")", content: "\(waypoint.longDescription ?? "\(latitude), \(longitude)")", - target: "map" + target: "map", + path: "meshtastic://open-waypoint?id=\(waypoint.id)" ) ] manager.schedule() diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index 48ae0608..a2fde04b 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -31,8 +31,27 @@ $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLIconFile + alpha + CFBundleURLName + org.meshtastic + CFBundleURLSchemes + + meshtastic + + + CFBundleVersion $(CURRENT_PROJECT_VERSION) + INIntentsSupported + + Intent + ITSAppUsesNonExemptEncryption LSApplicationCategoryType @@ -48,17 +67,17 @@ NSBluetoothAlwaysUsageDescription We use bluetooth to connect to nearby Meshtastic Devices NSBluetoothPeripheralUsageDescription - Bluetooth is used to connect an iPhone to a user's meshtastic device to allow text messaging and location data for the mesh network. + Bluetooth is used to connect an iPhone to a user's meshtastic device to allow text messaging and location data for the mesh network. NSCameraUsageDescription We use the camera to share channels using a QR Code + NSLocationAlwaysAndWhenInUseUsageDescription + We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. Route Recording uses location in the background. + NSLocationAlwaysUsageDescription + We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. NSLocationUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. NSLocationWhenInUseUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. - NSLocationAlwaysUsageDescription - We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. - NSLocationAlwaysAndWhenInUseUsageDescription - We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. Route Recording uses location in the background. NSSupportsLiveActivities Privacy – Bluetooth Always Usage Description @@ -66,7 +85,7 @@ UIApplicationSceneManifest UIApplicationSupportsMultipleScenes - + UIApplicationSupportsIndirectInputEvents diff --git a/Meshtastic/Meshtastic.entitlements b/Meshtastic/Meshtastic.entitlements index 26f4ce01..241de35a 100644 --- a/Meshtastic/Meshtastic.entitlements +++ b/Meshtastic/Meshtastic.entitlements @@ -18,5 +18,7 @@ com.apple.security.personal-information.location + com.apple.developer.carplay-communication + diff --git a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion index 550e3369..66c74a0d 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion +++ b/Meshtastic/Meshtastic.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - MeshtasticDataModelV 23.xcdatamodel + MeshtasticDataModelV 24.xcdatamodel diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 24.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 24.xcdatamodel/contents new file mode 100644 index 00000000..be02a857 --- /dev/null +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 24.xcdatamodel/contents @@ -0,0 +1,410 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index 48e4c020..e21b96e9 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -153,4 +153,5 @@ class AppState: ObservableObject { @Published var unreadChannelMessages: Int = 0 @Published var firmwareVersion: String = "0.0.0" @Published var connectedNode: NodeInfoEntity? + @Published var navigationPath: String? } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index ab108787..94a6df6c 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -33,6 +33,8 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification 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")") if targetValue == "map" { AppState.shared.tabSelection = Tab.map } else if targetValue == "message" { diff --git a/Meshtastic/Protobufs/meshtastic/apponly.pb.swift b/Meshtastic/Protobufs/meshtastic/apponly.pb.swift index dfa98782..ffce4849 100644 --- a/Meshtastic/Protobufs/meshtastic/apponly.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/apponly.pb.swift @@ -33,24 +33,27 @@ struct ChannelSet { /// /// Channel list with settings - var settings: [ChannelSettings] = [] + var settings: [ChannelSettings] { + get {return _storage._settings} + set {_uniqueStorage()._settings = newValue} + } /// /// LoRa config var loraConfig: Config.LoRaConfig { - get {return _loraConfig ?? Config.LoRaConfig()} - set {_loraConfig = newValue} + get {return _storage._loraConfig ?? Config.LoRaConfig()} + set {_uniqueStorage()._loraConfig = newValue} } /// Returns true if `loraConfig` has been explicitly set. - var hasLoraConfig: Bool {return self._loraConfig != nil} + var hasLoraConfig: Bool {return _storage._loraConfig != nil} /// Clears the value of `loraConfig`. Subsequent reads from it will return its default value. - mutating func clearLoraConfig() {self._loraConfig = nil} + mutating func clearLoraConfig() {_uniqueStorage()._loraConfig = nil} var unknownFields = SwiftProtobuf.UnknownStorage() init() {} - fileprivate var _loraConfig: Config.LoRaConfig? = nil + fileprivate var _storage = _StorageClass.defaultInstance } #if swift(>=5.5) && canImport(_Concurrency) @@ -68,36 +71,70 @@ extension ChannelSet: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 2: .standard(proto: "lora_config"), ] + fileprivate class _StorageClass { + var _settings: [ChannelSettings] = [] + var _loraConfig: Config.LoRaConfig? = nil + + static let defaultInstance = _StorageClass() + + private init() {} + + init(copying source: _StorageClass) { + _settings = source._settings + _loraConfig = source._loraConfig + } + } + + fileprivate mutating func _uniqueStorage() -> _StorageClass { + if !isKnownUniquelyReferenced(&_storage) { + _storage = _StorageClass(copying: _storage) + } + return _storage + } + mutating func decodeMessage(decoder: inout D) throws { - while let fieldNumber = try decoder.nextFieldNumber() { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every case branch when no optimizations are - // enabled. https://github.com/apple/swift-protobuf/issues/1034 - switch fieldNumber { - case 1: try { try decoder.decodeRepeatedMessageField(value: &self.settings) }() - case 2: try { try decoder.decodeSingularMessageField(value: &self._loraConfig) }() - default: break + _ = _uniqueStorage() + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + while let fieldNumber = try decoder.nextFieldNumber() { + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every case branch when no optimizations are + // enabled. https://github.com/apple/swift-protobuf/issues/1034 + switch fieldNumber { + case 1: try { try decoder.decodeRepeatedMessageField(value: &_storage._settings) }() + case 2: try { try decoder.decodeSingularMessageField(value: &_storage._loraConfig) }() + default: break + } } } } func traverse(visitor: inout V) throws { - // The use of inline closures is to circumvent an issue where the compiler - // allocates stack space for every if/case branch local when no optimizations - // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and - // https://github.com/apple/swift-protobuf/issues/1182 - if !self.settings.isEmpty { - try visitor.visitRepeatedMessageField(value: self.settings, fieldNumber: 1) + try withExtendedLifetime(_storage) { (_storage: _StorageClass) in + // The use of inline closures is to circumvent an issue where the compiler + // allocates stack space for every if/case branch local when no optimizations + // are enabled. https://github.com/apple/swift-protobuf/issues/1034 and + // https://github.com/apple/swift-protobuf/issues/1182 + if !_storage._settings.isEmpty { + try visitor.visitRepeatedMessageField(value: _storage._settings, fieldNumber: 1) + } + try { if let v = _storage._loraConfig { + try visitor.visitSingularMessageField(value: v, fieldNumber: 2) + } }() } - try { if let v = self._loraConfig { - try visitor.visitSingularMessageField(value: v, fieldNumber: 2) - } }() try unknownFields.traverse(visitor: &visitor) } static func ==(lhs: ChannelSet, rhs: ChannelSet) -> Bool { - if lhs.settings != rhs.settings {return false} - if lhs._loraConfig != rhs._loraConfig {return false} + if lhs._storage !== rhs._storage { + let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in + let _storage = _args.0 + let rhs_storage = _args.1 + if _storage._settings != rhs_storage._settings {return false} + if _storage._loraConfig != rhs_storage._loraConfig {return false} + return true + } + if !storagesAreEqual {return false} + } if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/config.pb.swift b/Meshtastic/Protobufs/meshtastic/config.pb.swift index 5fc6a263..b984ef22 100644 --- a/Meshtastic/Protobufs/meshtastic/config.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/config.pb.swift @@ -1044,6 +1044,10 @@ struct Config { /// in ignore_incoming will have packets they send dropped on receive (by router.cpp) var ignoreIncoming: [UInt32] = [] + /// + /// If true, the device will not process any packets received via LoRa that passed via MQTT anywhere on the path towards it. + var ignoreMqtt: Bool = false + var unknownFields = SwiftProtobuf.UnknownStorage() enum RegionCode: SwiftProtobuf.Enum { @@ -2220,6 +2224,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem 13: .standard(proto: "sx126x_rx_boosted_gain"), 14: .standard(proto: "override_frequency"), 103: .standard(proto: "ignore_incoming"), + 104: .standard(proto: "ignore_mqtt"), ] mutating func decodeMessage(decoder: inout D) throws { @@ -2243,6 +2248,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem case 13: try { try decoder.decodeSingularBoolField(value: &self.sx126XRxBoostedGain) }() case 14: try { try decoder.decodeSingularFloatField(value: &self.overrideFrequency) }() case 103: try { try decoder.decodeRepeatedUInt32Field(value: &self.ignoreIncoming) }() + case 104: try { try decoder.decodeSingularBoolField(value: &self.ignoreMqtt) }() default: break } } @@ -2294,6 +2300,9 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if !self.ignoreIncoming.isEmpty { try visitor.visitPackedUInt32Field(value: self.ignoreIncoming, fieldNumber: 103) } + if self.ignoreMqtt != false { + try visitor.visitSingularBoolField(value: self.ignoreMqtt, fieldNumber: 104) + } try unknownFields.traverse(visitor: &visitor) } @@ -2313,6 +2322,7 @@ extension Config.LoRaConfig: SwiftProtobuf.Message, SwiftProtobuf._MessageImplem if lhs.sx126XRxBoostedGain != rhs.sx126XRxBoostedGain {return false} if lhs.overrideFrequency != rhs.overrideFrequency {return false} if lhs.ignoreIncoming != rhs.ignoreIncoming {return false} + if lhs.ignoreMqtt != rhs.ignoreMqtt {return false} if lhs.unknownFields != rhs.unknownFields {return false} return true } diff --git a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift index b8ebf212..31ebb6a2 100644 --- a/Meshtastic/Protobufs/meshtastic/mesh.pb.swift +++ b/Meshtastic/Protobufs/meshtastic/mesh.pb.swift @@ -220,6 +220,16 @@ enum HardwareModel: SwiftProtobuf.Enum { /// EBYTE SPI LoRa module and ESP32-S3 case ebyteEsp32S3 // = 54 + /// + /// Waveshare ESP32-S3-PICO with PICO LoRa HAT and 2.9inch e-Ink + case esp32S3Pico // = 55 + + /// + /// CircuitMess Chatter 2 LLCC68 Lora Module and ESP32 Wroom + /// Lora module can be swapped out for a Heltec RA-62 which is "almost" pin compatible + /// with one cut and one jumper Meshtastic works + case chatter2 // = 56 + /// /// ------------------------------------------------------------------------------------------------------------------------------------------ /// 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. @@ -280,6 +290,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case 52: self = .picomputerS3 case 53: self = .heltecHt62 case 54: self = .ebyteEsp32S3 + case 55: self = .esp32S3Pico + case 56: self = .chatter2 case 255: self = .privateHw default: self = .UNRECOGNIZED(rawValue) } @@ -334,6 +346,8 @@ enum HardwareModel: SwiftProtobuf.Enum { case .picomputerS3: return 52 case .heltecHt62: return 53 case .ebyteEsp32S3: return 54 + case .esp32S3Pico: return 55 + case .chatter2: return 56 case .privateHw: return 255 case .UNRECOGNIZED(let i): return i } @@ -393,6 +407,8 @@ extension HardwareModel: CaseIterable { .picomputerS3, .heltecHt62, .ebyteEsp32S3, + .esp32S3Pico, + .chatter2, .privateHw, ] } @@ -1498,6 +1514,13 @@ struct MeshPacket { set {_uniqueStorage()._delayed = newValue} } + /// + /// Describes whether this packet passed via MQTT somewhere along the path it currently took. + var viaMqtt: Bool { + get {return _storage._viaMqtt} + set {_uniqueStorage()._viaMqtt = newValue} + } + var unknownFields = SwiftProtobuf.UnknownStorage() enum OneOf_PayloadVariant: Equatable { @@ -2570,6 +2593,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding { 52: .same(proto: "PICOMPUTER_S3"), 53: .same(proto: "HELTEC_HT62"), 54: .same(proto: "EBYTE_ESP32_S3"), + 55: .same(proto: "ESP32_S3_PICO"), + 56: .same(proto: "CHATTER_2"), 255: .same(proto: "PRIVATE_HW"), ] } @@ -3285,6 +3310,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio 11: .same(proto: "priority"), 12: .standard(proto: "rx_rssi"), 13: .same(proto: "delayed"), + 14: .standard(proto: "via_mqtt"), ] fileprivate class _StorageClass { @@ -3300,6 +3326,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio var _priority: MeshPacket.Priority = .unset var _rxRssi: Int32 = 0 var _delayed: MeshPacket.Delayed = .noDelay + var _viaMqtt: Bool = false static let defaultInstance = _StorageClass() @@ -3318,6 +3345,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio _priority = source._priority _rxRssi = source._rxRssi _delayed = source._delayed + _viaMqtt = source._viaMqtt } } @@ -3368,6 +3396,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio case 11: try { try decoder.decodeSingularEnumField(value: &_storage._priority) }() case 12: try { try decoder.decodeSingularInt32Field(value: &_storage._rxRssi) }() case 13: try { try decoder.decodeSingularEnumField(value: &_storage._delayed) }() + case 14: try { try decoder.decodeSingularBoolField(value: &_storage._viaMqtt) }() default: break } } @@ -3424,6 +3453,9 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._delayed != .noDelay { try visitor.visitSingularEnumField(value: _storage._delayed, fieldNumber: 13) } + if _storage._viaMqtt != false { + try visitor.visitSingularBoolField(value: _storage._viaMqtt, fieldNumber: 14) + } } try unknownFields.traverse(visitor: &visitor) } @@ -3445,6 +3477,7 @@ extension MeshPacket: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementatio if _storage._priority != rhs_storage._priority {return false} if _storage._rxRssi != rhs_storage._rxRssi {return false} if _storage._delayed != rhs_storage._delayed {return false} + if _storage._viaMqtt != rhs_storage._viaMqtt {return false} return true } if !storagesAreEqual {return false} diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index be590909..e1a8811d 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -190,6 +190,20 @@ struct ChannelMessageList: View { .fixedSize() .padding(.bottom, 1) } + .onAppear { + if !tapback.read { + tapback.read = true + do { + try context.save() + print("📖 Read message \(message.messageId) ") + appState.unreadChannelMessages = myInfo.unreadMessages + UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages + context.refresh(myInfo, mergeChanges: true) + } catch { + print("Failed to read tapback \(tapback.messageId)") + } + } + } } } .padding(10) @@ -199,6 +213,7 @@ struct ChannelMessageList: View { ) } } + HStack { if currentUser && message.receivedACK { // Ack Received diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index 8116f7fd..d0c9c0e1 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -167,6 +167,20 @@ struct UserMessageList: View { .fixedSize() .padding(.bottom, 1) } + .onAppear { + if !tapback.read { + tapback.read = true + do { + try context.save() + print("📖 Read tapback \(tapback.messageId) ") + appState.unreadDirectMessages = user.unreadMessages + UIApplication.shared.applicationIconBadgeNumber = appState.unreadChannelMessages + appState.unreadDirectMessages + + } catch { + print("Failed to read tapback \(tapback.messageId)") + } + } + } } } .padding(10) diff --git a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift index b3d338e0..ba84dafc 100644 --- a/Meshtastic/Views/Nodes/DeviceMetricsLog.swift +++ b/Meshtastic/Views/Nodes/DeviceMetricsLog.swift @@ -86,7 +86,7 @@ struct DeviceMetricsLog: View { } let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") - if UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac { + if UIScreen.main.bounds.size.width > 768 && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) { // Add a table for mac and ipad // Table(Array(deviceMetrics),id: \.self) { Table(deviceMetrics) { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index 850c4e7e..23eca04b 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -86,7 +86,7 @@ struct NodeMapSwiftUI: View { if waypoints.count > 0 && showWaypoints { ForEach(Array(waypoints), id: \.id) { waypoint in Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - ZStack { + LazyVStack { CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 35) .onTapGesture(coordinateSpace: .named("nodemap")) { location in selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) @@ -100,47 +100,49 @@ struct NodeMapSwiftUI: View { let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 771)) let headingDegrees = Angle.degrees(Double(position.heading)) Annotation(position.latest ? node.user?.shortName ?? "?": "", coordinate: position.coordinate) { - ZStack { + LazyVStack { if position.latest { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .frame(width: 50, height: 50) - if pf.contains(.Heading) { - Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(nodeColor.darker())) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .onTapGesture { - selectedPosition = (selectedPosition == position ? nil : position) - } - .popover(item: $selectedPosition) { selection in - PositionPopover(position: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - - } else { - Image(systemName: "flipphone") - .symbolEffect(.pulse.byLayer) - .padding(5) - .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(node.num)).darker())) - .clipShape(Circle()) - .onTapGesture { - selectedPosition = (selectedPosition == position ? nil : position) - } - .popover(item: $selectedPosition) { selection in - PositionPopover(position: selection) - .padding() - .opacity(0.8) - .presentationCompactAdaptation(.popover) - } - + ZStack { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) + .frame(width: 50, height: 50) + if pf.contains(.Heading) { + Image(systemName: pf.contains(.Speed) && position.speed > 1 ? "location.north" : "octagon") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(nodeColor.darker())) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + .onTapGesture { + selectedPosition = (selectedPosition == position ? nil : position) + } + .popover(item: $selectedPosition) { selection in + PositionPopover(position: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + + } else { + Image(systemName: "flipphone") + .symbolEffect(.pulse.byLayer) + .padding(5) + .foregroundStyle(Color(nodeColor).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(node.num)).darker())) + .clipShape(Circle()) + .onTapGesture { + selectedPosition = (selectedPosition == position ? nil : position) + } + .popover(item: $selectedPosition) { selection in + PositionPopover(position: selection) + .padding() + .opacity(0.8) + .presentationCompactAdaptation(.popover) + } + + } } } else { if showNodeHistory { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 621e9f08..dfed0c38 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -43,10 +43,9 @@ struct MeshMap: View { var delay: Double = 0 @State private var scale: CGFloat = 0.5 - - /// "time >= %@ && nodePosition != nil && latest == true" + /// && time >= %@ @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)], - predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -30, to: Date())! as NSDate), animation: .none) + predicate: NSPredicate(format: "nodePosition != nil && latest == true", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none) private var positions: FetchedResults @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], @@ -68,19 +67,6 @@ struct MeshMap: View { ZStack { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - /// Waypoint Annotations - if waypoints.count > 0 && showWaypoints { - ForEach(Array(waypoints), id: \.id) { waypoint in - Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { - ZStack { - CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) - .onTapGesture(perform: { location in - selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) - }) - } - } - } - } /// Convex Hull if showConvexHull { if lineCoords.count > 0 { @@ -95,32 +81,34 @@ struct MeshMap: View { /// Node color from node.num let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) Annotation(position.nodePosition?.user?.longName ?? "?", coordinate: position.coordinate) { - ZStack { - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - if position.nodePosition?.isOnline ?? false { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .scaleEffect(scale) - .animation( - Animation.easeInOut(duration: 0.6) - .repeatForever().delay(delay), value: scale - ) - .onAppear { - self.scale = 1 - } - .frame(width: 60, height: 60) - } - if position.nodePosition?.hasDetectionSensorMetrics ?? false { - Image(systemName: "sensor.fill") - .symbolRenderingMode(.palette) - .symbolEffect(.variableColor) - .padding() - .foregroundStyle(.white) - .background(Color(nodeColor)) - .clipShape(Circle()) - } else { - CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) + LazyVStack { + ZStack { + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) + if position.nodePosition?.isOnline ?? false { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) + .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) + .scaleEffect(scale) + .animation( + Animation.easeInOut(duration: 0.6) + .repeatForever().delay(delay), value: scale + ) + .onAppear { + self.scale = 1 + } + .frame(width: 60, height: 60) + } + if position.nodePosition?.hasDetectionSensorMetrics ?? false { + Image(systemName: "sensor.fill") + .symbolRenderingMode(.palette) + .symbolEffect(.variableColor) + .padding() + .foregroundStyle(.white) + .background(Color(nodeColor)) + .clipShape(Circle()) + } else { + CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) + } } } .onTapGesture { location in @@ -151,12 +139,12 @@ struct MeshMap: View { } } .annotationTitles(.automatic) - let dashed = StrokeStyle( + let solid = StrokeStyle( lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [7, 10] + lineCap: .round, lineJoin: .round ) MapPolyline(coordinates: routeCoords) - .stroke(Color(UIColor(hex: UInt32(route.color))), style: dashed) + .stroke(Color(UIColor(hex: UInt32(route.color))), style: solid) } /// Node Route Lines @@ -179,31 +167,45 @@ struct MeshMap: View { /// Node History ForEach(Array(position.nodePosition!.positions!) as! [PositionEntity], id: \.self) { (mappin: PositionEntity) in if showNodeHistory { - if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false { - let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771)) - let headingDegrees = Angle.degrees(Double(mappin.heading)) - Annotation("", coordinate: mappin.coordinate) { - ZStack { - if pf.contains(.Heading) { - Image(systemName: "location.north.circle") - .resizable() - .scaledToFit() - .foregroundStyle(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .frame(width: 16, height: 16) - - } else { - Circle() - .fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) - .strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2) - .frame(width: 12, height: 12) + if mappin.latest == false && mappin.nodePosition?.user?.vip ?? false { + let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771)) + let headingDegrees = Angle.degrees(Double(mappin.heading)) + Annotation("", coordinate: mappin.coordinate) { + LazyVStack { + if pf.contains(.Heading) { + Image(systemName: "location.north.circle") + .resizable() + .scaledToFit() + .foregroundStyle(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white) + .background(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) + .clipShape(Circle()) + .rotationEffect(headingDegrees) + .frame(width: 16, height: 16) + + } else { + Circle() + .fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) + .strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white ,lineWidth: 2) + .frame(width: 12, height: 12) + } } } + .annotationTitles(.hidden) + .annotationSubtitles(.hidden) } - .annotationTitles(.hidden) - .annotationSubtitles(.hidden) + } + } + } + + /// Waypoint Annotations + if waypoints.count > 0 && showWaypoints { + ForEach(Array(waypoints), id: \.id) { waypoint in + Annotation(waypoint.name ?? "?", coordinate: waypoint.coordinate) { + LazyVStack { + CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40) + .onTapGesture(perform: { location in + selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint) + }) } } } @@ -253,6 +255,33 @@ struct MeshMap: View { .sheet(isPresented: $isEditingSettings) { MapSettingsForm(nodeHistory: $showNodeHistory, routeLines: $showRouteLines, convexHull: $showConvexHull, traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer) } + .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") + return + } + showWaypoints = true + position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) + } + } .onChange(of: (selectedMapLayer)) { newMapLayer in switch selectedMapLayer { case .standard: diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 39d69d8e..88bcd2ab 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -25,6 +25,7 @@ struct Channels: View { var node: NodeInfoEntity? @State var hasChanges = false + @State var hasValidKey = false @State private var isPresentingEditView = false @State private var isPresentingSaveConfirm: Bool = false @State private var channelIndex: Int32 = 0 @@ -167,16 +168,34 @@ struct Channels: View { HStack(alignment: .top) { Text("Key") Spacer() - Text(channelKey) - .foregroundColor(Color.gray) - .textSelection(.enabled) -// TextField( -// "", -// text: $channelKey, -// axis: .vertical -// ) -// .foregroundColor(Color.gray) -// .disabled(true) + TextField( + "Key", + text: $channelKey + ) + .padding(4) + .disableAutocorrection(true) + .keyboardType(.alphabet) + .foregroundColor(Color.gray) + .textSelection(.enabled) + .background( + RoundedRectangle(cornerRadius: 25.0) + .stroke( + hasValidKey ? + Color.green : + Color.red + , lineWidth: 2.0) + ) + .onChange(of: channelKey, perform: { _ in + let tempKey = Data(base64Encoded: channelKey) ?? Data() + if tempKey.count == channelKeySize || channelKeySize == -1{ + hasValidKey = true + } + else { + hasValidKey = false + } + hasChanges = true + }) + .disabled(channelKeySize <= 0) } Picker("Channel Role", selection: $channelRole) { if channelRole == 1 { @@ -256,7 +275,7 @@ struct Channels: View { } label: { Label("save", systemImage: "square.and.arrow.down") } - .disabled(bleManager.connectedPeripheral == nil || !hasChanges) + .disabled(bleManager.connectedPeripheral == nil || !hasChanges || !hasValidKey) .buttonStyle(.bordered) .buttonBorderShape(.capsule) .controlSize(.large) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index 674c951f..5572c330 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -103,7 +103,7 @@ struct DeviceConfig: View { } Section(header: Text("GPIO")) { Picker("Button GPIO", selection: $buttonGPIO) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -113,7 +113,7 @@ struct DeviceConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Buzzer GPIO", selection: $buzzerGPIO) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index ceb142e8..b746dfcb 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -43,6 +43,7 @@ struct LoRaConfig: View { @State var codingRate = 0 @State var rxBoostedGain = false @State var overrideFrequency: Float = 0.0 + @State var ignoreMqtt = false let floatFormatter: NumberFormatter = { let formatter = NumberFormatter() @@ -111,6 +112,11 @@ struct LoRaConfig: View { } } Section(header: Text("Advanced")) { + + Toggle(isOn: $ignoreMqtt) { + Label("Ignore MQTT", systemImage: "server.rack") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Toggle(isOn: $txEnabled) { Label("Transmit Enabled", systemImage: "waveform.path") @@ -227,6 +233,7 @@ struct LoRaConfig: View { lc.spreadFactor = UInt32(spreadFactor) lc.sx126XRxBoostedGain = rxBoostedGain lc.overrideFrequency = overrideFrequency + lc.ignoreMqtt = ignoreMqtt let adminMessageId = bleManager.saveLoRaConfig(config: lc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true @@ -319,6 +326,11 @@ struct LoRaConfig: View { if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true } } } + .onChange(of: ignoreMqtt) { newIgnoreMqtt in + if node != nil && node!.loRaConfig != nil { + if newIgnoreMqtt != node!.loRaConfig!.ignoreMqtt { hasChanges = true } + } + } } func setLoRaValues() { self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3) @@ -333,6 +345,7 @@ struct LoRaConfig: View { self.spreadFactor = Int(node?.loRaConfig?.spreadFactor ?? 0) self.rxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false self.overrideFrequency = node?.loRaConfig?.overrideFrequency ?? 0.0 + self.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index ded4f262..cb5585d2 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -124,7 +124,7 @@ struct CannedMessagesConfig: View { .disabled(configPreset > 0) Section(header: Text("Inputs")) { Picker("Pin A", selection: $inputbrokerPinA) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -136,7 +136,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder A port.") .font(.caption) Picker("Pin B", selection: $inputbrokerPinB) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -148,7 +148,7 @@ struct CannedMessagesConfig: View { Text("GPIO pin for rotary encoder B port.") .font(.caption) Picker("Press Pin", selection: $inputbrokerPinPress) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 7b874aa0..23b33a90 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -139,7 +139,7 @@ struct DetectionSensorConfig: View { .listRowSeparator(.visible) .offset(y: -10) Picker("GPIO Pin to monitor", selection: $monitorPin) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index 512f09ae..511385d8 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -99,7 +99,7 @@ struct ExternalNotificationConfig: View { Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.") .font(.caption) Picker("Output pin GPIO", selection: $output) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -147,7 +147,7 @@ struct ExternalNotificationConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -157,7 +157,7 @@ struct ExternalNotificationConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("Output pin vibra GPIO", selection: $outputVibra) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 6a6ba7e0..be78fb96 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -23,6 +23,9 @@ struct MQTTConfig: View { @State var jsonEnabled = false @State var tlsEnabled = true @State var root = "msh" + @State var mqttConnected: Bool = false + + var body: some View { VStack { @@ -54,6 +57,7 @@ struct MQTTConfig: View { .foregroundColor(.orange) } Section(header: Text("options")) { + Toggle(isOn: $enabled) { Label("enabled", systemImage: "dot.radiowaves.right") @@ -64,7 +68,13 @@ struct MQTTConfig: View { Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right") } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.") + if enabled && proxyToClientEnabled { + Toggle(isOn: $mqttConnected) { + Label(mqttConnected ? "mqtt.disconnect".localized : "mqtt.connect".localized, systemImage: "server.rack") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Text("If both MQTT and the client proxy are enabled your mobile device will utilize an available network connection to connect to the specified MQTT server.") .font(.caption2) Toggle(isOn: $encryptionEnabled) { @@ -242,7 +252,7 @@ struct MQTTConfig: View { .navigationTitle("mqtt.config") .navigationBarItems(trailing: ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") + ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected) }) .onAppear { if self.bleManager.context == nil { @@ -314,6 +324,17 @@ struct MQTTConfig: View { if newTlsEnabled != node!.mqttConfig!.tlsEnabled { hasChanges = true } } } + .onChange(of: mqttConnected) { newMqttConnected in + if newMqttConnected == false { + if bleManager.mqttProxyConnected { + bleManager.mqttManager.disconnect() + } + } else { + if !bleManager.mqttProxyConnected && node != nil { + bleManager.mqttManager.connectFromConfigSettings(node: node!) + } + } + } } func setMqttValues() { self.enabled = (node?.mqttConfig?.enabled ?? false) @@ -325,6 +346,7 @@ struct MQTTConfig: View { self.encryptionEnabled = (node?.mqttConfig?.encryptionEnabled ?? false) self.jsonEnabled = (node?.mqttConfig?.jsonEnabled ?? false) self.tlsEnabled = (node?.mqttConfig?.tlsEnabled ?? false) + self.mqttConnected = bleManager.mqttProxyConnected self.hasChanges = false } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 9af01055..e1838f96 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -96,7 +96,7 @@ struct RtttlConfig: View { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) if connectedNode != nil { - let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + let adminMessageId = bleManager.saveRtttlConfig(ringtone: ringtone.trimmingCharacters(in: .whitespacesAndNewlines), fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) if adminMessageId > 0 { // Should show a saved successfully alert once I know that to be true // for now just disable the button after a successful save diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 0e36460e..c3fa234d 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -99,7 +99,7 @@ struct SerialConfig: View { Section(header: Text("GPIO")) { Picker("Receive data (rxd) GPIO pin", selection: $rxd) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -110,7 +110,7 @@ struct SerialConfig: View { .pickerStyle(DefaultPickerStyle()) Picker("Transmit data (txd) GPIO pin", selection: $txd) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index 0775a16e..a92f06ef 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -154,17 +154,6 @@ struct PositionConfig: View { } .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - if includeAltitude { - Toggle(isOn: $includeAltitudeMsl) { - Label("Altitude is Mean Sea Level", systemImage: "arrow.up.to.line.compact") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - Toggle(isOn: $includeGeoidalSeparation) { - Label("Altitude Geoidal Separation", systemImage: "globe.americas") - } - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - } - Toggle(isOn: $includeSatsinview) { Label("Number of satellites", systemImage: "skew") } @@ -192,6 +181,17 @@ struct PositionConfig: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("Advanced Position Flags")) { + + if includeAltitude { + Toggle(isOn: $includeAltitudeMsl) { + Label("Altitude is Mean Sea Level", systemImage: "arrow.up.to.line.compact") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + Toggle(isOn: $includeGeoidalSeparation) { + Label("Altitude Geoidal Separation", systemImage: "globe.americas") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } Toggle(isOn: $includeDop) { Text("Dilution of precision (DOP) PDOP used by default") @@ -213,7 +213,7 @@ struct PositionConfig: View { if deviceGpsEnabled { Picker("GPS Receive GPIO", selection: $rxGpio) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -223,7 +223,7 @@ struct PositionConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("GPS Transmit GPIO", selection: $txGpio) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { @@ -233,7 +233,7 @@ struct PositionConfig: View { } .pickerStyle(DefaultPickerStyle()) Picker("GPS EN GPIO", selection: $gpsEnGpio) { - ForEach(0..<48) { + ForEach(0..<49) { if $0 == 0 { Text("unset") } else { diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index c0141523..94894d57 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -112,8 +112,7 @@ struct Firmware: View { if bleManager.sendEnterDfuMode(fromUser: connectedNode!.user!, toUser: node!.user!) { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - bleManager.automaticallyReconnect = false - bleManager.disconnectPeripheral() + bleManager.disconnectPeripheral(reconnect: false) } } else { print("Enter DFU Failed") diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index 342d7aca..71794725 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -257,6 +257,12 @@ // .presentationDetents([.fraction(0.30), .fraction(0.65)]) // .presentationDragIndicator(.hidden) // .interactiveDismissDisabled(false) +// .onAppear { +// UIApplication.shared.isIdleTimerDisabled = true +// } +// .onDisappear(perform: { +// UIApplication.shared.isIdleTimerDisabled = false +// }) // .onChange(of: locationsHandler.locationsArray.last) { newLoc in // if locationsHandler.isRecording { // if let loc = newLoc { diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index 6c1de5a0..14e01d0f 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -181,12 +181,12 @@ struct Routes: View { } } .annotationTitles(.automatic) - let dashed = StrokeStyle( + let solid = StrokeStyle( lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [7, 10] + lineCap: .round, lineJoin: .round ) MapPolyline(coordinates: lineCoords) - .stroke(Color(UIColor(hex: UInt32(selectedRoute?.color ?? 0))), style: dashed) + .stroke(Color(UIColor(hex: UInt32(selectedRoute?.color ?? 0))), style: solid) } .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .bottom, alignment: UIDevice.current.userInterfaceIdiom == .phone ? .leading : .trailing) { diff --git a/Meshtastic/Views/Settings/ShareChannels.swift b/Meshtastic/Views/Settings/ShareChannels.swift index e1b45f14..8fa6bf32 100644 --- a/Meshtastic/Views/Settings/ShareChannels.swift +++ b/Meshtastic/Views/Settings/ShareChannels.swift @@ -252,6 +252,7 @@ struct ShareChannels: View { loRaConfig.usePreset = node?.loRaConfig?.usePreset ?? true loRaConfig.channelNum = UInt32(node?.loRaConfig?.channelNum ?? 0) loRaConfig.sx126XRxBoostedGain = node?.loRaConfig?.sx126xRxBoostedGain ?? false + loRaConfig.ignoreMqtt = node?.loRaConfig?.ignoreMqtt ?? false channelSet.loraConfig = loRaConfig if node?.myInfo?.channels != nil && node?.myInfo?.channels?.count ?? 0 > 0 { for ch in node?.myInfo?.channels?.array as? [ChannelEntity] ?? [] { diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 1b6b3060..8ef6b857 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -198,8 +198,10 @@ "mode"="Modus"; "module.configuration"="Modul Konfiguration"; "mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; "mqtt.config"="MQTT Config"; "mqtt.clientproxy"="MQTT Client Proxy"; +"mqtt.disconnect"="Disconnect from MQTT"; "mqtt.username"="Benutzername"; "name"="Name"; "network"="Netzwerk"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index aeb1b117..3a5ab36e 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -202,8 +202,10 @@ "mode"="Mode"; "module.configuration"="Module Configuration"; "mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; "mqtt.config"="MQTT Config"; "mqtt.clientproxy"="MQTT Client Proxy"; +"mqtt.disconnect"="Disconnect from MQTT"; "mqtt.username"="Username"; "name"="Name"; "network"="Network"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index db76ed47..73b8cef2 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -200,8 +200,10 @@ "mode"="Tryb"; "module.configuration"="Konfiguracja modułu"; "mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; "mqtt.config"="Konfiguracja MQTT"; "mqtt.clientproxy"="Klient Proxy MQTT"; +"mqtt.disconnect"="Disconnect from MQTT"; "mqtt.username"="Nazwa użytkownika"; "name"="Nazwa"; "network"="Sieć"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index ed5d4d3e..1b9411d7 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -198,8 +198,10 @@ "mode"="模式"; "module.configuration"="模块配置"; "mqtt"="MQTT"; +"mqtt.connect"="Connect to MQTT"; "mqtt.config"="MQTT 配置"; "mqtt.clientproxy"="MQTT 客户端代理"; +"mqtt.disconnect"="Disconnect from MQTT"; "mqtt.username"="用户名称"; "name"="名称"; "network"="网络";