Merge pull request #853 from meshtastic/pki

Encrypted Direct Messages Config and Display
This commit is contained in:
Garth Vander Houwen 2024-08-19 16:09:16 -07:00 committed by GitHub
commit 7c72ff4e40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
60 changed files with 1741 additions and 691 deletions

View file

@ -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" : {

View file

@ -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 = "<group>"; };
DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFormatters.swift; sourceTree = "<group>"; };
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = "<group>"; };
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = "<group>"; };
DD1BF2F82776FE2E008C8D2F /* UserMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserMessageList.swift; sourceTree = "<group>"; };
DD2160AE28C5552500C17253 /* MQTTConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MQTTConfig.swift; sourceTree = "<group>"; };
DD23A50E26FD1B4400D9B90C /* PeripheralModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PeripheralModel.swift; sourceTree = "<group>"; };
@ -343,6 +349,11 @@
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CannedMessagesConfig.swift; sourceTree = "<group>"; };
DD6193782863875F00E59241 /* SerialConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialConfig.swift; sourceTree = "<group>"; };
DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 40.xcdatamodel"; sourceTree = "<group>"; };
DD6F65712C6AB8EC0053C113 /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = "<group>"; };
DD6F65732C6CB80A0053C113 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
DD6F65752C6EA5490053C113 /* AckErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AckErrors.swift; sourceTree = "<group>"; };
DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesHelp.swift; sourceTree = "<group>"; };
DD6F657A2C6EC2900053C113 /* LockLegend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockLegend.swift; sourceTree = "<group>"; };
DD73FD1028750779000852D6 /* PositionLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionLog.swift; sourceTree = "<group>"; };
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceMetricsLog.swift; sourceTree = "<group>"; };
DD77093A2AA1ABB8007A8BF0 /* BluetoothTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothTips.swift; sourceTree = "<group>"; };
@ -406,7 +417,6 @@
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrength.swift; sourceTree = "<group>"; };
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryLevelCompact.swift; sourceTree = "<group>"; };
DDB8F40F2A9EE5B400230ECE /* Messages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Messages.swift; sourceTree = "<group>"; };
DDB8F4112A9EE5DD00230ECE /* UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserList.swift; sourceTree = "<group>"; };
DDB8F4132A9EE5F000230ECE /* ChannelList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelList.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
DD6F65772C6EAB860053C113 /* Help */ = {
isa = PBXGroup;
children = (
DD6F65752C6EA5490053C113 /* AckErrors.swift */,
DD6F65782C6EADE60053C113 /* DirectMessagesHelp.swift */,
DD6F657A2C6EC2900053C113 /* LockLegend.swift */,
);
path = Help;
sourceTree = "<group>";
};
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 = "<group>";
@ -995,6 +1017,7 @@
DDD5BB172C2F9C36007E03CA /* OSLogEntryLog.swift */,
DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */,
DDD5BB0C2C285F00007E03CA /* Logger.swift */,
DD6F65732C6CB80A0053C113 /* View.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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 = "";

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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] {

View file

@ -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 {

View file

@ -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
}

View file

@ -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()
}
}
}

View file

@ -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)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
meshPacket.channel = 0
var dataMessage = DataMessage()
if let serializedData: Data = try? adminPacket.serializedData() {
dataMessage.payload = serializedData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
} else {
return false
}
let messageDescription = "🕛 Sent Set Time Admin Message to the connectecd node."
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
return false
}
public func sendShutdown(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> 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)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.wantAck = true
var dataMessage = DataMessage()
guard let adminData: Data = try? adminPacket.serializedData() else {
return 0
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
meshPacket.decoded = dataMessage
let messageDescription = "🛟 Saved Security Config for \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
upsertSecurityConfigPacket(config: config, nodeNum: toUser.num, context: context)
return Int64(meshPacket.id)
}
return 0
}
public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> 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)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2094,7 +2212,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.externalNotification = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2123,7 +2243,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.paxcounter = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2153,7 +2275,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setRingtoneMessage = ringtone
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2183,7 +2307,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.mqtt = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2213,7 +2339,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.rangeTest = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2243,7 +2371,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.serial = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2272,7 +2402,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.storeForward = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2301,7 +2433,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.setModuleConfig.telemetry = config
if fromUser != toUser {
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
}
var meshPacket: MeshPacket = MeshPacket()
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
meshPacket.to = UInt32(toUser.num)
@ -2403,7 +2537,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2434,7 +2567,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.deviceConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2465,7 +2597,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.displayConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2496,7 +2627,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.loraConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2529,7 +2659,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.networkConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2559,7 +2688,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.positionConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2589,7 +2717,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
var adminPacket = AdminMessage()
adminPacket.getConfigRequest = AdminMessage.ConfigType.powerConfig
var meshPacket: MeshPacket = MeshPacket()
meshPacket.to = UInt32(toUser.num)
meshPacket.from = UInt32(fromUser.num)
@ -2615,11 +2742,39 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
return false
}
public func requestSecurityConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> 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)..<UInt32.max)
meshPacket.priority = MeshPacket.Priority.reliable
meshPacket.channel = UInt32(adminIndex)
meshPacket.wantAck = true
var dataMessage = DataMessage()
guard let adminData: Data = try? adminPacket.serializedData() else {
return false
}
dataMessage.payload = adminData
dataMessage.portnum = PortNum.adminApp
dataMessage.wantResponse = true
meshPacket.decoded = dataMessage
let messageDescription = "🛎️ Requested Security Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
return true
}
return false
}
public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> 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)

View file

@ -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

View file

@ -155,7 +155,9 @@
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
@ -224,6 +226,8 @@
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="peripheralId" optional="YES" attributeType="String"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sessionExpiration" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="sessionPasskey" optional="YES" attributeType="Binary"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="viaMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
@ -245,6 +249,7 @@
<relationship name="powerConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PowerConfigEntity" inverseName="powerConfigNode" inverseEntity="PowerConfigEntity"/>
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
<relationship name="securityConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecurityConfigEntity" inverseName="securityConfigNode" inverseEntity="SecurityConfigEntity"/>
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
@ -334,6 +339,17 @@
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SecurityConfigEntity" representedClassName="SecurityConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="adminChannelEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="adminKey" optional="YES" attributeType="Binary"/>
<attribute name="bluetoothLoggingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="debugLogApiEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="isManaged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="privateKey" optional="YES" attributeType="Binary"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="serialEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<relationship name="securityConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="securityConfig" inverseEntity="NodeInfoEntity"/>
</entity>
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
@ -418,11 +434,15 @@
<attribute name="hwModel" attributeType="String"/>
<attribute name="hwModelId" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="keyMatch" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="longName" attributeType="String"/>
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="newPublicKey" optional="YES" attributeType="Binary"/>
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="numString" optional="YES" attributeType="String"/>
<attribute name="pkiEncrypted" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="publicKey" optional="YES" attributeType="Binary"/>
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="shortName" attributeType="String"/>
<attribute name="userId" attributeType="String"/>

View file

@ -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.

View file

@ -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)")

View file

@ -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"

View file

@ -46,6 +46,7 @@ enum SettingsNavigationState: String {
case paxCounter
case ringtone
case serial
case security
case storeAndForward
case telemetry
case meshLog

View file

@ -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")
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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()
}
}
}

View file

@ -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<String>) {
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)
}
}
}
}
}

View file

@ -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))
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -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 {

View file

@ -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<Int> = []
@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")

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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))
}
}

View file

@ -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")

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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<Int> = []
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)

View file

@ -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")

View file

@ -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) {

View file

@ -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() {

View file

@ -23,11 +23,12 @@ struct ConfigHeader<T>: 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)

View file

@ -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 {

View file

@ -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() {

View file

@ -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() {

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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")

View file

@ -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) {

View file

@ -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 }
}
}
}

View file

@ -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)

View file

@ -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 }
}
}

View file

@ -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 {

View file

@ -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")

View file

@ -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() {

View file

@ -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) ||

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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?

View file

@ -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 {

View file

@ -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:

View file

@ -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