mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch 'main'
This commit is contained in:
commit
9bc56f8097
59 changed files with 1725 additions and 649 deletions
|
|
@ -109,7 +109,7 @@
|
|||
"%@ Channels?" : {
|
||||
|
||||
},
|
||||
"%@ config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log." : {
|
||||
"%@ config data was requested over the admin channel but no response has been returned from the remote node." : {
|
||||
|
||||
},
|
||||
"%@ dB" : {
|
||||
|
|
@ -140,6 +140,16 @@
|
|||
},
|
||||
"%@%%" : {
|
||||
|
||||
},
|
||||
"%@%% %@%%" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "%1$@%% %2$@%%"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@°F" : {
|
||||
|
||||
|
|
@ -1166,6 +1176,9 @@
|
|||
},
|
||||
"Are you sure you want to delete this message?" : {
|
||||
|
||||
},
|
||||
"Are you sure you want to factory reset the node?" : {
|
||||
|
||||
},
|
||||
"are.you.sure" : {
|
||||
"localizations" : {
|
||||
|
|
@ -1415,6 +1428,9 @@
|
|||
},
|
||||
"Bad" : {
|
||||
|
||||
},
|
||||
"Bad Packets: %d" : {
|
||||
|
||||
},
|
||||
"Bandwidth" : {
|
||||
|
||||
|
|
@ -1849,9 +1865,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Bluetooth Logs" : {
|
||||
|
||||
},
|
||||
"bluetooth.config" : {
|
||||
"localizations" : {
|
||||
|
|
@ -4568,9 +4581,6 @@
|
|||
},
|
||||
"Configure" : {
|
||||
|
||||
},
|
||||
"Configuring Node" : {
|
||||
|
||||
},
|
||||
"Connect to a Node" : {
|
||||
|
||||
|
|
@ -4895,15 +4905,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Coordinates: %@, %@" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Coordinates: %1$@, %2$@"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Coordinates:" : {
|
||||
|
||||
},
|
||||
"copy" : {
|
||||
"localizations" : {
|
||||
|
|
@ -5061,6 +5064,9 @@
|
|||
},
|
||||
"Debug Log" : {
|
||||
|
||||
},
|
||||
"Debug Logs" : {
|
||||
|
||||
},
|
||||
"Debug Logs%@" : {
|
||||
|
||||
|
|
@ -5352,9 +5358,6 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Developer" : {
|
||||
|
||||
},
|
||||
"Developers" : {
|
||||
|
||||
|
|
@ -5420,10 +5423,7 @@
|
|||
"Device GPS" : {
|
||||
|
||||
},
|
||||
"Device is managed by a mesh administrator." : {
|
||||
|
||||
},
|
||||
"Device Logging Enabled" : {
|
||||
"Device is managed by a mesh administrator, the user is unable to access any of the device settings." : {
|
||||
|
||||
},
|
||||
"Device Metrics" : {
|
||||
|
|
@ -6325,7 +6325,7 @@
|
|||
"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 new public key infrastructure for encryption. Requires firmware version 2.5 or greater." : {
|
||||
|
||||
},
|
||||
"Direct messages are using the shared key for the channel." : {
|
||||
|
|
@ -6712,6 +6712,9 @@
|
|||
},
|
||||
"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." : {
|
||||
|
||||
},
|
||||
"Drop Pin in Maps" : {
|
||||
|
||||
},
|
||||
"echo" : {
|
||||
"localizations" : {
|
||||
|
|
@ -6917,9 +6920,6 @@
|
|||
},
|
||||
"Enabling Ethernet will disable the bluetooth connection to the app." : {
|
||||
|
||||
},
|
||||
"Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it." : {
|
||||
|
||||
},
|
||||
"Enabling WiFi will disable the bluetooth connection to the app." : {
|
||||
|
||||
|
|
@ -7227,12 +7227,12 @@
|
|||
},
|
||||
"Favorites" : {
|
||||
|
||||
},
|
||||
"Fetch the latest position of a cetain node" : {
|
||||
|
||||
},
|
||||
"Favorites and nodes with recent messages show up at the top of the contact list." : {
|
||||
|
||||
|
||||
},
|
||||
"Fetch the latest position of a cetain node" : {
|
||||
|
||||
},
|
||||
"Fifteen Minutes" : {
|
||||
|
||||
|
|
@ -8148,6 +8148,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Group Message" : {
|
||||
|
||||
},
|
||||
"Gusts %@" : {
|
||||
|
||||
|
|
@ -10912,6 +10915,9 @@
|
|||
},
|
||||
"Location" : {
|
||||
|
||||
},
|
||||
"Location:" : {
|
||||
|
||||
},
|
||||
"Location: %@" : {
|
||||
|
||||
|
|
@ -14565,9 +14571,10 @@
|
|||
|
||||
},
|
||||
"Message content exceeds 228 bytes." : {
|
||||
|
||||
},
|
||||
"Message Status Options" : {
|
||||
|
||||
|
||||
},
|
||||
"message.details" : {
|
||||
"localizations" : {
|
||||
|
|
@ -16057,7 +16064,7 @@
|
|||
"Other data sources" : {
|
||||
|
||||
},
|
||||
"Output live debug logging over serial." : {
|
||||
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth." : {
|
||||
|
||||
},
|
||||
"Output pin buzzer GPIO " : {
|
||||
|
|
@ -16071,6 +16078,12 @@
|
|||
},
|
||||
"Override automatic OLED screen detection." : {
|
||||
|
||||
},
|
||||
"Packets Received: %d" : {
|
||||
|
||||
},
|
||||
"Packets Sent: %d" : {
|
||||
|
||||
},
|
||||
"password" : {
|
||||
"localizations" : {
|
||||
|
|
@ -16319,6 +16332,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Perform a factory reset on the node you are connected to" : {
|
||||
|
||||
},
|
||||
"phone.gps" : {
|
||||
"localizations" : {
|
||||
|
|
@ -17024,6 +17040,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Reboot Node?" : {
|
||||
|
||||
},
|
||||
"reboot.node" : {
|
||||
"localizations" : {
|
||||
|
|
@ -17386,9 +17405,18 @@
|
|||
},
|
||||
"Requires that there be an accelerometer on your device." : {
|
||||
|
||||
},
|
||||
"Reset App Settings" : {
|
||||
|
||||
},
|
||||
"Reset NodeDB" : {
|
||||
|
||||
},
|
||||
"Restart" : {
|
||||
|
||||
},
|
||||
"Restart to the node you are connected to" : {
|
||||
|
||||
},
|
||||
"restore" : {
|
||||
|
||||
|
|
@ -19131,13 +19159,16 @@
|
|||
"Send" : {
|
||||
|
||||
},
|
||||
"Send a channel message" : {
|
||||
"Send a Group Message" : {
|
||||
|
||||
},
|
||||
"Send a message to a certain meshtastic channel" : {
|
||||
|
||||
},
|
||||
"Send a waypoint" : {
|
||||
"Send a shutdown to the node you are connected to" : {
|
||||
|
||||
},
|
||||
"Send a Waypoint" : {
|
||||
|
||||
},
|
||||
"Send ASCII bell with alert message. Useful for triggering external notification on bell." : {
|
||||
|
|
@ -19233,9 +19264,6 @@
|
|||
},
|
||||
"Serial Console over the Stream API." : {
|
||||
|
||||
},
|
||||
"Serial Debug Logs" : {
|
||||
|
||||
},
|
||||
"serial.config" : {
|
||||
"localizations" : {
|
||||
|
|
@ -19876,6 +19904,12 @@
|
|||
},
|
||||
"Show Weather" : {
|
||||
|
||||
},
|
||||
"Shut Down" : {
|
||||
|
||||
},
|
||||
"Shut Down Node?" : {
|
||||
|
||||
},
|
||||
"Shutdown Node?" : {
|
||||
|
||||
|
|
@ -21072,6 +21106,9 @@
|
|||
},
|
||||
"The minimum distance change in meters to be considered for a smart position broadcast." : {
|
||||
|
||||
},
|
||||
"The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also 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 public key authorized to send admin messages to this node." : {
|
||||
|
||||
|
|
@ -22241,7 +22278,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Updated Device Metrics Data." : {
|
||||
"Updated Node Stats Data." : {
|
||||
|
||||
},
|
||||
"Updated: %@" : {
|
||||
|
|
@ -22459,12 +22496,6 @@
|
|||
},
|
||||
"Via Mqtt" : {
|
||||
|
||||
},
|
||||
"View and export position-redacted device logs over Bluetooth" : {
|
||||
|
||||
},
|
||||
"View Logs" : {
|
||||
|
||||
},
|
||||
"voltage" : {
|
||||
"localizations" : {
|
||||
|
|
@ -22678,7 +22709,7 @@
|
|||
"Your Firmware is up to date" : {
|
||||
|
||||
},
|
||||
"Your MQTT Server must support TLS." : {
|
||||
"Your MQTT Server must support TLS. Not available via the public mqtt server." : {
|
||||
|
||||
},
|
||||
"Your position has been sent with a request for a response with their position. You will receive a notification when a position is returned." : {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@
|
|||
BCB613832C672A2600485544 /* MessageChannelIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613822C672A2600485544 /* MessageChannelIntent.swift */; };
|
||||
BCB613852C68703800485544 /* NodePositionIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613842C68703800485544 /* NodePositionIntent.swift */; };
|
||||
BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613862C69A0FB00485544 /* AppIntentErrors.swift */; };
|
||||
BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */; };
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */; };
|
||||
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */; };
|
||||
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */; };
|
||||
C9697FA527933B8C00250207 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = C9697FA427933B8C00250207 /* SQLite */; };
|
||||
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */; };
|
||||
D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D93068D42B812B700066FBC8 /* MessageDestination.swift */; };
|
||||
|
|
@ -275,6 +279,10 @@
|
|||
BCB613822C672A2600485544 /* MessageChannelIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageChannelIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613842C68703800485544 /* NodePositionIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodePositionIntent.swift; sourceTree = "<group>"; };
|
||||
BCB613862C69A0FB00485544 /* AppIntentErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIntentErrors.swift; sourceTree = "<group>"; };
|
||||
BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShutDownNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartNodeIntent.swift; sourceTree = "<group>"; };
|
||||
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsProvider.swift; sourceTree = "<group>"; };
|
||||
BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactoryResetNodeIntent.swift; sourceTree = "<group>"; };
|
||||
D93068D22B8129510066FBC8 /* MessageContextMenuItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageContextMenuItems.swift; sourceTree = "<group>"; };
|
||||
D93068D42B812B700066FBC8 /* MessageDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDestination.swift; sourceTree = "<group>"; };
|
||||
D93068D62B8146690066FBC8 /* MessageText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageText.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -368,6 +376,7 @@
|
|||
DD77093C2AA1AFA3007A8BF0 /* ChannelTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelTips.swift; sourceTree = "<group>"; };
|
||||
DD77093E2AA1B146007A8BF0 /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = "<group>"; };
|
||||
DD798B062915928D005217CD /* ChannelMessageList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelMessageList.swift; sourceTree = "<group>"; };
|
||||
DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 43.xcdatamodel"; sourceTree = "<group>"; };
|
||||
DD8169F8271F1A6100F4AB02 /* MeshLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLogger.swift; sourceTree = "<group>"; };
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshLog.swift; sourceTree = "<group>"; };
|
||||
DD8169FE272476C700F4AB02 /* LogDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDocument.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -573,6 +582,10 @@
|
|||
BCB613822C672A2600485544 /* MessageChannelIntent.swift */,
|
||||
BCB613842C68703800485544 /* NodePositionIntent.swift */,
|
||||
BCB613862C69A0FB00485544 /* AppIntentErrors.swift */,
|
||||
BCE2D3C22C7ADF42008E6199 /* ShutDownNodeIntent.swift */,
|
||||
BCE2D3C42C7AE369008E6199 /* RestartNodeIntent.swift */,
|
||||
BCE2D3C82C7C377F008E6199 /* FactoryResetNodeIntent.swift */,
|
||||
BCE2D3C62C7B0D0A008E6199 /* ShortcutsProvider.swift */,
|
||||
);
|
||||
path = AppIntents;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -706,8 +719,8 @@
|
|||
DD41582528582E9B009B0E59 /* DeviceConfig.swift */,
|
||||
DD8EBF42285058FA00426DCA /* DisplayConfig.swift */,
|
||||
DD2553562855B02500E55709 /* LoRaConfig.swift */,
|
||||
DD2553582855B52700E55709 /* PositionConfig.swift */,
|
||||
DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */,
|
||||
DD2553582855B52700E55709 /* PositionConfig.swift */,
|
||||
D93068DA2B81C85E0066FBC8 /* PowerConfig.swift */,
|
||||
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */,
|
||||
DD61937B2863877A00E59241 /* Module */,
|
||||
|
|
@ -1257,6 +1270,7 @@
|
|||
25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */,
|
||||
25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */,
|
||||
259792252C2F114500AD1659 /* ChannelEntityExtension.swift in Sources */,
|
||||
BCE2D3C52C7AE369008E6199 /* RestartNodeIntent.swift in Sources */,
|
||||
259792262C2F114500AD1659 /* PositionEntityExtension.swift in Sources */,
|
||||
259792272C2F114500AD1659 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
DDDB444829F8A9C900EE2349 /* String.swift in Sources */,
|
||||
|
|
@ -1361,6 +1375,7 @@
|
|||
D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */,
|
||||
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */,
|
||||
DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */,
|
||||
BCE2D3C72C7B0D0A008E6199 /* ShortcutsProvider.swift in Sources */,
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */,
|
||||
DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */,
|
||||
DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */,
|
||||
|
|
@ -1421,6 +1436,7 @@
|
|||
BCB613872C69A0FB00485544 /* AppIntentErrors.swift in Sources */,
|
||||
DD73FD1128750779000852D6 /* PositionLog.swift in Sources */,
|
||||
DD15E4F52B8BFC8E00654F61 /* PaxCounterLog.swift in Sources */,
|
||||
BCE2D3C32C7ADF42008E6199 /* ShutDownNodeIntent.swift in Sources */,
|
||||
25F5D5C22C3F6E4B008036E3 /* AppState.swift in Sources */,
|
||||
DD3CC6C028E7A60700FA9159 /* MessagingEnums.swift in Sources */,
|
||||
DD6F657B2C6EC2900053C113 /* LockLegend.swift in Sources */,
|
||||
|
|
@ -1436,6 +1452,7 @@
|
|||
DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */,
|
||||
DD77093F2AA1B146007A8BF0 /* UIColor.swift in Sources */,
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */,
|
||||
BCE2D3C92C7C377F008E6199 /* FactoryResetNodeIntent.swift in Sources */,
|
||||
DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */,
|
||||
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */,
|
||||
DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */,
|
||||
|
|
@ -1669,7 +1686,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.2;
|
||||
MARKETING_VERSION = 2.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1704,7 +1721,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.2;
|
||||
MARKETING_VERSION = 2.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1736,7 +1753,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.2;
|
||||
MARKETING_VERSION = 2.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1769,7 +1786,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.2;
|
||||
MARKETING_VERSION = 2.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1881,6 +1898,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */,
|
||||
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */,
|
||||
DD2984A82C5AEF7500B1268D /* MeshtasticDataModelV 41.xcdatamodel */,
|
||||
DD68BAE72C417A74004C01A0 /* MeshtasticDataModelV 40.xcdatamodel */,
|
||||
|
|
@ -1924,7 +1942,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */;
|
||||
currentVersion = DD7E235F2C7AA3E50078ACDF /* MeshtasticDataModelV 43.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
|
||||
class AppIntentErrors {
|
||||
enum AppIntentError: Swift.Error, CustomLocalizedStringResourceConvertible {
|
||||
|
|
@ -14,8 +15,12 @@ class AppIntentErrors {
|
|||
|
||||
var localizedStringResource: LocalizedStringResource {
|
||||
switch self {
|
||||
case let .message(message): return "Error: \(message)"
|
||||
case .notConnected: return "No Connected Node"
|
||||
case let .message(message):
|
||||
Logger.services.error("App Intent: \(message)")
|
||||
return "Error: \(message)"
|
||||
case .notConnected:
|
||||
Logger.services.error("App Intent: No Connected Node")
|
||||
return "No Connected Node"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
Meshtastic/AppIntents/FactoryResetNodeIntent.swift
Normal file
41
Meshtastic/AppIntents/FactoryResetNodeIntent.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// FactoryResetNodeIntent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 8/25/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
struct FactoryResetNodeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Factory Reset"
|
||||
static var description: IntentDescription = "Perform a factory reset on the node you are connected to"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Request user confirmation before performing the factory reset
|
||||
try await requestConfirmation(result: .result(dialog: "Are you sure you want to factory reset the node?"),confirmationActionName: ConfirmationActionName
|
||||
.custom(acceptLabel: "Factory Reset", acceptAlternatives: [], denyLabel: "Cancel", denyAlternatives: [], destructive: true))
|
||||
|
||||
// Ensure the node is connected
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
// Safely unwrap the connected node information
|
||||
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user {
|
||||
|
||||
// Attempt to send a factory reset command, throw an error if it fails
|
||||
if !BLEManager.shared.sendFactoryReset(fromUser: fromUser, toUser: toUser) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to perform factory reset")
|
||||
}
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
|
||||
}
|
||||
//
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import Foundation
|
|||
import AppIntents
|
||||
|
||||
struct MessageChannelIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Send a channel message"
|
||||
static var title: LocalizedStringResource = "Send a Group Message"
|
||||
|
||||
static var description: IntentDescription = "Send a message to a certain meshtastic channel"
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ struct NodePositionIntent: AppIntent {
|
|||
}
|
||||
|
||||
let nodeInfo = fetchedNode[0]
|
||||
nodeInfo.latestEnvironmentMetrics?.batteryLevel
|
||||
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
|
||||
let nodeLocation = CLLocation(latitude: latitude, longitude: longitude)
|
||||
|
|
|
|||
39
Meshtastic/AppIntents/RestartNodeIntent.swift
Normal file
39
Meshtastic/AppIntents/RestartNodeIntent.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// RestartNodeIntent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 8/24/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
struct RestartNodeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Restart"
|
||||
|
||||
static var description: IntentDescription = "Restart to the node you are connected to"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
||||
try await requestConfirmation(result: .result(dialog: "Reboot Node?"))
|
||||
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
// Safely unwrap the connectedNode using if let
|
||||
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user,
|
||||
let adminIndex = connectedNode.myInfo?.adminIndex {
|
||||
|
||||
// Attempt to send shutdown, throw an error if it fails
|
||||
if !BLEManager.shared.sendReboot(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to restart")
|
||||
}
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import MeshtasticProtobufs
|
|||
|
||||
struct SendWaypointIntent: AppIntent {
|
||||
|
||||
static var title = LocalizedStringResource("Send a waypoint")
|
||||
static var title = LocalizedStringResource("Send a Waypoint")
|
||||
|
||||
@Parameter(title: "Name", default: "Dropped Pin")
|
||||
var nameParameter: String?
|
||||
|
|
|
|||
36
Meshtastic/AppIntents/ShortcutsProvider.swift
Normal file
36
Meshtastic/AppIntents/ShortcutsProvider.swift
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// ShortcutsProvider.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 8/24/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
struct ShortcutsProvider: AppShortcutsProvider {
|
||||
static var appShortcuts: [AppShortcut] {
|
||||
AppShortcut(intent: ShutDownNodeIntent(),
|
||||
phrases: ["Shut down \(.applicationName) node",
|
||||
"Shut down my \(.applicationName) node",
|
||||
"Turn off \(.applicationName) node",
|
||||
"Power down \(.applicationName) node",
|
||||
"Deactivate \(.applicationName) node"],
|
||||
shortTitle: "Shut Down",
|
||||
systemImageName: "power")
|
||||
|
||||
AppShortcut(intent: RestartNodeIntent(),
|
||||
phrases: ["Restart \(.applicationName) node",
|
||||
"Restart my \(.applicationName) node",
|
||||
"Reboot \(.applicationName) node",
|
||||
"Reboot my \(.applicationName) node"],
|
||||
shortTitle: "Restart",
|
||||
systemImageName: "arrow.circlepath")
|
||||
|
||||
AppShortcut(intent: MessageChannelIntent(),
|
||||
phrases: ["Message a \(.applicationName) channel",
|
||||
"Send a \(.applicationName) group message"],
|
||||
shortTitle: "Group Message",
|
||||
systemImageName: "message")
|
||||
}
|
||||
}
|
||||
39
Meshtastic/AppIntents/ShutDownNodeIntent.swift
Normal file
39
Meshtastic/AppIntents/ShutDownNodeIntent.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// ShutDownNodeIntent.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 8/24/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AppIntents
|
||||
|
||||
struct ShutDownNodeIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Shut Down"
|
||||
|
||||
static var description: IntentDescription = "Send a shutdown to the node you are connected to"
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
try await requestConfirmation(result: .result(dialog: "Shut Down Node?"))
|
||||
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
// Safely unwrap the connectedNode using if let
|
||||
if let connectedPeripheralNum = BLEManager.shared.connectedPeripheral?.num,
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheralNum, context: PersistenceController.shared.container.viewContext),
|
||||
let fromUser = connectedNode.user,
|
||||
let toUser = connectedNode.user,
|
||||
let adminIndex = connectedNode.myInfo?.adminIndex {
|
||||
|
||||
// Attempt to send shutdown, throw an error if it fails
|
||||
if !BLEManager.shared.sendShutdown(fromUser: fromUser, toUser: toUser, adminIndex: adminIndex) {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to shut down")
|
||||
}
|
||||
} else {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to retrieve connected node or required data")
|
||||
}
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ enum RoutingError: Int, CaseIterable, Identifiable {
|
|||
case .notAuthorized:
|
||||
return true
|
||||
case .pkiFailed:
|
||||
return false
|
||||
return true
|
||||
case .pkiUnknownPubkey:
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ extension NodeInfoEntity {
|
|||
}
|
||||
|
||||
var isOnline: Bool {
|
||||
let fifteenMinutesAgo = Calendar.current.date(byAdding: .minute, value: -15, to: Date())
|
||||
if lastHeard?.compare(fifteenMinutesAgo!) == .orderedDescending {
|
||||
let twoHoursAgo = Calendar.current.date(byAdding: .minute, value: -120, to: Date())
|
||||
if lastHeard?.compare(twoHoursAgo!) == .orderedDescending {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@
|
|||
import SwiftUI
|
||||
|
||||
public extension View {
|
||||
func onFirstAppear(_ action: @escaping () -> ()) -> some View {
|
||||
func onFirstAppear(_ action: @escaping () -> Void) -> some View {
|
||||
modifier(FirstAppear(action: action))
|
||||
}
|
||||
}
|
||||
|
||||
private struct FirstAppear: ViewModifier {
|
||||
let action: () -> ()
|
||||
let action: () -> Void
|
||||
|
||||
// Use this to only fire your block one time
|
||||
@State private var hasAppeared = false
|
||||
|
|
|
|||
|
|
@ -1153,7 +1153,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
|
||||
@MainActor
|
||||
public func getPositionFromPhoneGPS(destNum: Int64) -> Position? {
|
||||
public func getPositionFromPhoneGPS(destNum: Int64, fixedPosition: Bool) -> Position? {
|
||||
var positionPacket = Position()
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
|
||||
|
|
@ -1172,7 +1172,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(lastLocation.altitude)
|
||||
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
|
||||
|
||||
let currentSpeed = lastLocation.speed
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed)
|
||||
|
|
@ -1181,6 +1180,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
/// Set location source for time
|
||||
if !fixedPosition {
|
||||
/// From GPS treat time as good
|
||||
positionPacket.locationSource = Position.LocSource.locExternal
|
||||
} else {
|
||||
/// From GPS, but time can be old and have drifted
|
||||
positionPacket.locationSource = Position.LocSource.locManual
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
|
|
@ -1199,6 +1206,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if (currentHeading > 0 && currentHeading <= 360) && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
/// Set location source for time
|
||||
if !fixedPosition {
|
||||
/// From GPS treat time as good
|
||||
positionPacket.locationSource = Position.LocSource.locExternal
|
||||
} else {
|
||||
/// From GPS, but time can be old and have drifted
|
||||
positionPacket.locationSource = Position.LocSource.locManual
|
||||
}
|
||||
}
|
||||
return positionPacket
|
||||
}
|
||||
|
|
@ -1206,7 +1221,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
@MainActor
|
||||
public func setFixedPosition(fromUser: UserEntity, channel: Int32) -> Bool {
|
||||
var adminPacket = AdminMessage()
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num) else {
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: fromUser.num, fixedPosition: true) else {
|
||||
return false
|
||||
}
|
||||
adminPacket.setFixedPosition = positionPacket
|
||||
|
|
@ -1261,7 +1276,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
@MainActor
|
||||
public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum) else {
|
||||
guard let positionPacket = getPositionFromPhoneGPS(destNum: destNum, fixedPosition: false) else {
|
||||
Logger.services.error("Unable to get position data from device GPS to send to node")
|
||||
return false
|
||||
}
|
||||
|
|
@ -1314,7 +1329,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var adminPacket = AdminMessage()
|
||||
adminPacket.setTimeOnly = UInt32(Date().timeIntervalSince1970)
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = 0
|
||||
meshPacket.to = UInt32(self.connectedPeripheral.num)
|
||||
meshPacket.from = UInt32(self.connectedPeripheral.num)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
|
|
@ -2548,6 +2564,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.getConfigRequest = AdminMessage.ConfigType.bluetoothConfig
|
||||
if UserDefaults.enableAdministration {
|
||||
adminPacket.sessionPasskey = toUser.userNode?.sessionPasskey ?? Data()
|
||||
}
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ func generateMessageMarkdown (message: String) -> String {
|
|||
|
||||
func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||||
|
||||
let remote = nodeNum != UserDefaults.preferredPeripheralNum
|
||||
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) {
|
||||
|
|
@ -365,7 +366,7 @@ func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObje
|
|||
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 {
|
||||
if fetchedNode[0].user?.publicKey == nil && !nodeInfo.user.publicKey.isEmpty {
|
||||
fetchedNode[0].user?.pkiEncrypted = true
|
||||
fetchedNode[0].user?.publicKey = nodeInfo.user.publicKey
|
||||
}
|
||||
|
|
@ -496,19 +497,19 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getConfigResponse(adminMessage.getConfigResponse) {
|
||||
let config = adminMessage.getConfigResponse
|
||||
if config.payloadVariant == Config.OneOf_PayloadVariant.bluetooth(config.bluetooth) {
|
||||
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), context: context)
|
||||
upsertBluetoothConfigPacket(config: config.bluetooth, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.device(config.device) {
|
||||
upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), context: context)
|
||||
upsertDeviceConfigPacket(config: config.device, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.display(config.display) {
|
||||
upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), context: context)
|
||||
upsertDisplayConfigPacket(config: config.display, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.lora(config.lora) {
|
||||
upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), context: context)
|
||||
upsertLoRaConfigPacket(config: config.lora, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.network(config.network) {
|
||||
upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), context: context)
|
||||
upsertNetworkConfigPacket(config: config.network, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.position(config.position) {
|
||||
upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), context: context)
|
||||
upsertPositionConfigPacket(config: config.position, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, context: context)
|
||||
} else if config.payloadVariant == Config.OneOf_PayloadVariant.power(config.power) {
|
||||
upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), context: context)
|
||||
upsertPowerConfigPacket(config: config.power, nodeNum: Int64(packet.from), sessionPasskey: adminMessage.sessionPasskey, 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)
|
||||
}
|
||||
|
|
@ -632,11 +633,6 @@ 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
|
||||
|
|
@ -681,14 +677,10 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
|
||||
if let telemetryMessage = try? Telemetry(serializedData: packet.decoded.payload) {
|
||||
|
||||
// Only log telemetry from the mesh not the connected device
|
||||
if connectedNode != Int64(packet.from) {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
|
||||
MeshLogger.log("📈 \(logString)")
|
||||
} else {
|
||||
// If it is the connected node
|
||||
}
|
||||
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
|
||||
MeshLogger.log("📈 \(logString)")
|
||||
|
||||
if telemetryMessage.variant != Telemetry.OneOf_Variant.deviceMetrics(telemetryMessage.deviceMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) && telemetryMessage.variant != Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
|
||||
/// Other unhandled telemetry packets
|
||||
return
|
||||
}
|
||||
|
|
@ -727,6 +719,18 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
telemetry.windLull = telemetryMessage.environmentMetrics.windLull
|
||||
telemetry.windDirection = Int32(truncatingIfNeeded: telemetryMessage.environmentMetrics.windDirection)
|
||||
telemetry.metricsType = 1
|
||||
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.localStats(telemetryMessage.localStats) {
|
||||
// Local Stats for Live activity
|
||||
telemetry.uptimeSeconds = Int32(telemetryMessage.localStats.uptimeSeconds)
|
||||
telemetry.channelUtilization = telemetryMessage.localStats.channelUtilization
|
||||
telemetry.airUtilTx = telemetryMessage.localStats.airUtilTx
|
||||
telemetry.numPacketsTx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsTx)
|
||||
telemetry.numPacketsRx = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRx)
|
||||
telemetry.numPacketsRxBad = Int32(truncatingIfNeeded: telemetryMessage.localStats.numPacketsRxBad)
|
||||
telemetry.numOnlineNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numOnlineNodes)
|
||||
telemetry.numTotalNodes = Int32(truncatingIfNeeded: telemetryMessage.localStats.numTotalNodes)
|
||||
telemetry.metricsType = 6
|
||||
Logger.statistics.info("📈 [Mesh Statistics] Channel Utilization: \(telemetryMessage.localStats.channelUtilization, privacy: .public) Airtime: \(telemetryMessage.localStats.airUtilTx, privacy: .public) Packets Sent: \(telemetryMessage.localStats.numPacketsTx, privacy: .public) Packets Received: \(telemetryMessage.localStats.numPacketsRx, privacy: .public) Bad Packets Received: \(telemetryMessage.localStats.numPacketsRxBad, privacy: .public) Nodes Online: \(telemetryMessage.localStats.numOnlineNodes, privacy: .public) of \(telemetryMessage.localStats.numTotalNodes, privacy: .public) nodes for Node: \(packet.from.toHex(), privacy: .public)")
|
||||
}
|
||||
telemetry.snr = packet.rxSnr
|
||||
telemetry.rssi = packet.rxRssi
|
||||
|
|
@ -743,34 +747,45 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
|
||||
}
|
||||
try context.save()
|
||||
// Only log telemetry from the mesh not the connected device
|
||||
if connectedNode != Int64(packet.from) {
|
||||
Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())")
|
||||
} else if telemetry.metricsType == 0 {
|
||||
|
||||
Logger.data.info("💾 [TelemetryEntity] Saved for Node: \(packet.from.toHex())")
|
||||
if telemetry.metricsType == 0 {
|
||||
// Connected Device Metrics
|
||||
// ------------------------
|
||||
// Low Battery notification
|
||||
if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 {
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(UUID().uuidString)"),
|
||||
title: "Critically Low Battery!",
|
||||
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
|
||||
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
|
||||
target: "nodes",
|
||||
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
if connectedNode == Int64(packet.from) {
|
||||
if UserDefaults.lowBatteryNotifications && telemetry.batteryLevel > 0 && telemetry.batteryLevel < 4 {
|
||||
let manager = LocalNotificationManager()
|
||||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(UUID().uuidString)"),
|
||||
title: "Critically Low Battery!",
|
||||
subtitle: "AKA \(telemetry.nodeTelemetry?.user?.shortName ?? "UNK")",
|
||||
content: "Time to charge your radio, there is \(telemetry.batteryLevel)% battery remaining.",
|
||||
target: "nodes",
|
||||
path: "meshtastic:///nodes?nodenum=\(telemetry.nodeTelemetry?.num ?? 0)"
|
||||
)
|
||||
]
|
||||
manager.schedule()
|
||||
}
|
||||
}
|
||||
} else if telemetry.metricsType == 6 {
|
||||
// Update our live activity if there is one running, not available on mac iOS >= 16.2
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
|
||||
let oneMinuteLater = Calendar.current.date(byAdding: .minute, value: (Int(1) ), to: Date())!
|
||||
let date = Date.now...oneMinuteLater
|
||||
let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(timerRange: date, connected: true, channelUtilization: telemetry.channelUtilization, airtime: telemetry.airUtilTx, batteryLevel: UInt32(telemetry.batteryLevel), nodes: 17, nodesOnline: 9)
|
||||
let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Device Metrics Data.", sound: .default)
|
||||
let fifteenMinutesLater = Calendar.current.date(byAdding: .minute, value: (Int(15) ), to: Date())!
|
||||
let date = Date.now...fifteenMinutesLater
|
||||
let updatedMeshStatus = MeshActivityAttributes.MeshActivityStatus(uptimeSeconds: UInt32(telemetry.uptimeSeconds),
|
||||
channelUtilization: telemetry.channelUtilization,
|
||||
airtime: telemetry.airUtilTx,
|
||||
sentPackets: UInt32(telemetry.numPacketsTx),
|
||||
receivedPackets: UInt32(telemetry.numPacketsRx),
|
||||
badReceivedPackets: UInt32(telemetry.numPacketsRxBad),
|
||||
nodesOnline: UInt32(telemetry.numOnlineNodes),
|
||||
totalNodes: UInt32(telemetry.numTotalNodes),
|
||||
timerRange: date)
|
||||
|
||||
let alertConfiguration = AlertConfiguration(title: "Mesh activity update", body: "Updated Node Stats Data.", sound: .default)
|
||||
let updatedContent = ActivityContent(state: updatedMeshStatus, staleDate: nil)
|
||||
|
||||
let meshActivity = Activity<MeshActivityAttributes>.activities.first(where: { $0.attributes.nodeNum == connectedNode })
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV 42.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV 43.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,475 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22758" systemVersion="23G5075b" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceLoggingEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="ledHeartbeatEnabled" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="tzdef" optional="YES" attributeType="String"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="useI2SAsBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ignoreMqtt" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<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"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPositionPrecision" optional="YES" attributeType="Integer 32" defaultValueString="13" usesScalarValueType="YES"/>
|
||||
<attribute name="mapPublishIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mapReportingEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="favorite" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="firstHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="hopsAway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<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"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="pax" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PaxCounterEntity" inverseName="paxNode" inverseEntity="PaxCounterEntity"/>
|
||||
<relationship name="paxCounterConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PaxCounterConfigEntity" inverseName="paxCounterConfigNode" inverseEntity="PaxCounterConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<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"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PaxCounterConfigEntity" representedClassName="PaxCounterConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleThreshold" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="updateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiThreshold" optional="YES" attributeType="Integer 32" defaultValueString="-80" usesScalarValueType="YES"/>
|
||||
<relationship name="paxCounterConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="paxCounterConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PaxCounterEntity" representedClassName="PaxCounterEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ble" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="paxNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="pax" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsEnGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="precisionBits" optional="YES" attributeType="Integer 32" defaultValueString="32" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PowerConfigEntity" representedClassName="PowerConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adcMultiplierOverride" optional="YES" attributeType="Float" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceBatteryInaAddress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isPowerSaving" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="lsSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="minWakeSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="onBatteryShutdownAfterSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="waitBluetoothSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="powerConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="powerConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="distance" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="elevationGain" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="endDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<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"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideConsoleSerialPort" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isRouter" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeartbeat" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="lastRequest" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="powerMeasurementEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="powerScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="powerUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="iaq" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numOnlineNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsRxBad" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numPacketsTx" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="numTotalNodes" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="uptimeSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="weight" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windDirection" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="windGust" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windLull" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="windSpeed" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hasPositions" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwDisplayName" optional="YES" attributeType="String"/>
|
||||
<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"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -209,6 +209,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
} else {
|
||||
if packet.from > Constants.minimumNodeNum {
|
||||
let newUser = createUser(num: Int64(packet.from), context: context)
|
||||
newNode.user?.pkiEncrypted = packet.pkiEncrypted
|
||||
newNode.user?.publicKey = packet.publicKey
|
||||
newNode.user = newUser
|
||||
}
|
||||
}
|
||||
|
|
@ -269,6 +271,11 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
fetchedNode[0].user!.role = Int32(nodeInfoMessage.user.role.rawValue)
|
||||
fetchedNode[0].user!.hwModel = String(describing: nodeInfoMessage.user.hwModel).uppercased()
|
||||
fetchedNode[0].user!.hwModelId = Int32(nodeInfoMessage.user.hwModel.rawValue)
|
||||
|
||||
if !packet.publicKey.isEmpty {
|
||||
fetchedNode[0].user!.pkiEncrypted = packet.pkiEncrypted
|
||||
fetchedNode[0].user!.publicKey = packet.publicKey
|
||||
}
|
||||
Task {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
let dh = hw.first(where: { $0.hwModel == fetchedNode[0].user?.hwModelId ?? 0 })
|
||||
|
|
@ -409,13 +416,11 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64,
|
|||
newBluetoothConfig.enabled = config.enabled
|
||||
newBluetoothConfig.mode = Int32(config.mode.rawValue)
|
||||
newBluetoothConfig.fixedPin = Int32(config.fixedPin)
|
||||
newBluetoothConfig.deviceLoggingEnabled = config.deviceLoggingEnabled
|
||||
fetchedNode[0].bluetoothConfig = newBluetoothConfig
|
||||
} else {
|
||||
fetchedNode[0].bluetoothConfig?.enabled = config.enabled
|
||||
fetchedNode[0].bluetoothConfig?.mode = Int32(config.mode.rawValue)
|
||||
fetchedNode[0].bluetoothConfig?.fixedPin = Int32(config.fixedPin)
|
||||
fetchedNode[0].bluetoothConfig?.deviceLoggingEnabled = config.deviceLoggingEnabled
|
||||
}
|
||||
if sessionPasskey != nil {
|
||||
fetchedNode[0].sessionPasskey = sessionPasskey
|
||||
|
|
|
|||
|
|
@ -55,17 +55,7 @@ enum SettingsNavigationState: String {
|
|||
case firmwareUpdates
|
||||
}
|
||||
|
||||
enum NavigationState: Hashable {
|
||||
case messages(MessagesNavigationState? = nil)
|
||||
case bluetooth
|
||||
case nodes(selectedNodeNum: Int64? = nil)
|
||||
case map(MapNavigationState? = nil)
|
||||
case settings(SettingsNavigationState? = nil)
|
||||
}
|
||||
|
||||
// MARK: Tab Bar
|
||||
|
||||
extension NavigationState {
|
||||
struct NavigationState: Hashable {
|
||||
enum Tab: String, Hashable {
|
||||
case messages
|
||||
case bluetooth
|
||||
|
|
@ -74,34 +64,9 @@ extension NavigationState {
|
|||
case settings
|
||||
}
|
||||
|
||||
var tab: Tab {
|
||||
get {
|
||||
switch self {
|
||||
case .messages:
|
||||
.messages
|
||||
case .bluetooth:
|
||||
.bluetooth
|
||||
case .nodes:
|
||||
.nodes
|
||||
case .map:
|
||||
.map
|
||||
case .settings:
|
||||
.settings
|
||||
}
|
||||
}
|
||||
set {
|
||||
self = switch newValue {
|
||||
case .messages:
|
||||
.messages()
|
||||
case .bluetooth:
|
||||
.bluetooth
|
||||
case .nodes:
|
||||
.nodes()
|
||||
case .map:
|
||||
.map()
|
||||
case .settings:
|
||||
.settings()
|
||||
}
|
||||
}
|
||||
}
|
||||
var selectedTab: Tab = .bluetooth
|
||||
var messages: MessagesNavigationState?
|
||||
var nodeListSelectedNodeNum: Int64?
|
||||
var map: MapNavigationState?
|
||||
var settings: SettingsNavigationState?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ class Router: ObservableObject {
|
|||
private var cancellables: Set<AnyCancellable> = []
|
||||
|
||||
init(
|
||||
navigationState: NavigationState = .bluetooth
|
||||
navigationState: NavigationState = NavigationState(
|
||||
selectedTab: .bluetooth
|
||||
)
|
||||
) {
|
||||
self.navigationState = navigationState
|
||||
|
||||
|
|
@ -21,10 +23,6 @@ class Router: ObservableObject {
|
|||
}.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func route(to destination: NavigationState) {
|
||||
navigationState = destination
|
||||
}
|
||||
|
||||
func route(url: URL) {
|
||||
guard url.scheme == "meshtastic" else {
|
||||
Logger.services.error("🛣 Received routing URL \(url, privacy: .public) with invalid scheme. Ignoring route.")
|
||||
|
|
@ -38,7 +36,7 @@ class Router: ObservableObject {
|
|||
if components.path == "/messages" {
|
||||
routeMessages(components)
|
||||
} else if components.path == "/bluetooth" {
|
||||
route(to: .bluetooth)
|
||||
navigationState.selectedTab = .bluetooth
|
||||
} else if components.path == "/nodes" {
|
||||
routeNodes(components)
|
||||
} else if components.path == "/map" {
|
||||
|
|
@ -75,7 +73,8 @@ class Router: ObservableObject {
|
|||
} else {
|
||||
nil
|
||||
}
|
||||
route(to: .messages(state))
|
||||
navigationState.selectedTab = .messages
|
||||
navigationState.messages = state
|
||||
}
|
||||
|
||||
private func routeNodes(_ components: URLComponents) {
|
||||
|
|
@ -83,7 +82,9 @@ class Router: ObservableObject {
|
|||
.first(where: { $0.name == "nodenum" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
route(to: .nodes(selectedNodeNum: nodeId))
|
||||
|
||||
navigationState.selectedTab = .nodes
|
||||
navigationState.nodeListSelectedNodeNum = nodeId
|
||||
}
|
||||
|
||||
private func routeMap(_ components: URLComponents) {
|
||||
|
|
@ -95,12 +96,14 @@ class Router: ObservableObject {
|
|||
.first(where: { $0.name == "waypointId" })?
|
||||
.value
|
||||
.flatMap(Int64.init)
|
||||
if let nodeId {
|
||||
route(to: .map(.selectedNode(nodeId)))
|
||||
|
||||
navigationState.selectedTab = .map
|
||||
navigationState.map = if let nodeId {
|
||||
.selectedNode(nodeId)
|
||||
} else if let waypointId {
|
||||
route(to: .map(.waypoint(waypointId)))
|
||||
.waypoint(waypointId)
|
||||
} else {
|
||||
route(to: .map())
|
||||
nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +115,7 @@ class Router: ObservableObject {
|
|||
.flatMap(String.init)
|
||||
.flatMap(SettingsNavigationState.init(rawValue:))
|
||||
|
||||
route(to: .settings(settingFromPath))
|
||||
navigationState.selectedTab = .settings
|
||||
navigationState.settings = settingFromPath
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,38 +52,80 @@ struct Connect: View {
|
|||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
}
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90)
|
||||
}
|
||||
.padding(.trailing)
|
||||
VStack(alignment: .leading) {
|
||||
if node != nil {
|
||||
Text(connectedPeripheral.longName).font(.title2)
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
VStack(alignment: .center) {
|
||||
CircleText(text: node?.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node?.num ?? 0))), circleSize: 90)
|
||||
.padding(.trailing, 5)
|
||||
if node?.latestDeviceMetrics != nil {
|
||||
BatteryCompact(batteryLevel: node?.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
}
|
||||
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
if node != nil {
|
||||
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
|
||||
.padding(.trailing)
|
||||
VStack(alignment: .leading) {
|
||||
if node != nil {
|
||||
Text(connectedPeripheral.longName).font(.title2)
|
||||
}
|
||||
Text("ble.name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name ?? "unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
}
|
||||
if bleManager.isSubscribed {
|
||||
Text("subscribed").font(.callout)
|
||||
.foregroundColor(.green)
|
||||
} else {
|
||||
|
||||
HStack {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Image(systemName: "square.stack.3d.down.forward")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
if node != nil {
|
||||
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
}
|
||||
if bleManager.isSubscribed {
|
||||
Text("subscribed").font(.callout)
|
||||
.foregroundColor(.green)
|
||||
} else {
|
||||
HStack {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
Image(systemName: "square.stack.3d.down.forward")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3))
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Text("communicating").font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Text("communicating").font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6")).lastObject as? TelemetryEntity
|
||||
if localStats != nil {
|
||||
Divider()
|
||||
if localStats?.numTotalNodes ?? 0 >= 100 {
|
||||
Text("\(String(format: "Connected: %d nodes online", localStats?.numOnlineNodes ?? 0))")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("\(String(format: "Connected: %d of %d nodes online", localStats?.numOnlineNodes ?? 0, localStats?.numTotalNodes ?? 0))")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
}
|
||||
Text("\(String(format: "Ch. Util: %.2f", localStats?.channelUtilization ?? 0))% \(String(format: "Airtime: %.2f", localStats?.airUtilTx ?? 0))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Packets Sent: \(localStats?.numPacketsTx ?? 0)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Packets Received: \(localStats?.numPacketsRx ?? 0)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
Text("Bad Packets: \(localStats?.numPacketsRxBad ?? 0)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(Color.gray)
|
||||
|
|
@ -327,17 +369,25 @@ struct Connect: View {
|
|||
#if canImport(ActivityKit)
|
||||
func startNodeActivity() {
|
||||
liveActivityStarted = true
|
||||
let timerSeconds = 60
|
||||
let deviceMetrics = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0"))
|
||||
let mostRecent = deviceMetrics?.lastObject as? TelemetryEntity
|
||||
// 15 Minutes Local Stats Interval
|
||||
let timerSeconds = 900
|
||||
let localStats = node?.telemetries?.filtered(using: NSPredicate(format: "metricsType == 6"))
|
||||
let mostRecent = localStats?.lastObject as? TelemetryEntity
|
||||
|
||||
let activityAttributes = MeshActivityAttributes(nodeNum: Int(node?.num ?? 0), name: node?.user?.longName ?? "unknown")
|
||||
|
||||
let future = Date(timeIntervalSinceNow: Double(timerSeconds))
|
||||
let initialContentState = MeshActivityAttributes.ContentState(uptimeSeconds: UInt32(mostRecent?.uptimeSeconds ?? 0),
|
||||
channelUtilization: mostRecent?.channelUtilization ?? 0.0,
|
||||
airtime: mostRecent?.airUtilTx ?? 0.0,
|
||||
sentPackets: UInt32(mostRecent?.numPacketsTx ?? 0),
|
||||
receivedPackets: UInt32(mostRecent?.numPacketsRx ?? 0),
|
||||
badReceivedPackets: UInt32(mostRecent?.numPacketsRxBad ?? 0),
|
||||
nodesOnline: UInt32(mostRecent?.numOnlineNodes ?? 0),
|
||||
totalNodes: UInt32(mostRecent?.numTotalNodes ?? 0),
|
||||
timerRange: Date.now...future)
|
||||
|
||||
let initialContentState = MeshActivityAttributes.ContentState(timerRange: Date.now...future, connected: true, channelUtilization: mostRecent?.channelUtilization ?? 0.0, airtime: mostRecent?.airUtilTx ?? 0.0, batteryLevel: UInt32(mostRecent?.batteryLevel ?? 0), nodes: 17, nodesOnline: 9)
|
||||
|
||||
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 2, to: Date())!)
|
||||
let activityContent = ActivityContent(state: initialContentState, staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!)
|
||||
|
||||
do {
|
||||
let myActivity = try Activity<MeshActivityAttributes>.request(attributes: activityAttributes, content: activityContent,
|
||||
|
|
|
|||
|
|
@ -13,14 +13,7 @@ struct ContentView: View {
|
|||
var router: Router
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: Binding(
|
||||
get: {
|
||||
appState.router.navigationState.tab
|
||||
},
|
||||
set: { newValue in
|
||||
appState.router.navigationState.tab = newValue
|
||||
}
|
||||
)) {
|
||||
TabView(selection: $appState.router.navigationState.selectedTab) {
|
||||
Messages(
|
||||
router: appState.router,
|
||||
unreadChannelMessages: $appState.unreadChannelMessages,
|
||||
|
|
|
|||
|
|
@ -98,9 +98,6 @@ struct ChannelMessageList: View {
|
|||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
} else {
|
||||
let messageDate = message.timestamp
|
||||
Text(" \(messageDate.formattedDate(format: MessageText.dateFormatString))").font(.caption2).foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,21 +26,6 @@ struct Messages: View {
|
|||
@Binding
|
||||
var unreadDirectMessages: Int
|
||||
|
||||
// Aliases the navigation state for the NavigationSplitView sidebar selection
|
||||
private var messagesSelection: Binding<MessagesNavigationState?> {
|
||||
Binding(
|
||||
get: {
|
||||
guard case .messages(let state) = router.navigationState else {
|
||||
return nil
|
||||
}
|
||||
return state
|
||||
},
|
||||
set: { newValue in
|
||||
router.navigationState = .messages(newValue)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@State var node: NodeInfoEntity?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var channelSelection: ChannelEntity? // Nothing selected by default.
|
||||
|
|
@ -49,7 +34,7 @@ struct Messages: View {
|
|||
|
||||
var body: some View {
|
||||
NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
List(selection: messagesSelection) {
|
||||
List(selection: $router.navigationState.messages) {
|
||||
NavigationLink(value: MessagesNavigationState.channels()) {
|
||||
Label {
|
||||
Text("channels")
|
||||
|
|
@ -88,11 +73,12 @@ struct Messages: View {
|
|||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
} content: {
|
||||
if case .messages(.channels) = router.navigationState {
|
||||
switch router.navigationState.messages {
|
||||
case .channels(let channelId, let messageId):
|
||||
ChannelList(node: $node, channelSelection: $channelSelection)
|
||||
} else if case .messages(.directMessages) = router.navigationState {
|
||||
case .directMessages(let userNum, let messageId):
|
||||
UserList(node: $node, userSelection: $userSelection)
|
||||
} else if case .messages(nil) = router.navigationState {
|
||||
case nil:
|
||||
Text("Select a conversation type")
|
||||
}
|
||||
} detail: {
|
||||
|
|
@ -100,9 +86,9 @@ struct Messages: View {
|
|||
ChannelMessageList(myInfo: myInfo, channel: channelSelection)
|
||||
} else if let userSelection {
|
||||
UserMessageList(user: userSelection)
|
||||
} else if case .messages(.channels) = router.navigationState {
|
||||
} else if case .channels = router.navigationState.messages {
|
||||
Text("Select a channel")
|
||||
} else if case .messages(.directMessages) = router.navigationState {
|
||||
} else if case .directMessages = router.navigationState.messages {
|
||||
Text("Select a conversation")
|
||||
}
|
||||
}.onChange(of: router.navigationState) { _ in
|
||||
|
|
@ -116,11 +102,7 @@ struct Messages: View {
|
|||
node = getNodeInfo(id: nodeId, context: context)
|
||||
}
|
||||
|
||||
guard case .messages(let state) = router.navigationState else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let state else {
|
||||
guard let state = router.navigationState.messages else {
|
||||
channelSelection = nil
|
||||
userSelection = nil
|
||||
return
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ struct UserList: View {
|
|||
var boolFilters: [Bool] {[
|
||||
isFavorite,
|
||||
isOnline,
|
||||
isPkiEncrypted,
|
||||
isEnvironment,
|
||||
distanceFilter,
|
||||
roleFilter
|
||||
|
|
@ -45,10 +44,12 @@ struct UserList: View {
|
|||
sortDescriptors: [NSSortDescriptor(key: "lastMessage", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.favorite", ascending: false),
|
||||
NSSortDescriptor(key: "pkiEncrypted", ascending: false),
|
||||
NSSortDescriptor(key: "userNode.lastHeard", ascending: false),
|
||||
NSSortDescriptor(key: "longName", ascending: true)],
|
||||
predicate: NSPredicate(format: "longName != ''"),
|
||||
animation: .default
|
||||
)
|
||||
private var users: FetchedResults<UserEntity>
|
||||
var users: FetchedResults<UserEntity>
|
||||
|
||||
@Binding var node: NodeInfoEntity?
|
||||
@Binding var userSelection: UserEntity?
|
||||
|
|
@ -201,34 +202,55 @@ struct UserList: View {
|
|||
DirectMessagesHelp()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaLora) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaMqtt = true
|
||||
}
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: viaMqtt) { _ in
|
||||
if !viaLora && !viaMqtt {
|
||||
viaLora = true
|
||||
}
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [deviceRoles]) { _ in
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: hopsAway) { _ in
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: [boolFilters]) { _ in
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: maxDistance) { _ in
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onChange(of: isPkiEncrypted) { _ in
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
searchUserList()
|
||||
Task {
|
||||
await searchUserList()
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom, alignment: .leading) {
|
||||
HStack {
|
||||
|
|
@ -266,8 +288,7 @@ struct UserList: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
}
|
||||
}
|
||||
|
||||
private func searchUserList() {
|
||||
private func searchUserList() async {
|
||||
|
||||
/// Case Insensitive Search Text Predicates
|
||||
let searchPredicates = ["userId", "numString", "hwModel", "hwDisplayName", "longName", "shortName"].map { property in
|
||||
|
|
@ -307,7 +328,7 @@ struct UserList: View {
|
|||
}
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
let isOnlinePredicate = NSPredicate(format: "userNode.lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate)
|
||||
predicates.append(isOnlinePredicate)
|
||||
}
|
||||
/// Encrypted
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ struct UserMessageList: View {
|
|||
} else if currentUser && message.ackError > 0 {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,9 @@ struct NodeMapSwiftUI: View {
|
|||
if node.positions?.count ?? 0 > 1 {
|
||||
position = .automatic
|
||||
} else {
|
||||
position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60))
|
||||
if let mrCoord = mostRecent?.coordinate {
|
||||
position = .camera(MapCamera(centerCoordinate: mrCoord, distance: 8000, heading: 0, pitch: 60))
|
||||
}
|
||||
}
|
||||
if self.scene == nil {
|
||||
Task {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ struct WaypointForm: View {
|
|||
@State private var expire: Date = Date.now.addingTimeInterval(60 * 480) // 1 minute * 480 = 8 Hours
|
||||
@State private var locked: Bool = false
|
||||
@State private var lockedTo: Int64 = 0
|
||||
@State private var detents: Set<PresentationDetent> = [.medium, .fraction(0.85)]
|
||||
@State private var selectedDetent: PresentationDetent = .medium
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
|
|
@ -39,9 +41,12 @@ struct WaypointForm: View {
|
|||
let distance = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
|
||||
Section(header: Text("Coordinate") ) {
|
||||
HStack {
|
||||
Text("Location: \(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
Text("Location:")
|
||||
.foregroundColor(.secondary)
|
||||
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(Color.gray)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
HStack {
|
||||
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
|
||||
|
|
@ -124,6 +129,7 @@ struct WaypointForm: View {
|
|||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
/// Send a new or exiting waypoint
|
||||
|
|
@ -239,7 +245,7 @@ struct WaypointForm: View {
|
|||
} else {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 65)
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
|
||||
Spacer()
|
||||
Text(waypoint.name ?? "?")
|
||||
.font(.largeTitle)
|
||||
|
|
@ -250,6 +256,7 @@ struct WaypointForm: View {
|
|||
} else {
|
||||
Button {
|
||||
editMode = true
|
||||
selectedDetent = .fraction(0.85)
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil" )
|
||||
.font(.largeTitle)
|
||||
|
|
@ -269,22 +276,30 @@ struct WaypointForm: View {
|
|||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "doc.plaintext")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Coordinate
|
||||
Label {
|
||||
Text("Coordinates: \(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
Text("Coordinates:")
|
||||
.foregroundColor(.primary)
|
||||
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
|
||||
.textSelection(.enabled)
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption2)
|
||||
} icon: {
|
||||
Image(systemName: "mappin.and.ellipse")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
Image(systemName: "mappin.circle")
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom)
|
||||
// Drop Maps Pin
|
||||
Button(action: {
|
||||
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
|
||||
}
|
||||
.padding(.bottom)
|
||||
/// Created
|
||||
Label {
|
||||
Text("Created: \(waypoint.created?.formatted() ?? "?")")
|
||||
|
|
@ -292,9 +307,8 @@ struct WaypointForm: View {
|
|||
} icon: {
|
||||
Image(systemName: "clock.badge.checkmark")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom)
|
||||
/// Updated
|
||||
if waypoint.lastUpdated != nil {
|
||||
Label {
|
||||
|
|
@ -303,9 +317,8 @@ struct WaypointForm: View {
|
|||
} icon: {
|
||||
Image(systemName: "clock.arrow.circlepath")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
.padding(.bottom)
|
||||
}
|
||||
/// Expires
|
||||
if waypoint.expire != nil {
|
||||
|
|
@ -378,7 +391,8 @@ struct WaypointForm: View {
|
|||
longitude = waypoint.coordinate.longitude
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.75)])
|
||||
.presentationDetents(detents, selection: $selectedDetent)
|
||||
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@ struct NodeDetail: View {
|
|||
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)
|
||||
Text("The most recent public key for this node does not match the previously recorded key. You can delete the node and let it exchange keys again, but this also 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.")
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.callout)
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: "key.slash.fill")
|
||||
|
|
|
|||
|
|
@ -30,8 +30,22 @@ struct NodeListItem: View {
|
|||
}
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
if node.user?.pkiEncrypted ?? false {
|
||||
if !(node.user?.keyMatch ?? false) {
|
||||
/// 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(node.user?.longName ?? "unknown".localized)
|
||||
.font(.headline)
|
||||
.fontWeight(.regular)
|
||||
.allowsTightening(true)
|
||||
if node.favorite {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -123,10 +123,10 @@ struct MeshMap: View {
|
|||
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap)
|
||||
}
|
||||
.onChange(of: router.navigationState) {
|
||||
guard case .map(let selectedNodeNum) = router.navigationState else { return }
|
||||
guard case .map = router.navigationState.selectedTab else { return }
|
||||
// TODO: handle deep link for waypoints
|
||||
}
|
||||
.onChange(of: (selectedMapLayer)) { newMapLayer in
|
||||
.onChange(of: selectedMapLayer) { newMapLayer in
|
||||
switch selectedMapLayer {
|
||||
case .standard:
|
||||
UserDefaults.mapLayer = newMapLayer
|
||||
|
|
|
|||
|
|
@ -335,11 +335,8 @@ struct NodeList: View {
|
|||
}
|
||||
}
|
||||
.onChange(of: router.navigationState) { _ in
|
||||
// Handle deep link routing
|
||||
if case .nodes(let selected) = router.navigationState {
|
||||
self.selectedNode = selected.flatMap {
|
||||
getNodeInfo(id: $0, context: context)
|
||||
}
|
||||
if let selected = router.navigationState.nodeListSelectedNodeNum {
|
||||
self.selectedNode = getNodeInfo(id: selected, context: context)
|
||||
} else {
|
||||
self.selectedNode = nil
|
||||
}
|
||||
|
|
@ -390,7 +387,7 @@ struct NodeList: View {
|
|||
}
|
||||
/// Online
|
||||
if isOnline {
|
||||
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -15, to: Date())! as NSDate)
|
||||
let isOnlinePredicate = NSPredicate(format: "lastHeard >= %@", Calendar.current.date(byAdding: .minute, value: -120, to: Date())! as NSDate)
|
||||
predicates.append(isOnlinePredicate)
|
||||
}
|
||||
/// Encrypted
|
||||
|
|
|
|||
|
|
@ -76,9 +76,14 @@ struct AppSettings: View {
|
|||
}
|
||||
clearCoreDataDatabase(context: context, includeRoutes: true)
|
||||
context.refreshAllObjects()
|
||||
UserDefaults.standard.reset()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
UserDefaults.standard.reset()
|
||||
} label: {
|
||||
Label("Reset App Settings", systemImage: "arrow.counterclockwise.circle")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
if totalDownloadedTileSize != "0MB" {
|
||||
Section(header: Text("Map Tile Data")) {
|
||||
|
|
|
|||
|
|
@ -92,13 +92,21 @@ struct Channels: View {
|
|||
positionPrecision = 32
|
||||
preciseLocation = true
|
||||
positionsEnabled = true
|
||||
|
||||
if channelKey == "AQ==" {
|
||||
positionPrecision = 14
|
||||
preciseLocation = false
|
||||
}
|
||||
} else if !supportedVersion && channelRole == 2 {
|
||||
positionPrecision = 0
|
||||
preciseLocation = false
|
||||
positionsEnabled = false
|
||||
} else {
|
||||
if positionPrecision == 32 {
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
if (positionPrecision > 0 && positionPrecision < 11) || positionPrecision > 14 {
|
||||
positionPrecision = 14
|
||||
}
|
||||
} else if positionPrecision == 32 {
|
||||
preciseLocation = true
|
||||
positionsEnabled = true
|
||||
} else {
|
||||
|
|
@ -217,7 +225,7 @@ struct Channels: View {
|
|||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil || !hasChanges || !hasValidKey)
|
||||
.disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
|
|
@ -253,7 +261,6 @@ struct Channels: View {
|
|||
positionPrecision = 0
|
||||
uplink = false
|
||||
downlink = false
|
||||
hasChanges = true
|
||||
|
||||
let newChannel = ChannelEntity(context: context)
|
||||
newChannel.id = channelIndex
|
||||
|
|
@ -265,6 +272,7 @@ struct Channels: View {
|
|||
newChannel.psk = Data(base64Encoded: channelKey) ?? Data()
|
||||
newChannel.positionPrecision = Int32(positionPrecision)
|
||||
selectedChannel = newChannel
|
||||
hasChanges = true
|
||||
|
||||
} label: {
|
||||
Label("Add Channel", systemImage: "plus.square")
|
||||
|
|
|
|||
|
|
@ -130,7 +130,6 @@ struct ChannelForm: View {
|
|||
}
|
||||
|
||||
Section(header: Text("position")) {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $positionsEnabled) {
|
||||
Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash")
|
||||
|
|
@ -140,24 +139,26 @@ struct ChannelForm: View {
|
|||
}
|
||||
|
||||
if positionsEnabled {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { pl in
|
||||
if pl == false {
|
||||
positionPrecision = 13
|
||||
if channelKey != "AQ==" && channelRole > 0 {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.disabled(!supportedVersion)
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { pl in
|
||||
if pl == false {
|
||||
positionPrecision = 14
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
Slider(value: $positionPrecision, in: 10...19, step: 1) {
|
||||
|
||||
Slider(value: $positionPrecision, in: 11...14, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
|
|
@ -199,11 +200,24 @@ struct ChannelForm: View {
|
|||
.onChange(of: channelKey) { _ in
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: channelKeySize) { _ in
|
||||
if channelKeySize == -1 {
|
||||
if channelRole == 0 {
|
||||
preciseLocation = false
|
||||
}
|
||||
channelKey = "AQ=="
|
||||
}
|
||||
}
|
||||
.onChange(of: channelRole) { _ in
|
||||
hasChanges = true
|
||||
}
|
||||
.onChange(of: preciseLocation) { loc in
|
||||
if loc == true {
|
||||
if channelKey == "AQ==" {
|
||||
preciseLocation = false
|
||||
} else {
|
||||
positionPrecision = 32
|
||||
}
|
||||
positionPrecision = 32
|
||||
} else {
|
||||
positionPrecision = 14
|
||||
|
|
@ -216,7 +230,7 @@ struct ChannelForm: View {
|
|||
.onChange(of: positionsEnabled) { pe in
|
||||
if pe {
|
||||
if positionPrecision == 0 {
|
||||
positionPrecision = 32
|
||||
positionPrecision = 14
|
||||
}
|
||||
} else {
|
||||
positionPrecision = 0
|
||||
|
|
@ -229,7 +243,7 @@ struct ChannelForm: View {
|
|||
.onChange(of: downlink) { _ in
|
||||
hasChanges = true
|
||||
}
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
let tempKey = Data(base64Encoded: channelKey) ?? Data()
|
||||
if tempKey.count == channelKeySize || channelKeySize == -1 {
|
||||
hasValidKey = true
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ struct BluetoothConfig: View {
|
|||
@State var mode = 0
|
||||
@State var fixedPin = "123456"
|
||||
@State var shortPin = false
|
||||
@State var deviceLoggingEnabled = false
|
||||
var pinLength: Int = 6
|
||||
let numberFormatter: NumberFormatter = {
|
||||
let formatter = NumberFormatter()
|
||||
|
|
@ -70,10 +69,6 @@ struct BluetoothConfig: View {
|
|||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $deviceLoggingEnabled) {
|
||||
Label("Device Logging Enabled", systemImage: "ladybug")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.bluetoothConfig == nil)
|
||||
|
|
@ -85,7 +80,6 @@ struct BluetoothConfig: View {
|
|||
bc.enabled = enabled
|
||||
bc.mode = BluetoothModes(rawValue: mode)?.protoEnumValue() ?? Config.BluetoothConfig.PairingMode.randomPin
|
||||
bc.fixedPin = UInt32(fixedPin) ?? 123456
|
||||
bc.deviceLoggingEnabled = deviceLoggingEnabled
|
||||
let adminMessageId = bleManager.saveBluetoothConfig(config: bc, fromUser: connectedNode.user!, toUser: node!.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
@ -106,13 +100,24 @@ struct BluetoothConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// Need to request a BluetoothConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node, node.bluetoothConfig == nil {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty bluetooth config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.bluetoothConfig == nil {
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestBluetoothConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,15 +130,11 @@ struct BluetoothConfig: View {
|
|||
.onChange(of: fixedPin) { newFixedPin in
|
||||
if newFixedPin != String(node?.bluetoothConfig?.fixedPin ?? -1) { hasChanges = true }
|
||||
}
|
||||
.onChange(of: deviceLoggingEnabled) {
|
||||
if $0 != node?.bluetoothConfig?.deviceLoggingEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
func setBluetoothValues() {
|
||||
self.enabled = node?.bluetoothConfig?.enabled ?? true
|
||||
self.mode = Int(node?.bluetoothConfig?.mode ?? 0)
|
||||
self.fixedPin = String(node?.bluetoothConfig?.fixedPin ?? 123456)
|
||||
self.deviceLoggingEnabled = node?.bluetoothConfig?.deviceLoggingEnabled ?? false
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ struct ConfigHeader<T>: View {
|
|||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?[keyPath: config] == nil {
|
||||
Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
let expiration = node?.sessionExpiration ?? Date()
|
||||
if node?[keyPath: config] == nil || expiration < node?.sessionExpiration ?? Date() {
|
||||
Text("\(title) config data was requested over the admin channel but no response has been returned from the remote node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ struct DeviceConfig: View {
|
|||
@State var nodeInfoBroadcastSecs = 10800
|
||||
@State var doubleTapAsButtonPress = false
|
||||
@State var ledHeartbeatEnabled = true
|
||||
@State var isManaged = false
|
||||
@State var tzdef = ""
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -62,12 +61,6 @@ struct DeviceConfig: View {
|
|||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Toggle(isOn: $isManaged) {
|
||||
Label("Managed Device", systemImage: "gearshape.arrow.triangle.2.circlepath")
|
||||
Text("Enabling Managed mode will restrict access to all radio configurations, such as short/long names, regions, channels, modules, etc. and will only be accessible through the Admin channel. To avoid being locked out, make sure the Admin channel is working properly before enabling it.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Picker("Node Info Broadcast Interval", selection: $nodeInfoBroadcastSecs ) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
if ui.rawValue >= 3600 {
|
||||
|
|
@ -213,13 +206,8 @@ struct DeviceConfig: View {
|
|||
dc.rebroadcastMode = RebroadcastModes(rawValue: rebroadcastMode)?.protoEnumValue() ?? RebroadcastModes.all.protoEnumValue()
|
||||
dc.nodeInfoBroadcastSecs = UInt32(nodeInfoBroadcastSecs)
|
||||
dc.doubleTapAsButtonPress = doubleTapAsButtonPress
|
||||
dc.isManaged = isManaged
|
||||
dc.tzdef = tzdef
|
||||
dc.ledHeartbeatDisabled = !ledHeartbeatEnabled
|
||||
if isManaged {
|
||||
serialEnabled = false
|
||||
debugLogEnabled = false
|
||||
}
|
||||
let adminMessageId = bleManager.saveDeviceConfig(config: dc, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
|
|
@ -242,13 +230,26 @@ struct DeviceConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// Need to request a DeviceConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.deviceConfig == nil {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty device config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? -1, context: context)
|
||||
if node != nil && connectedNode != nil && connectedNode?.user != nil {
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.deviceConfig == nil {
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
if node.deviceConfig == nil {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestDeviceConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -276,9 +277,6 @@ struct DeviceConfig: View {
|
|||
.onChange(of: doubleTapAsButtonPress) {
|
||||
if $0 != node?.deviceConfig?.doubleTapAsButtonPress { hasChanges = true }
|
||||
}
|
||||
.onChange(of: isManaged) {
|
||||
if $0 != node?.deviceConfig?.isManaged { hasChanges = true }
|
||||
}
|
||||
.onChange(of: tzdef) { newTzdef in
|
||||
if newTzdef != node?.deviceConfig?.tzdef { hasChanges = true }
|
||||
}
|
||||
|
|
@ -301,7 +299,6 @@ struct DeviceConfig: View {
|
|||
}
|
||||
self.doubleTapAsButtonPress = node?.deviceConfig?.doubleTapAsButtonPress ?? false
|
||||
self.ledHeartbeatEnabled = node?.deviceConfig?.ledHeartbeatEnabled ?? true
|
||||
self.isManaged = node?.deviceConfig?.isManaged ?? false
|
||||
self.tzdef = node?.deviceConfig?.tzdef ?? ""
|
||||
if self.tzdef.isEmpty {
|
||||
self.tzdef = TimeZone.current.posixDescription
|
||||
|
|
|
|||
|
|
@ -163,13 +163,24 @@ struct DisplayConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// Need to request a LoRaConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.displayConfig == nil {
|
||||
.onFirstAppear {
|
||||
// Need to request a DisplayConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty display config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.displayConfig == nil {
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestDisplayConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,13 +232,24 @@ struct LoRaConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// Need to request a LoRaConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.loRaConfig == nil {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty lora config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.loRaConfig == nil {
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestLoRaConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
import MeshtasticProtobufs
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AmbientLightingConfig: View {
|
||||
|
|
@ -85,12 +86,24 @@ struct AmbientLightingConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// 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)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty ambient lighting config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.ambientLightingConfig == nil {
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,13 +233,24 @@ struct CannedMessagesConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// 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")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty canned message config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.cannedMessageConfig == nil {
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestCannedMessagesModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,13 +189,24 @@ struct DetectionSensorConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// 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")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a DetectionSensorModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty detection sensor config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.detectionSensorConfig == nil {
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestDetectionSensorModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,13 +199,24 @@ struct ExternalNotificationConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// 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")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a ExternalNotificationModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty external notificaiton module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.externalNotificationConfig == nil {
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestExternalNotificationModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -279,7 +290,7 @@ struct ExternalNotificationConfig: View {
|
|||
if newNagTimeout != node!.externalNotificationConfig!.nagTimeout { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: useI2SAsBuzzer) {
|
||||
.onChange(of: useI2SAsBuzzer) {
|
||||
if let val = node?.externalNotificationConfig?.useI2SAsBuzzer {
|
||||
hasChanges = $0 != val
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct MQTTConfig: View {
|
|||
@State var password = ""
|
||||
@State var encryptionEnabled = true
|
||||
@State var jsonEnabled = false
|
||||
@State var tlsEnabled = true
|
||||
@State var tlsEnabled = false
|
||||
@State var root = "msh"
|
||||
@State var selectedTopic = ""
|
||||
@State var mqttConnected: Bool = false
|
||||
|
|
@ -32,8 +32,7 @@ struct MQTTConfig: View {
|
|||
@State var nearbyTopics = [String]()
|
||||
@State var mapReportingEnabled = false
|
||||
@State var mapPublishIntervalSecs = 3600
|
||||
@State var preciseLocation: Bool = false
|
||||
@State var mapPositionPrecision: Double = 13.0
|
||||
@State var mapPositionPrecision: Double = 14.0
|
||||
|
||||
let locale = Locale.current
|
||||
|
||||
|
|
@ -105,35 +104,17 @@ struct MQTTConfig: View {
|
|||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $preciseLocation) {
|
||||
Label("Precise Location", systemImage: "scope")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
.onChange(of: preciseLocation) { pl in
|
||||
if pl == false {
|
||||
mapPositionPrecision = 12
|
||||
} else {
|
||||
mapPositionPrecision = 32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !preciseLocation {
|
||||
VStack(alignment: .leading) {
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
Slider(value: $mapPositionPrecision, in: 11...16, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
Label("Approximate Location", systemImage: "location.slash.circle.fill")
|
||||
Slider(value: $mapPositionPrecision, in: 11...14, step: 1) {
|
||||
} minimumValueLabel: {
|
||||
Image(systemName: "minus")
|
||||
} maximumValueLabel: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
Text(PositionPrecision(rawValue: Int(mapPositionPrecision))?.description ?? "")
|
||||
.foregroundColor(.gray)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -234,7 +215,7 @@ struct MQTTConfig: View {
|
|||
.listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/)
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
Text("Your MQTT Server must support TLS. Not available via the public mqtt server.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
|
@ -288,9 +269,6 @@ struct MQTTConfig: View {
|
|||
jsonEnabled = false
|
||||
}
|
||||
if newProxyToClientEnabled != node?.mqttConfig?.proxyToClientEnabled { hasChanges = true }
|
||||
if newProxyToClientEnabled {
|
||||
jsonEnabled = false
|
||||
}
|
||||
}
|
||||
.onChange(of: address) { newAddress in
|
||||
if node != nil && node?.mqttConfig != nil {
|
||||
|
|
@ -324,8 +302,12 @@ struct MQTTConfig: View {
|
|||
}
|
||||
if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: tlsEnabled) {
|
||||
if $0 != node?.mqttConfig?.tlsEnabled { hasChanges = true }
|
||||
.onChange(of: tlsEnabled) { newTlsEnabled in
|
||||
if address.lowercased() == "mqtt.meshtastic.org" {
|
||||
tlsEnabled = false
|
||||
} else {
|
||||
if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: mqttConnected) { newMqttConnected in
|
||||
if newMqttConnected == false {
|
||||
|
|
@ -346,13 +328,24 @@ struct MQTTConfig: View {
|
|||
if newMapPublishIntervalSecs != node!.mqttConfig!.mapPublishIntervalSecs { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.mqttConfig == nil {
|
||||
.onFirstAppear {
|
||||
// Need to request a MqttModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty mqtt module config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.mqttConfig == nil {
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestMqttModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -417,11 +410,12 @@ struct MQTTConfig: View {
|
|||
self.mqttConnected = bleManager.mqttProxyConnected
|
||||
self.mapReportingEnabled = node?.mqttConfig?.mapReportingEnabled ?? false
|
||||
self.mapPublishIntervalSecs = Int(node?.mqttConfig?.mapPublishIntervalSecs ?? 3600)
|
||||
self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 12)
|
||||
if mapPositionPrecision == 0.0 {
|
||||
self.mapPositionPrecision = 12
|
||||
self.mapPositionPrecision = Double(node?.mqttConfig?.mapPositionPrecision ?? 14)
|
||||
if mapPositionPrecision < 11 || mapPositionPrecision > 14 {
|
||||
self.mapPositionPrecision = 14
|
||||
self.hasChanges = true
|
||||
} else {
|
||||
self.hasChanges = false
|
||||
}
|
||||
self.preciseLocation = mapPositionPrecision == 32
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import MeshtasticProtobufs
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
struct PaxCounterConfig: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
|
|
@ -57,12 +58,24 @@ struct PaxCounterConfig: View {
|
|||
name: "\(bleManager.connectedPeripheral?.shortName ?? "?")"
|
||||
)
|
||||
})
|
||||
.onAppear {
|
||||
// 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)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a PaxCounterModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty pax counter module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.paxCounterConfig == nil {
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestPaxCounterModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -81,13 +81,24 @@ struct RangeTestConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// 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")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a RangeTestModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty range test module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.rangeTestConfig == nil {
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestRangeTestModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
|
||||
struct RtttlConfig: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -71,12 +72,24 @@ struct RtttlConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// 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)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a RtttlConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty range test module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.rtttlConfig == nil {
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestRtttlConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -136,20 +136,31 @@ struct SerialConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// 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")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty serial module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.serialConfig == nil {
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestSerialModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: enabled) {
|
||||
if $0 != node?.serialConfig?.enabled { hasChanges = true }
|
||||
}
|
||||
.onChange(of: echo) {
|
||||
.onChange(of: echo) {
|
||||
if $0 != node?.serialConfig?.echo { hasChanges = true }
|
||||
}
|
||||
.onChange(of: rxd) { newRxd in
|
||||
|
|
|
|||
|
|
@ -146,13 +146,24 @@ struct StoreForwardConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
// Need to request a Detection Sensor Module Config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.storeForwardConfig == nil {
|
||||
Logger.mesh.debug("empty store and forward module config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
.onFirstAppear {
|
||||
// Need to request a StoreForwardModuleConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty store & forward module config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.storeForwardConfig == nil {
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestStoreAndForwardModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,13 +134,24 @@ struct TelemetryConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
// Need to request a TelemetryModuleConfig from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.telemetryConfig == nil {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty telemetry module config")
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration && node.num != connectedNode.num {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.telemetryConfig == nil {
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestTelemetryModuleConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,27 @@ struct NetworkConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onFirstAppear {
|
||||
// Need to request a NetworkConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty network config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.networkConfig == nil {
|
||||
_ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestNetworkConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: wifiEnabled) {
|
||||
if $0 != node?.networkConfig?.wifiEnabled { hasChanges = true }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -376,18 +376,25 @@ struct PositionConfig: View {
|
|||
)
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
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 {
|
||||
// Need to request a NetworkConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty position config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let node, let connectedNode {
|
||||
_ = bleManager.requestPositionConfig(
|
||||
fromUser: connectedNode.user!,
|
||||
toUser: node.user!,
|
||||
adminIndex: connectedNode.myInfo?.adminIndex ?? 0
|
||||
)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.positionConfig == nil {
|
||||
_ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestPositionConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import SwiftUI
|
||||
import MeshtasticProtobufs
|
||||
import OSLog
|
||||
|
||||
struct PowerConfig: View {
|
||||
@Environment(\.managedObjectContext) private var context
|
||||
|
|
@ -117,7 +118,7 @@ struct PowerConfig: View {
|
|||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
.onFirstAppear {
|
||||
Api().loadDeviceHardwareData { (hw) in
|
||||
for device in hw {
|
||||
let currentHardware = node?.user?.hwModel ?? "UNSET"
|
||||
|
|
@ -127,11 +128,23 @@ struct PowerConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
// 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)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestPowerConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
// Need to request a NetworkConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty power config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.powerConfig == nil {
|
||||
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestPowerConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ struct SecurityConfig: View {
|
|||
@State var isManaged = false
|
||||
@State var serialEnabled = false
|
||||
@State var debugLogApiEnabled = false
|
||||
@State var bluetoothLoggingEnabled = false
|
||||
@State var adminChannelEnabled = false
|
||||
|
||||
var body: some View {
|
||||
|
|
@ -71,10 +70,14 @@ struct SecurityConfig: View {
|
|||
}
|
||||
}
|
||||
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")!)
|
||||
Toggle(isOn: $serialEnabled) {
|
||||
Label("Serial Console", systemImage: "terminal")
|
||||
Text("Serial Console over the Stream API.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $debugLogApiEnabled) {
|
||||
Label("Debug Logs", systemImage: "ant.fill")
|
||||
Text("Output live debug logging over serial, view and export position-redacted device logs over Bluetooth.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
|
@ -82,7 +85,7 @@ struct SecurityConfig: View {
|
|||
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.")
|
||||
Text("Device is managed by a mesh administrator, the user is unable to access any of the device settings.")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
|
|
@ -92,20 +95,6 @@ struct SecurityConfig: View {
|
|||
}
|
||||
.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)
|
||||
|
|
@ -126,9 +115,6 @@ struct SecurityConfig: View {
|
|||
.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 }
|
||||
}
|
||||
|
|
@ -162,11 +148,25 @@ struct SecurityConfig: View {
|
|||
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)
|
||||
// Need to request a DeviceConfig from the remote node before allowing changes
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, let node {
|
||||
Logger.mesh.info("empty security config")
|
||||
let connectedNode = getNodeInfo(id: connectedPeripheral.num, context: context)
|
||||
if let connectedNode {
|
||||
if node.num != connectedNode.num {
|
||||
if UserDefaults.enableAdministration {
|
||||
/// 2.5 Administration with session passkey
|
||||
let expiration = node.sessionExpiration ?? Date()
|
||||
if expiration < Date() || node.securityConfig == nil {
|
||||
_ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
} else {
|
||||
if node.deviceConfig == nil {
|
||||
/// Legacy Administration
|
||||
_ = bleManager.requestSecurityConfig(fromUser: connectedNode.user!, toUser: node.user!, adminIndex: connectedNode.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,7 +190,6 @@ struct SecurityConfig: View {
|
|||
config.isManaged = isManaged
|
||||
config.serialEnabled = serialEnabled
|
||||
config.debugLogApiEnabled = debugLogApiEnabled
|
||||
config.bluetoothLoggingEnabled = bluetoothLoggingEnabled
|
||||
config.adminChannelEnabled = adminChannelEnabled
|
||||
|
||||
let adminMessageId = bleManager.saveSecurityConfig(
|
||||
|
|
@ -215,7 +214,6 @@ struct SecurityConfig: View {
|
|||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,13 +299,10 @@ struct Settings: View {
|
|||
NavigationStack(
|
||||
path: Binding<[SettingsNavigationState]>(
|
||||
get: {
|
||||
guard case .settings(let route) = router.navigationState, let setting = route else {
|
||||
return []
|
||||
}
|
||||
return [setting]
|
||||
[router.navigationState.settings].compactMap { $0 }
|
||||
},
|
||||
set: { newPath in
|
||||
router.navigationState = .settings(newPath.first)
|
||||
router.navigationState.settings = newPath.first
|
||||
}
|
||||
)
|
||||
) {
|
||||
|
|
@ -349,7 +346,7 @@ struct Settings: View {
|
|||
if bleManager.connectedPeripheral != nil {
|
||||
Section("Configure") {
|
||||
if node?.canRemoteAdmin ?? false {
|
||||
Picker("Configuring Node", selection: $selectedNode) {
|
||||
Picker("Node", selection: $selectedNode) {
|
||||
if selectedNode == 0 {
|
||||
Text("Connect to a Node").tag(0)
|
||||
}
|
||||
|
|
@ -368,6 +365,7 @@ struct Settings: View {
|
|||
} icon: {
|
||||
Image(systemName: "av.remote")
|
||||
}
|
||||
.font(.caption2)
|
||||
.tag(Int(node.num))
|
||||
} else if !UserDefaults.enableAdministration && node.metadata != nil { /// Nodes using the old admin system
|
||||
Label {
|
||||
|
|
@ -393,8 +391,7 @@ struct Settings: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.automatic)
|
||||
.labelsHidden()
|
||||
.pickerStyle(.navigationLink)
|
||||
.onChange(of: selectedNode) { newValue in
|
||||
if selectedNode > 0 {
|
||||
let node = nodes.first(where: { $0.num == newValue })
|
||||
|
|
|
|||
|
|
@ -5,53 +5,144 @@ import XCTest
|
|||
|
||||
final class RouterTests: XCTestCase {
|
||||
|
||||
func testInitialState() throws {
|
||||
XCTAssertEqual(Router().navigationState, .bluetooth)
|
||||
func testInitialState() async throws {
|
||||
let router = await Router()
|
||||
let tab = await router.navigationState.selectedTab
|
||||
XCTAssertEqual(tab, .bluetooth)
|
||||
}
|
||||
|
||||
func testRouteTo() throws {
|
||||
let router = Router(navigationState: .bluetooth)
|
||||
router.route(to: .settings(.about))
|
||||
XCTAssertEqual(router.navigationState, .settings(.about))
|
||||
func testRouteMessages() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages",
|
||||
NavigationState(selectedTab: .messages)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteURL() throws {
|
||||
// Messages
|
||||
try assertRoute("meshtastic:///messages", .messages())
|
||||
try assertRoute(
|
||||
func testRouteMessagesWithChannelIdAndMessageId() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages?channelId=0&messageId=1122334455",
|
||||
.messages(.channels(channelId: 0, messageId: 1122334455))
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .channels(
|
||||
channelId: 0,
|
||||
messageId: 1122334455
|
||||
)
|
||||
)
|
||||
)
|
||||
try assertRoute(
|
||||
}
|
||||
|
||||
func testRouteMessagesWithUserNumAndMessageId() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///messages?userNum=123456789&messageId=9876543210",
|
||||
.messages(.directMessages(userNum: 123456789, messageId: 9876543210))
|
||||
NavigationState(
|
||||
selectedTab: .messages,
|
||||
messages: .directMessages(
|
||||
userNum: 123456789,
|
||||
messageId: 9876543210
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Bluetooth
|
||||
try assertRoute("meshtastic:///bluetooth", .bluetooth)
|
||||
func testRouteBluetooth() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///bluetooth",
|
||||
NavigationState(selectedTab: .bluetooth)
|
||||
)
|
||||
}
|
||||
|
||||
// Nodes
|
||||
try assertRoute("meshtastic:///nodes", .nodes())
|
||||
try assertRoute("meshtastic:///nodes?nodenum=1234567890", .nodes(selectedNodeNum: 1234567890))
|
||||
func testRouteNodes() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///nodes",
|
||||
NavigationState(selectedTab: .nodes)
|
||||
)
|
||||
}
|
||||
|
||||
// Map
|
||||
try assertRoute("meshtastic:///map", .map())
|
||||
try assertRoute("meshtastic:///map?waypointId=123456", .map(.waypoint(123456)))
|
||||
try assertRoute("meshtastic:///map?nodenum=1234567890", .map(.selectedNode(1234567890)))
|
||||
func testRouteNodesWithNodeNum() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///nodes?nodenum=1234567890",
|
||||
NavigationState(
|
||||
selectedTab: .nodes,
|
||||
nodeListSelectedNodeNum: 1234567890
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Settings
|
||||
try assertRoute("meshtastic:///settings", .settings())
|
||||
try assertRoute("meshtastic:///settings/about", .settings(.about))
|
||||
try assertRoute("meshtastic:///settings/invalidSetting", .settings())
|
||||
func testRouteMap() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map",
|
||||
NavigationState(selectedTab: .map)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteMapWithWaypointId() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map?waypointId=123456",
|
||||
NavigationState(
|
||||
selectedTab: .map,
|
||||
map: .waypoint(123456)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteMapWithNodeNum() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///map?nodenum=1234567890",
|
||||
NavigationState(
|
||||
selectedTab: .map,
|
||||
map: .selectedNode(1234567890)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettings() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings",
|
||||
NavigationState(
|
||||
selectedTab: .settings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettingsAbout() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings/about",
|
||||
NavigationState(
|
||||
selectedTab: .settings,
|
||||
settings: .about
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRouteSettingsInvalidSetting() async throws {
|
||||
try await assertRoute(
|
||||
router: Router(),
|
||||
"meshtastic:///settings/invalidSetting",
|
||||
NavigationState(
|
||||
selectedTab: .settings
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private func assertRoute(
|
||||
router: Router = Router(),
|
||||
router: Router,
|
||||
_ urlString: String,
|
||||
_ destination: NavigationState
|
||||
) throws {
|
||||
) async throws {
|
||||
let url = try XCTUnwrap(URL(string: urlString))
|
||||
router.route(url: url)
|
||||
XCTAssertEqual(router.navigationState, destination)
|
||||
await router.route(url: url)
|
||||
let state = await router.navigationState
|
||||
XCTAssertEqual(state, destination)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,13 +15,15 @@ struct MeshActivityAttributes: ActivityAttributes {
|
|||
public typealias MeshActivityStatus = ContentState
|
||||
public struct ContentState: Codable, Hashable {
|
||||
// Dynamic stateful properties about your activity go here!
|
||||
var timerRange: ClosedRange<Date>
|
||||
var connected: Bool
|
||||
var uptimeSeconds: UInt32
|
||||
var channelUtilization: Float
|
||||
var airtime: Float
|
||||
var batteryLevel: UInt32
|
||||
var nodes: Int
|
||||
var nodesOnline: Int
|
||||
var sentPackets: UInt32
|
||||
var receivedPackets: UInt32
|
||||
var badReceivedPackets: UInt32
|
||||
var nodesOnline: UInt32
|
||||
var totalNodes: UInt32
|
||||
var timerRange: ClosedRange<Date>
|
||||
}
|
||||
|
||||
// Fixed non-changing properties about your activity go here!
|
||||
|
|
|
|||
|
|
@ -13,64 +13,84 @@ struct WidgetsLiveActivity: Widget {
|
|||
var body: some WidgetConfiguration {
|
||||
|
||||
ActivityConfiguration(for: MeshActivityAttributes.self) { context in
|
||||
LiveActivityView(nodeName: context.attributes.name, channelUtilization: context.state.channelUtilization, airtime: context.state.airtime, batteryLevel: context.state.batteryLevel, nodes: 17, nodesOnline: 7, timerRange: context.state.timerRange)
|
||||
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
|
||||
LiveActivityView(nodeName: context.attributes.name,
|
||||
uptimeSeconds: 0, // context.attributes.uptimeSeconds,
|
||||
channelUtilization: context.state.channelUtilization,
|
||||
airtime: context.state.airtime,
|
||||
sentPackets: context.state.sentPackets,
|
||||
receivedPackets: context.state.receivedPackets,
|
||||
badReceivedPackets: context.state.badReceivedPackets,
|
||||
nodesOnline: context.state.nodesOnline,
|
||||
totalNodes: context.state.totalNodes,
|
||||
timerRange: context.state.timerRange)
|
||||
.widgetURL(URL(string: "meshtastic:///bluetooth"))
|
||||
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
Text("Network")
|
||||
.font(.headline)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
.padding(.top, 10)
|
||||
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
|
||||
Spacer()
|
||||
Text("Mesh")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.bottom, 10)
|
||||
.fixedSize()
|
||||
Spacer()
|
||||
}
|
||||
if context.state.nodesOnline >= 100 {
|
||||
Text("100+ online")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("\(context.state.nodesOnline) of \(context.state.totalNodes) online")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
}
|
||||
Text("\(String(format: "Ch. Util: %.2f", context.state.channelUtilization))%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Text("\(String(format: "Airtime: %.2f", context.state.airtime))%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Spacer()
|
||||
}
|
||||
DynamicIslandExpandedRegion(.center) {
|
||||
VStack(alignment: .center, spacing: 0) {
|
||||
BatteryIcon(batteryLevel: Int32(context.state.batteryLevel), font: .title, color: .accentColor)
|
||||
if context.state.batteryLevel == 0 {
|
||||
Text("< 1%")
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
} else if context.state.batteryLevel < 101 {
|
||||
Text(String(context.state.batteryLevel) + "%")
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("PWD")
|
||||
.font(.title3)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing, priority: 1) {
|
||||
TimerView(timerRange: context.state.timerRange)
|
||||
.tint(Color("LightIndigo"))
|
||||
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing, priority: 1) {
|
||||
HStack(alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) {
|
||||
Spacer()
|
||||
Text("Packets")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.primary)
|
||||
.padding(.bottom, 10)
|
||||
.fixedSize()
|
||||
Spacer()
|
||||
}
|
||||
Text("Sent: \(context.state.sentPackets)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Text("Received: \(context.state.receivedPackets)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
Text("Bad \(context.state.badReceivedPackets)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.fixedSize()
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
Text(context.attributes.name)
|
||||
.font(context.attributes.name.count > 14 ? .callout : .title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Last Heard: \(Date().formatted())")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(.tint)
|
||||
.fixedSize()
|
||||
}
|
||||
|
||||
|
|
@ -95,85 +115,63 @@ struct WidgetsLiveActivity: Widget {
|
|||
.contentMargins(.trailing, 32, for: .expanded)
|
||||
.contentMargins([.leading, .top, .bottom], 6, for: .compactLeading)
|
||||
.contentMargins(.all, 6, for: .minimal)
|
||||
.widgetURL(URL(string: "meshtastic:///node/\(context.attributes.name)"))
|
||||
.widgetURL(URL(string: "meshtastic:///bluetooth"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct WidgetsLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
|
||||
static let state = MeshActivityAttributes.ContentState(
|
||||
timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9)
|
||||
|
||||
static var previews: some View {
|
||||
attributes
|
||||
.previewContext(state, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Compact")
|
||||
attributes
|
||||
.previewContext(state, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Minimal")
|
||||
attributes
|
||||
.previewContext(state, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Expanded")
|
||||
attributes
|
||||
.previewContext(state, viewKind: .content)
|
||||
.previewDisplayName("Notification")
|
||||
}
|
||||
}
|
||||
//struct WidgetsLiveActivity_Previews: PreviewProvider {
|
||||
// static let attributes = MeshActivityAttributes(nodeNum: 123456789, name: "RAK Compact Rotary Handset Gray 8E6G")
|
||||
// static let state = MeshActivityAttributes.ContentState(
|
||||
// timerRange: Date.now...Date(timeIntervalSinceNow: 60), connected: true, channelUtilization: 25.84, airtime: 10.01, batteryLevel: 39, nodes: 17, nodesOnline: 9)
|
||||
//
|
||||
// static var previews: some View {
|
||||
// attributes
|
||||
// .previewContext(state, viewKind: .dynamicIsland(.compact))
|
||||
// .previewDisplayName("Compact")
|
||||
// attributes
|
||||
// .previewContext(state, viewKind: .dynamicIsland(.minimal))
|
||||
// .previewDisplayName("Minimal")
|
||||
// attributes
|
||||
// .previewContext(state, viewKind: .dynamicIsland(.expanded))
|
||||
// .previewDisplayName("Expanded")
|
||||
// attributes
|
||||
// .previewContext(state, viewKind: .content)
|
||||
// .previewDisplayName("Notification")
|
||||
// }
|
||||
//}
|
||||
|
||||
struct LiveActivityView: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@Environment(\.isLuminanceReduced) var isLuminanceReduced
|
||||
|
||||
var nodeName: String
|
||||
// var connected: Bool
|
||||
var uptimeSeconds: UInt32
|
||||
var channelUtilization: Float
|
||||
var airtime: Float
|
||||
var batteryLevel: UInt32
|
||||
var nodes: Int
|
||||
var nodesOnline: Int
|
||||
var sentPackets: UInt32
|
||||
var receivedPackets: UInt32
|
||||
var badReceivedPackets: UInt32
|
||||
var nodesOnline: UInt32
|
||||
var totalNodes: UInt32
|
||||
var timerRange: ClosedRange<Date>
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(colorScheme == .light ? "m-logo-black" : "m-logo-white")
|
||||
.resizable()
|
||||
.clipShape(ContainerRelativeShape())
|
||||
.opacity(isLuminanceReduced ? 0.5 : 1.0)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 65)
|
||||
.frame(minWidth: 25, idealWidth: 45, maxWidth: 55)
|
||||
Spacer()
|
||||
NodeInfoView(nodeName: nodeName, timerRange: timerRange, channelUtilization: channelUtilization, airtime: airtime, batteryLevel: batteryLevel, nodes: nodes, nodesOnline: nodesOnline)
|
||||
NodeInfoView(isLuminanceReduced: _isLuminanceReduced, nodeName: nodeName, uptimeSeconds: uptimeSeconds, channelUtilization: channelUtilization, airtime: airtime, sentPackets: sentPackets, receivedPackets: receivedPackets, badReceivedPackets: badReceivedPackets, nodesOnline: nodesOnline, totalNodes: totalNodes, timerRange: timerRange)
|
||||
Spacer()
|
||||
VStack {
|
||||
BatteryIcon(batteryLevel: Int32(batteryLevel), font: .title, color: .secondary)
|
||||
if batteryLevel == 0 {
|
||||
Text("< 1%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
} else if batteryLevel < 101 {
|
||||
Text(String(batteryLevel) + "%")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("Plugged In")
|
||||
.font(.headline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
.tint(.primary)
|
||||
.padding([.leading, .top, .bottom])
|
||||
.padding(.trailing, 32)
|
||||
.padding(.trailing, 25)
|
||||
.activityBackgroundTint(colorScheme == .light ? Color("LiveActivityBackground") : Color("AccentColorDimmed"))
|
||||
.activitySystemActionForegroundColor(.primary)
|
||||
}
|
||||
|
|
@ -183,12 +181,15 @@ struct NodeInfoView: View {
|
|||
@Environment(\.isLuminanceReduced) var isLuminanceReduced
|
||||
|
||||
var nodeName: String
|
||||
var timerRange: ClosedRange<Date>
|
||||
var uptimeSeconds: UInt32
|
||||
var channelUtilization: Float
|
||||
var airtime: Float
|
||||
var batteryLevel: UInt32
|
||||
var nodes: Int
|
||||
var nodesOnline: Int
|
||||
var sentPackets: UInt32
|
||||
var receivedPackets: UInt32
|
||||
var badReceivedPackets: UInt32
|
||||
var nodesOnline: UInt32
|
||||
var totalNodes: UInt32
|
||||
var timerRange: ClosedRange<Date>
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
|
|
@ -196,24 +197,45 @@ struct NodeInfoView: View {
|
|||
.font(nodeName.count > 14 ? .callout : .title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.tint)
|
||||
Text("\(String(format: "Ch. Util: %.2f", channelUtilization))%")
|
||||
.font(.headline)
|
||||
Text("\(String(format: "Ch. Util: %.2f", channelUtilization))% \(String(format: "Airtime: %.2f", airtime))%")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
Text("\(String(format: "Airtime: %.2f", airtime))%")
|
||||
.font(.headline)
|
||||
Text("Packets Sent: \(sentPackets)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
// Text("\(String(format: "Connected: %d of %d online", nodesOnline, nodes))")
|
||||
// .font(.callout)
|
||||
// .fontWeight(.medium)
|
||||
// .foregroundStyle(.secondary)
|
||||
// .opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
// .fixedSize()
|
||||
Text("Packets Received: \(receivedPackets)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
Text("Bad Packets: \(badReceivedPackets)")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
if totalNodes >= 100 {
|
||||
Text("\(String(format: "Connected: %d nodes online", nodesOnline))")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
} else {
|
||||
Text("\(String(format: "Connected: %d of %d nodes online", nodesOnline, totalNodes))")
|
||||
.font(.caption)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.8 : 1.0)
|
||||
.fixedSize()
|
||||
}
|
||||
let now = Date()
|
||||
Text("Last Heard: \(now.formatted())")
|
||||
.font(.caption)
|
||||
|
|
@ -255,8 +277,9 @@ struct TimerView: View {
|
|||
|
||||
var body: some View {
|
||||
VStack(alignment: .center) {
|
||||
Text("NEXT UPDATE")
|
||||
.font(.caption)
|
||||
Text("UPDATE IN")
|
||||
.font(.caption2)
|
||||
.allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.secondary)
|
||||
.opacity(isLuminanceReduced ? 0.5 : 1.0)
|
||||
|
|
@ -268,10 +291,12 @@ struct TimerView: View {
|
|||
.fontWeight(.semibold)
|
||||
.foregroundStyle(.tint)
|
||||
Image(systemName: "timer")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
.resizable()
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(width: 30, height: 30)
|
||||
.opacity(isLuminanceReduced ? 0.5 : 1.0)
|
||||
.offset(y: -5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue