diff --git a/Localizable.xcstrings b/Localizable.xcstrings index d80208dd..c518a26f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -334,9 +334,6 @@ }, "Ack Time: %@" : { - }, - "Acknowledged" : { - }, "Acknowledged by another node" : { @@ -481,6 +478,12 @@ } } } + }, + "Admin & Direct Message Keys" : { + + }, + "Admin Key" : { + }, "admin.log" : { "extractionState" : "manual", @@ -703,7 +706,10 @@ "All" : { }, - "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth." : { + "All device and app data will be deleted." : { + + }, + "Allow incoming device control over the insecure legacy admin channel." : { }, "Allow Position Requests" : { @@ -1843,6 +1849,9 @@ } } } + }, + "Bluetooth Logs" : { + }, "bluetooth.config" : { "localizations" : { @@ -4801,9 +4810,6 @@ } } } - }, - "Contacts" : { - }, "contacts %@" : { "extractionState" : "migrated", @@ -5337,6 +5343,9 @@ } } } + }, + "Developer" : { + }, "Developers" : { @@ -5401,6 +5410,9 @@ }, "Device GPS" : { + }, + "Device is managed by a mesh administrator." : { + }, "Device Logging Enabled" : { @@ -6300,6 +6312,15 @@ }, "Direct" : { + }, + "Direct Message Help" : { + + }, + "Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater." : { + + }, + "Direct messages are using the shared key for the channel." : { + }, "direct.messages" : { "localizations" : { @@ -6682,9 +6703,6 @@ }, "Drag & Drop is the recommended way to update firmware for NRF devices. If your iPhone or iPad is USB-C it will work with your regular USB-C charging cable, for lightning devices you need the Apple Lightning to USB camera adaptor." : { - }, - "Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation." : { - }, "echo" : { "localizations" : { @@ -6954,6 +6972,9 @@ } } } + }, + "Encrypted" : { + }, "Encryption Enabled" : { @@ -7188,6 +7209,9 @@ }, "Favorites" : { + }, + "Favorites and nodes with recent messages show up at the top of the contact list." : { + }, "Fifteen Minutes" : { @@ -8252,11 +8276,14 @@ "Hops Away" : { }, - "Hops Away %d) dB" : { + "Hops Away %d" : { }, "Hops Away:" : { + }, + "Hops Away: %d" : { + }, "Hour" : { @@ -10843,6 +10870,9 @@ }, "LED State" : { + }, + "Legacy Administration" : { + }, "Licensed Operator" : { @@ -11248,6 +11278,9 @@ }, "Long Name: %@" : { + }, + "Long press to favorite or mute the contact or delete a conversation." : { + }, "Longitude" : { @@ -14500,6 +14533,12 @@ } } } + }, + "Message" : { + + }, + "Message Status Options" : { + }, "message.details" : { "localizations" : { @@ -15976,6 +16015,9 @@ }, "Other data sources" : { + }, + "Output live debug logging over serial." : { + }, "Output pin buzzer GPIO " : { @@ -16626,9 +16668,21 @@ }, "Primary GPIO" : { + }, + "Private Key" : { + }, "Project information" : { + }, + "Public Key" : { + + }, + "Public Key Encryption" : { + + }, + "Public Key Mismatch" : { + }, "PWD" : { @@ -17207,7 +17261,10 @@ "Remote administration for: %@" : { }, - "Remote: %@" : { + "Remote Legacy Admin: %@" : { + + }, + "Remote PKI Admin: %@" : { }, "Remove" : { @@ -17277,7 +17334,10 @@ } } }, - "Request Admin: %@" : { + "Request Legacy Admin: %@" : { + + }, + "Request PKI Admin: %@" : { }, "Requires that there be an accelerometer on your device." : { @@ -18407,6 +18467,28 @@ } } }, + "routing.pkifailed" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Encrypted Send Failed" + } + } + } + }, + "routing.pkiunknownpubkey" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unknown Public Key" + } + } + } + }, "routing.timeout" : { "extractionState" : "migrated", "localizations" : { @@ -18801,6 +18883,12 @@ }, "Secondary" : { + }, + "Security" : { + + }, + "Security Config" : { + }, "Select a channel" : { @@ -19016,6 +19104,9 @@ }, "Sensor Options" : { + }, + "Sent out to other nodes on the mesh to allow them to compute a shared secret key." : { + }, "Sequence number" : { @@ -19083,6 +19174,12 @@ }, "Serial Console" : { + }, + "Serial Console over the Stream API." : { + + }, + "Serial Debug Logs" : { + }, "serial.config" : { "localizations" : { @@ -19687,6 +19784,9 @@ } } } + }, + "Shared Key" : { + }, "Short Name" : { @@ -20916,6 +21016,12 @@ }, "The minimum distance change in meters to be considered for a smart position broadcast." : { + }, + "The public key authorized to send admin messages to this node." : { + + }, + "The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action." : { + }, "The region where you will be using your radios." : { @@ -22127,6 +22233,9 @@ }, "Use PWM Buzzer" : { + }, + "Used to create a shared key with a remote device." : { + }, "user" : { "localizations" : { @@ -22294,6 +22403,12 @@ }, "Via Mqtt" : { + }, + "View and export position-redacted device logs over Bluetooth" : { + + }, + "View Logs" : { + }, "voltage" : { "localizations" : { @@ -22431,6 +22546,9 @@ }, "Website" : { + }, + "What does the lock mean?" : { + }, "What is Meshtastic?" : { @@ -22449,6 +22567,12 @@ }, "WIND" : { + }, + "Wind Direction" : { + + }, + "Wind Speed" : { + }, "x" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cca267e7..7d5f9023 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -58,6 +58,7 @@ DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1B8F3F2B35E2F10022AABC /* GPSStatus.swift */; }; DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; }; DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; }; + DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; }; DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */; }; DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2160AE28C5552500C17253 /* MQTTConfig.swift */; }; DD23A50F26FD1B4400D9B90C /* PeripheralModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */; }; @@ -91,6 +92,11 @@ DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */; }; DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */; }; DD6193792863875F00E59241 /* SerialConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6193782863875F00E59241 /* SerialConfig.swift */; }; + DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65712C6AB8EC0053C113 /* SecureInput.swift */; }; + DD6F65742C6CB80A0053C113 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65732C6CB80A0053C113 /* View.swift */; }; + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65752C6EA5490053C113 /* AckErrors.swift */; }; + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */; }; + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6F657A2C6EC2900053C113 /* LockLegend.swift */; }; DD73FD1128750779000852D6 /* PositionLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD73FD1028750779000852D6 /* PositionLog.swift */; }; DD769E0328D18BF1001A3F05 /* DeviceMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */; }; DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */; }; @@ -146,7 +152,6 @@ DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; }; DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; }; DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; }; - DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */; }; DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F40F2A9EE5B400230ECE /* Messages.swift */; }; DDB8F4122A9EE5DD00230ECE /* UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4112A9EE5DD00230ECE /* UserList.swift */; }; DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB8F4132A9EE5F000230ECE /* ChannelList.swift */; }; @@ -292,6 +297,7 @@ DD1BD0EA2C601795008C0C70 /* CLLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLLocation.swift; sourceTree = ""; }; DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = ""; }; DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; + DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = ""; }; DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = ""; }; DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = ""; }; @@ -343,6 +349,11 @@ DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = ""; }; DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = ""; }; DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = ""; }; + DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; + DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = ""; }; + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = ""; }; + DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = ""; }; DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = ""; }; DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = ""; }; DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = ""; }; @@ -406,7 +417,6 @@ DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = ""; }; DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = ""; }; DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrength.swift; sourceTree = ""; }; - DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevelCompact.swift; sourceTree = ""; }; DDB8F40F2A9EE5B400230ECE /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = ""; }; DDB8F4112A9EE5DD00230ECE /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = ""; }; DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = ""; }; @@ -680,6 +690,7 @@ DD2553582855B52700E55709 /* PositionConfig.swift */, DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */, D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */, + DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */, DD61937B2863877A00E59241 /* Module */, ); path = Config; @@ -703,6 +714,16 @@ path = Module; sourceTree = ""; }; + DD6F65772C6EAB860053C113 /* Help */ = { + isa = PBXGroup; + children = ( + DD6F65752C6EA5490053C113 /* AckErrors.swift */, + DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */, + DD6F657A2C6EC2900053C113 /* LockLegend.swift */, + ); + path = Help; + sourceTree = ""; + }; DD7709392AA1ABA1007A8BF0 /* Tips */ = { isa = PBXGroup; children = ( @@ -895,9 +916,9 @@ DDC2E18D26CE25CB0042C5E4 /* Helpers */ = { isa = PBXGroup; children = ( + DD6F65772C6EAB860053C113 /* Help */, DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */, DD3CC24B2C498D6C001BD3A2 /* BatteryCompact.swift */, - DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */, DD47E3D526F17ED900029299 /* CircleText.swift */, DDF924C926FBB953009FE055 /* ConnectedDevice.swift */, @@ -909,6 +930,7 @@ DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */, DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */, DD5E523D298F5A7D00D21B61 /* Weather */, + DD6F65712C6AB8EC0053C113 /* SecureInput.swift */, ); path = Helpers; sourceTree = ""; @@ -995,6 +1017,7 @@ DDD5BB172C2F9C36007E03CA /* OSLogEntryLog.swift */, DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, DDD5BB0C2C285F00007E03CA /* Logger.swift */, + DD6F65732C6CB80A0053C113 /* View.swift */, ); path = Extensions; sourceTree = ""; @@ -1276,19 +1299,21 @@ DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */, DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */, DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, + DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, + DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */, DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */, - DDB75A232A13CDA9006ED576 /* BatteryLevelCompact.swift in Sources */, DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */, DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */, DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */, DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */, DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */, DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */, + DD6F65792C6EADE60053C113 /* DirectMessagesHelp.swift in Sources */, 25F5D5C02C3F6DA6008036E3 /* Router.swift in Sources */, DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, 25C49D902C471AEA0024FBD1 /* Constants.swift in Sources */, @@ -1347,6 +1372,7 @@ 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */, D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */, DD86D4112881D16900BAEB7A /* WriteCsvFile.swift in Sources */, + DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */, DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */, DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */, DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */, @@ -1366,6 +1392,7 @@ DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */, DDDCD5702BB26F5C00BE6B60 /* NodeListFilter.swift in Sources */, + DD6F65742C6CB80A0053C113 /* View.swift in Sources */, DD1933762B0835D500771CD5 /* PositionAltitudeChart.swift in Sources */, DD415828285859C4009B0E59 /* TelemetryConfig.swift in Sources */, DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */, @@ -1373,6 +1400,7 @@ DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */, 25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */, DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */, + DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */, DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */, DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, @@ -1617,7 +1645,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1652,7 +1680,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1684,7 +1712,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1717,7 +1745,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.4.2; + MARKETING_VERSION = 2.5.0; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Enums/LoraConfigEnums.swift b/Meshtastic/Enums/LoraConfigEnums.swift index 7da6268e..b0dd9966 100644 --- a/Meshtastic/Enums/LoraConfigEnums.swift +++ b/Meshtastic/Enums/LoraConfigEnums.swift @@ -210,6 +210,7 @@ enum ModemPresets: Int, CaseIterable, Identifiable { case medFast = 4 case shortSlow = 5 case shortFast = 6 + case shortTurbo = 8 var id: Int { self.rawValue } var description: String { @@ -230,6 +231,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "Short Range - Slow" case .shortFast: return "Short Range - Fast" + case .shortTurbo: + return "Short Range - Turbo" } } var name: String { @@ -250,6 +253,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return "ShortSlow" case .shortFast: return "ShortFast" + case .shortTurbo: + return "ShortTurbo" } } func snrLimit() -> Float { @@ -270,6 +275,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return -10 case .shortFast: return -7.5 + case .shortTurbo: + return -7.5 } } func protoEnumValue() -> Config.LoRaConfig.ModemPreset { @@ -290,6 +297,8 @@ enum ModemPresets: Int, CaseIterable, Identifiable { return Config.LoRaConfig.ModemPreset.shortSlow case .shortFast: return Config.LoRaConfig.ModemPreset.shortFast + case .shortTurbo: + return Config.LoRaConfig.ModemPreset.shortTurbo } } } diff --git a/Meshtastic/Enums/RoutingError.swift b/Meshtastic/Enums/RoutingError.swift index 0773265b..108fedd2 100644 --- a/Meshtastic/Enums/RoutingError.swift +++ b/Meshtastic/Enums/RoutingError.swift @@ -5,6 +5,7 @@ // Copyright(c) Garth Vander Houwen 8/4/22. // import Foundation +import SwiftUI import MeshtasticProtobufs enum RoutingError: Int, CaseIterable, Identifiable { @@ -21,6 +22,8 @@ enum RoutingError: Int, CaseIterable, Identifiable { case dutyCycleLimit = 9 case badRequest = 32 case notAuthorized = 33 + case pkiFailed = 34 + case pkiUnknownPubkey = 35 var id: Int { self.rawValue } var display: String { @@ -50,6 +53,51 @@ enum RoutingError: Int, CaseIterable, Identifiable { return "routing.badRequest".localized case .notAuthorized: return "routing.notauthorized".localized + case .pkiFailed: + return "routing.pkifailed".localized + case .pkiUnknownPubkey: + return "routing.pkiunknownpubkey".localized + } + } + var color: Color { + if self == .none { + return Color.secondary + } else if self.canRetry { + return Color.orange + } else { + return Color.red + } + } + var canRetry: Bool { + switch self { + case .none: + return false + case .noRoute: + return true + case .gotNak: + return true + case .timeout: + return true + case .noInterface: + return true + case .maxRetransmit: + return true + case .noChannel: + return true + case .tooLarge: + return false + case .noResponse: + return true + case .dutyCycleLimit: + return true + case .badRequest: + return true + case .notAuthorized: + return true + case .pkiFailed: + return false + case .pkiUnknownPubkey: + return false } } func protoEnumValue() -> Routing.Error { @@ -80,7 +128,10 @@ enum RoutingError: Int, CaseIterable, Identifiable { return Routing.Error.badRequest case .notAuthorized: return Routing.Error.notAuthorized - + case .pkiFailed: + return Routing.Error.pkiFailed + case .pkiUnknownPubkey: + return Routing.Error.pkiUnknownPubkey } } } diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index 18d97f6c..70f36c3d 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -19,7 +19,8 @@ extension MessageEntity { } var canRetry: Bool { - return ackError == 9 || ackError == 5 || ackError == 3 + let re = RoutingError(rawValue: Int(ackError)) + return re?.canRetry ?? false } var tapbacks: [MessageEntity] { diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 66c915df..07ee9117 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -14,6 +14,10 @@ extension NodeInfoEntity { return self.positions?.lastObject as? PositionEntity } + var latestDeviceMetrics: TelemetryEntity? { + return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity + } + var latestEnvironmentMetrics: TelemetryEntity? { return self.telemetries?.filtered(using: NSPredicate(format: "metricsType == 1")).lastObject as? TelemetryEntity } @@ -54,6 +58,15 @@ extension NodeInfoEntity { } return false } + + var canRemoteAdmin: Bool { + if UserDefaults.enableAdministration { + return true + } else { + let adminChannel = myInfo?.channels?.filter { ($0 as AnyObject).name?.lowercased() == "admin" } + return adminChannel?.count ?? 0 > 0 + } + } } public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity { diff --git a/Meshtastic/Extensions/UserDefaults.swift b/Meshtastic/Extensions/UserDefaults.swift index ca8441be..740f04e2 100644 --- a/Meshtastic/Extensions/UserDefaults.swift +++ b/Meshtastic/Extensions/UserDefaults.swift @@ -71,6 +71,7 @@ extension UserDefaults { case modemPreset case firmwareVersion case environmentEnableWeatherKit + case enableAdministration case testIntEnum } @@ -162,6 +163,9 @@ extension UserDefaults { @UserDefault(.environmentEnableWeatherKit, defaultValue: true) static var environmentEnableWeatherKit: Bool + @UserDefault(.enableAdministration, defaultValue: false) + static var enableAdministration: Bool + @UserDefault(.testIntEnum, defaultValue: .one) static var testIntEnum: TestIntEnum } diff --git a/Meshtastic/Extensions/View.swift b/Meshtastic/Extensions/View.swift new file mode 100644 index 00000000..7349f36d --- /dev/null +++ b/Meshtastic/Extensions/View.swift @@ -0,0 +1,30 @@ +// +// View.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/14/24. +// + +import SwiftUI + +public extension View { + func onFirstAppear(_ action: @escaping () -> ()) -> some View { + modifier(FirstAppear(action: action)) + } +} + +private struct FirstAppear: ViewModifier { + let action: () -> () + + // Use this to only fire your block one time + @State private var hasAppeared = false + + func body(content: Content) -> some View { + // And then, track it here + content.onAppear { + guard !hasAppeared else { return } + hasAppeared = true + action() + } + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index 475235de..326f7966 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -893,6 +893,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate lastConnectionError = "" isSubscribed = true Logger.mesh.info("🤜 [BLE] Want Config Complete. ID:\(decodedInfo.configCompleteID)") + sendTime() peripherals.removeAll(where: { $0.peripheral.state == CBPeripheralState.disconnected }) // Config conplete returns so we don't read the characteristic again @@ -1000,6 +1001,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if toUserNum > 0 { newMessage.toUser = fetchedUsers.first(where: { $0.num == toUserNum }) newMessage.toUser?.lastMessage = Date() + if newMessage.toUser?.pkiEncrypted ?? false { + newMessage.publicKey = newMessage.toUser?.publicKey + newMessage.pkiEncrypted = true + } } newMessage.fromUser = fetchedUsers.first(where: { $0.num == fromUserNum }) newMessage.isEmoji = isEmoji @@ -1022,6 +1027,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate dataMessage.portnum = dataType var meshPacket = MeshPacket() + if newMessage.toUser?.pkiEncrypted ?? false { + meshPacket.pkiEncrypted = true + meshPacket.publicKey = newMessage.toUser?.publicKey ?? Data() + } meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) @@ -1140,6 +1149,11 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate guard let lastLocation = LocationsHandler.shared.locationsArray.last else { return nil } + + if lastLocation == CLLocation(latitude: 0, longitude: 0) { + return nil + } + positionPacket.latitudeI = Int32(lastLocation.coordinate.latitude * 1e7) positionPacket.longitudeI = Int32(lastLocation.coordinate.longitude * 1e7) let timestamp = lastLocation.timestamp @@ -1285,9 +1299,36 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } } + public func sendTime() -> Bool { + var adminPacket = AdminMessage() + adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970) + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = 0 + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.shutdownSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1313,6 +1354,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendReboot(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1338,6 +1382,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendRebootOta(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool { var adminPacket = AdminMessage() adminPacket.rebootOtaSeconds = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1363,6 +1410,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendEnterDfuMode(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.enterDfuModeRequest = true + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1388,7 +1438,10 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendFactoryReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() - adminPacket.factoryReset = 5 + adminPacket.factoryResetConfig = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1414,6 +1467,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func sendNodeDBReset(fromUser: UserEntity, toUser: UserEntity) -> Bool { var adminPacket = AdminMessage() adminPacket.nodedbReset = 5 + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = 0 // UInt32(fromUser.num) @@ -1626,6 +1682,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveUser(config: User, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setOwner = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1748,6 +1807,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveLicensedUser(ham: HamParameters, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setHamMode = ham + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1771,6 +1833,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveBluetoothConfig(config: Config.BluetoothConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.bluetooth = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1788,7 +1853,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } @@ -1799,7 +1864,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.device = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1816,7 +1883,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } return 0 @@ -1825,6 +1892,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate public func saveDisplayConfig(config: Config.DisplayConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { var adminPacket = AdminMessage() adminPacket.setConfig.display = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1843,7 +1913,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate meshPacket.decoded = dataMessage let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context) return Int64(meshPacket.id) } return 0 @@ -1853,6 +1923,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.lora = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1870,7 +1943,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)" if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) { - upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, context: context) + upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey,context: context) return Int64(meshPacket.id) } @@ -1881,7 +1954,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.position = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1943,7 +2018,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setConfig.network = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -1970,11 +2047,46 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate return 0 } + public func saveSecurityConfig(config: Config.SecurityConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 { + + var adminPacket = AdminMessage() + adminPacket.setConfig.security = config + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.channel = UInt32(adminIndex) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Int64 { var adminPacket = AdminMessage() adminPacket.setModuleConfig.ambientLighting = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2004,7 +2116,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setModuleConfig.cannedMessage = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2034,7 +2148,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setCannedMessageModuleMessages = messages - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2065,7 +2181,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.setModuleConfig.detectionSensor = config - + if fromUser != toUser { + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() + } var meshPacket: MeshPacket = MeshPacket() meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { + + var adminPacket = AdminMessage() + adminPacket.getConfigRequest = AdminMessage.ConfigType.securityConfig + var meshPacket: MeshPacket = MeshPacket() + meshPacket.to = UInt32(toUser.num) + meshPacket.from = UInt32(fromUser.num) + meshPacket.id = UInt32.random(in: UInt32(UInt8.max).. Bool { var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2649,7 +2804,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.cannedmsgConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2679,7 +2833,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.extnotifConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2709,7 +2862,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.paxcounterConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2739,7 +2891,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getRingtoneRequest = true - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2769,7 +2920,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.rangetestConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2799,7 +2949,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.mqttConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2829,7 +2978,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.detectionsensorConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2859,7 +3007,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.serialConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2889,7 +3036,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.storeforwardConfig - var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) @@ -2919,7 +3065,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var adminPacket = AdminMessage() adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.telemetryConfig - + adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data() var meshPacket: MeshPacket = MeshPacket() meshPacket.to = UInt32(toUser.num) meshPacket.from = UInt32(fromUser.num) diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index cfff9567..d3bef97a 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -49,7 +49,7 @@ func generateMessageMarkdown (message: String) -> String { } func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) { - // We don't care about any of the Power settings, config is available for everything else + if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) { @@ -64,6 +64,8 @@ func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int6 upsertPositionConfigPacket(config: config.position, nodeNum: nodeNum, context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { upsertPowerConfigPacket(config: config.power, nodeNum: nodeNum, context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: nodeNum, context: context) } } @@ -196,7 +198,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo } } -func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NSManagedObjectContext) { +func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { if metadata.isInitialized { let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex()) @@ -230,6 +232,10 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS newNode.metadata = newMetadata } } + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() } catch { @@ -276,6 +282,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje newTelemetries.append(telemetry) newNode.telemetries? = NSOrderedSet(array: newTelemetries) } + newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) newNode.snr = nodeInfo.snr @@ -296,6 +303,10 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje } newUser.isLicensed = nodeInfo.user.isLicensed newUser.role = Int32(nodeInfo.user.role.rawValue) + if !nodeInfo.user.publicKey.isEmpty { + newUser.pkiEncrypted = true + newUser.publicKey = nodeInfo.user.publicKey + } newNode.user = newUser } else if nodeInfo.num > Constants.minimumNodeNum { let newUser = createUser(num: Int64(nodeInfo.num), context: context) @@ -353,6 +364,11 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje if fetchedNode[0].user == nil { fetchedNode[0].user = UserEntity(context: context) } + // Set the public key for a user if it is empty, don't update + if fetchedNode[0].user?.publicKey?.isEmpty == nil && !nodeInfo.user.publicKey.isEmpty { + fetchedNode[0].user?.pkiEncrypted = true + fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey + } fetchedNode[0].user!.userId = nodeInfo.user.id fetchedNode[0].user!.num = Int64(nodeInfo.num) fetchedNode[0].user!.numString = String(nodeInfo.num) @@ -476,7 +492,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getChannelResponse(adminMessage.getChannelResponse) { channelPacket(channel: adminMessage.getChannelResponse, fromNum: Int64(packet.from), context: context) } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getDeviceMetadataResponse(adminMessage.getDeviceMetadataResponse) { - deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), context: context) + deviceMetadataPacket(metadata: adminMessage.getDeviceMetadataResponse, fromNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) { let config = adminMessage.getConfigResponse if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) { @@ -493,6 +509,8 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) { upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), context: context) } else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) { upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), context: context) + } else if config.payloadVariant == Config.OneOf_PayloadVariant.security(config.security) { + upsertSecurityConfigPacket(config: config.security, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context) } } else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) { let moduleConfig = adminMessage.getModuleConfigResponse @@ -614,13 +632,21 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana } } fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue) + if routingError == RoutingError.pkiFailed { + fetchedMessage[0].toUser?.keyMatch = false + fetchedMessage[0].toUser?.newPublicKey = fetchedMessage[0].publicKey + } if routingMessage.errorReason == Routing.Error.none { fetchedMessage[0].receivedACK = true } fetchedMessage[0].ackSNR = packet.rxSnr - fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + if packet.rxTime > 0 { + fetchedMessage[0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) + } else { + fetchedMessage[0].ackTimestamp = Int32(Date().timeIntervalSince1970) + } if fetchedMessage[0].toUser != nil { fetchedMessage[0].toUser!.objectWillChange.send() @@ -709,7 +735,11 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage return } mutableTelemetries.add(telemetry) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(truncatingIfNeeded: packet.rxTime))) + if packet.rxTime > 0 { + fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(packet.rxTime)) + } else { + fetchedNode[0].lastHeard = Date() + } fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet } try context.save() @@ -808,10 +838,10 @@ func textMessageAppPacket( let fetchedUsers = try context.fetch(messageUsers) let newMessage = MessageEntity(context: context) newMessage.messageId = Int64(packet.id) - if packet.rxTime == 0 { - newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) + if packet.rxTime > 0 { + newMessage.messageTimestamp = Int32(bitPattern: packet.rxTime) } else { - newMessage.messageTimestamp = Int32(packet.rxTime) + newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970) } newMessage.receivedACK = false newMessage.snr = packet.rxSnr @@ -819,6 +849,8 @@ func textMessageAppPacket( newMessage.isEmoji = packet.decoded.emoji == 1 newMessage.channel = Int32(packet.channel) newMessage.portNum = Int32(packet.decoded.portnum.rawValue) + newMessage.publicKey = packet.publicKey + newMessage.pkiEncrypted = packet.pkiEncrypted if packet.decoded.portnum == PortNum.detectionSensorApp { if !UserDefaults.enableDetectionNotifications { newMessage.read = true @@ -835,8 +867,21 @@ func textMessageAppPacket( } if fetchedUsers.first(where: { $0.num == packet.from }) != nil { newMessage.fromUser = fetchedUsers.first(where: { $0.num == packet.from }) + if !(newMessage.fromUser?.publicKey?.isEmpty ?? true) { + /// We have a key, check if it matches + if newMessage.fromUser?.publicKey != newMessage.publicKey { + newMessage.fromUser?.keyMatch = false + newMessage.fromUser?.newPublicKey = newMessage.publicKey + } + } else { + /// We have no key, set it + newMessage.fromUser?.publicKey = packet.publicKey + newMessage.fromUser?.pkiEncrypted = packet.pkiEncrypted + } if packet.rxTime > 0 { newMessage.fromUser?.userNode?.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newMessage.fromUser?.userNode?.lastHeard = Date() } } newMessage.messagePayload = messageText diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents index 6b2eaa00..40947b9f 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 42.xcdatamodel/contents @@ -155,7 +155,9 @@ + + @@ -224,6 +226,8 @@ + + @@ -245,6 +249,7 @@ + @@ -334,6 +339,17 @@ + + + + + + + + + + + @@ -418,11 +434,15 @@ + + + + diff --git a/Meshtastic/Persistence/Persistence.swift b/Meshtastic/Persistence/Persistence.swift index 93ba7f16..70e9f898 100644 --- a/Meshtastic/Persistence/Persistence.swift +++ b/Meshtastic/Persistence/Persistence.swift @@ -104,8 +104,8 @@ extension NSPersistentContainer { } /// Restore backup persistent stores located in the directory referenced by `backupURL`. - /// - /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. + /// **Be very careful with this**. To restore a persistent store, the current persistent store must be removed from the container. When that happens, **all currently loaded Core Data objects** will become invalid. Using them after restoring will cause your app to crash. + /// When calling this method you **must** ensure that you do not continue to use any previously fetched managed objects or existing fetched results controllers. **If this method does not throw, that does not mean your app is safe.** You need to take extra steps to prevent crashes. The details vary depending on the nature of your app. /// - Parameter backupURL: A file URL containing backup copies of all currently loaded persistent stores. /// - Throws: `CopyPersistentStoreError` in various situations. /// - Returns: Nothing. If no errors are thrown, the restore is complete. diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index c407f414..6f2520ee 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -148,6 +148,9 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if packet.rxTime > 0 { newNode.firstHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + newNode.firstHeard = Date() + newNode.lastHeard = Date() } newNode.snr = packet.rxSnr newNode.rssi = packet.rxRssi @@ -178,6 +181,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) newUser.role = Int32(newUserMessage.role.rawValue) newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() newUser.hwModelId = Int32(newUserMessage.hwModel.rawValue) + newUser.pkiEncrypted = packet.pkiEncrypted + newUser.publicKey = packet.publicKey Task { Api().loadDeviceHardwareData { (hw) in let dh = hw.first(where: { $0.hwModel == newUser.hwModelId }) @@ -231,9 +236,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].num = Int64(packet.from) if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - if fetchedNode[0].firstHeard == nil { - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) - } + } else { + fetchedNode[0].lastHeard = Date() } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi @@ -361,6 +365,8 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(positionMessage.time))) } else if packet.rxTime > 0 { fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(packet.rxTime))) + } else { + fetchedNode[0].lastHeard = Date() } fetchedNode[0].snr = packet.rxSnr fetchedNode[0].rssi = packet.rxRssi @@ -391,7 +397,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) } } -func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum)) MeshLogger.log("📶 \(logString)") @@ -416,6 +422,10 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin) fetchedNode[0].bluetoothConfig?.deviceLoggingEnabled = config.deviceLoggingEnabled } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [BluetoothConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -433,7 +443,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, } } -func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.device.config %@".localized, String(nodeNum)) MeshLogger.log("📟 \(logString)") @@ -471,6 +481,10 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, conte fetchedNode[0].deviceConfig?.isManaged = config.isManaged fetchedNode[0].deviceConfig?.tzdef = config.tzdef } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [DeviceConfigEntity] Updated Device Config for node number: \(nodeNum.toHex(), privacy: .public)") @@ -486,7 +500,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, conte } } -func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.display.config %@".localized, nodeNum.toHex()) MeshLogger.log("🖥️ \(logString)") @@ -512,7 +526,6 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con newDisplayConfig.units = Int32(config.units.rawValue) newDisplayConfig.headingBold = config.headingBold fetchedNode[0].displayConfig = newDisplayConfig - } else { fetchedNode[0].displayConfig?.gpsFormat = Int32(config.gpsFormat.rawValue) @@ -525,7 +538,10 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con fetchedNode[0].displayConfig?.units = Int32(config.units.rawValue) fetchedNode[0].displayConfig?.headingBold = config.headingBold } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() @@ -550,7 +566,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, con } } -func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.lora.config %@".localized, nodeNum.toHex()) MeshLogger.log("📻 \(logString)") @@ -598,6 +614,10 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: fetchedNode[0].loRaConfig?.ignoreMqtt = config.ignoreMqtt fetchedNode[0].loRaConfig?.sx126xRxBoostedGain = config.sx126XRxBoostedGain } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [LoRaConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -615,7 +635,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, context: } } -func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.network.config %@".localized, String(nodeNum)) MeshLogger.log("🌐 \(logString)") @@ -640,7 +660,10 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, con fetchedNode[0].networkConfig?.wifiSsid = config.wifiSsid fetchedNode[0].networkConfig?.wifiPsk = config.wifiPsk } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [NetworkConfigEntity] Updated Network Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -659,7 +682,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, con } } -func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.position.config %@".localized, String(nodeNum)) MeshLogger.log("🗺️ \(logString)") @@ -702,6 +725,10 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, c fetchedNode[0].positionConfig?.gpsUpdateInterval = Int32(config.gpsUpdateInterval) fetchedNode[0].positionConfig?.positionFlags = Int32(config.positionFlags) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PositionConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -719,7 +746,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, c } } -func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.power.config %@".localized, String(nodeNum)) MeshLogger.log("🗺️ \(logString)") @@ -749,6 +776,10 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context fetchedNode[0].powerConfig?.onBatteryShutdownAfterSecs = Int32(truncatingIfNeeded: config.onBatteryShutdownAfterSecs) fetchedNode[0].powerConfig?.waitBluetoothSecs = Int32(truncatingIfNeeded: config.waitBluetoothSecs) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PowerConfigEntity] Updated Power Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -766,7 +797,60 @@ func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, context } } -func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { + + let logString = String.localizedStringWithFormat("mesh.log.security.config %@".localized, String(nodeNum)) + MeshLogger.log("🛡️ \(logString)") + + let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() + fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) + + do { + let fetchedNode = try context.fetch(fetchNodeInfoRequest) + // Found a node, save Security Config + if !fetchedNode.isEmpty { + if fetchedNode[0].securityConfig == nil { + let newSecurityConfig = SecurityConfigEntity(context: context) + newSecurityConfig.publicKey = config.publicKey + newSecurityConfig.privateKey = config.privateKey + newSecurityConfig.adminKey = config.adminKey + newSecurityConfig.isManaged = config.isManaged + newSecurityConfig.serialEnabled = config.serialEnabled + newSecurityConfig.debugLogApiEnabled = config.debugLogApiEnabled + newSecurityConfig.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + fetchedNode[0].securityConfig = newSecurityConfig + } else { + fetchedNode[0].securityConfig?.publicKey = config.publicKey + fetchedNode[0].securityConfig?.privateKey = config.privateKey + fetchedNode[0].securityConfig?.adminKey = config.adminKey + fetchedNode[0].securityConfig?.isManaged = config.isManaged + fetchedNode[0].securityConfig?.serialEnabled = config.serialEnabled + fetchedNode[0].securityConfig?.debugLogApiEnabled = config.debugLogApiEnabled + fetchedNode[0].securityConfig?.bluetoothLoggingEnabled = config.bluetoothLoggingEnabled + } + if sessionPasskey?.count != 0 { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } + do { + try context.save() + Logger.data.info("💾 [SecurityConfigEntity] Updated Security Config for node: \(nodeNum.toHex(), privacy: .public)") + + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("💥 [SecurityConfigEntity] Error Updating Core Data: \(nsError, privacy: .public)") + } + } else { + Logger.data.error("💥 [SecurityConfigEntity] No Nodes found in local database matching node \(nodeNum.toHex(), privacy: .public) unable to save Security Config") + } + } catch { + let nsError = error as NSError + Logger.data.error("💥 [SecurityConfigEntity] Fetching node for core data failed: \(nsError, privacy: .public)") + } +} + +func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum)) MeshLogger.log("🏮 \(logString)") @@ -780,16 +864,13 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context) - newAmbientLightingConfig.ledState = config.ledState newAmbientLightingConfig.current = Int32(config.current) newAmbientLightingConfig.red = Int32(config.red) newAmbientLightingConfig.green = Int32(config.green) newAmbientLightingConfig.blue = Int32(config.blue) fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig - } else { if fetchedNode[0].ambientLightingConfig == nil { @@ -801,7 +882,10 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin fetchedNode[0].ambientLightingConfig?.green = Int32(config.green) fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [AmbientLightingConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -819,7 +903,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin } } -func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum)) MeshLogger.log("🥫 \(logString)") @@ -833,9 +917,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo if !fetchedNode.isEmpty { if fetchedNode[0].cannedMessageConfig == nil { - let newCannedMessageConfig = CannedMessageConfigEntity(context: context) - newCannedMessageConfig.enabled = config.enabled newCannedMessageConfig.sendBell = config.sendBell newCannedMessageConfig.rotary1Enabled = config.rotary1Enabled @@ -846,11 +928,8 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo newCannedMessageConfig.inputbrokerEventCw = Int32(config.inputbrokerEventCw.rawValue) newCannedMessageConfig.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) newCannedMessageConfig.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) - fetchedNode[0].cannedMessageConfig = newCannedMessageConfig - } else { - fetchedNode[0].cannedMessageConfig?.enabled = config.enabled fetchedNode[0].cannedMessageConfig?.sendBell = config.sendBell fetchedNode[0].cannedMessageConfig?.rotary1Enabled = config.rotary1Enabled @@ -862,7 +941,10 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo fetchedNode[0].cannedMessageConfig?.inputbrokerEventCcw = Int32(config.inputbrokerEventCcw.rawValue) fetchedNode[0].cannedMessageConfig?.inputbrokerEventPress = Int32(config.inputbrokerEventPress.rawValue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [CannedMessageConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -880,7 +962,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo } } -func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum)) MeshLogger.log("🕵️ \(logString)") @@ -892,21 +974,17 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Detection Sensor Config if !fetchedNode.isEmpty { - if fetchedNode[0].detectionSensorConfig == nil { - let newConfig = DetectionSensorConfigEntity(context: context) newConfig.enabled = config.enabled newConfig.sendBell = config.sendBell newConfig.name = config.name - newConfig.monitorPin = Int32(config.monitorPin) newConfig.detectionTriggeredHigh = config.detectionTriggeredHigh newConfig.usePullup = config.usePullup newConfig.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) newConfig.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) fetchedNode[0].detectionSensorConfig = newConfig - } else { fetchedNode[0].detectionSensorConfig?.enabled = config.enabled fetchedNode[0].detectionSensorConfig?.sendBell = config.sendBell @@ -917,7 +995,10 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso fetchedNode[0].detectionSensorConfig?.minimumBroadcastSecs = Int32(truncatingIfNeeded: config.minimumBroadcastSecs) fetchedNode[0].detectionSensorConfig?.stateBroadcastSecs = Int32(truncatingIfNeeded: config.stateBroadcastSecs) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [DetectionSensorConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -938,7 +1019,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso } } -func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum)) MeshLogger.log("📣 \(logString)") @@ -969,7 +1050,6 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig - } else { fetchedNode[0].externalNotificationConfig?.enabled = config.enabled fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm @@ -987,7 +1067,10 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [ExternalNotificationConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1005,7 +1088,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN } } -func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum)) MeshLogger.log("🧑‍🤝‍🧑 \(logString)") @@ -1017,19 +1100,19 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save PAX Counter Config if !fetchedNode.isEmpty { - if fetchedNode[0].paxCounterConfig == nil { let newPaxCounterConfig = PaxCounterConfigEntity(context: context) newPaxCounterConfig.enabled = config.enabled newPaxCounterConfig.updateInterval = Int32(config.paxcounterUpdateInterval) - fetchedNode[0].paxCounterConfig = newPaxCounterConfig - } else { fetchedNode[0].paxCounterConfig?.enabled = config.enabled fetchedNode[0].paxCounterConfig?.updateInterval = Int32(config.paxcounterUpdateInterval) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [PaxCounterConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1047,7 +1130,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n } } -func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum)) MeshLogger.log("⛰️ \(logString)") @@ -1066,6 +1149,10 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage } else { fetchedNode[0].rtttlConfig?.ringtone = ringtone } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [RtttlConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1083,7 +1170,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, context: NSManage } } -func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.mqtt.config %@".localized, String(nodeNum)) MeshLogger.log("🌉 \(logString)") @@ -1095,7 +1182,6 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save MQTT Config if !fetchedNode.isEmpty { - if fetchedNode[0].mqttConfig == nil { let newMQTTConfig = MQTTConfigEntity(context: context) newMQTTConfig.enabled = config.enabled @@ -1125,6 +1211,10 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [MQTTConfigEntity] Updated for node number: \(nodeNum.toHex(), privacy: .public)") @@ -1142,7 +1232,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6 } } -func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.rangetest.config %@".localized, String(nodeNum)) MeshLogger.log("⛰️ \(logString)") @@ -1165,6 +1255,10 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod fetchedNode[0].rangeTestConfig?.enabled = config.enabled fetchedNode[0].rangeTestConfig?.save = config.save } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [RangeTestConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1182,7 +1276,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod } } -func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.serial.config %@".localized, String(nodeNum)) MeshLogger.log("🤖 \(logString)") @@ -1194,9 +1288,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Device Config if !fetchedNode.isEmpty { - if fetchedNode[0].serialConfig == nil { - let newSerialConfig = SerialConfigEntity(context: context) newSerialConfig.enabled = config.enabled newSerialConfig.echo = config.echo @@ -1206,7 +1298,6 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: newSerialConfig.timeout = Int32(config.timeout) newSerialConfig.mode = Int32(config.mode.rawValue) fetchedNode[0].serialConfig = newSerialConfig - } else { fetchedNode[0].serialConfig?.enabled = config.enabled fetchedNode[0].serialConfig?.echo = config.echo @@ -1216,7 +1307,10 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: fetchedNode[0].serialConfig?.timeout = Int32(config.timeout) fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue) } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [SerialConfigEntity]Updated Serial Module Config for node: \(nodeNum.toHex(), privacy: .public)") @@ -1237,7 +1331,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: } } -func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum)) MeshLogger.log("📬 \(logString)") @@ -1249,9 +1343,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Store & Forward Sensor Config if !fetchedNode.isEmpty { - if fetchedNode[0].storeForwardConfig == nil { - let newConfig = StoreForwardConfigEntity(context: context) newConfig.enabled = config.enabled newConfig.heartbeat = config.heartbeat @@ -1259,7 +1351,6 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi newConfig.historyReturnMax = Int32(config.historyReturnMax) newConfig.historyReturnWindow = Int32(config.historyReturnWindow) fetchedNode[0].storeForwardConfig = newConfig - } else { fetchedNode[0].storeForwardConfig?.enabled = config.enabled fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat @@ -1267,6 +1358,10 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax) fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow) } + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [StoreForwardConfigEntity] Updated for node: \(nodeNum.toHex(), privacy: .public)") @@ -1284,21 +1379,18 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi } } -func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, context: NSManagedObjectContext) { +func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) { let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum)) MeshLogger.log("📈 \(logString)") let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest() fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) - do { let fetchedNode = try context.fetch(fetchNodeInfoRequest) // Found a node, save Telemetry Config if !fetchedNode.isEmpty { - if fetchedNode[0].telemetryConfig == nil { - let newTelemetryConfig = TelemetryConfigEntity(context: context) newTelemetryConfig.deviceUpdateInterval = Int32(config.deviceUpdateInterval) newTelemetryConfig.environmentUpdateInterval = Int32(config.environmentUpdateInterval) @@ -1309,7 +1401,6 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod newTelemetryConfig.powerUpdateInterval = Int32(config.powerUpdateInterval) newTelemetryConfig.powerScreenEnabled = config.powerScreenEnabled fetchedNode[0].telemetryConfig = newTelemetryConfig - } else { fetchedNode[0].telemetryConfig?.deviceUpdateInterval = Int32(config.deviceUpdateInterval) fetchedNode[0].telemetryConfig?.environmentUpdateInterval = Int32(config.environmentUpdateInterval) @@ -1320,7 +1411,10 @@ func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nod fetchedNode[0].telemetryConfig?.powerUpdateInterval = Int32(config.powerUpdateInterval) fetchedNode[0].telemetryConfig?.powerScreenEnabled = config.powerScreenEnabled } - + if sessionPasskey != nil { + fetchedNode[0].sessionPasskey = sessionPasskey + fetchedNode[0].sessionExpiration = Date().addingTimeInterval(300) + } do { try context.save() Logger.data.info("💾 [TelemetryConfigEntity] Updated Telemetry Module Config for node: \(nodeNum.toHex(), privacy: .public)") diff --git a/Meshtastic/Resources/DeviceHardware.json b/Meshtastic/Resources/DeviceHardware.json index 09eb2e6c..cbad8df9 100644 --- a/Meshtastic/Resources/DeviceHardware.json +++ b/Meshtastic/Resources/DeviceHardware.json @@ -370,7 +370,7 @@ { "hwModel": 66, "hwModelSlug": "HELTEC_VISION_MASTER_T190", - "platformioTarget": "heltec-vision-master-T190", + "platformioTarget": "heltec-vision-master-t190", "architecture": "esp32-s3", "activelySupported": true, "displayName": "Heltec Vision Master T190" diff --git a/Meshtastic/Router/NavigationState.swift b/Meshtastic/Router/NavigationState.swift index b33fc48a..0ff61108 100644 --- a/Meshtastic/Router/NavigationState.swift +++ b/Meshtastic/Router/NavigationState.swift @@ -46,6 +46,7 @@ enum SettingsNavigationState: String { case paxCounter case ringtone case serial + case security case storeAndForward case telemetry case meshLog diff --git a/Meshtastic/Tips/MessagesTips.swift b/Meshtastic/Tips/MessagesTips.swift index d78daa0e..4771e386 100644 --- a/Meshtastic/Tips/MessagesTips.swift +++ b/Meshtastic/Tips/MessagesTips.swift @@ -25,22 +25,3 @@ struct MessagesTip: Tip { Image(systemName: "bubble.left.and.bubble.right") } } - -@available(iOS 17.0, macOS 14.0, *) -struct ContactsTip: Tip { - - var id: String { - return "tip.messages.contacts" - } - var title: Text { - // Text("tip.messages.contacts.title") - Text("Contacts") - } - var message: Text? { - // Text("tip.messages.contacts.message") - Text("Each node is an available contact. Contacts with recent messages or marked as favorites show up at the top of the list. Select a contact to send or view messages. Long press to favorite or mute the contact or delete the conversation.") - } - var image: Image? { - Image(systemName: "person.circle") - } -} diff --git a/Meshtastic/Views/Helpers/BatteryCompact.swift b/Meshtastic/Views/Helpers/BatteryCompact.swift index 28f62a57..bc714da6 100644 --- a/Meshtastic/Views/Helpers/BatteryCompact.swift +++ b/Meshtastic/Views/Helpers/BatteryCompact.swift @@ -7,7 +7,7 @@ import SwiftUI struct BatteryCompact: View { - @State var batteryLevel: Int32 + var batteryLevel: Int32 var font: Font var iconFont: Font var color: Color diff --git a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift b/Meshtastic/Views/Helpers/BatteryLevelCompact.swift deleted file mode 100644 index e1354e23..00000000 --- a/Meshtastic/Views/Helpers/BatteryLevelCompact.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// BatteryIcon.swift -// Meshtastic -// -// Copyright Garth Vander Houwen 3/24/23. -// -import SwiftUI - -struct BatteryLevelCompact: View { - - @ObservedObject var node: NodeInfoEntity - - var font: Font - var iconFont: Font - var color: Color - - var body: some View { - let deviceMetrics = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")) - let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity - let batteryLevel = mostRecent?.batteryLevel ?? 0 - if deviceMetrics?.count ?? 0 > 0 { - BatteryCompact(batteryLevel: batteryLevel, font: font, iconFont: iconFont, color: color) - } - } -} diff --git a/Meshtastic/Views/Helpers/Help/AckErrors.swift b/Meshtastic/Views/Helpers/Help/AckErrors.swift new file mode 100644 index 00000000..c20a58f9 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/AckErrors.swift @@ -0,0 +1,44 @@ +// +// IAQScale.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 4/24/24. +// + +import SwiftUI + +struct AckErrors: View { + + var body: some View { + VStack(alignment: .leading) { + Text("Message Status Options") + .font(.title2) + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(.orange) + .frame(width: 20, height: 12) + Text("Acknowledged by another node") + .font(.caption) + .foregroundStyle(.orange) + } + ForEach(RoutingError.allCases) { re in + HStack { + RoundedRectangle(cornerRadius: 5) + .fill(re.color) + .frame(width: 20, height: 12) + Text(re.display) + .font(.caption) + .foregroundStyle(re.color) + } + } + } + } +} + +struct AckErrorsPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift new file mode 100644 index 00000000..fd0f0414 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/DirectMessagesHelp.swift @@ -0,0 +1,76 @@ +// +// DirectMessagesHelp.swift +// Meshtastic +// +// Copyright Garth Vander Houwen on 8/15/24. +// + +import SwiftUI + +struct DirectMessagesHelp: View { + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.dismiss) private var dismiss + + var body: some View { + ScrollView { + Label("Direct Message Help", systemImage: "questionmark.circle") + .font(.title) + .padding(.vertical) + VStack(alignment: .leading) { + HStack { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .padding(.bottom) + Text("Favorites and nodes with recent messages show up at the top of the contact list.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + HStack { + Image(systemName: "hand.tap") + .padding(.bottom) + Text("Long press to favorite or mute the contact or delete a conversation.") + .fixedSize(horizontal: false, vertical: true) + .padding(.bottom) + } + } + if idiom == .phone { + VStack(alignment: .leading) { + LockLegend() + AckErrors() + } + } else { + HStack(alignment: .top) { + LockLegend() + AckErrors() + .padding(.trailing) + } + } +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .frame(minHeight: 0, maxHeight: .infinity, alignment: .leading) + .padding() + .presentationDetents([.large]) + .presentationContentInteraction(.scrolls) + .presentationDragIndicator(.visible) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) + } +} + +struct DirectMessagesHelpPreviews: PreviewProvider { + static var previews: some View { + VStack { + AckErrors() + } + } +} diff --git a/Meshtastic/Views/Helpers/Help/LockLegend.swift b/Meshtastic/Views/Helpers/Help/LockLegend.swift new file mode 100644 index 00000000..36716798 --- /dev/null +++ b/Meshtastic/Views/Helpers/Help/LockLegend.swift @@ -0,0 +1,66 @@ +// +// LockLegend.swift +// Meshtastic +// +// Copyright Garth Vander Houwen 8/15/24. +// + +import SwiftUI + +struct LockLegend: View { + + var body: some View { + VStack(alignment: .leading) { + Text("What does the lock mean?") + .font(.title2) + .padding(.bottom, 5) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + Text("Shared Key") + .fontWeight(.semibold) + } + Text("Direct messages are using the shared key for the channel.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "lock.fill") + .foregroundColor(.green) + Text("Public Key Encryption") + .fontWeight(.semibold) + } + Text("Direct messages are using the new public key infrastructure for encryption. Reguires firmware version 2.5 or greater.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + VStack(alignment: .leading) { + HStack { + Image(systemName: "key.slash") + .foregroundColor(.red) + Text("Public Key Mismatch") + .fontWeight(.semibold) + } + Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + .padding(.bottom) + } + } +} + +struct LockLegendPreviews: PreviewProvider { + static var previews: some View { + VStack { + LockLegend() + } + } +} diff --git a/Meshtastic/Views/Helpers/SecureInput.swift b/Meshtastic/Views/Helpers/SecureInput.swift new file mode 100644 index 00000000..b4fa54eb --- /dev/null +++ b/Meshtastic/Views/Helpers/SecureInput.swift @@ -0,0 +1,57 @@ +// +// SecureInput.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/12/24. +// + +import SwiftUI + +struct SecureInput: View { + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Binding private var text: String + @State private var isSecure: Bool = true + private var title: String + + init(_ title: String, text: Binding) { + self.title = title + self._text = text + } + + var body: some View { + ZStack(alignment: .trailing) { + Group { + if isSecure { + SecureField(title, text: $text) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() + .keyboardType(.alphabet) + .foregroundStyle(.tertiary) + .disableAutocorrection(true) + .textSelection(.enabled) + } else { + TextField(title, text: $text, axis: .vertical) + .font(idiom == .phone ? .caption : .callout) + .allowsTightening(true) + .monospaced() + .keyboardType(.alphabet) + .foregroundStyle(.tertiary) + .disableAutocorrection(true) + .textSelection(.enabled) + .lineLimit(...3) + } + }.padding(.trailing, 36) + + if !text.isEmpty { + Button(action: { + isSecure.toggle() + }) { + Image(systemName: self.isSecure ? "eye.slash" : "eye") + .accentColor(.secondary) + } + } + } + } +} diff --git a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift index 8f057610..27675141 100644 --- a/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift +++ b/Meshtastic/Views/Helpers/Weather/LocalWeatherConditions.swift @@ -101,7 +101,7 @@ struct WeatherConditionsCompactWidget: View { .font(temperature.length < 4 ? .system(size: 90) : .system(size: 60) ) } .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -123,7 +123,7 @@ struct HumidityCompactWidget: View { } .padding(.horizontal) .frame(maxWidth: .infinity) - .frame(height: 175) + .frame(height: 150) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -135,16 +135,15 @@ struct PressureCompactWidget: View { var body: some View { VStack(alignment: .leading) { Label { Text("PRESSURE") } icon: { Image(systemName: "gauge").symbolRenderingMode(.multicolor) } - .font(.caption2) + .font(.callout) Text(pressure) .font(pressure.length < 7 ? .system(size: 35) : .system(size: 30) ) Text(low ? "LOW" : "HIGH") .padding(.bottom) Text(unit) } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + .padding(.horizontal, 5) + .frame(width: 175, height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } @@ -156,17 +155,14 @@ struct WindCompactWidget: View { var body: some View { VStack(alignment: .leading) { Label { Text("WIND") } icon: { Image(systemName: "wind").foregroundColor(.accentColor) } - .font(.caption) Text("\(direction)") - .font(.caption) .padding(.bottom, 10) Text(speed) .font(.system(size: 35)) Text("Gusts \(gust)") } - .padding(.horizontal) - .frame(maxWidth: .infinity) - .frame(height: 175) + //.padding(.horizontal) + .frame(width: 175, height: 175) .background(.tertiary, in: RoundedRectangle(cornerRadius: 20, style: .continuous)) } } diff --git a/Meshtastic/Views/Messages/ChannelMessageList.swift b/Meshtastic/Views/Messages/ChannelMessageList.swift index b996e790..95b727f0 100644 --- a/Meshtastic/Views/Messages/ChannelMessageList.swift +++ b/Meshtastic/Views/Messages/ChannelMessageList.swift @@ -84,16 +84,14 @@ struct ChannelMessageList: View { } HStack { - if currentUser && message.receivedACK { - // Ack Received - Text("Acknowledged").font(.caption2).foregroundColor(.gray) - } else if currentUser && message.ackError == 0 { + if currentUser && message.ackError == 0 { // Empty Error Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange) } else if currentUser && message.ackError > 0 { let ackErrorVal = RoutingError(rawValue: Int(message.ackError)) Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) + .foregroundStyle(ackErrorVal?.color ?? .red) + .font(.caption2) } else if isDetectionSensorMessage { let messageDate = message.timestamp Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray) diff --git a/Meshtastic/Views/Messages/MessageContextMenuItems.swift b/Meshtastic/Views/Messages/MessageContextMenuItems.swift index ab363f59..c9c37cdc 100644 --- a/Meshtastic/Views/Messages/MessageContextMenuItems.swift +++ b/Meshtastic/Views/Messages/MessageContextMenuItems.swift @@ -13,6 +13,9 @@ struct MessageContextMenuItems: View { var body: some View { VStack { + if message.pkiEncrypted { + Label("Encrypted", systemImage: "lock") + } Text("channel") + Text(": \(message.channel)") } @@ -53,6 +56,7 @@ struct MessageContextMenuItems: View { let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp)) Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray) } + if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 { VStack { Text("SNR \(String(format: "%.2f", message.snr)) dB") @@ -60,7 +64,7 @@ struct MessageContextMenuItems: View { } } else if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) { VStack { - Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)) dB") + Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)") } } if isCurrentUser && message.receivedACK { diff --git a/Meshtastic/Views/Messages/MessageText.swift b/Meshtastic/Views/Messages/MessageText.swift index 0cdd4be8..93f8cf25 100644 --- a/Meshtastic/Views/Messages/MessageText.swift +++ b/Meshtastic/Views/Messages/MessageText.swift @@ -24,11 +24,25 @@ struct MessageText: View { let markdownText = LocalizedStringKey(message.messagePayloadMarkdown ?? (message.messagePayload ?? "EMPTY MESSAGE")) return Text(markdownText) .tint(Self.linkBlue) - .padding(10) + .padding(.vertical, 10) + .padding(.horizontal, 8) .foregroundColor(.white) .background(isCurrentUser ? .accentColor : Color(.gray)) .cornerRadius(15) .overlay { + if message.pkiEncrypted { + VStack(alignment: .trailing) { + Spacer() + HStack { + Spacer() + Image(systemName: "lock.circle.fill") + .symbolRenderingMode(.palette) + .foregroundStyle(.white, .green) + .font(.system(size: 20)) + .offset(x: 8, y: 8) + } + } + } let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue) if tapBackDestination.overlaySensorMessage { VStack { diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index 13eb837d..cb3582ab 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -20,6 +20,7 @@ struct UserList: View { @State private var viaLora = true @State private var viaMqtt = true @State private var isOnline = false + @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isEnvironment = false @State private var distanceFilter = false @@ -27,11 +28,23 @@ struct UserList: View { @State private var hopsAway: Double = -1.0 @State private var roleFilter = false @State private var deviceRoles: Set = [] - @State var isEditingFilters = false + @State private var editingFilters = false + @State private var showingHelp = false + @State private var showingTrustConfirm: Bool = false + + var boolFilters: [Bool] {[ + isFavorite, + isOnline, + isPkiEncrypted, + isEnvironment, + distanceFilter, + roleFilter + ]} @FetchRequest( sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false), NSSortDescriptor(key: "userNode.favorite", ascending: false), + NSSortDescriptor(key: "pkiEncrypted", ascending: false), NSSortDescriptor(key: "longName", ascending: true)], animation: .default ) @@ -47,9 +60,6 @@ struct UserList: View { let dateFormatString = (localeDateFormat ?? "MM/dd/YY") VStack { List(selection: $userSelection) { - if #available(iOS 17.0, macOS 14.0, *) { - TipView(ContactsTip(), arrowEdge: .bottom) - } ForEach(users) { (user: UserEntity) in let mostRecent = user.messageList.last let lastMessageTime = Date(timeIntervalSince1970: TimeInterval(Int64((mostRecent?.messageTimestamp ?? 0 )))) @@ -69,8 +79,22 @@ struct UserList: View { VStack(alignment: .leading) { HStack { + if user.pkiEncrypted { + if !user.keyMatch { + /// Public Key on the User and the Public Key on the Last Message don't match + Image(systemName: "key.slash") + .foregroundColor(.red) + } else { + Image(systemName: "lock.fill") + .foregroundColor(.green) + } + } else { + Image(systemName: "lock.open.fill") + .foregroundColor(.yellow) + } Text(user.longName ?? "unknown".localized) .font(.headline) + .allowsTightening(true) Spacer() if user.userNode?.favorite ?? false { Image(systemName: "star.fill") @@ -169,9 +193,12 @@ struct UserList: View { } } .listStyle(.plain) - .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count - 1))) - .sheet(isPresented: $isEditingFilters) { - NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) + .navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count))) + .sheet(isPresented: $editingFilters) { + NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles) + } + .sheet(isPresented: $showingHelp) { + DirectMessagesHelp() } .onChange(of: searchText) { _ in searchUserList() @@ -194,40 +221,46 @@ struct UserList: View { .onChange(of: hopsAway) { _ in searchUserList() } - .onChange(of: isOnline) { _ in - searchUserList() - } - .onChange(of: isFavorite) { _ in + .onChange(of: [boolFilters]) { _ in searchUserList() } .onChange(of: maxDistance) { _ in searchUserList() } - .onChange(of: distanceFilter) { _ in + .onFirstAppear { searchUserList() } - .onAppear { - searchUserList() - } - .safeAreaInset(edge: .bottom, alignment: .trailing) { + .safeAreaInset(edge: .bottom, alignment: .leading) { HStack { Button(action: { withAnimation { - isEditingFilters = !isEditingFilters + showingHelp = !showingHelp } }) { - Image(systemName: !isEditingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") + Image(systemName: !editingFilters ? "questionmark.circle" : "questionmark.circle.fill") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + Spacer() + Button(action: { + withAnimation { + editingFilters = !editingFilters + } + }) { + Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) - } .controlSize(.regular) .padding(5) } .padding(.bottom, 5) + .padding(.bottom, 5) .searchable(text: $searchText, placement: users.count > 10 ? .navigationBarDrawer(displayMode: .always) : .automatic, prompt: "Find a contact") .disableAutocorrection(true) .scrollDismissesKeyboard(.immediately) @@ -277,6 +310,11 @@ struct UserList: View { let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } + /// Encrypted + if isPkiEncrypted { + let isPkiEncryptedPredicate = NSPredicate(format: "pkiEncrypted == YES") + predicates.append(isPkiEncryptedPredicate) + } /// Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "userNode.favorite == YES") diff --git a/Meshtastic/Views/Messages/UserMessageList.swift b/Meshtastic/Views/Messages/UserMessageList.swift index ce0eb182..b0ec3a10 100644 --- a/Meshtastic/Views/Messages/UserMessageList.swift +++ b/Meshtastic/Views/Messages/UserMessageList.swift @@ -72,7 +72,9 @@ struct UserMessageList: View { if currentUser && message.receivedACK { // Ack Received if message.realACK { - Text("\(ackErrorVal?.display ?? "Empty Ack Error")").font(.caption2).foregroundColor(.gray) + Text("\(ackErrorVal?.display ?? "Empty Ack Error")") + .font(.caption2) + .foregroundStyle(ackErrorVal?.color ?? Color.secondary) } else { Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange) } @@ -81,7 +83,7 @@ struct UserMessageList: View { Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow) } else if currentUser && message.ackError > 0 { Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true) - .font(.caption2).foregroundColor(.red) + .foregroundStyle(ackErrorVal?.color ?? Color.red) } } } diff --git a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift index f0d2dd04..571511cf 100644 --- a/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift +++ b/Meshtastic/Views/Nodes/EnvironmentMetricsLog.swift @@ -93,6 +93,12 @@ struct EnvironmentMetricsLog: View { IndoorAirQuality(iaq: Int(em.iaq), displayMode: IaqDisplayMode.dot ) } } + TableColumn("Wind Speed") { em in + Text("\(String(format: "%.1f", em.windSpeed)) hPa") + } + TableColumn("Wind Direction") { em in + Text("\(String(format: "%.1f", em.windDirection)) hPa") + } TableColumn("timestamp") { em in Text(em.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized) } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index b3b9c18e..6a717629 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -14,6 +14,7 @@ struct PositionPopover: View { @ObservedObject var locationsHandler = LocationsHandler.shared @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } @Environment(\.dismiss) private var dismiss var position: PositionEntity var popover: Bool = true @@ -52,9 +53,13 @@ struct PositionPopover: View { VStack(alignment: .leading) { /// Time Label { - Text("heard".localized + ":") + if idiom != .phone { + Text("heard".localized + ":") + } LastHeardText(lastHeard: position.time) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } icon: { Image(systemName: position.nodePosition?.isOnline ?? false ? "checkmark.circle.fill" : "moon.circle.fill") .symbolRenderingMode(.hierarchical) @@ -67,12 +72,28 @@ struct PositionPopover: View { Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))") .textSelection(.enabled) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } icon: { Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) + /// Hops Away + if position.nodePosition?.hopsAway ?? 0 > 0 { + Label { + Text("Hops Away: \(position.nodePosition?.hopsAway ?? 0)") + .textSelection(.enabled) + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "hare") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } /// Altitude Label { let formatter = MeasurementFormatter() @@ -81,9 +102,11 @@ struct PositionPopover: View { if Locale.current.measurementSystem == .metric { Text(altitudeFormatter.string(from: distanceInMeters)) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } else { Text(altitudeFormatter.string(from: distanceInFeet)) .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } } icon: { @@ -98,6 +121,7 @@ struct PositionPopover: View { Label { Text("Sats in view: \(String(position.satsInView))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "sparkles") .symbolRenderingMode(.hierarchical) @@ -110,6 +134,7 @@ struct PositionPopover: View { Label { Text("Sequence: \(String(position.seqNo))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "number") .symbolRenderingMode(.hierarchical) @@ -129,11 +154,28 @@ struct PositionPopover: View { .rotationEffect(degrees) } .padding(.bottom, 5) + /// Distance + if let lastLocation = locationsHandler.locationsArray.last { + /// Distance + if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) + Label { + Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + } + } /// Speed let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) Label { Text("Speed: \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))") .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "gauge.with.dots.needle.33percent") .symbolRenderingMode(.hierarchical) @@ -144,6 +186,7 @@ struct PositionPopover: View { Label { Text("MQTT") + .font(idiom == .phone ? .callout : .body) } icon: { Image(systemName: "network") .symbolRenderingMode(.hierarchical) @@ -152,20 +195,6 @@ struct PositionPopover: View { } .padding(.bottom, 5) } - if let lastLocation = locationsHandler.locationsArray.last { - /// Distance - if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) - Label { - Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") - .foregroundColor(.primary) - } icon: { - Image(systemName: "lines.measurement.horizontal") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - } - } Spacer() } Spacer() @@ -223,9 +252,9 @@ struct PositionPopover: View { #endif } } - .presentationDetents([.medium, .large]) + .presentationDetents([.fraction(0.65), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 68769280..8a78c08c 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -44,6 +44,24 @@ struct NodeDetail: View { NodeInfoItem(node: node) } Section("Node") { + if let user = node.user { + if !user.keyMatch { + Label { + VStack(alignment: .leading) { + Text("Public Key Mismatch") + .font(.title3) + .foregroundStyle(.red) + Text("The public key does not match the recorded key. You may delete the node and let it exchange keys again, but this may indicate a more serious security problem. Contact the user through another trusted channel, to determine if the key change was due to a factory reset or other intentional action.") + .font(.caption) + .foregroundStyle(.red) + } + } icon: { + Image(systemName: "key.slash.fill") + .symbolRenderingMode(.multicolor) + .foregroundStyle(.red) + } + } + } HStack { Label { Text("Node Number") diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift index 6236f7b7..66d88b28 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListFilter.swift @@ -15,6 +15,7 @@ struct NodeListFilter: View { @Binding var viaLora: Bool @Binding var viaMqtt: Bool @Binding var isOnline: Bool + @Binding var isPkiEncrypted: Bool @Binding var isFavorite: Bool @Binding var isEnvironment: Bool @Binding var distanceFilter: Bool @@ -64,6 +65,19 @@ struct NodeListFilter: View { .toggleStyle(SwitchToggleStyle(tint: .accentColor)) .listRowSeparator(.visible) + Toggle(isOn: $isPkiEncrypted) { + + Label { + Text("Encrypted") + } icon: { + Image(systemName: "lock.fill") + .foregroundColor(.green) + .symbolRenderingMode(.hierarchical) + } + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + .listRowSeparator(.visible) + Toggle(isOn: $isFavorite) { Label { @@ -173,9 +187,9 @@ struct NodeListFilter: View { .padding(.bottom) #endif } - .presentationDetents([.medium, .large]) + .presentationDetents([.fraction(0.75), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) - .presentationBackgroundInteraction(.enabled(upThrough: .medium)) + .presentationBackgroundInteraction(.enabled(upThrough: .large)) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index ba7dee6e..63aff338 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -23,14 +23,16 @@ struct NodeListItem: View { VStack(alignment: .leading) { CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70) .padding(.trailing, 5) - BatteryLevelCompact(node: node, font: .caption, iconFont: .callout, color: .accentColor) - .padding(.trailing, 5) + if node.latestDeviceMetrics != nil { + BatteryCompact(batteryLevel: node.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor) + .padding(.trailing, 5) + } } VStack(alignment: .leading) { HStack { Text(node.user?.longName ?? "unknown".localized) - .fontWeight(.medium) .font(.headline) + .allowsTightening(true) if node.favorite { Spacer() Image(systemName: "star.fill") @@ -98,7 +100,7 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.gray) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let headingDegrees = Angle.degrees(trueBearing) Image(systemName: "location.north") .font(.callout) @@ -124,7 +126,7 @@ struct NodeListItem: View { DistanceText(meters: metersAway) .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) .foregroundColor(.secondary) - let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: CLLocation(latitude: lastPostion.coordinate.latitude, longitude: lastPostion.coordinate.longitude)) + let trueBearing = getBearingBetweenTwoPoints(point1: myCoord, point2: nodeCoord) let headingDegrees = Angle.degrees(trueBearing) Image(systemName: "location.north") .font(.callout) @@ -145,7 +147,6 @@ struct NodeListItem: View { HStack { Image(systemName: "\(node.channel).circle.fill") .font(.title2) - .symbolRenderingMode(.multicolor) .frame(width: 30) Text("Channel") .foregroundColor(.secondary) @@ -216,7 +217,6 @@ struct NodeListItem: View { .font(UIDevice.current.userInterfaceIdiom == .phone ? .callout : .caption) Image(systemName: "\(node.hopsAway).square") .font(.title2) - .symbolRenderingMode(.multicolor) } } else { if node.snr != 0 && !node.viaMqtt { diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 2dc8d105..9e09a0c0 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -33,13 +33,27 @@ struct MeshMap: View { @Namespace var mapScope @State var mapStyle: MapStyle = MapStyle.standard(elevation: .flat, emphasis: MapStyle.StandardEmphasis.muted, pointsOfInterest: .excludingAll, showsTraffic: false) @State var position = MapCameraPosition.automatic - @State var isEditingSettings = false + @State private var editingSettings = false + @State private var editingFilters = false @State var selectedPosition: PositionEntity? @State var editingWaypoint: WaypointEntity? @State var selectedWaypoint: WaypointEntity? @State var selectedWaypointId: String? @State var newWaypointCoord: CLLocationCoordinate2D? @State var isMeshMap = true + /// Filter + @State private var searchText = "" + @State private var viaLora = true + @State private var viaMqtt = true + @State private var isOnline = false + @State private var isPkiEncrypted = false + @State private var isFavorite = false + @State private var isEnvironment = false + @State private var distanceFilter = false + @State private var maxDistance: Double = 800000 + @State private var hopsAway: Double = -1.0 + @State private var roleFilter = false + @State private var deviceRoles: Set = [] var body: some View { @@ -48,7 +62,6 @@ struct MeshMap: View { MapReader { reader in Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { MeshMapContent(showUserLocation: $showUserLocation, showTraffic: $showTraffic, showPointsOfInterest: $showPointsOfInterest, selectedMapLayer: $selectedMapLayer, selectedPosition: $selectedPosition, selectedWaypoint: $selectedWaypoint) - } .mapScope(mapScope) .mapStyle(mapStyle) @@ -106,7 +119,7 @@ struct MeshMap: View { WaypointForm(waypoint: selection, editMode: true) .padding() } - .sheet(isPresented: $isEditingSettings) { + .sheet(isPresented: $editingSettings) { MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) } .onChange(of: router.navigationState) { @@ -128,19 +141,46 @@ struct MeshMap: View { return } } + .sheet(isPresented: $editingFilters) { + NodeListFilter( + viaLora: $viaLora, + viaMqtt: $viaMqtt, + isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, + isFavorite: $isFavorite, + isEnvironment: $isEnvironment, + distanceFilter: $distanceFilter, + maximumDistance: $maxDistance, + hopsAway: $hopsAway, + roleFilter: $roleFilter, + deviceRoles: $deviceRoles + ) + } .safeAreaInset(edge: .bottom, alignment: .trailing) { HStack { + Spacer() Button(action: { withAnimation { - isEditingSettings = !isEditingSettings + editingSettings = !editingSettings } }) { - Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") + Image(systemName: editingSettings ? "info.circle.fill" : "info.circle") .padding(.vertical, 5) } .tint(Color(UIColor.secondarySystemBackground)) .foregroundColor(.accentColor) .buttonStyle(.borderedProminent) +// Button(action: { +// withAnimation { +// editingFilters = !editingFilters +// } +// }) { +// Image(systemName: !editingFilters ? "line.3.horizontal.decrease.circle" : "line.3.horizontal.decrease.circle.fill") +// .padding(.vertical, 5) +// } +// .tint(Color(UIColor.secondarySystemBackground)) +// .foregroundColor(.accentColor) +// .buttonStyle(.borderedProminent) } .controlSize(.regular) .padding(5) diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index e05cb34b..e252aa95 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -24,6 +24,7 @@ struct NodeList: View { @State private var viaLora = true @State private var viaMqtt = true @State private var isOnline = false + @State private var isPkiEncrypted = false @State private var isFavorite = false @State private var isEnvironment = false @State private var distanceFilter = false @@ -38,8 +39,9 @@ struct NodeList: View { @State private var deleteNodeId: Int64 = 0 var boolFilters: [Bool] {[ - isOnline, isFavorite, + isOnline, + isPkiEncrypted, isEnvironment, distanceFilter, roleFilter @@ -86,8 +88,15 @@ struct NodeList: View { context: context, node: node ) - /// Don't show trace route, position exchange or delete context menu items for the connected node + /// Don't show message, trace route, position exchange or delete context menu items for the connected node if connectedNode.num != node.num { + Button(action: { + if let url = URL(string: "meshtastic:///messages?userNum=\(node.num)") { + UIApplication.shared.open(url) + } + }) { + Label("Message", systemImage: "message") + } Button { let traceRouteSent = bleManager.sendTraceRouteRequest( destNum: node.num, @@ -153,6 +162,7 @@ struct NodeList: View { viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, + isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, @@ -299,7 +309,7 @@ struct NodeList: View { await searchNodeList() } } - .onChange(of: boolFilters) { _ in + .onChange(of: [boolFilters]) { _ in Task { await searchNodeList() } @@ -334,7 +344,7 @@ struct NodeList: View { self.selectedNode = nil } } - .onAppear { + .onFirstAppear { Task { await searchNodeList() } @@ -383,6 +393,11 @@ struct NodeList: View { let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate) predicates.append(isOnlinePredicate) } + /// Encrypted + if isPkiEncrypted { + let isPkiEncryptedPredicate = NSPredicate(format: "user.pkiEncrypted == YES") + predicates.append(isPkiEncryptedPredicate) + } /// Favorites if isFavorite { let isFavoritePredicate = NSPredicate(format: "favorite == YES") diff --git a/Meshtastic/Views/Settings/AppSettings.swift b/Meshtastic/Views/Settings/AppSettings.swift index f9f46cbc..ef3bc384 100644 --- a/Meshtastic/Views/Settings/AppSettings.swift +++ b/Meshtastic/Views/Settings/AppSettings.swift @@ -13,6 +13,7 @@ struct AppSettings: View { @State private var isPresentingCoreDataResetConfirm = false @State private var isPresentingDeleteMapTilesConfirm = false @AppStorage("environmentEnableWeatherKit") private var environmentEnableWeatherKit: Bool = true + @AppStorage("enableAdministration") private var enableAdministration: Bool = false var body: some View { VStack { Form { @@ -23,6 +24,10 @@ struct AppSettings: View { UIApplication.shared.open(url) } } + Toggle(isOn: $enableAdministration) { + Label("Administration", systemImage: "gearshape.2") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } Section(header: Text("environment")) { VStack(alignment: .leading) { diff --git a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift index b43813c2..75132df1 100644 --- a/Meshtastic/Views/Settings/Config/BluetoothConfig.swift +++ b/Meshtastic/Views/Settings/Config/BluetoothConfig.swift @@ -107,7 +107,6 @@ struct BluetoothConfig: View { } ) .onAppear { - setBluetoothValues() // Need to request a BluetoothConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil { Logger.mesh.info("empty bluetooth config") @@ -117,25 +116,17 @@ struct BluetoothConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.bluetoothConfig != nil { - if newEnabled != node!.bluetoothConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.bluetoothConfig?.enabled { hasChanges = true } } - .onChange(of: mode) { newMode in - if node != nil && node!.bluetoothConfig != nil { - if newMode != node!.bluetoothConfig!.mode { hasChanges = true } - } + .onChange(of: mode) { + if $0 != node?.bluetoothConfig?.mode ?? -1 { hasChanges = true } } .onChange(of: fixedPin) { newFixedPin in - if node != nil && node!.bluetoothConfig != nil { - if newFixedPin != String(node!.bluetoothConfig!.fixedPin) { hasChanges = true } - } + if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true } } - .onChange(of: deviceLoggingEnabled) { newDeviceLogging in - if node != nil && node!.bluetoothConfig != nil { - if newDeviceLogging != node!.bluetoothConfig!.deviceLoggingEnabled { hasChanges = true } - } + .onChange(of: deviceLoggingEnabled) { + if $0 != node?.bluetoothConfig?.deviceLoggingEnabled { hasChanges = true } } } func setBluetoothValues() { diff --git a/Meshtastic/Views/Settings/Config/ConfigHeader.swift b/Meshtastic/Views/Settings/Config/ConfigHeader.swift index 3ff815f8..3f59a01a 100644 --- a/Meshtastic/Views/Settings/Config/ConfigHeader.swift +++ b/Meshtastic/Views/Settings/Config/ConfigHeader.swift @@ -23,11 +23,12 @@ struct ConfigHeader: View { .foregroundColor(.orange) } else { Text("Remote administration for: \(node?.user?.longName ?? "Unknown")") + .onFirstAppear(onAppear) .font(.title3) - .onAppear(perform: onAppear) } } else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? -1 { Text("Configuration for: \(node?.user?.longName ?? "Unknown")") + .onFirstAppear(onAppear) } else { Text("Please connect to a radio to configure settings.") .font(.callout) diff --git a/Meshtastic/Views/Settings/Config/DeviceConfig.swift b/Meshtastic/Views/Settings/Config/DeviceConfig.swift index b7f98e36..13d3aa83 100644 --- a/Meshtastic/Views/Settings/Config/DeviceConfig.swift +++ b/Meshtastic/Views/Settings/Config/DeviceConfig.swift @@ -155,7 +155,7 @@ struct DeviceConfig: View { .disabled(node?.user == nil) .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.leading) .confirmationDialog( "are.you.sure", @@ -180,10 +180,10 @@ struct DeviceConfig: View { .disabled(node?.user == nil) .buttonStyle(.bordered) .buttonBorderShape(.capsule) - .controlSize(.large) + .controlSize(.regular) .padding(.trailing) .confirmationDialog( - "All device and app data will be deleted. You will also need to forget your devices under Settings > Bluetooth.", + "All device and app data will be deleted.", isPresented: $isPresentingFactoryResetConfirm, titleVisibility: .visible ) { @@ -233,13 +233,17 @@ struct DeviceConfig: View { Spacer() } .navigationTitle("device.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDeviceValues() - // Need to request a LoRaConfig from the remote node before allowing changes + // Need to request a DeviceConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil { Logger.mesh.info("empty device config") let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context) @@ -248,55 +252,35 @@ struct DeviceConfig: View { } } } - .onChange(of: deviceRole) { newRole in - if node != nil && node?.deviceConfig != nil { - if newRole != node!.deviceConfig!.role { hasChanges = true } - } + .onChange(of: deviceRole) { + if $0 != node?.deviceConfig?.role ?? -1 { hasChanges = true } } - .onChange(of: serialEnabled) { newSerial in - if node != nil && node?.deviceConfig != nil { - if newSerial != node!.deviceConfig!.serialEnabled { hasChanges = true } - } + .onChange(of: serialEnabled) { + if $0 != node?.deviceConfig?.serialEnabled { hasChanges = true } } - .onChange(of: debugLogEnabled) { newDebugLog in - if node != nil && node?.deviceConfig != nil { - if newDebugLog != node!.deviceConfig!.debugLogEnabled { hasChanges = true } - } + .onChange(of: debugLogEnabled) { + if $0 != node?.deviceConfig?.debugLogEnabled { hasChanges = true } } .onChange(of: buttonGPIO) { newButtonGPIO in - if node != nil && node?.deviceConfig != nil { - if newButtonGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + if newButtonGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } .onChange(of: buzzerGPIO) { newBuzzerGPIO in - if node != nil && node?.deviceConfig != nil { - if newBuzzerGPIO != node!.deviceConfig!.buttonGpio { hasChanges = true } - } + if newBuzzerGPIO != node?.deviceConfig?.buttonGpio ?? -1 { hasChanges = true } } .onChange(of: rebroadcastMode) { newRebroadcastMode in - if node != nil && node?.deviceConfig != nil { - if newRebroadcastMode != node!.deviceConfig!.rebroadcastMode { hasChanges = true } - } + if newRebroadcastMode != node?.deviceConfig?.rebroadcastMode ?? -1 { hasChanges = true } } .onChange(of: nodeInfoBroadcastSecs) { newNodeInfoBroadcastSecs in - if node != nil && node?.deviceConfig != nil { - if newNodeInfoBroadcastSecs != node!.deviceConfig!.nodeInfoBroadcastSecs { hasChanges = true } - } + if newNodeInfoBroadcastSecs != node?.deviceConfig?.nodeInfoBroadcastSecs ?? -1 { hasChanges = true } } - .onChange(of: doubleTapAsButtonPress) { newDoubleTapAsButtonPress in - if node != nil && node?.deviceConfig != nil { - if newDoubleTapAsButtonPress != node!.deviceConfig!.doubleTapAsButtonPress { hasChanges = true } - } + .onChange(of: doubleTapAsButtonPress) { + if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true } } - .onChange(of: isManaged) { newIsManaged in - if node != nil && node?.deviceConfig != nil { - if newIsManaged != node!.deviceConfig!.isManaged { hasChanges = true } - } + .onChange(of: isManaged) { + if $0 != node?.deviceConfig?.isManaged { hasChanges = true } } .onChange(of: tzdef) { newTzdef in - if node != nil && node?.deviceConfig != nil { - if newTzdef != node!.deviceConfig!.tzdef { hasChanges = true } - } + if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true } } .onChange(of: ledHeartbeatEnabled) { newLedHeartbeatEnabled in if node != nil && node?.deviceConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/DisplayConfig.swift b/Meshtastic/Views/Settings/Config/DisplayConfig.swift index f800ebb6..1f3f6de4 100644 --- a/Meshtastic/Views/Settings/Config/DisplayConfig.swift +++ b/Meshtastic/Views/Settings/Config/DisplayConfig.swift @@ -154,13 +154,16 @@ struct DisplayConfig: View { } .navigationTitle("display.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDisplayValues() - // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.displayConfig == nil { Logger.mesh.info("empty display config") @@ -171,49 +174,31 @@ struct DisplayConfig: View { } } .onChange(of: screenOnSeconds) { newScreenSecs in - if node != nil && node!.displayConfig != nil { - if newScreenSecs != node!.displayConfig!.screenOnSeconds { hasChanges = true } - } + if newScreenSecs != node?.displayConfig?.screenOnSeconds ?? -1 { hasChanges = true } } .onChange(of: screenCarouselInterval) { newCarouselSecs in - if node != nil && node!.displayConfig != nil { - if newCarouselSecs != node!.displayConfig!.screenCarouselInterval { hasChanges = true } - } + if newCarouselSecs != node?.displayConfig?.screenCarouselInterval ?? -1 { hasChanges = true } } - .onChange(of: compassNorthTop) { newCompassNorthTop in - if node != nil && node!.displayConfig != nil { - if newCompassNorthTop != node!.displayConfig!.compassNorthTop { hasChanges = true } - } + .onChange(of: compassNorthTop) { + if $0 != node?.displayConfig?.compassNorthTop { hasChanges = true } } - .onChange(of: wakeOnTapOrMotion) { newWakeOnTapOrMotion in - if node != nil && node!.displayConfig != nil { - if newWakeOnTapOrMotion != node!.displayConfig!.wakeOnTapOrMotion { hasChanges = true } - } + .onChange(of: wakeOnTapOrMotion) { + if $0 != node?.displayConfig?.wakeOnTapOrMotion { hasChanges = true } } .onChange(of: gpsFormat) { newGpsFormat in - if node != nil && node!.displayConfig != nil { - if newGpsFormat != node!.displayConfig!.gpsFormat { hasChanges = true } - } + if newGpsFormat != node?.displayConfig?.gpsFormat ?? -1 { hasChanges = true } } - .onChange(of: flipScreen) { newFlipScreen in - if node != nil && node!.displayConfig != nil { - if newFlipScreen != node!.displayConfig!.flipScreen { hasChanges = true } - } + .onChange(of: flipScreen) { + if $0 != node?.displayConfig?.flipScreen { hasChanges = true } } .onChange(of: oledType) { newOledType in - if node != nil && node!.displayConfig != nil { - if newOledType != node!.displayConfig!.oledType { hasChanges = true } - } + if newOledType != node?.displayConfig?.oledType ?? -1 { hasChanges = true } } .onChange(of: displayMode) { newDisplayMode in - if node != nil && node!.displayConfig != nil { - if newDisplayMode != node!.displayConfig!.displayMode { hasChanges = true } - } + if newDisplayMode != node?.displayConfig?.displayMode ?? -1 { hasChanges = true } } .onChange(of: units) { newUnits in - if node != nil && node!.displayConfig != nil { - if newUnits != node!.displayConfig!.units { hasChanges = true } - } + if newUnits != node?.displayConfig?.units ?? -1 { hasChanges = true } } } func setDisplayValues() { diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index 8239282c..dad7c1a4 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -223,12 +223,16 @@ struct LoRaConfig: View { } } .navigationTitle("lora.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setLoRaValues() // Need to request a LoRaConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil { Logger.mesh.info("empty lora config") @@ -239,69 +243,43 @@ struct LoRaConfig: View { } } .onChange(of: region) { newRegion in - if node != nil && node!.loRaConfig != nil { - if newRegion != node!.loRaConfig!.regionCode { hasChanges = true } - } + if newRegion != node?.loRaConfig?.regionCode ?? -1 { hasChanges = true } } - .onChange(of: usePreset) { newUsePreset in - if node != nil && node!.loRaConfig != nil { - if newUsePreset != node!.loRaConfig!.usePreset { hasChanges = true } - } + .onChange(of: usePreset) { + if $0 != node?.loRaConfig?.usePreset { hasChanges = true } } .onChange(of: modemPreset) { newModemPreset in - if node != nil && node!.loRaConfig != nil { - if newModemPreset != node!.loRaConfig!.modemPreset { hasChanges = true } - } + if newModemPreset != node?.loRaConfig?.modemPreset ?? -1 { hasChanges = true } } .onChange(of: hopLimit) { newHopLimit in - if node != nil && node!.loRaConfig != nil { - if newHopLimit != node!.loRaConfig!.hopLimit { hasChanges = true } - } + if newHopLimit != node?.loRaConfig?.hopLimit ?? -1 { hasChanges = true } } .onChange(of: channelNum) { newChannelNum in - if node != nil && node!.loRaConfig != nil { - if newChannelNum != node!.loRaConfig!.channelNum { hasChanges = true } - } + if newChannelNum != node?.loRaConfig?.channelNum ?? -1 { hasChanges = true } } .onChange(of: bandwidth) { newBandwidth in - if node != nil && node!.loRaConfig != nil { - if newBandwidth != node!.loRaConfig!.bandwidth { hasChanges = true } - } + if newBandwidth != node?.loRaConfig?.bandwidth ?? -1 { hasChanges = true } } .onChange(of: codingRate) { newCodingRate in - if node != nil && node!.loRaConfig != nil { - if newCodingRate != node!.loRaConfig!.codingRate { hasChanges = true } - } + if newCodingRate != node?.loRaConfig?.codingRate ?? -1 { hasChanges = true } } .onChange(of: spreadFactor) { newSpreadFactor in - if node != nil && node!.loRaConfig != nil { - if newSpreadFactor != node!.loRaConfig!.spreadFactor { hasChanges = true } - } + if newSpreadFactor != node?.loRaConfig?.spreadFactor ?? -1 { hasChanges = true } } - .onChange(of: rxBoostedGain) { newRxBoostedGain in - if node != nil && node!.loRaConfig != nil { - if newRxBoostedGain != node!.loRaConfig!.sx126xRxBoostedGain { hasChanges = true } - } + .onChange(of: rxBoostedGain) { + if $0 != node?.loRaConfig?.sx126xRxBoostedGain { hasChanges = true } } .onChange(of: overrideFrequency) { newOverrideFrequency in - if node != nil && node!.loRaConfig != nil { - if newOverrideFrequency != node!.loRaConfig!.overrideFrequency { hasChanges = true } - } + if newOverrideFrequency != node?.loRaConfig?.overrideFrequency { hasChanges = true } } .onChange(of: txPower) { newTxPower in - if node != nil && node!.loRaConfig != nil { - if newTxPower != node!.loRaConfig!.txPower { hasChanges = true } - } + if newTxPower != node?.loRaConfig?.txPower ?? -1 { hasChanges = true } } - .onChange(of: txEnabled) { newTxEnabled in - if node != nil && node!.loRaConfig != nil { - if newTxEnabled != node!.loRaConfig!.txEnabled { hasChanges = true } - } + .onChange(of: txEnabled) { + if $0 != node?.loRaConfig?.txEnabled { hasChanges = true } } - .onChange(of: ignoreMqtt) { newIgnoreMqtt in - if node != nil && node!.loRaConfig != nil { - if newIgnoreMqtt != node!.loRaConfig!.ignoreMqtt { hasChanges = true } - } + .onChange(of: ignoreMqtt) { + if $0 != node?.loRaConfig?.ignoreMqtt { hasChanges = true } } } func setLoRaValues() { diff --git a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift index 3fe5560d..76c98752 100644 --- a/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/AmbientLightingConfig.swift @@ -50,10 +50,6 @@ struct AmbientLightingConfig: View { Stepper("Current: \(current)", value: $current, in: 0...31, step: 1) .padding(5) } - .onChange(of: color, initial: true) { - components = color.resolve(in: environment) - hasChanges = true - } } } .disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil) @@ -80,12 +76,16 @@ struct AmbientLightingConfig: View { } } .navigationTitle("ambient.lighting.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setAmbientLightingConfigValue() // Need to request a Ambient Lighting Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) @@ -94,9 +94,19 @@ struct AmbientLightingConfig: View { } } } - .onChange(of: ledState) { newLedState in - if node != nil && node!.ambientLightingConfig != nil { - if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true } + .onChange(of: ledState) { + if let val = node?.ambientLightingConfig?.ledState { + hasChanges = $0 != val + } + } + .onChange(of: current) { + if let val = node?.ambientLightingConfig?.current { + hasChanges = $0 != val + } + } + .onChange(of: color) { c in + if color != c { + hasChanges = true } } } diff --git a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift index 670e24e1..b4ec0b63 100644 --- a/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/CannedMessagesConfig.swift @@ -224,12 +224,16 @@ struct CannedMessagesConfig: View { } } .navigationTitle("canned.messages.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setCannedMessagesValues() // Need to request a CannedMessagesModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.cannedMessageConfig == nil { Logger.mesh.info("empty canned messages module config") @@ -268,24 +272,24 @@ struct CannedMessagesConfig: View { hasChanges = true } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.cannedMessageConfig != nil { - if newEnabled != node!.cannedMessageConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.cannedMessageConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: sendBell) { newBell in - if node != nil && node!.cannedMessageConfig != nil { - if newBell != node!.cannedMessageConfig!.sendBell { hasChanges = true } + .onChange(of: sendBell) { + if let val = node?.cannedMessageConfig?.sendBell { + hasChanges = $0 != val } } - .onChange(of: rotary1Enabled) { newRot1 in - if node != nil && node!.cannedMessageConfig != nil { - if newRot1 != node!.cannedMessageConfig!.rotary1Enabled { hasChanges = true } + .onChange(of: rotary1Enabled) { + if let val = node?.cannedMessageConfig?.rotary1Enabled { + hasChanges = $0 != val } } - .onChange(of: updown1Enabled) { newUpDown in - if node != nil && node!.cannedMessageConfig != nil { - if newUpDown != node!.cannedMessageConfig!.updown1Enabled { hasChanges = true } + .onChange(of: updown1Enabled) { + if let val = node?.cannedMessageConfig?.updown1Enabled { + hasChanges = $0 != val } } .onChange(of: inputbrokerPinA) { newPinA in diff --git a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift index 480c4e24..1ff1ed86 100644 --- a/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/DetectionSensorConfig.swift @@ -180,12 +180,16 @@ struct DetectionSensorConfig: View { } } .navigationTitle("detection.sensor.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setDetectionSensorValues() // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.detectionSensorConfig == nil { Logger.mesh.info("empty detection sensor module config") @@ -195,14 +199,14 @@ struct DetectionSensorConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.detectionSensorConfig != nil { - if newEnabled != node!.detectionSensorConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.detectionSensorConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: sendBell) { newSendBell in - if node != nil && node?.detectionSensorConfig != nil { - if newSendBell != node!.detectionSensorConfig!.sendBell { hasChanges = true } + .onChange(of: sendBell) { + if let val = node?.detectionSensorConfig?.sendBell { + hasChanges = $0 != val } } .onChange(of: detectionTriggeredHigh) { newDetectionTriggeredHigh in @@ -210,9 +214,9 @@ struct DetectionSensorConfig: View { if newDetectionTriggeredHigh != node!.detectionSensorConfig!.detectionTriggeredHigh { hasChanges = true } } } - .onChange(of: usePullup) { newUsePullup in - if node != nil && node?.detectionSensorConfig != nil { - if newUsePullup != node!.detectionSensorConfig!.usePullup { hasChanges = true } + .onChange(of: usePullup) { + if let val = node?.detectionSensorConfig?.usePullup { + hasChanges = $0 != val } } .onChange(of: name) { newName in diff --git a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift index abdca8a2..32cacbd3 100644 --- a/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/ExternalNotificationConfig.swift @@ -190,12 +190,16 @@ struct ExternalNotificationConfig: View { } } .navigationTitle("external.notification.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setExternalNotificationValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.externalNotificationConfig == nil { Logger.mesh.info("empty external notification module config") @@ -205,44 +209,44 @@ struct ExternalNotificationConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.externalNotificationConfig != nil { - if newEnabled != node!.externalNotificationConfig!.enabled { hasChanges = true } + .onChange(of: enabled) { + if let val = node?.externalNotificationConfig?.enabled { + hasChanges = $0 != val } } - .onChange(of: alertBell) { newAlertBell in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBell != node!.externalNotificationConfig!.alertBell { hasChanges = true } + .onChange(of: alertBell) { + if let val = node?.externalNotificationConfig?.alertBell { + hasChanges = $0 != val } } - .onChange(of: alertBellBuzzer) { newAlertBellBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellBuzzer != node!.externalNotificationConfig!.alertBellBuzzer { hasChanges = true } + .onChange(of: alertBellBuzzer) { + if let val = node?.externalNotificationConfig?.alertBellBuzzer { + hasChanges = $0 != val } } - .onChange(of: alertBellVibra) { newAlertBellVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertBellVibra != node!.externalNotificationConfig!.alertBellVibra { hasChanges = true } + .onChange(of: alertBellVibra) { + if let val = node?.externalNotificationConfig?.alertBellVibra { + hasChanges = $0 != val } } - .onChange(of: alertMessage) { newAlertMessage in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessage != node!.externalNotificationConfig!.alertMessage { hasChanges = true } + .onChange(of: alertMessage) { + if let val = node?.externalNotificationConfig?.alertMessage { + hasChanges = $0 != val } } - .onChange(of: alertMessageBuzzer) { newAlertMessageBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageBuzzer != node!.externalNotificationConfig!.alertMessageBuzzer { hasChanges = true } + .onChange(of: alertMessageBuzzer) { + if let val = node?.externalNotificationConfig?.alertMessageBuzzer { + hasChanges = $0 != val } } - .onChange(of: alertMessageVibra) { newAlertMessageVibra in - if node != nil && node!.externalNotificationConfig != nil { - if newAlertMessageVibra != node!.externalNotificationConfig!.alertMessageVibra { hasChanges = true } + .onChange(of: alertMessageVibra) { + if let val = node?.externalNotificationConfig?.alertMessageVibra { + hasChanges = $0 != val } } - .onChange(of: active) { newActive in - if node != nil && node!.externalNotificationConfig != nil { - if newActive != node!.externalNotificationConfig!.active { hasChanges = true } + .onChange(of: active) { + if let val = node?.externalNotificationConfig?.active { + hasChanges = $0 != val } } .onChange(of: output) { newOutput in @@ -265,9 +269,9 @@ struct ExternalNotificationConfig: View { if newOutputMs != node!.externalNotificationConfig!.outputMilliseconds { hasChanges = true } } } - .onChange(of: usePWM) { newUsePWM in - if node != nil && node!.externalNotificationConfig != nil { - if newUsePWM != node!.externalNotificationConfig!.usePWM { hasChanges = true } + .onChange(of: usePWM) { + if let val = node?.externalNotificationConfig?.usePWM { + hasChanges = $0 != val } } .onChange(of: nagTimeout) { newNagTimeout in @@ -275,9 +279,9 @@ struct ExternalNotificationConfig: View { if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true } } } - .onChange(of: useI2SAsBuzzer) { newUseI2SAsBuzzer in - if node != nil && node!.externalNotificationConfig != nil { - if newUseI2SAsBuzzer != node!.externalNotificationConfig!.useI2SAsBuzzer { hasChanges = true } + .onChange(of: useI2SAsBuzzer) { + if let val = node?.externalNotificationConfig?.useI2SAsBuzzer { + hasChanges = $0 != val } } } diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index f923e560..d6176526 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -271,10 +271,27 @@ struct MQTTConfig: View { } } .navigationTitle("mqtt.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?", mqttProxyConnected: bleManager.mqttProxyConnected) - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) + .onChange(of: enabled) { + if $0 != node?.mqttConfig?.enabled { hasChanges = true } + } + .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in + if newProxyToClientEnabled { + jsonEnabled = false + } + if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true } + if newProxyToClientEnabled { + jsonEnabled = false + } + } .onChange(of: address) { newAddress in if node != nil && node?.mqttConfig != nil { if newAddress != node!.mqttConfig!.address { hasChanges = true } @@ -298,39 +315,17 @@ struct MQTTConfig: View { .onChange(of: selectedTopic) { newSelectedTopic in root = newSelectedTopic } - .onChange(of: enabled) { newEnabled in - if node != nil && node?.mqttConfig != nil { - if newEnabled != node!.mqttConfig!.enabled { hasChanges = true } - } - } - .onChange(of: proxyToClientEnabled) { newProxyToClientEnabled in - if newProxyToClientEnabled { - jsonEnabled = false - } - if node != nil && node?.mqttConfig != nil { - if newProxyToClientEnabled != node!.mqttConfig!.proxyToClientEnabled { hasChanges = true } - if newProxyToClientEnabled { - jsonEnabled = false - } - } - } - .onChange(of: encryptionEnabled) { newEncryptionEnabled in - if node != nil && node?.mqttConfig != nil { - if newEncryptionEnabled != node!.mqttConfig!.encryptionEnabled { hasChanges = true } - } + .onChange(of: encryptionEnabled) { + if $0 != node?.mqttConfig?.encryptionEnabled { hasChanges = true } } .onChange(of: jsonEnabled) { newJsonEnabled in if newJsonEnabled { proxyToClientEnabled = false } - if node != nil && node?.mqttConfig != nil { - if newJsonEnabled != node!.mqttConfig!.jsonEnabled { hasChanges = true } - } + if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } - .onChange(of: tlsEnabled) { newTlsEnabled in - if node != nil && node?.mqttConfig != nil { - if newTlsEnabled != node!.mqttConfig!.tlsEnabled { hasChanges = true } - } + .onChange(of: tlsEnabled) { + if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true } } .onChange(of: mqttConnected) { newMqttConnected in if newMqttConnected == false { @@ -343,13 +338,8 @@ struct MQTTConfig: View { } } } - .onChange(of: mapReportingEnabled) { newMapReportingEnabled in - if node != nil && node?.mqttConfig != nil { - if newMapReportingEnabled != node!.mqttConfig!.mapReportingEnabled { hasChanges = true } - } - } - .onChange(of: preciseLocation) { _ in - hasChanges = true + .onChange(of: mapReportingEnabled) { + if $0 != node?.mqttConfig?.mapReportingEnabled { hasChanges = true } } .onChange(of: mapPublishIntervalSecs) { newMapPublishIntervalSecs in if node != nil && node?.mqttConfig != nil { @@ -357,7 +347,6 @@ struct MQTTConfig: View { } } .onAppear { - setMqttValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil { Logger.mesh.info("empty mqtt module config") diff --git a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift index d7670a7c..6e7bef08 100644 --- a/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/PaxCounterConfig.swift @@ -58,7 +58,6 @@ struct PaxCounterConfig: View { ) }) .onAppear { - setPaxValues() // Need to request a PAX Counter module config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.paxCounterConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) @@ -68,14 +67,10 @@ struct PaxCounterConfig: View { } } .onChange(of: enabled) { - if let val = node?.paxCounterConfig?.enabled { - hasChanges = $0 != val - } + if $0 != node?.paxCounterConfig?.enabled { hasChanges = true } } .onChange(of: paxcounterUpdateInterval) { - if let val = node?.paxCounterConfig?.updateInterval { - hasChanges = $0 != val - } + if $0 != node?.paxCounterConfig?.updateInterval ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift index 0bf99d65..001ad2b1 100644 --- a/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RangeTestConfig.swift @@ -72,12 +72,16 @@ struct RangeTestConfig: View { } } .navigationTitle("range.test.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setRangeTestValues() // Need to request a RangeTestModule Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.rangeTestConfig == nil { Logger.mesh.debug("empty range test module config") @@ -87,20 +91,14 @@ struct RangeTestConfig: View { } } } - .onChange(of: enabled) { newEnabled in - if node != nil && node!.rangeTestConfig != nil { - if newEnabled != node!.rangeTestConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.rangeTestConfig?.enabled { hasChanges = true } } - .onChange(of: save) { newSave in - if node != nil && node!.rangeTestConfig != nil { - if newSave != node!.rangeTestConfig!.save { hasChanges = true } - } + .onChange(of: save) { + if $0 != node?.rangeTestConfig?.save { hasChanges = true } } - .onChange(of: sender) { newSender in - if node != nil && node!.rangeTestConfig != nil { - if newSender != node!.rangeTestConfig!.sender { hasChanges = true } - } + .onChange(of: sender) { + if $0 != node?.rangeTestConfig?.sender ?? -1 { hasChanges = true } } } } diff --git a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift index 3128fffe..95f237a3 100644 --- a/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/RtttlConfig.swift @@ -62,12 +62,16 @@ struct RtttlConfig: View { } } .navigationTitle("config.ringtone.title") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setRtttLConfigValue() // Need to request a Rtttl Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && (node?.rtttlConfig == nil || node?.rtttlConfig?.ringtone?.count ?? 0 == 0) { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context) diff --git a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift index 813e1328..c7243a87 100644 --- a/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/SerialConfig.swift @@ -127,13 +127,16 @@ struct SerialConfig: View { } } .navigationTitle("serial.config") - .navigationBarItems(trailing: - - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setSerialValues() // Need to request a SerialModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.serialConfig == nil { Logger.mesh.debug("empty serial module config") @@ -142,61 +145,40 @@ struct SerialConfig: View { _ = bleManager.requestSerialModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } - } - .onChange(of: enabled) { newEnabled in - - if node != nil && node!.serialConfig != nil { - - if newEnabled != node!.serialConfig!.enabled { hasChanges = true } - } + .onChange(of: enabled) { + if $0 != node?.serialConfig?.enabled { hasChanges = true } } - .onChange(of: echo) { newEcho in - - if node != nil && node!.serialConfig != nil { - - if newEcho != node!.serialConfig!.echo { hasChanges = true } - } + .onChange(of: echo) { + if $0 != node?.serialConfig?.echo { hasChanges = true } } .onChange(of: rxd) { newRxd in - if node != nil && node!.serialConfig != nil { - if newRxd != node!.serialConfig!.rxd { hasChanges = true } } } .onChange(of: txd) { newTxd in - if node != nil && node!.serialConfig != nil { - if newTxd != node!.serialConfig!.txd { hasChanges = true } } } .onChange(of: baudRate) { newBaud in - if node != nil && node!.serialConfig != nil { - if newBaud != node!.serialConfig!.baudRate { hasChanges = true } } } .onChange(of: timeout) { newTimeout in - if node != nil && node!.serialConfig != nil { - if newTimeout != node!.serialConfig!.timeout { hasChanges = true } } } .onChange(of: overrideConsoleSerialPort) { newOverrideConsoleSerialPort in - if node != nil && node!.serialConfig != nil { - if newOverrideConsoleSerialPort != node!.serialConfig!.overrideConsoleSerialPort { hasChanges = true } } } .onChange(of: mode) { newMode in - if node != nil && node!.serialConfig != nil { - if newMode != node!.serialConfig!.mode { hasChanges = true } } } diff --git a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift index e73380f3..4829a5fa 100644 --- a/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/StoreForwardConfig.swift @@ -137,10 +137,15 @@ struct StoreForwardConfig: View { } } .navigationTitle("storeforward.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { // Need to request a Detection Sensor Module Config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil { @@ -150,7 +155,6 @@ struct StoreForwardConfig: View { _ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) } } - setStoreAndForwardValues() } .onChange(of: enabled) { newEnabled in if node != nil && node?.storeForwardConfig != nil { diff --git a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift index a1f827b7..df811677 100644 --- a/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/TelemetryConfig.swift @@ -125,12 +125,16 @@ struct TelemetryConfig: View { } } .navigationTitle("telemetry.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setTelemetryValues() // Need to request a TelemetryModuleConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil { Logger.mesh.info("empty telemetry module config") diff --git a/Meshtastic/Views/Settings/Config/NetworkConfig.swift b/Meshtastic/Views/Settings/Config/NetworkConfig.swift index d969eab9..0554c766 100644 --- a/Meshtastic/Views/Settings/Config/NetworkConfig.swift +++ b/Meshtastic/Views/Settings/Config/NetworkConfig.swift @@ -109,12 +109,16 @@ struct NetworkConfig: View { } } .navigationTitle("network.config") - .navigationBarItems(trailing: - ZStack { - ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + .navigationBarItems( + trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: bleManager.connectedPeripheral?.shortName ?? "?" + ) + } + ) .onAppear { - setNetworkValues() // Need to request a NetworkConfig from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.networkConfig == nil { Logger.mesh.info("empty network config") @@ -124,30 +128,20 @@ struct NetworkConfig: View { } } } - .onChange(of: wifiEnabled) { newEnabled in - if node != nil && node!.networkConfig != nil { - if newEnabled != node!.networkConfig!.wifiEnabled { hasChanges = true } - } + .onChange(of: wifiEnabled) { + if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true } } .onChange(of: wifiSsid) { newSSID in - if node != nil && node!.networkConfig != nil { - if newSSID != node!.networkConfig!.wifiSsid { hasChanges = true } - } + if newSSID != node?.networkConfig?.wifiSsid { hasChanges = true } } .onChange(of: wifiPsk) { newPsk in - if node != nil && node!.networkConfig != nil { - if newPsk != node!.networkConfig!.wifiPsk { hasChanges = true } - } + if newPsk != node?.networkConfig?.wifiPsk { hasChanges = true } } - .onChange(of: wifiMode) { newMode in - if node != nil && node!.networkConfig != nil { - if newMode != node!.networkConfig!.wifiMode { hasChanges = true } - } + .onChange(of: wifiMode) { + if $0 != node?.networkConfig?.wifiMode ?? -1 { hasChanges = true } } - .onChange(of: ethEnabled) { newEthEnabled in - if node != nil && node!.networkConfig != nil { - if newEthEnabled != node!.networkConfig!.ethEnabled { hasChanges = true } - } + .onChange(of: ethEnabled) { + if $0 != node?.networkConfig?.ethEnabled { hasChanges = true } } } func setNetworkValues() { diff --git a/Meshtastic/Views/Settings/Config/PositionConfig.swift b/Meshtastic/Views/Settings/Config/PositionConfig.swift index aa6960a0..0f71b379 100644 --- a/Meshtastic/Views/Settings/Config/PositionConfig.swift +++ b/Meshtastic/Views/Settings/Config/PositionConfig.swift @@ -377,7 +377,6 @@ struct PositionConfig: View { } ) .onAppear { - setPositionValues() supportedVersion = bleManager.connectedVersion == "0.0.0" || self.minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(bleManager.connectedVersion, options: .numeric) == .orderedSame // Need to request a PositionConfig from the remote node before allowing changes if let connectedPeripheral = bleManager.connectedPeripheral, node?.positionConfig == nil { @@ -405,51 +404,39 @@ struct PositionConfig: View { } } } - .onChange(of: gpsMode) { _ in - handleChanges() + .onChange(of: gpsMode) { newGpsMode in + if newGpsMode != node?.positionConfig?.gpsMode ?? 0 { hasChanges = true } } - .onChange(of: rxGpio) { _ in - handleChanges() + .onChange(of: rxGpio) { newRxGpio in + if newRxGpio != node?.positionConfig?.rxGpio ?? 0 { hasChanges = true } } - .onChange(of: txGpio) { _ in - handleChanges() + .onChange(of: txGpio) { newTxGpio in + if newTxGpio != node?.positionConfig?.txGpio ?? 0 { hasChanges = true } } - .onChange(of: gpsEnGpio) { _ in - handleChanges() + .onChange(of: gpsEnGpio) { newGpsEnGpio in + if newGpsEnGpio != node?.positionConfig?.gpsEnGpio ?? 0 { hasChanges = true } } - .onChange(of: smartPositionEnabled) { _ in - handleChanges() + .onChange(of: smartPositionEnabled) { newSmartPositionEnabled in + if newSmartPositionEnabled != node?.positionConfig?.smartPositionEnabled { hasChanges = true } } - .onChange(of: positionBroadcastSeconds) { _ in - handleChanges() + .onChange(of: positionBroadcastSeconds) { newPositionBroadcastSeconds in + if newPositionBroadcastSeconds != node?.positionConfig?.positionBroadcastSeconds ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumIntervalSecs) { _ in - handleChanges() + .onChange(of: broadcastSmartMinimumIntervalSecs) { newBroadcastSmartMinimumIntervalSecs in + if newBroadcastSmartMinimumIntervalSecs != node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 0 { hasChanges = true } } - .onChange(of: broadcastSmartMinimumDistance) { _ in - handleChanges() + .onChange(of: broadcastSmartMinimumDistance) { newBroadcastSmartMinimumDistance in + if newBroadcastSmartMinimumDistance != node?.positionConfig?.broadcastSmartMinimumDistance ?? 0 { hasChanges = true } } - .onChange(of: gpsUpdateInterval) { _ in - handleChanges() - } - .onChange(of: positionFlags) { _ in - handleChanges() + .onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in + if newGpsUpdateInterval != node?.positionConfig?.gpsUpdateInterval ?? 0 { hasChanges = true } } } - func handleChanges() { + func handlePositionFlagtChanges() { guard let positionConfig = node?.positionConfig else { return } let pf = PositionFlags(rawValue: self.positionFlags) - hasChanges = positionConfig.deviceGpsEnabled != deviceGpsEnabled || - positionConfig.gpsMode != gpsMode || - positionConfig.rxGpio != rxGpio || - positionConfig.txGpio != txGpio || - positionConfig.gpsEnGpio != gpsEnGpio || - positionConfig.smartPositionEnabled != smartPositionEnabled || - positionConfig.positionBroadcastSeconds != positionBroadcastSeconds || - positionConfig.broadcastSmartMinimumIntervalSecs != broadcastSmartMinimumIntervalSecs || - positionConfig.broadcastSmartMinimumDistance != broadcastSmartMinimumDistance || - positionConfig.gpsUpdateInterval != gpsUpdateInterval || + hasChanges = pf.contains(.Altitude) || pf.contains(.AltitudeMsl) || pf.contains(.Satsinview) || diff --git a/Meshtastic/Views/Settings/Config/PowerConfig.swift b/Meshtastic/Views/Settings/Config/PowerConfig.swift index d8de7e44..42b6a534 100644 --- a/Meshtastic/Views/Settings/Config/PowerConfig.swift +++ b/Meshtastic/Views/Settings/Config/PowerConfig.swift @@ -119,7 +119,6 @@ struct PowerConfig: View { } .onAppear { Api().loadDeviceHardwareData { (hw) in - for device in hw { let currentHardware = node?.user?.hwModel ?? "UNSET" let deviceString = device.hwModelSlug.replacingOccurrences(of: "_", with: "") @@ -128,8 +127,6 @@ struct PowerConfig: View { } } } - setPowerValues() - // Need to request a Power config from the remote node before allowing changes if bleManager.connectedPeripheral != nil && node?.powerConfig == nil { let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) @@ -139,40 +136,30 @@ struct PowerConfig: View { } } .onChange(of: isPowerSaving) { - if let val = node?.powerConfig?.isPowerSaving { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.isPowerSaving { hasChanges = true } } - .onChange(of: shutdownOnPowerLoss) { _ in - hasChanges = true + .onChange(of: shutdownOnPowerLoss) { newShutdownOnPowerLoss in + if newShutdownOnPowerLoss { + hasChanges = true + } } .onChange(of: shutdownAfterSecs) { - if let val = node?.powerConfig?.onBatteryShutdownAfterSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } .onChange(of: adcOverride) { _ in hasChanges = true } - .onChange(of: adcMultiplier) { - if let val = node?.powerConfig?.adcMultiplierOverride { - hasChanges = $0 != val - } + .onChange(of: adcMultiplier) { newAdcMultiplier in + if newAdcMultiplier != node?.powerConfig?.adcMultiplierOverride ?? -1 { hasChanges = true } } .onChange(of: waitBluetoothSecs) { - if let val = node?.powerConfig?.waitBluetoothSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.waitBluetoothSecs ?? -1 { hasChanges = true } } .onChange(of: lsSecs) { - if let val = node?.powerConfig?.lsSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.lsSecs ?? -1 { hasChanges = true } } .onChange(of: minWakeSecs) { - if let val = node?.powerConfig?.minWakeSecs { - hasChanges = $0 != val - } + if $0 != node?.powerConfig?.minWakeSecs ?? -1 { hasChanges = true } } SaveConfigButton(node: node, hasChanges: $hasChanges) { diff --git a/Meshtastic/Views/Settings/Config/SecurityConfig.swift b/Meshtastic/Views/Settings/Config/SecurityConfig.swift new file mode 100644 index 00000000..caf64e57 --- /dev/null +++ b/Meshtastic/Views/Settings/Config/SecurityConfig.swift @@ -0,0 +1,185 @@ +// +// Security.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 8/7/24. +// + +import Foundation +import SwiftUI +import CoreData +import MeshtasticProtobufs +import OSLog + +struct SecurityConfig: View { + + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + @Environment(\.managedObjectContext) var context + @EnvironmentObject var bleManager: BLEManager + @Environment(\.dismiss) private var goBack + + var node: NodeInfoEntity? + + @State var hasChanges = false + @State var publicKey = "" + @State var privateKey = "" + @State var adminKey = "" + @State var isManaged = false + @State var serialEnabled = false + @State var debugLogApiEnabled = false + @State var bluetoothLoggingEnabled = false + @State var adminChannelEnabled = false + + var body: some View { + VStack { + Form { + ConfigHeader(title: "Security", config: \.securityConfig, node: node, onAppear: setSecurityValues) + + Section(header: Text("Admin & Direct Message Keys")) { + VStack(alignment: .leading) { + Label("Public Key", systemImage: "key") + SecureInput("Public Key", text: $publicKey) + Text("Sent out to other nodes on the mesh to allow them to compute a shared secret key.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } + VStack(alignment: .leading) { + Label("Private Key", systemImage: "key.fill") + SecureInput("Private Key", text: $privateKey) + Text("Used to create a shared key with a remote device.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } + VStack(alignment: .leading) { + Label("Admin Key", systemImage: "key.viewfinder") + SecureInput("Private Key", text: $adminKey) + Text("The public key authorized to send admin messages to this node.") + .foregroundStyle(.secondary) + .font(idiom == .phone ? .caption : .callout) + } + } + Section(header: Text("Logs")) { + Toggle(isOn: $bluetoothLoggingEnabled) { + Label("Bluetooth Logs", systemImage: "dot.radiowaves.right") + Text("View and export position-redacted device logs over Bluetooth") + Link("View Logs", destination: URL(string: "meshtastic:///settings/debugLogs")!) + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("Administration")) { + if adminKey.length > 0 || adminChannelEnabled { + Toggle(isOn: $isManaged) { + Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath") + Text("Device is managed by a mesh administrator.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Toggle(isOn: $adminChannelEnabled) { + Label("Legacy Administration", systemImage: "lock.slash") + Text("Allow incoming device control over the insecure legacy admin channel.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + Section(header: Text("Developer")) { + Toggle(isOn: $serialEnabled) { + Label("Serial Console", systemImage: "terminal") + Text("Serial Console over the Stream API.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + if serialEnabled { + Toggle(isOn: $debugLogApiEnabled) { + Label("Serial Debug Logs", systemImage: "ant.fill") + Text("Output live debug logging over serial.") + } + .toggleStyle(SwitchToggleStyle(tint: .accentColor)) + } + } + } + } + .scrollDismissesKeyboard(.immediately) + .navigationTitle("Security Config") + .navigationBarItems(trailing: ZStack { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: "\(bleManager.connectedPeripheral?.shortName ?? "?")" + ) + }) + .onChange(of: isManaged) { + if $0 != node?.securityConfig?.isManaged { hasChanges = true } + } + .onChange(of: serialEnabled) { + if $0 != node?.securityConfig?.serialEnabled { hasChanges = true } + } + .onChange(of: debugLogApiEnabled) { + if $0 != node?.securityConfig?.debugLogApiEnabled { hasChanges = true } + } + .onChange(of: bluetoothLoggingEnabled) { + if $0 != node?.securityConfig?.bluetoothLoggingEnabled { hasChanges = true } + } + .onChange(of: adminChannelEnabled) { + if $0 != node?.securityConfig?.adminChannelEnabled { hasChanges = true } + } + .onChange(of: publicKey) { _ in + hasChanges = true + } + .onChange(of: privateKey) { _ in + hasChanges = true + } + .onChange(of: adminKey) { _ in + hasChanges = true + } + .onFirstAppear { + // Need to request a Power config from the remote node before allowing changes + if bleManager.connectedPeripheral != nil && node?.securityConfig == nil { + let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context) + if node != nil && connectedNode != nil { + _ = bleManager.requestSecurityConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0) + } + } + } + + SaveConfigButton(node: node, hasChanges: $hasChanges) { + guard let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context), + let fromUser = connectedNode.user, + let toUser = node?.user else { + return + } + + var config = Config.SecurityConfig() + config.publicKey = Data(base64Encoded: publicKey) ?? Data() + config.privateKey = Data(base64Encoded: privateKey) ?? Data() + config.adminKey = Data(base64Encoded: adminKey) ?? Data() + config.isManaged = isManaged + config.serialEnabled = serialEnabled + config.debugLogApiEnabled = debugLogApiEnabled + config.bluetoothLoggingEnabled = bluetoothLoggingEnabled + config.adminChannelEnabled = adminChannelEnabled + + let adminMessageId = bleManager.saveSecurityConfig( + config: config, + fromUser: fromUser, + toUser: toUser, + 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 + hasChanges = false + goBack() + } + } + } + + func setSecurityValues() { + self.publicKey = node?.securityConfig?.publicKey?.base64EncodedString() ?? "" + self.privateKey = node?.securityConfig?.privateKey?.base64EncodedString() ?? "" + self.adminKey = node?.securityConfig?.adminKey?.base64EncodedString() ?? "" + self.isManaged = node?.securityConfig?.isManaged ?? false + self.serialEnabled = node?.securityConfig?.serialEnabled ?? false + self.debugLogApiEnabled = node?.securityConfig?.debugLogApiEnabled ?? false + self.bluetoothLoggingEnabled = node?.securityConfig?.bluetoothLoggingEnabled ?? false + self.adminChannelEnabled = node?.securityConfig?.adminChannelEnabled ?? false + self.hasChanges = false + } +} diff --git a/Meshtastic/Views/Settings/Firmware.swift b/Meshtastic/Views/Settings/Firmware.swift index e591761f..9a450775 100644 --- a/Meshtastic/Views/Settings/Firmware.swift +++ b/Meshtastic/Views/Settings/Firmware.swift @@ -13,7 +13,7 @@ struct Firmware: View { @Environment(\.managedObjectContext) var context @EnvironmentObject var bleManager: BLEManager var node: NodeInfoEntity? - @State var minimumVersion = "2.4.0" + @State var minimumVersion = "2.4.2" @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 fee7877b..ac8138fa 100644 --- a/Meshtastic/Views/Settings/SaveChannelQRCode.swift +++ b/Meshtastic/Views/Settings/SaveChannelQRCode.swift @@ -50,6 +50,18 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() .disabled(!connectedToDevice) +#if targetEnvironment(macCatalyst) + Button { + dismiss() + } label: { + Label("cancel", systemImage: "xmark") + + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding() +#endif } else { Button { dismiss() @@ -62,19 +74,6 @@ struct SaveChannelQRCode: View { .controlSize(.large) .padding() } - - #if targetEnvironment(macCatalyst) - Button { - dismiss() - } label: { - Label("cancel", systemImage: "xmark") - - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding() - #endif } } .onAppear { diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 880dabf2..814f3ab9 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -17,6 +17,8 @@ struct Settings: View { @FetchRequest( sortDescriptors: [ NSSortDescriptor(key: "favorite", ascending: false), + NSSortDescriptor(key: "user.pkiEncrypted", ascending: false), + NSSortDescriptor(key: "viaMqtt", ascending: true), NSSortDescriptor(key: "user.longName", ascending: true) ], animation: .default @@ -73,6 +75,14 @@ struct Settings: View { } .disabled(selectedNode > 0 && selectedNode != preferredNodeNum) + NavigationLink(value: SettingsNavigationState.security) { + Label { + Text("Security") + } icon: { + Image(systemName: "lock.shield") + } + } + NavigationLink(value: SettingsNavigationState.shareQRCode) { Label { Text("share.channels") @@ -335,18 +345,16 @@ struct Settings: View { } } - let hasAdmin = node?.myInfo?.adminIndex ?? 0 > 0 - if !(node?.deviceConfig?.isManaged ?? false) { if bleManager.connectedPeripheral != nil { Section("Configure") { - if hasAdmin { + if node?.canRemoteAdmin ?? false { Picker("Configuring Node", selection: $selectedNode) { if selectedNode == 0 { Text("Connect to a Node").tag(0) } - ForEach(nodes) { node in + /// Connected Node if node.num == bleManager.connectedPeripheral?.num ?? 0 { Label { Text("BLE: \(node.user?.longName ?? "unknown".localized)") @@ -354,16 +362,30 @@ struct Settings: View { Image(systemName: "antenna.radiowaves.left.and.right") } .tag(Int(node.num)) - } else if node.metadata != nil { + } else if node.canRemoteAdmin && UserDefaults.enableAdministration && node.sessionPasskey != nil { /// Nodes using the new PKI system Label { - Text("Remote: \(node.user?.longName ?? "unknown".localized)") + Text("Remote PKI Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "av.remote") } .tag(Int(node.num)) - } else if hasAdmin { + } else if !UserDefaults.enableAdministration && node.metadata != nil { /// Nodes using the old admin system Label { - Text("Request Admin: \(node.user?.longName ?? "unknown".localized)") + Text("Remote Legacy Admin: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "av.remote") + } + .tag(Int(node.num)) + } else if UserDefaults.enableAdministration && node.user?.pkiEncrypted ?? false { + Label { + Text("Request PKI Admin: \(node.user?.longName ?? "unknown".localized)") + } icon: { + Image(systemName: "rectangle.and.hand.point.up.left") + } + .tag(Int(node.num)) + } else if !UserDefaults.enableAdministration { + Label { + Text("Request Legacy Admin: \(node.user?.longName ?? "unknown".localized)") } icon: { Image(systemName: "rectangle.and.hand.point.up.left") } @@ -378,7 +400,7 @@ struct Settings: View { let node = nodes.first(where: { $0.num == newValue }) let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) preferredNodeNum = Int(connectedNode?.num ?? 0)// Int(bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral?.num ?? 0 : 0) - if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil && node?.metadata == nil { + if connectedNode != nil && connectedNode?.user != nil && connectedNode?.myInfo != nil && node?.user != nil {// && node?.metadata == nil { let adminMessageId = bleManager.requestDeviceMetadata(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode!.myInfo!.adminIndex, context: context) if adminMessageId > 0 { Logger.mesh.info("Sent node metadata request from node details") @@ -461,6 +483,8 @@ struct Settings: View { PaxCounterConfig(node: nodes.first(where: { $0.num == selectedNode })) case .ringtone: RtttlConfig(node: nodes.first(where: { $0.num == selectedNode })) + case .security: + SecurityConfig(node: nodes.first(where: { $0.num == selectedNode })) case .serial: SerialConfig(node: nodes.first(where: { $0.num == selectedNode })) case .storeAndForward: diff --git a/scripts/gen_protos.sh b/scripts/gen_protos.sh index a587a8e3..d07bc798 100755 --- a/scripts/gen_protos.sh +++ b/scripts/gen_protos.sh @@ -1,12 +1,5 @@ #!/bin/bash -# simple sanity checking for repo -if [ ! -d "./protobufs" ]; then - git submodule update --init -else - git submodule update --remote --merge -fi - # simple sanity checking for executable if [ ! -x "$(which protoc)" ]; then brew install swift-protobuf