mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge branch 'main' of github.com:meshtastic/Meshtastic-Apple
This commit is contained in:
commit
ba84a5d566
148 changed files with 22168 additions and 18309 deletions
34608
Localizable.xcstrings
34608
Localizable.xcstrings
File diff suppressed because it is too large
Load diff
|
|
@ -56,12 +56,12 @@
|
|||
B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; };
|
||||
B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; };
|
||||
BC47C2EF2CE0017D008245CA /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC47C2EE2CE0017D008245CA /* MessageNodeIntent.swift */; };
|
||||
BC5EBA3C2D002A2000C442FF /* MessageNodeIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5EBA3B2D002A2000C442FF /* MessageNodeIntent.swift */; };
|
||||
BC6B45FF2CB2F98900723CEB /* SaveChannelSettingsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC6B45FE2CB2F98900723CEB /* SaveChannelSettingsIntent.swift */; };
|
||||
BCB613812C67290800485544 /* SendWaypointIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB613802C67290800485544 /* SendWaypointIntent.swift */; };
|
||||
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 */; };
|
||||
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDDFA992DBB180D0065189C /* ScrollToBottomButton.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 */; };
|
||||
|
|
@ -145,7 +145,6 @@
|
|||
DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8EBF42285058FA00426DCA /* DisplayConfig.swift */; };
|
||||
DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */; };
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */; };
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD90860D26F69BAE00DC5189 /* NodeMap.swift */; };
|
||||
DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */; };
|
||||
DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */; };
|
||||
DD93800E2BA74D0C008BEC06 /* ChannelForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */; };
|
||||
|
|
@ -176,8 +175,6 @@
|
|||
DDB6CCFB2AAF805100945AF6 /* NodeMapSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6CCFA2AAF805100945AF6 /* NodeMapSwiftUI.swift */; };
|
||||
DDB75A0F2A05920E006ED576 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A0E2A05920E006ED576 /* FileManager.swift */; };
|
||||
DDB75A112A059258006ED576 /* Url.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A102A059258006ED576 /* Url.swift */; };
|
||||
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */; };
|
||||
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A152A0594AD006ED576 /* TileOverlay.swift */; };
|
||||
DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */ = {isa = PBXBuildFile; fileRef = DDB75A192A05EB67006ED576 /* alpha.png */; };
|
||||
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */; };
|
||||
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */; };
|
||||
|
|
@ -326,6 +323,7 @@
|
|||
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>"; };
|
||||
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.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>"; };
|
||||
|
|
@ -437,7 +435,6 @@
|
|||
DD8ED9C42898D51F00B3B0AB /* NetworkConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfig.swift; sourceTree = "<group>"; };
|
||||
DD8ED9C7289CE4B900B3B0AB /* RoutingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingError.swift; sourceTree = "<group>"; };
|
||||
DD90860A26F645B700DC5189 /* Meshtastic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Meshtastic.entitlements; sourceTree = "<group>"; };
|
||||
DD90860D26F69BAE00DC5189 /* NodeMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMap.swift; sourceTree = "<group>"; };
|
||||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotificationManager.swift; sourceTree = "<group>"; };
|
||||
DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeMapContent.swift; sourceTree = "<group>"; };
|
||||
DD93800D2BA74D0C008BEC06 /* ChannelForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelForm.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -475,8 +472,6 @@
|
|||
DDB759E12A04B264006ED576 /* MeshtasticDataModelV12.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV12.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDB75A0E2A05920E006ED576 /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
DDB75A102A059258006ED576 /* Url.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Url.swift; sourceTree = "<group>"; };
|
||||
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OfflineTileManager.swift; sourceTree = "<group>"; };
|
||||
DDB75A152A0594AD006ED576 /* TileOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileOverlay.swift; sourceTree = "<group>"; };
|
||||
DDB75A192A05EB67006ED576 /* alpha.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = alpha.png; sourceTree = "<group>"; };
|
||||
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoRaSignalStrengthIndicator.swift; sourceTree = "<group>"; };
|
||||
DDB75A1F2A10766D006ED576 /* MeshtasticDataModelV13.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV13.xcdatamodel; sourceTree = "<group>"; };
|
||||
|
|
@ -733,7 +728,6 @@
|
|||
DDDB263E2AABEE20003AFCB7 /* NodeList.swift */,
|
||||
DD769E0228D18BF0001A3F05 /* DeviceMetricsLog.swift */,
|
||||
DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */,
|
||||
DD90860D26F69BAE00DC5189 /* NodeMap.swift */,
|
||||
DD73FD1028750779000852D6 /* PositionLog.swift */,
|
||||
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */,
|
||||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
|
||||
|
|
@ -918,15 +912,6 @@
|
|||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDB75A122A0593CD006ED576 /* Map */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DDB75A132A0593E2006ED576 /* OfflineTileManager.swift */,
|
||||
DDB75A152A0594AD006ED576 /* TileOverlay.swift */,
|
||||
);
|
||||
path = Map;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DDC2E14B26CE248E0042C5E4 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -1064,7 +1049,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
|
||||
DDB75A122A0593CD006ED576 /* Map */,
|
||||
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */,
|
||||
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */,
|
||||
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
|
||||
|
|
@ -1121,6 +1105,7 @@
|
|||
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */,
|
||||
DDDCD56F2BB26F5C00BE6B60 /* NodeListFilter.swift */,
|
||||
251926882C3BAF2E00249DF5 /* Actions */,
|
||||
BCDDFA992DBB180D0065189C /* ScrollToBottomButton.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1251,7 +1236,7 @@
|
|||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1540;
|
||||
LastUpgradeCheck = 1600;
|
||||
LastUpgradeCheck = 1630;
|
||||
TargetAttributes = {
|
||||
25F5D5C62C4375A8008036E3 = {
|
||||
CreatedOnToolsVersion = 15.4;
|
||||
|
|
@ -1278,10 +1263,9 @@
|
|||
pl,
|
||||
he,
|
||||
fr,
|
||||
"zh-Hant-TW",
|
||||
se,
|
||||
"pt-PT",
|
||||
sr,
|
||||
it,
|
||||
);
|
||||
mainGroup = DDC2E14B26CE248E0042C5E4;
|
||||
packageReferences = (
|
||||
|
|
@ -1416,7 +1400,6 @@
|
|||
DDC4D568275499A500A4208E /* Persistence.swift in Sources */,
|
||||
DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */,
|
||||
DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */,
|
||||
DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */,
|
||||
D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */,
|
||||
DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */,
|
||||
DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */,
|
||||
|
|
@ -1446,7 +1429,6 @@
|
|||
DDDB444229F8A88700EE2349 /* Double.swift in Sources */,
|
||||
DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */,
|
||||
DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */,
|
||||
DDB75A162A0594AD006ED576 /* TileOverlay.swift in Sources */,
|
||||
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */,
|
||||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
|
||||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
|
||||
|
|
@ -1499,7 +1481,6 @@
|
|||
251926872C3BAE2200249DF5 /* NodeAlertsButton.swift in Sources */,
|
||||
DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */,
|
||||
DDD5BB092C285DDC007E03CA /* AppLog.swift in Sources */,
|
||||
BC5EBA3C2D002A2000C442FF /* MessageNodeIntent.swift in Sources */,
|
||||
DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */,
|
||||
233E99C52D84A0B600CC3A77 /* CompactWidget.swift in Sources */,
|
||||
DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */,
|
||||
|
|
@ -1521,7 +1502,6 @@
|
|||
DD6F65762C6EA5490053C113 /* AckErrors.swift in Sources */,
|
||||
DDDB445029F8AC9C00EE2349 /* UIImage.swift in Sources */,
|
||||
DD86D40F2881BE4C00BAEB7A /* CsvDocument.swift in Sources */,
|
||||
DDB75A142A0593E2006ED576 /* OfflineTileManager.swift in Sources */,
|
||||
DDB75A1E2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift in Sources */,
|
||||
DDA6B2E928419CF2003E8C16 /* MeshPackets.swift in Sources */,
|
||||
DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */,
|
||||
|
|
@ -1562,6 +1542,7 @@
|
|||
DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */,
|
||||
DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */,
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */,
|
||||
BCDDFA9A2DBB180D0065189C /* ScrollToBottomButton.swift in Sources */,
|
||||
DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */,
|
||||
2344A2AF2D6697A700170A77 /* TelemetryEntity+CoreDataClass.swift in Sources */,
|
||||
2344A2B02D6697A700170A77 /* TelemetryEntity+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -1621,7 +1602,6 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
|
|
@ -1645,7 +1625,6 @@
|
|||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
|
|
@ -1695,6 +1674,7 @@
|
|||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
|
|
@ -1759,6 +1739,7 @@
|
|||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
|
@ -1795,7 +1776,6 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
|
|
@ -1805,7 +1785,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.22;
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1829,7 +1809,6 @@
|
|||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
INFOPLIST_FILE = Meshtastic/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Meshtastic;
|
||||
|
|
@ -1839,7 +1818,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.22;
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1860,7 +1839,6 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
|
||||
|
|
@ -1871,7 +1849,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.22;
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1893,7 +1871,6 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = GCH7VS5Y9R;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Widgets;
|
||||
|
|
@ -1904,7 +1881,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.5.22;
|
||||
MARKETING_VERSION = 2.6.2;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1600"
|
||||
LastUpgradeVersion = "1630"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1600"
|
||||
LastUpgradeVersion = "1630"
|
||||
version = "2.0">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class AppIntentErrors {
|
|||
var localizedStringResource: LocalizedStringResource {
|
||||
switch self {
|
||||
case let .message(message):
|
||||
Logger.services.error("App Intent: \(message,privacy: .public)")
|
||||
Logger.services.error("App Intent: \(message, privacy: .public)")
|
||||
return "Error: \(message)"
|
||||
case .notConnected:
|
||||
Logger.services.error("App Intent: No Connected Node")
|
||||
|
|
|
|||
|
|
@ -13,38 +13,38 @@ import UIKit
|
|||
|
||||
@available(iOS 16.4, *)
|
||||
struct NavigateToNodeIntent: ForegroundContinuableIntent {
|
||||
|
||||
|
||||
static var title: LocalizedStringResource = "Navigate to Node Position"
|
||||
static var openAppWhenRun: Bool = false
|
||||
|
||||
|
||||
@Parameter(title: "Node Number")
|
||||
var nodeNum: Int
|
||||
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ProvidesDialog {
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
}
|
||||
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
|
||||
do {
|
||||
guard let fetchedNode = try PersistenceController.shared.container.viewContext.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity],
|
||||
fetchedNode.count == 1 else {
|
||||
throw $nodeNum.needsValueError("Could not find node")
|
||||
}
|
||||
|
||||
|
||||
let nodeInfo = fetchedNode[0]
|
||||
if let latitude = nodeInfo.latestPosition?.coordinate.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.coordinate.longitude {
|
||||
|
||||
|
||||
let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)")
|
||||
|
||||
|
||||
if let mapURL = url, UIApplication.shared.canOpenURL(mapURL) {
|
||||
// Request to continue in foreground before opening the app
|
||||
try await requestToContinueInForeground()
|
||||
|
||||
|
||||
// Open Apple Maps for navigation
|
||||
UIApplication.shared.open(mapURL, options: [:], completionHandler: nil)
|
||||
return .result(dialog: "Navigating to node location.")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct RestartNodeIntent: AppIntent {
|
|||
|
||||
func perform() async throws -> some IntentResult {
|
||||
|
||||
try await requestConfirmation(result: .result(dialog: "Reboot Node?"))
|
||||
try await requestConfirmation(result: .result(dialog: "Reboot node?"))
|
||||
|
||||
if !BLEManager.shared.isConnected {
|
||||
throw AppIntentErrors.AppIntentError.notConnected
|
||||
|
|
|
|||
|
|
@ -29,12 +29,9 @@ struct SaveChannelSettingsIntent: AppIntent {
|
|||
if channelUrl.absoluteString.lowercased().contains("meshtastic.org/e/#") {
|
||||
// Split the URL to get the portion after "#"
|
||||
let components = channelUrl.absoluteString.components(separatedBy: "#")
|
||||
|
||||
// Add channels flag based on the URL query parameter (if present)
|
||||
let addChannels = Bool(channelUrl["add"] ?? "false") ?? false
|
||||
|
||||
var channelSettings: String?
|
||||
|
||||
// Extract the Base64 encoded channel settings (after "#")
|
||||
if let lastComponent = components.last {
|
||||
channelSettings = lastComponent.components(separatedBy: "?").first // Ignore any query parameters
|
||||
|
|
@ -44,7 +41,6 @@ struct SaveChannelSettingsIntent: AppIntent {
|
|||
if let channelSettings = channelSettings {
|
||||
// Call the BLEManager to save the channel settings
|
||||
let saveResult = BLEManager.shared.saveChannelSet(base64UrlString: channelSettings, addChannels: addChannels)
|
||||
|
||||
if !saveResult {
|
||||
throw AppIntentErrors.AppIntentError.message("Failed to save the channel settings.")
|
||||
}
|
||||
|
|
|
|||
12
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "thinknode_m1.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
109
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg
vendored
Normal file
109
Meshtastic/Assets.xcassets/THINKNODEM1.imageset/thinknode_m1.svg
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1" id="svg105" sodipodi:docname="thinknode_m1.svg" inkscape:version="1.4 (e7c3feb1, 2024-10-09)" viewBox="397.31 77.24 361 863.17">
|
||||
<sodipodi:namedview id="namedview105" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="4.066786" inkscape:cx="564.94244" inkscape:cy="741.49463" inkscape:window-width="1472" inkscape:window-height="890" inkscape:window-x="0" inkscape:window-y="38" inkscape:window-maximized="1" inkscape:current-layer="Layer_3"/>
|
||||
<defs id="defs1">
|
||||
<style id="style1">.cls-1{fill:#353535;}.cls-2{fill:#262626;}.cls-3{fill:#cccccb;}.cls-4{fill:#2b2b2b;}.cls-5{fill:#f05043;}.cls-6{fill:#3d3d3d;}.cls-7{fill:#231f20;}.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}</style>
|
||||
</defs>
|
||||
<g id="Layer_3" data-name="Layer 3">
|
||||
<path class="cls-1" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path1"/>
|
||||
<rect class="cls-2" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect1"/>
|
||||
<rect class="cls-1" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect2"/>
|
||||
<rect class="cls-3" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect3"/>
|
||||
<path class="cls-1" d="M507.38,77.74H472.16a7,7,0,0,0-7,7V359.93L452.2,396.26v39.91H561V396.26l-13.3-36V84.15a6.41,6.41,0,0,0-6.41-6.41Z" id="path3"/>
|
||||
<rect class="cls-2" x="454.25" y="436.17" width="104.38" height="3.65" id="rect4"/>
|
||||
<polygon class="cls-1" points="442.68 449.91 448.16 440.69 562.98 440.69 570.51 449.91 442.68 449.91" id="polygon4"/>
|
||||
<rect class="cls-1" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.8" id="rect5"/>
|
||||
<path class="cls-2" d="M611.2,356v-5.48a3.13,3.13,0,0,1,3.13-3.13h86.35a3.13,3.13,0,0,1,3.13,3.13V356Z" id="path5"/>
|
||||
<rect class="cls-2" x="611.07" y="416.61" width="92.74" height="23.22" id="rect6"/>
|
||||
<polygon class="cls-1" points="592.99 449.91 598.47 440.69 713.42 440.69 720.82 449.91 592.99 449.91" id="polygon6"/>
|
||||
<rect class="cls-2" x="751.94" y="555.13" width="5.87" height="47.48" id="rect7"/>
|
||||
<path class="cls-2" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path7"/>
|
||||
<path class="cls-2" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path8"/>
|
||||
<path class="cls-4" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path9"/>
|
||||
<rect class="cls-2" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect9"/>
|
||||
<circle class="cls-5" cx="518.72" cy="853.89" r="5.48" id="circle9"/>
|
||||
<circle class="cls-1" cx="640.14" cy="853.89" r="5.48" id="circle10"/>
|
||||
<circle class="cls-1" cx="541.83" cy="853.89" r="5.48" id="circle11"/>
|
||||
<circle class="cls-1" cx="567.67" cy="853.89" r="5.48" id="circle12"/>
|
||||
<circle class="cls-1" cx="593.51" cy="853.89" r="5.48" id="circle13"/>
|
||||
<circle class="cls-1" cx="616.82" cy="853.89" r="5.48" id="circle14"/>
|
||||
<path class="cls-4" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path14"/>
|
||||
<path class="cls-4" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path15"/>
|
||||
<path class="cls-6" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path16"/>
|
||||
<path class="cls-6" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path17"/>
|
||||
<path class="cls-6" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path18"/>
|
||||
<path class="cls-6" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path19"/>
|
||||
<path class="cls-6" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path20"/>
|
||||
<path class="cls-6" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path21"/>
|
||||
<path class="cls-1" d="M477,924.32h-3V903.09h-7.46v-2.65h17.9v2.65H477Z" id="path51"/>
|
||||
<path class="cls-1" d="M490.59,906.36c0,.43,0,.86,0,1.31s-.07.85-.12,1.2h.2a5.17,5.17,0,0,1,1.44-1.54,7.08,7.08,0,0,1,1.94-.92,7.81,7.81,0,0,1,2.21-.31,8.61,8.61,0,0,1,3.63.68,4.62,4.62,0,0,1,2.19,2.13,8.22,8.22,0,0,1,.73,3.74v11.67h-2.9V912.85a4.72,4.72,0,0,0-1-3.24,3.92,3.92,0,0,0-3.05-1.07,5.65,5.65,0,0,0-3.14.75,4.07,4.07,0,0,0-1.62,2.21,11.17,11.17,0,0,0-.49,3.56v9.26h-2.94V898.91h2.94Z" id="path52"/>
|
||||
<path class="cls-1" d="M509.82,899.67a1.73,1.73,0,0,1,1.19.46,2.17,2.17,0,0,1,0,2.82,1.74,1.74,0,0,1-1.19.47,1.77,1.77,0,0,1-1.24-.47,2.24,2.24,0,0,1,0-2.82A1.76,1.76,0,0,1,509.82,899.67Zm1.44,6.73v17.92h-2.94V906.4Z" id="path53"/>
|
||||
<path class="cls-1" d="M525.58,906.06a6.8,6.8,0,0,1,4.85,1.56c1.09,1,1.63,2.71,1.63,5v11.67h-2.91V912.85a4.67,4.67,0,0,0-1-3.24,3.88,3.88,0,0,0-3-1.07c-2,0-3.36.56-4.11,1.67a8.54,8.54,0,0,0-1.14,4.82v9.29H517V906.4h2.37l.44,2.44h.16a5.68,5.68,0,0,1,1.49-1.56,6.41,6.41,0,0,1,2-.92A8.13,8.13,0,0,1,525.58,906.06Z" id="path54"/>
|
||||
<path class="cls-1" d="M540.53,912.18c0,.36,0,.83,0,1.41s-.07,1.08-.09,1.5h.14l.6-.77.82-1c.28-.34.52-.63.72-.85l5.72-6.05h3.44l-7.26,7.66,7.76,10.26h-3.54L542.57,916l-2,1.78v6.58h-2.91V898.91h2.91Z" id="path55"/>
|
||||
<path class="cls-1" d="M574.81,924.32H571.3l-12.78-19.83h-.13c0,.51.08,1.12.11,1.82s.07,1.45.1,2.24,0,1.61,0,2.43v13.34h-2.77V900.44h3.48l12.74,19.77h.13c0-.36-.05-.89-.08-1.6s-.07-1.5-.1-2.35,0-1.62,0-2.34V900.44h2.81Z" id="path56"/>
|
||||
<path class="cls-1" d="M596.48,915.33a12.32,12.32,0,0,1-.58,4,8.32,8.32,0,0,1-1.67,2.93,7,7,0,0,1-2.65,1.82,9.31,9.31,0,0,1-3.46.62,8.63,8.63,0,0,1-3.28-.62,7.29,7.29,0,0,1-2.61-1.82,8.57,8.57,0,0,1-1.72-2.93,11.76,11.76,0,0,1-.62-4,11.43,11.43,0,0,1,1-5,7.12,7.12,0,0,1,2.87-3.14,8.76,8.76,0,0,1,4.45-1.09,8.4,8.4,0,0,1,4.3,1.09,7.45,7.45,0,0,1,2.91,3.14A11,11,0,0,1,596.48,915.33Zm-13.54,0a10.93,10.93,0,0,0,.55,3.66,4.82,4.82,0,0,0,1.72,2.39,5.69,5.69,0,0,0,5.95,0,4.78,4.78,0,0,0,1.73-2.39,10.93,10.93,0,0,0,.55-3.66,10.36,10.36,0,0,0-.57-3.65,4.84,4.84,0,0,0-1.72-2.32,5,5,0,0,0-3-.82,4.5,4.5,0,0,0-4,1.8A8.79,8.79,0,0,0,582.94,915.33Z" id="path57"/>
|
||||
<path class="cls-1" d="M607.49,924.66a6.67,6.67,0,0,1-5.35-2.33c-1.34-1.54-2-3.86-2-6.94s.67-5.4,2-7a6.74,6.74,0,0,1,5.37-2.36,7.71,7.71,0,0,1,2.44.35,6.37,6.37,0,0,1,1.81,1,6.58,6.58,0,0,1,1.3,1.34h.2c0-.29-.06-.72-.11-1.29s-.09-1-.09-1.36v-7.15H616v25.41h-2.38l-.43-2.4h-.14a6.51,6.51,0,0,1-1.3,1.38,5.9,5.9,0,0,1-1.82,1A7.4,7.4,0,0,1,607.49,924.66Zm.46-2.44c1.9,0,3.23-.52,4-1.56a7.84,7.84,0,0,0,1.16-4.7v-.53a9.84,9.84,0,0,0-1.11-5.14c-.73-1.19-2.09-1.79-4.08-1.79a4,4,0,0,0-3.56,1.89,9.46,9.46,0,0,0-1.19,5.07,9,9,0,0,0,1.19,5A4,4,0,0,0,608,922.22Z" id="path58"/>
|
||||
<path class="cls-1" d="M628.62,906.06a7.53,7.53,0,0,1,4,1,6.62,6.62,0,0,1,2.54,2.82,9.69,9.69,0,0,1,.89,4.27v1.77H623.74a6.75,6.75,0,0,0,1.56,4.63,5.42,5.42,0,0,0,4.16,1.59,12.58,12.58,0,0,0,3-.32,16.78,16.78,0,0,0,2.72-.92v2.58a13.84,13.84,0,0,1-2.71.89,16.15,16.15,0,0,1-3.17.28,9.46,9.46,0,0,1-4.5-1,7.15,7.15,0,0,1-3-3.09,10.61,10.61,0,0,1-1.09-5,12,12,0,0,1,1-5,7.33,7.33,0,0,1,6.94-4.38Zm0,2.41a4.26,4.26,0,0,0-3.33,1.36,6.34,6.34,0,0,0-1.45,3.76h9.13a7.05,7.05,0,0,0-.47-2.68,3.94,3.94,0,0,0-1.42-1.79A4.33,4.33,0,0,0,628.59,908.47Z" id="path59"/>
|
||||
<path class="cls-1" d="M639.36,913h8.1v2.74h-8.1Z" id="path60"/>
|
||||
<path class="cls-1" d="M662.87,924.32,655,903.39h-.13c0,.44.08,1,.12,1.7s.06,1.45.08,2.26,0,1.65,0,2.49v14.48h-2.77V900.44h4.45L664.15,920h.13l7.49-19.57h4.42v23.88h-3V909.64c0-.78,0-1.55,0-2.32s.06-1.5.1-2.18.08-1.25.1-1.72h-.13l-8,20.9Z" id="path61"/>
|
||||
<path class="cls-1" d="M688.16,924.32V909.41c0-.63,0-1.3,0-2s0-1.42.07-2.1,0-1.27,0-1.76c-.35.38-.66.69-.92.92s-.6.54-1,.92l-2.41,2-1.57-2,6.26-4.89H691v23.88Z" id="path62"/>
|
||||
<path class="cls-1" d="M502.55,894.19l-2.22-2.37a14.1,14.1,0,0,1,18.94-.33l-2.13,2.44a10.9,10.9,0,0,0-7.16-2.69A10.78,10.78,0,0,0,502.55,894.19Z" id="path63"/>
|
||||
<path class="cls-1" d="M506.38,897.85l-2.4-2.18a8.08,8.08,0,0,1,11.61-.39l-2.24,2.33a4.85,4.85,0,0,0-7,.24Z" id="path64"/>
|
||||
</g>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<path class="cls-8" d="M472.55,77.74h68.09a7.43,7.43,0,0,1,7.43,7.43V360.26a0,0,0,0,1,0,0h-83a0,0,0,0,1,0,0V85.17A7.43,7.43,0,0,1,472.55,77.74Z" id="path65"/>
|
||||
<line class="cls-8" x1="465.12" y1="123.91" x2="548.07" y2="123.91" id="line65"/>
|
||||
<line class="cls-8" x1="465.12" y1="149.74" x2="548.07" y2="149.74" id="line66"/>
|
||||
<polyline class="cls-8" points="465.12 360.26 452.2 396.26 452.2 436.17 560.99 436.17 560.99 396.26 548.07 360.26" id="polyline66"/>
|
||||
<line class="cls-8" x1="452.2" y1="396.26" x2="560.98" y2="396.26" id="line67"/>
|
||||
<path class="cls-8" d="M449.69,440.17H562a3.26,3.26,0,0,1,2.56,1.55l5.93,8.19H442.68l4-7.49A3.65,3.65,0,0,1,449.69,440.17Z" id="path67"/>
|
||||
<path class="cls-8" d="M600,440.17H712.33a3.24,3.24,0,0,1,2.56,1.55l5.93,8.19H593l4-7.49A3.65,3.65,0,0,1,600,440.17Z" id="path68"/>
|
||||
<line class="cls-8" x1="454.45" y1="436.17" x2="454.45" y2="439.83" id="line68"/>
|
||||
<line class="cls-8" x1="558.64" y1="436.17" x2="558.64" y2="439.83" id="line69"/>
|
||||
<rect class="cls-8" x="604.37" y="355.96" width="105.26" height="60.65" rx="4.87" id="rect69"/>
|
||||
<line class="cls-8" x1="611.07" y1="416.61" x2="611.07" y2="439.83" id="line70"/>
|
||||
<line class="cls-8" x1="703.81" y1="416.61" x2="703.81" y2="439.83" id="line71"/>
|
||||
<path class="cls-8" d="M614.2,347.35h86.48a3.13,3.13,0,0,1,3.13,3.13V356a0,0,0,0,1,0,0H611.07a0,0,0,0,1,0,0v-5.48A3.13,3.13,0,0,1,614.2,347.35Z" id="path71"/>
|
||||
<line class="cls-8" x1="570.51" y1="449.91" x2="592.99" y2="449.91" id="line72"/>
|
||||
<path class="cls-8" d="M720.82,449.91h11.45a19.68,19.68,0,0,1,19.67,19.68V905.12a28.48,28.48,0,0,1-28.47,28.48H425.72A27.77,27.77,0,0,1,397.81,906V470.82a21,21,0,0,1,21.13-20.91h23.74" id="path72"/>
|
||||
<rect class="cls-8" x="447.12" y="523.83" width="266.09" height="266.09" rx="22.7" id="rect72"/>
|
||||
<rect class="cls-8" x="465.51" y="542.22" width="229.3" height="229.3" rx="12.91" id="rect73"/>
|
||||
<rect class="cls-8" x="476.07" y="552.78" width="208.17" height="208.17" rx="7.83" id="rect74"/>
|
||||
<path class="cls-8" d="M494.46,449.91v5.59a12.22,12.22,0,0,0,1.05,4.95l8.6,19.42a9.43,9.43,0,0,0,8.62,5.61h3.61a9.43,9.43,0,0,0,9.43-9.43V449.91" id="path74"/>
|
||||
<path class="cls-8" d="M672.56,449.91v5.59a12.22,12.22,0,0,1-1,4.95l-8.6,19.42a9.43,9.43,0,0,1-8.62,5.61h-3.61a9.43,9.43,0,0,1-9.43-9.43V449.91" id="path75"/>
|
||||
<path class="cls-8" d="M532.42,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,532.42,449.91Z" id="path76"/>
|
||||
<path class="cls-8" d="M559.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,559.81,449.91Z" id="path77"/>
|
||||
<path class="cls-8" d="M587.2,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,587.2,449.91Z" id="path78"/>
|
||||
<path class="cls-8" d="M613.81,449.91h20.35a0,0,0,0,1,0,0v28.72a6.85,6.85,0,0,1-6.85,6.85h-6.65a6.85,6.85,0,0,1-6.85-6.85V449.91A0,0,0,0,1,613.81,449.91Z" id="path79"/>
|
||||
<rect class="cls-8" x="751.94" y="555.13" width="5.87" height="47.48" id="rect79"/>
|
||||
<path class="cls-8" d="M751.94,683.87h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V683.87A0,0,0,0,1,751.94,683.87Z" id="path80"/>
|
||||
<path class="cls-8" d="M751.94,781.43h2.72a3.15,3.15,0,0,1,3.15,3.15v49.17a3.15,3.15,0,0,1-3.15,3.15h-2.72a0,0,0,0,1,0,0V781.43A0,0,0,0,1,751.94,781.43Z" id="path81"/>
|
||||
<path class="cls-8" d="M425.72,933.6l17.46-41.05a15.2,15.2,0,0,1,14-9.25H702.88A15.19,15.19,0,0,1,717,892.9l15.52,39.22" id="path82"/>
|
||||
<rect class="cls-8" x="505.03" y="841.57" width="147.52" height="24.65" rx="12.33" id="rect82"/>
|
||||
<circle class="cls-8" cx="518.72" cy="853.89" r="5.48" id="circle82"/>
|
||||
<circle class="cls-8" cx="640.14" cy="853.89" r="5.48" id="circle83"/>
|
||||
<circle class="cls-8" cx="541.83" cy="853.89" r="5.48" id="circle84"/>
|
||||
<circle class="cls-8" cx="567.67" cy="853.89" r="5.48" id="circle85"/>
|
||||
<circle class="cls-8" cx="593.51" cy="853.89" r="5.48" id="circle86"/>
|
||||
<circle class="cls-8" cx="616.82" cy="853.89" r="5.48" id="circle87"/>
|
||||
<line class="cls-8" x1="430.68" y1="572.74" x2="430.68" y2="602.61" id="line87"/>
|
||||
<line class="cls-8" x1="424.42" y1="595.43" x2="424.42" y2="578.11" id="line88"/>
|
||||
<line class="cls-8" x1="438.21" y1="595.43" x2="438.21" y2="578.11" id="line89"/>
|
||||
<line class="cls-8" x1="430.68" y1="644.74" x2="430.68" y2="674.61" id="line90"/>
|
||||
<line class="cls-8" x1="424.42" y1="667.43" x2="424.42" y2="650.11" id="line91"/>
|
||||
<line class="cls-8" x1="438.21" y1="667.43" x2="438.21" y2="650.11" id="line92"/>
|
||||
<line class="cls-8" x1="430.68" y1="716.74" x2="430.68" y2="746.61" id="line93"/>
|
||||
<line class="cls-8" x1="424.42" y1="739.43" x2="424.42" y2="722.11" id="line94"/>
|
||||
<line class="cls-8" x1="438.21" y1="739.43" x2="438.21" y2="722.11" id="line95"/>
|
||||
<line class="cls-8" x1="730.03" y1="572.74" x2="730.03" y2="602.61" id="line96"/>
|
||||
<line class="cls-8" x1="723.77" y1="595.43" x2="723.77" y2="578.11" id="line97"/>
|
||||
<line class="cls-8" x1="737.56" y1="595.43" x2="737.56" y2="578.11" id="line98"/>
|
||||
<line class="cls-8" x1="730.03" y1="644.74" x2="730.03" y2="674.61" id="line99"/>
|
||||
<line class="cls-8" x1="723.77" y1="667.43" x2="723.77" y2="650.11" id="line100"/>
|
||||
<line class="cls-8" x1="737.56" y1="667.43" x2="737.56" y2="650.11" id="line101"/>
|
||||
<line class="cls-8" x1="730.03" y1="716.74" x2="730.03" y2="746.61" id="line102"/>
|
||||
<line class="cls-8" x1="723.77" y1="739.43" x2="723.77" y2="722.11" id="line103"/>
|
||||
<line class="cls-8" x1="737.56" y1="739.43" x2="737.56" y2="722.11" id="line104"/>
|
||||
<path class="cls-8" d="M428.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path104"/>
|
||||
<path class="cls-8" d="M713.2,933.6v4.1a2.21,2.21,0,0,0,2.22,2.21h11.22a2.21,2.21,0,0,0,2.21-2.21v-4.1" id="path105"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
12
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "thinknode_m2.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
391
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg
vendored
Normal file
391
Meshtastic/Assets.xcassets/THINKNODEM2.imageset/thinknode_m2.svg
vendored
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="svg75"
|
||||
sodipodi:docname="thinknode_m2.svg"
|
||||
inkscape:version="1.4 (e7c3feb1, 2024-10-09)"
|
||||
viewBox="388.5 121.73 413.05 787.86"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview75"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="3.7680002"
|
||||
inkscape:cx="265.65816"
|
||||
inkscape:cy="681.92672"
|
||||
inkscape:window-width="1472"
|
||||
inkscape:window-height="890"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="38"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="Layer_3" />
|
||||
<defs
|
||||
id="defs1">
|
||||
<style
|
||||
id="style1">.cls-1{fill:#262626;}.cls-2{fill:#353535;}.cls-3{fill:#303030;}.cls-4{fill:#f05043;}.cls-5,.cls-6,.cls-7,.cls-8{fill:none;stroke:#000;stroke-miterlimit:10;}.cls-6{stroke-width:0.88px;}.cls-7{stroke-width:0.95px;}.cls-8{stroke-width:1px;}.cls-9{fill:#acdee5;}</style>
|
||||
</defs>
|
||||
<g
|
||||
id="Layer_3"
|
||||
data-name="Layer 3">
|
||||
<polygon
|
||||
class="cls-1"
|
||||
points="575.63 361.13 575.34 354.09 574.7 338.7 574.41 331.65 479.53 331.65 479.23 338.7 478.57 354.09 478.26 361.13 575.63 361.13"
|
||||
id="polygon1" />
|
||||
<polyline
|
||||
class="cls-2"
|
||||
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
|
||||
id="polyline1" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
|
||||
id="path1" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
|
||||
id="path2" />
|
||||
<path
|
||||
class="cls-2"
|
||||
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
|
||||
id="path3" />
|
||||
<polyline
|
||||
class="cls-2"
|
||||
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
|
||||
id="polyline3" />
|
||||
<rect
|
||||
class="cls-1"
|
||||
x="394.16"
|
||||
y="403"
|
||||
width="396"
|
||||
height="501.26"
|
||||
rx="48.72"
|
||||
id="rect3" />
|
||||
<rect
|
||||
class="cls-2"
|
||||
x="405.9"
|
||||
y="417.86"
|
||||
width="372.52"
|
||||
height="471.54"
|
||||
rx="38.16"
|
||||
id="rect4" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
|
||||
id="path4" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M716.86,532.78H462.77a18,18,0,0,0-18,18V673.91a18,18,0,0,0,18,18H716.86a18,18,0,0,0,18-18V550.78A18,18,0,0,0,716.86,532.78Zm6.52,137.48a10.7,10.7,0,0,1-10.7,10.7H465.38a10.7,10.7,0,0,1-10.7-10.7V551.83a10.7,10.7,0,0,1,10.7-10.7h247.3a10.7,10.7,0,0,1,10.7,10.7Z"
|
||||
id="path5" />
|
||||
<rect
|
||||
x="454.68"
|
||||
y="541.13"
|
||||
width="268.7"
|
||||
height="139.83"
|
||||
rx="10.7"
|
||||
id="rect5" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
|
||||
id="path6" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
|
||||
id="path7" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
|
||||
id="path8" />
|
||||
<path
|
||||
class="cls-3"
|
||||
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
|
||||
id="path9" />
|
||||
<rect
|
||||
class="cls-1"
|
||||
x="502.29"
|
||||
y="782"
|
||||
width="180.26"
|
||||
height="24.65"
|
||||
rx="12.33"
|
||||
id="rect9" />
|
||||
<circle
|
||||
class="cls-4"
|
||||
cx="515.99"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle9" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="637.4"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle10" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="660.08"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle11" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="539.09"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle12" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="564.93"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle13" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="590.77"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle14" />
|
||||
<circle
|
||||
class="cls-2"
|
||||
cx="614.09"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle15" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M475.77,856.71h-3.41V832.6h-8.47v-3H484.2v3h-8.43Z"
|
||||
id="path15" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M491.19,836.32c0,.48,0,1-.06,1.48s-.08,1-.13,1.37h.23a5.69,5.69,0,0,1,1.63-1.75,7.82,7.82,0,0,1,2.2-1,8.81,8.81,0,0,1,2.51-.36,9.64,9.64,0,0,1,4.12.78,5.2,5.2,0,0,1,2.49,2.41,9.43,9.43,0,0,1,.83,4.25v13.25h-3.3v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21,6.41,6.41,0,0,0-3.57.85,4.68,4.68,0,0,0-1.84,2.51,12.81,12.81,0,0,0-.55,4v10.52h-3.34V827.85h3.34Z"
|
||||
id="path16" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M513,828.73a2,2,0,0,1,1.35.51,2,2,0,0,1,.59,1.61,2.07,2.07,0,0,1-.59,1.6A2,2,0,0,1,513,833a2,2,0,0,1-1.4-.53,2.1,2.1,0,0,1-.57-1.6,2.06,2.06,0,0,1,.57-1.61A2,2,0,0,1,513,828.73Zm1.64,7.63v20.35h-3.35V836.36Z"
|
||||
id="path17" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M530.91,836a7.69,7.69,0,0,1,5.5,1.77c1.24,1.17,1.86,3.08,1.86,5.71v13.25H535v-13a5.37,5.37,0,0,0-1.1-3.69,4.46,4.46,0,0,0-3.46-1.21q-3.37,0-4.67,1.9a9.7,9.7,0,0,0-1.29,5.47v10.55h-3.34V836.36h2.7l.49,2.77h.19a6.17,6.17,0,0,1,1.69-1.76,7.31,7.31,0,0,1,2.22-1A9.16,9.16,0,0,1,530.91,836Z"
|
||||
id="path18" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M547.88,842.93q0,.6-.06,1.59t-.09,1.71h.15l.68-.87.93-1.16c.32-.39.59-.72.82-1l6.49-6.87h3.91l-8.24,8.69,8.81,11.66h-4l-7.06-9.49-2.32,2v7.48h-3.3V827.85h3.3Z"
|
||||
id="path19" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M586.8,856.71h-4l-14.5-22.52h-.15c.05.59.09,1.28.13,2.07s.07,1.65.11,2.55.06,1.81.06,2.75v15.15h-3.15V829.6h4L583.72,852h.16c0-.4-.06-1-.1-1.82s-.08-1.7-.11-2.66-.06-1.85-.06-2.66V829.6h3.19Z"
|
||||
id="path20" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M611.4,846.5a14.11,14.11,0,0,1-.66,4.5,9.41,9.41,0,0,1-1.9,3.32,7.92,7.92,0,0,1-3,2.07,10.53,10.53,0,0,1-3.93.7,9.66,9.66,0,0,1-3.72-.7,8.18,8.18,0,0,1-3-2.07,9.67,9.67,0,0,1-2-3.32,13.29,13.29,0,0,1-.7-4.5,13,13,0,0,1,1.14-5.72,8.25,8.25,0,0,1,3.26-3.57A10,10,0,0,1,602,836a9.47,9.47,0,0,1,4.87,1.23,8.61,8.61,0,0,1,3.31,3.57A12.41,12.41,0,0,1,611.4,846.5Zm-15.37,0a12.33,12.33,0,0,0,.62,4.15,5.45,5.45,0,0,0,2,2.72,6.49,6.49,0,0,0,6.76,0,5.5,5.5,0,0,0,2-2.72,12.32,12.32,0,0,0,.63-4.15,11.76,11.76,0,0,0-.65-4.14,5.53,5.53,0,0,0-1.95-2.64,5.76,5.76,0,0,0-3.4-.93,5.08,5.08,0,0,0-4.52,2.05A9.87,9.87,0,0,0,596,846.5Z"
|
||||
id="path21" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M623.9,857.09a7.62,7.62,0,0,1-6.08-2.64c-1.52-1.76-2.28-4.38-2.28-7.88s.77-6.13,2.3-7.91a7.61,7.61,0,0,1,6.09-2.68,8.58,8.58,0,0,1,2.78.4,6.79,6.79,0,0,1,2,1.08,7.87,7.87,0,0,1,1.48,1.52h.22c0-.33-.07-.82-.13-1.46s-.09-1.16-.09-1.54v-8.13h3.34v28.86h-2.7l-.49-2.73h-.15a7.83,7.83,0,0,1-1.48,1.57,6.86,6.86,0,0,1-2.07,1.12A8.44,8.44,0,0,1,623.9,857.09Zm.53-2.77q3.23,0,4.53-1.77a8.85,8.85,0,0,0,1.31-5.33v-.61a11.15,11.15,0,0,0-1.25-5.83c-.83-1.35-2.38-2-4.63-2a4.45,4.45,0,0,0-4,2.15,10.68,10.68,0,0,0-1.35,5.75,10.12,10.12,0,0,0,1.35,5.66A4.56,4.56,0,0,0,624.43,854.32Z"
|
||||
id="path22" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M647.89,836a8.5,8.5,0,0,1,4.5,1.14,7.45,7.45,0,0,1,2.89,3.21,11,11,0,0,1,1,4.84v2H642.35a7.71,7.71,0,0,0,1.76,5.26,6.19,6.19,0,0,0,4.73,1.8,14.83,14.83,0,0,0,3.44-.36,19.71,19.71,0,0,0,3.09-1v2.92a16,16,0,0,1-3.07,1,18.05,18.05,0,0,1-3.61.32,10.7,10.7,0,0,1-5.11-1.18,8.12,8.12,0,0,1-3.45-3.51,12,12,0,0,1-1.24-5.71A13.57,13.57,0,0,1,640,841a8.32,8.32,0,0,1,7.88-5Zm0,2.73a4.83,4.83,0,0,0-3.77,1.54,7.25,7.25,0,0,0-1.66,4.27h10.37a8,8,0,0,0-.53-3,4.49,4.49,0,0,0-1.61-2A4.92,4.92,0,0,0,647.85,838.71Z"
|
||||
id="path23" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M660.08,843.88h9.19V847h-9.19Z"
|
||||
id="path24" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M686.77,856.71l-8.92-23.77h-.15c0,.51.09,1.15.13,1.94s.07,1.64.1,2.56,0,1.87,0,2.83v16.44h-3.15V829.6h5.05l8.36,22.21h.15l8.5-22.21h5v27.11h-3.38V840q0-1.32,0-2.64t.12-2.46c.05-.78.09-1.43.11-2h-.15l-9,23.73Z"
|
||||
id="path25" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M504.76,822.5l-2.52-2.69a16,16,0,0,1,21.51-.37l-2.42,2.77a12.35,12.35,0,0,0-16.57.29Z"
|
||||
id="path27" />
|
||||
<path
|
||||
class="cls-1"
|
||||
d="M509.12,826.65l-2.73-2.47a9.18,9.18,0,0,1,13.18-.45L517,826.38a5.51,5.51,0,0,0-7.9.27Z"
|
||||
id="path28" />
|
||||
<path
|
||||
d="M 727.48499,857.1333 H 709.11 v -3.80989 q 1.91406,-1.64063 3.82812,-3.28125 1.93229,-1.64062 3.59115,-3.26302 3.49999,-3.39062 4.79426,-5.3776 1.29427,-2.0052 1.29427,-4.32031 0,-2.11458 -1.40364,-3.29947 -1.38542,-1.20313 -3.88281,-1.20313 -1.65885,0 -3.59114,0.58334 -1.93229,0.58333 -3.77344,1.78645 h -0.18229 v -3.82812 q 1.29427,-0.63802 3.44531,-1.16666 2.16927,-0.52865 4.19271,-0.52865 4.17447,0 6.54426,2.02344 2.36979,2.0052 2.36979,5.45051 0,1.54948 -0.40104,2.89844 -0.38281,1.33073 -1.14844,2.53385 -0.71093,1.13021 -1.67708,2.22396 -0.94791,1.09375 -2.3151,2.42447 -1.95052,1.91406 -4.02864,3.71875 -2.07813,1.78646 -3.88281,3.31771 h 14.60155 z"
|
||||
id="text1"
|
||||
style="font-size:37.3333px;fill:#262626"
|
||||
aria-label="2" />
|
||||
</g>
|
||||
<g
|
||||
id="Layer_2"
|
||||
data-name="Layer 2">
|
||||
<rect
|
||||
class="cls-5"
|
||||
x="502.29"
|
||||
y="782"
|
||||
width="180.26"
|
||||
height="24.65"
|
||||
rx="12.33"
|
||||
id="rect28" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="515.99"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle28" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="637.4"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle29" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="660.08"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle30" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="539.09"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle31" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="564.93"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle32" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="590.77"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle33" />
|
||||
<circle
|
||||
class="cls-5"
|
||||
cx="614.09"
|
||||
cy="794.33"
|
||||
r="5.48"
|
||||
id="circle34" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M763.35,521.78v329A23.38,23.38,0,0,1,740,874.13H441.22a23.38,23.38,0,0,1-23.38-23.38V456.51a23.38,23.38,0,0,1,23.38-23.38H675.28A18,18,0,0,1,688,438.41L757.66,508A19.43,19.43,0,0,1,763.35,521.78Z"
|
||||
id="path34" />
|
||||
<rect
|
||||
class="cls-7"
|
||||
x="405.9"
|
||||
y="417.86"
|
||||
width="372.52"
|
||||
height="471.54"
|
||||
rx="38.16"
|
||||
id="rect34" />
|
||||
<rect
|
||||
class="cls-8"
|
||||
x="394.16"
|
||||
y="403"
|
||||
width="396"
|
||||
height="501.26"
|
||||
rx="48.72"
|
||||
id="rect35" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M763.35,462.15v30.34a5.64,5.64,0,0,1-9.62,4L700,442.75a5.63,5.63,0,0,1,4-9.62h30.34A29,29,0,0,1,763.35,462.15Z"
|
||||
id="path35" />
|
||||
<rect
|
||||
class="cls-6"
|
||||
x="444.77"
|
||||
y="532.78"
|
||||
width="290.09"
|
||||
height="159.13"
|
||||
rx="18"
|
||||
id="rect36" />
|
||||
<rect
|
||||
class="cls-6"
|
||||
x="454.68"
|
||||
y="541.13"
|
||||
width="268.7"
|
||||
height="139.83"
|
||||
rx="10.7"
|
||||
id="rect37" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M447,904.26v2.94a2,2,0,0,0,2,1.95h14.28a2,2,0,0,0,2-1.95v-2.94"
|
||||
id="path37" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M718.74,904.26v2.94a2,2,0,0,0,2,1.95H735a2,2,0,0,0,2-1.95v-2.94"
|
||||
id="path38" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M790.16,508.83h.39a10.56,10.56,0,0,1,10.56,10.56V656.57a10.56,10.56,0,0,1-10.56,10.56h-.39"
|
||||
id="path39" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M394.16,518.17h-3.31a1.91,1.91,0,0,0-1.91,1.91V797a1.9,1.9,0,0,0,1.91,1.91h3.31"
|
||||
id="path40" />
|
||||
<polyline
|
||||
class="cls-6"
|
||||
points="458.33 403 473.46 384.87 579.68 384.87 595.03 403"
|
||||
id="polyline40" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M579.68,384.87l-.87-21.35a2.51,2.51,0,0,0-2.5-2.39H477.56a2.5,2.5,0,0,0-2.49,2.39l-.87,21.35"
|
||||
id="path41" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M578.32,351.57,577.88,341a2.41,2.41,0,0,0-2.41-2.32H478.41A2.42,2.42,0,0,0,476,341l-.43,10.55a2.42,2.42,0,0,0,2.42,2.52H575.9A2.43,2.43,0,0,0,578.32,351.57Z"
|
||||
id="path42" />
|
||||
<path
|
||||
class="cls-6"
|
||||
d="M476.46,329.56a2,2,0,0,0,2,2.09h96.94a2,2,0,0,0,2-2.09l-7.89-193.08a14.91,14.91,0,0,0-14.9-14.31H499.26a14.91,14.91,0,0,0-14.9,14.31Z"
|
||||
id="path43" />
|
||||
<line
|
||||
class="cls-6"
|
||||
x1="479.53"
|
||||
y1="331.65"
|
||||
x2="479.23"
|
||||
y2="338.7"
|
||||
id="line43" />
|
||||
<line
|
||||
class="cls-6"
|
||||
x1="478.57"
|
||||
y1="354.09"
|
||||
x2="478.26"
|
||||
y2="361.13"
|
||||
id="line44" />
|
||||
<line
|
||||
class="cls-6"
|
||||
x1="574.7"
|
||||
y1="338.7"
|
||||
x2="574.41"
|
||||
y2="331.65"
|
||||
id="line45" />
|
||||
<line
|
||||
class="cls-6"
|
||||
x1="575.63"
|
||||
y1="361.13"
|
||||
x2="575.34"
|
||||
y2="354.09"
|
||||
id="line46" />
|
||||
<polyline
|
||||
class="cls-6"
|
||||
points="491.72 331.65 499.03 137.3 555.9 137.3 561.64 331.65"
|
||||
id="polyline46" />
|
||||
<rect
|
||||
class="cls-8"
|
||||
x="394.16"
|
||||
y="403"
|
||||
width="396"
|
||||
height="501.26"
|
||||
rx="48.72"
|
||||
id="rect46" />
|
||||
<rect
|
||||
class="cls-8"
|
||||
x="394.16"
|
||||
y="403"
|
||||
width="396"
|
||||
height="501.26"
|
||||
rx="48.72"
|
||||
id="rect47" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
12
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json
vendored
Normal file
12
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "seeed_xiao_nrf52_kit.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
1
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg
vendored
Normal file
1
Meshtastic/Assets.xcassets/XIAONRF52KIT.imageset/seeed_xiao_nrf52_kit.svg
vendored
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -19,17 +19,17 @@ enum MeshMapTypes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .standard:
|
||||
return "standard".localized
|
||||
return "Standard".localized
|
||||
case .mutedStandard:
|
||||
return "standard.muted".localized
|
||||
return "Standard Muted".localized
|
||||
case .hybrid:
|
||||
return "hybrid".localized
|
||||
return "Hybrid".localized
|
||||
case .hybridFlyover:
|
||||
return "hybrid.flyover".localized
|
||||
return "Hybrid Flyover".localized
|
||||
case .satellite:
|
||||
return "satellite".localized
|
||||
return "Satellite".localized
|
||||
case .satelliteFlyover:
|
||||
return "satellite.flyover".localized
|
||||
return "Satellite Flyover".localized
|
||||
}
|
||||
}
|
||||
func MKMapTypeValue() -> MKMapType {
|
||||
|
|
@ -66,7 +66,7 @@ enum MeshMapDistances: Double, CaseIterable, Identifiable {
|
|||
var id: Double { self.rawValue }
|
||||
var description: String {
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
return String.localizedStringWithFormat("nodelist.filter.distance %@".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
|
||||
return String.localizedStringWithFormat("up to %@ away".localized, distanceFormatter.string(fromDistance: Double(self.rawValue)))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,11 +78,11 @@ enum UserTrackingModes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return "map.usertrackingmode.none".localized
|
||||
return "None".localized
|
||||
case .follow:
|
||||
return "map.usertrackingmode.follow".localized
|
||||
return "Follow".localized
|
||||
case .followWithHeading:
|
||||
return "map.usertrackingmode.followwithheading".localized
|
||||
return "Follow with heading".localized
|
||||
}
|
||||
}
|
||||
var icon: String {
|
||||
|
|
@ -117,21 +117,21 @@ enum LocationUpdateInterval: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .fortyFiveSeconds:
|
||||
return "interval.fortyfive.seconds".localized
|
||||
return "Forty Five Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ enum ConfigPresets: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .unset:
|
||||
return "canned.messages.preset.manual".localized
|
||||
return "Manual Configuration".localized
|
||||
case .rakRotaryEncoder:
|
||||
return "canned.messages.preset.rakrotary".localized
|
||||
return "RAK Rotary Encoder".localized
|
||||
case .cardKB:
|
||||
return "canned.messages.preset.cardkb".localized
|
||||
return "M5 Stack Card KB / RAK Keypad".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -45,21 +45,21 @@ enum InputEventChars: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .none:
|
||||
return "inputevent.none".localized
|
||||
return "None".localized
|
||||
case .up:
|
||||
return "inputevent.up".localized
|
||||
return "Up".localized
|
||||
case .down:
|
||||
return "inputevent.down".localized
|
||||
return "Down".localized
|
||||
case .left:
|
||||
return "inputevent.left".localized
|
||||
return "Left".localized
|
||||
case .right:
|
||||
return "inputevent.right".localized
|
||||
return "Right".localized
|
||||
case .select:
|
||||
return "inputevent.select".localized
|
||||
return "Select".localized
|
||||
case .back:
|
||||
return "inputevent.back".localized
|
||||
return "Back".localized
|
||||
case .cancel:
|
||||
return "inputevent.cancel".localized
|
||||
return "Cancel".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> ModuleConfig.CannedMessageConfig.InputEventChar {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ enum ChannelRoles: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .disabled:
|
||||
return "channel.role.disabled".localized
|
||||
return "Disabled".localized
|
||||
case .primary:
|
||||
return "channel.role.primary".localized
|
||||
return "Primary".localized
|
||||
case .secondary:
|
||||
return "channel.role.secondary".localized
|
||||
return "Secondary".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Channel.Role {
|
||||
|
|
|
|||
|
|
@ -21,65 +21,60 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
case takTracker = 10
|
||||
case repeater = 4
|
||||
case router = 2
|
||||
case routerClient = 3
|
||||
case routerLate = 11
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var name: String {
|
||||
switch self {
|
||||
case .client:
|
||||
return "device.role.name.client".localized
|
||||
return "Client".localized
|
||||
case .clientMute:
|
||||
return "device.role.name.clientMute".localized
|
||||
return "Client Mute".localized
|
||||
case .router:
|
||||
return "device.role.name.router".localized
|
||||
case .routerClient:
|
||||
return "device.role.name.routerClient".localized
|
||||
return "Router".localized
|
||||
case .repeater:
|
||||
return "device.role.name.repeater".localized
|
||||
return "Repeater".localized
|
||||
case .tracker:
|
||||
return "device.role.name.tracker".localized
|
||||
return "Tracker".localized
|
||||
case .sensor:
|
||||
return "device.role.name.sensor".localized
|
||||
return "Sensor".localized
|
||||
case .tak:
|
||||
return "device.role.name.tak".localized
|
||||
return "TAK".localized
|
||||
case .takTracker:
|
||||
return "device.role.name.takTracker".localized
|
||||
return "TAK Tracker".localized
|
||||
case .clientHidden:
|
||||
return "device.role.name.clientHidden".localized
|
||||
return "Client Hidden".localized
|
||||
case .lostAndFound:
|
||||
return "device.role.name.lostAndFound".localized
|
||||
return "Lost and Found".localized
|
||||
case .routerLate:
|
||||
return "device.role.name.routerlate".localized
|
||||
return "Router Late".localized
|
||||
}
|
||||
|
||||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
case .client:
|
||||
return "device.role.client".localized
|
||||
return "App connected or stand alone messaging device.".localized
|
||||
case .clientMute:
|
||||
return "device.role.clientmute".localized
|
||||
return "Device that does not forward packets from other devices.".localized
|
||||
case .router:
|
||||
return "device.role.router".localized
|
||||
case .routerClient:
|
||||
return "device.role.routerclient".localized
|
||||
return "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Needs exceptional coverage. Visible in Nodes list.".localized
|
||||
case .repeater:
|
||||
return "device.role.repeater".localized
|
||||
return "Infrastructure node on a tower or mountain top only. Not to be used for roofs or mobile nodes. Relays messages with minimal overhead. Not visible in Nodes list.".localized
|
||||
case .tracker:
|
||||
return "device.role.tracker".localized
|
||||
return "Broadcasts GPS position packets as priority.".localized
|
||||
case .sensor:
|
||||
return "device.role.sensor".localized
|
||||
return "Broadcasts telemetry packets as priority.".localized
|
||||
case .tak:
|
||||
return "device.role.tak".localized
|
||||
return "Optimized for ATAK system communication, reduces routine broadcasts.".localized
|
||||
case .takTracker:
|
||||
return "device.role.taktracker".localized
|
||||
return "Enables automatic TAK PLI broadcasts and reduces routine broadcasts.".localized
|
||||
case .clientHidden:
|
||||
return "device.role.clienthidden".localized
|
||||
return "Device that only broadcasts as needed for stealth or power savings.".localized
|
||||
case .lostAndFound:
|
||||
return "device.role.lostandfound".localized
|
||||
return "Broadcasts location as message to default channel regularly for to assist with device recovery.".localized
|
||||
case .routerLate:
|
||||
return "device.role.routerlate".localized
|
||||
return "Infrastructure node that always rebroadcasts packets once but only after all other modes, ensuring additional coverage for local clusters. Visible in Nodes list.".localized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +84,7 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "apps.iphone"
|
||||
case .clientMute:
|
||||
return "speaker.slash"
|
||||
case .router, .routerClient, .routerLate:
|
||||
case .router, .routerLate:
|
||||
return "wifi.router"
|
||||
case .repeater:
|
||||
return "repeat"
|
||||
|
|
@ -116,8 +111,6 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return Config.DeviceConfig.Role.clientMute
|
||||
case .router:
|
||||
return Config.DeviceConfig.Role.router
|
||||
case .routerClient:
|
||||
return Config.DeviceConfig.Role.routerClient
|
||||
case .repeater:
|
||||
return Config.DeviceConfig.Role.repeater
|
||||
case .tracker:
|
||||
|
|
@ -144,36 +137,41 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
case allSkipDecoding = 1
|
||||
case localOnly = 2
|
||||
case knownOnly = 3
|
||||
case corePortnums = 4
|
||||
case none = 4
|
||||
case corePortnums = 5
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
||||
var name: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "All"
|
||||
return "All".localized
|
||||
case .allSkipDecoding:
|
||||
return "All Skip Decoding"
|
||||
return "All Skip Decoding".localized
|
||||
case .localOnly:
|
||||
return "Local Only"
|
||||
return "Local Only".localized
|
||||
case .knownOnly:
|
||||
return "Known Only"
|
||||
return "Known Only".localized
|
||||
case .none:
|
||||
return "None".localized
|
||||
case .corePortnums:
|
||||
return "Core Portnums Only"
|
||||
return "Core Portnums Only".localized
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params."
|
||||
return "Rebroadcast any observed message, if it was on our private channel or from another mesh with the same lora params.".localized
|
||||
case .allSkipDecoding:
|
||||
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior."
|
||||
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior.".localized
|
||||
case .localOnly:
|
||||
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels."
|
||||
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels.".localized
|
||||
case .knownOnly:
|
||||
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list."
|
||||
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list.".localized
|
||||
case .none:
|
||||
return "Only permitted for SENSOR, TRACKER and TAK_TRACKER roles, this will inhibit all rebroadcasts, not unlike CLIENT_MUTE role.".localized
|
||||
case .corePortnums:
|
||||
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing."
|
||||
return "Only rebroadcasts packets from the core portnums: NodeInfo, Text, Position, Telemetry, and Routing.".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode {
|
||||
|
|
@ -187,6 +185,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
return Config.DeviceConfig.RebroadcastMode.localOnly
|
||||
case .knownOnly:
|
||||
return Config.DeviceConfig.RebroadcastMode.knownOnly
|
||||
case .none:
|
||||
return Config.DeviceConfig.RebroadcastMode.none
|
||||
case .corePortnums:
|
||||
return Config.DeviceConfig.RebroadcastMode.corePortnumsOnly
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,21 +49,21 @@ enum ScreenOnIntervals: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
case .thirtyMinutes:
|
||||
return "interval.thirty.minutes".localized
|
||||
return "Thirty Minutes".localized
|
||||
case .oneHour:
|
||||
return "interval.one.hour".localized
|
||||
return "One Hour".localized
|
||||
case .max:
|
||||
return "Always On".localized
|
||||
}
|
||||
|
|
@ -87,17 +87,17 @@ enum ScreenCarouselIntervals: Int, CaseIterable, Identifiable {
|
|||
case .off:
|
||||
return "off".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -149,13 +149,13 @@ enum DisplayModes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .defaultMode:
|
||||
return "default.128x64.screen.layout".localized
|
||||
return "Default 128x64 screen layout".localized
|
||||
case .twoColor:
|
||||
return "optimized.for.2.color.displays".localized
|
||||
return "Optimized for 2 color displays".localized
|
||||
case .inverted:
|
||||
return "inverted.top.bar.for.2.color.display".localized
|
||||
return "Inverted top bar for 2 Color display".localized
|
||||
case .color:
|
||||
return "tft.full.color.displays".localized
|
||||
return "TFT Full Color Displays".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DisplayConfig.DisplayMode {
|
||||
|
|
|
|||
|
|
@ -22,21 +22,21 @@ enum NagIntervals: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .unset:
|
||||
return "unset".localized
|
||||
return "Unset".localized
|
||||
case .oneSecond:
|
||||
return "interval.one.second".localized
|
||||
return "One Second".localized
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
return "Five Seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,25 +59,25 @@ enum OutputIntervals: Int, CaseIterable, Identifiable {
|
|||
|
||||
switch self {
|
||||
case .unset:
|
||||
return "unset".localized
|
||||
return "Unset".localized
|
||||
case .oneSecond:
|
||||
return "interval.one.second".localized
|
||||
return "One Second".localized
|
||||
case .twoSeconds:
|
||||
return "interval.two.seconds".localized
|
||||
return "Two Seconds".localized
|
||||
case .threeSeconds:
|
||||
return "interval.three.seconds".localized
|
||||
return "Three Seconds".localized
|
||||
case .fourSeconds:
|
||||
return "interval.four.seconds".localized
|
||||
return "Four Seconds".localized
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
return "Five Seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,25 +100,25 @@ enum SenderIntervals: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .off:
|
||||
return "off".localized
|
||||
return "Off".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .fortyFiveSeconds:
|
||||
return "interval.fortyfive.seconds".localized
|
||||
return "Forty Five Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
case .thirtyMinutes:
|
||||
return "interval.thirty.minutes".localized
|
||||
return "Thirty Minutes".localized
|
||||
case .oneHour:
|
||||
return "interval.one.hour".localized
|
||||
return "One Hour".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,49 +153,49 @@ enum UpdateIntervals: Int, CaseIterable, Identifiable {
|
|||
|
||||
switch self {
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .fortyFiveSeconds:
|
||||
return "interval.fortyfive.seconds".localized
|
||||
return "Forty Five Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .twoMinutes:
|
||||
return "interval.two.minutes".localized
|
||||
return "Two Minutes".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
case .thirtyMinutes:
|
||||
return "interval.thirty.minutes".localized
|
||||
return "Thirty Minutes".localized
|
||||
case .oneHour:
|
||||
return "interval.one.hour".localized
|
||||
return "One Hour".localized
|
||||
case .twoHours:
|
||||
return "interval.two.hours".localized
|
||||
return "Two Hours".localized
|
||||
case .threeHours:
|
||||
return "interval.three.hours".localized
|
||||
return "Three Hours".localized
|
||||
case .fourHours:
|
||||
return "interval.four.hours".localized
|
||||
return "Four Hours".localized
|
||||
case .fiveHours:
|
||||
return "interval.five.hours".localized
|
||||
return "Five Hours".localized
|
||||
case .sixHours:
|
||||
return "interval.six.hours".localized
|
||||
return "Six Hours".localized
|
||||
case .twelveHours:
|
||||
return "interval.twelve.hours".localized
|
||||
return "Twelve Hours".localized
|
||||
case .eighteenHours:
|
||||
return "interval.eighteen.hours".localized
|
||||
return "Eighteen Hours".localized
|
||||
case .twentyFourHours:
|
||||
return "interval.twentyfour.hours".localized
|
||||
return "Twenty Four Hours".localized
|
||||
case .thirtySixHours:
|
||||
return "interval.thirtysix.hours".localized
|
||||
return "Thirty Six Hours".localized
|
||||
case .fortyeightHours:
|
||||
return "interval.fortyeight.hours".localized
|
||||
return "Forty Eight Hours".localized
|
||||
case .seventyTwoHours:
|
||||
return "interval.seventytwo.hours".localized
|
||||
return "Seventy Two Hours".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,27 +105,27 @@ enum RegionCodes: Int, CaseIterable, Identifiable {
|
|||
case .in:
|
||||
return "India".localized
|
||||
case .nz865:
|
||||
return "New Zealand 865mhz".localized
|
||||
return "New Zealand 865MHz".localized
|
||||
case .th:
|
||||
return "Thailand".localized
|
||||
case .ua433:
|
||||
return "Ukraine 433mhz".localized
|
||||
return "Ukraine 433MHz".localized
|
||||
case .ua868:
|
||||
return "Ukraine 868mhz".localized
|
||||
return "Ukraine 868MHz".localized
|
||||
case .lora24:
|
||||
return "2.4 Ghz".localized
|
||||
case .my433:
|
||||
return "Malaysia 433mhz".localized
|
||||
return "Malaysia 433MHz".localized
|
||||
case .my919:
|
||||
return "Malaysia 919mhz".localized
|
||||
return "Malaysia 919MHz".localized
|
||||
case .sg923:
|
||||
return "Singapore 923mhz".localized
|
||||
return "Singapore 923MHz".localized
|
||||
case .ph433:
|
||||
return "Philippines 433mhz".localized
|
||||
return "Philippines 433MHz".localized
|
||||
case .ph868:
|
||||
return "Philippines 868mhz".localized
|
||||
return "Philippines 868MHz".localized
|
||||
case .ph915:
|
||||
return "Philippines 915mhz".localized
|
||||
return "Philippines 915MHz".localized
|
||||
}
|
||||
}
|
||||
var dutyCycle: Int {
|
||||
|
|
@ -280,7 +280,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
|
|||
case longFast = 0
|
||||
case longSlow = 1
|
||||
case longModerate = 7
|
||||
case vLongSlow = 2
|
||||
case medSlow = 3
|
||||
case medFast = 4
|
||||
case shortSlow = 5
|
||||
|
|
@ -291,23 +290,21 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .longFast:
|
||||
return "long.range.fast".localized
|
||||
return "Long Range - Fast".localized
|
||||
case .longSlow:
|
||||
return "long.range.slow".localized
|
||||
return "Long Range - Slow".localized
|
||||
case .longModerate:
|
||||
return "long.range.moderate".localized
|
||||
case .vLongSlow:
|
||||
return "very.long.range.slow".localized
|
||||
return "Long Range - Moderate".localized
|
||||
case .medSlow:
|
||||
return "medium.range.slow".localized
|
||||
return "Medium Range - Slow".localized
|
||||
case .medFast:
|
||||
return "medium.range.fast".localized
|
||||
return "Medium Range - Fast".localized
|
||||
case .shortSlow:
|
||||
return "short.range.slow".localized
|
||||
return "Short Range - Slow".localized
|
||||
case .shortFast:
|
||||
return "short.range.fast".localized
|
||||
return "Short Range - Fast".localized
|
||||
case .shortTurbo:
|
||||
return "short.range.turbo".localized
|
||||
return "Short Range - Turbo".localized
|
||||
}
|
||||
}
|
||||
var name: String {
|
||||
|
|
@ -318,8 +315,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
|
|||
return "LongSlow"
|
||||
case .longModerate:
|
||||
return "LongModerate"
|
||||
case .vLongSlow:
|
||||
return "VLongFast"
|
||||
case .medSlow:
|
||||
return "MediumSlow"
|
||||
case .medFast:
|
||||
|
|
@ -340,8 +335,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
|
|||
return -7.5
|
||||
case .longModerate:
|
||||
return -17.5
|
||||
case .vLongSlow:
|
||||
return -20
|
||||
case .medSlow:
|
||||
return -15
|
||||
case .medFast:
|
||||
|
|
@ -362,8 +355,6 @@ enum ModemPresets: Int, CaseIterable, Identifiable {
|
|||
return Config.LoRaConfig.ModemPreset.longSlow
|
||||
case .longModerate:
|
||||
return Config.LoRaConfig.ModemPreset.longModerate
|
||||
case .vLongSlow:
|
||||
return Config.LoRaConfig.ModemPreset.veryLongSlow
|
||||
case .medSlow:
|
||||
return Config.LoRaConfig.ModemPreset.mediumSlow
|
||||
case .medFast:
|
||||
|
|
|
|||
|
|
@ -46,21 +46,21 @@ enum Tapbacks: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .wave:
|
||||
return "tapback.wave".localized
|
||||
return "Wave".localized
|
||||
case .heart:
|
||||
return "tapback.heart".localized
|
||||
return "Heart".localized
|
||||
case .thumbsUp:
|
||||
return "tapback.thumbsup".localized
|
||||
return "Thumbs Up".localized
|
||||
case .thumbsDown:
|
||||
return "tapback.thumbsdown".localized
|
||||
return "Thumbs Down".localized
|
||||
case .haHa:
|
||||
return "tapback.haha".localized
|
||||
return "HaHa".localized
|
||||
case .exclamation:
|
||||
return "tapback.exclamation".localized
|
||||
return "Exclamation".localized
|
||||
case .question:
|
||||
return "tapback.question".localized
|
||||
return "Question".localized
|
||||
case .poop:
|
||||
return "tapback.poop".localized
|
||||
return "Poop".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,17 +21,17 @@ enum GpsFormats: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .gpsFormatDec:
|
||||
return "gpsformat.dec".localized
|
||||
return "Decimal Degrees Format".localized
|
||||
case .gpsFormatDms:
|
||||
return "gpsformat.dms".localized
|
||||
return "Degrees Minutes Seconds".localized
|
||||
case .gpsFormatUtm:
|
||||
return "gpsformat.utm".localized
|
||||
return "Universal Transverse Mercator".localized
|
||||
case .gpsFormatMgrs:
|
||||
return "gpsformat.mgrs".localized
|
||||
return "Military Grid Reference System".localized
|
||||
case .gpsFormatOlc:
|
||||
return "gpsformat.olc".localized
|
||||
return "Open Location Code (aka Plus Codes)".localized
|
||||
case .gpsFormatOsgr:
|
||||
return "gpsformat.osgr".localized
|
||||
return "Ordnance Survey Grid Reference".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DisplayConfig.GpsCoordinateFormat {
|
||||
|
|
@ -73,29 +73,29 @@ enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .twoMinutes:
|
||||
return "interval.two.minutes".localized
|
||||
return "Two Minutes".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
return "Ten Minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
return "Fifteen Minutes".localized
|
||||
case .thirtyMinutes:
|
||||
return "interval.thirty.minutes".localized
|
||||
return "Thirty Minutes".localized
|
||||
case .oneHour:
|
||||
return "interval.one.hour".localized
|
||||
return "One Hour".localized
|
||||
case .sixHours:
|
||||
return "interval.six.hours".localized
|
||||
return "Six Hours".localized
|
||||
case .twelveHours:
|
||||
return "interval.twelve.hours".localized
|
||||
return "Twelve Hours".localized
|
||||
case .twentyFourHours:
|
||||
return "interval.twentyfour.hours".localized
|
||||
return "Twenty Four Hours".localized
|
||||
case .maxInt32:
|
||||
return "on.boot".localized
|
||||
return "On Boot Only".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,11 +110,11 @@ enum GpsMode: Int, CaseIterable, Equatable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .disabled:
|
||||
return "gpsmode.disabled".localized
|
||||
return "Disabled".localized
|
||||
case .enabled:
|
||||
return "gpsmode.enabled".localized
|
||||
return "Enabled".localized
|
||||
case .notPresent:
|
||||
return "gpsmode.notPresent".localized
|
||||
return "Not Present".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.PositionConfig.GpsMode {
|
||||
|
|
|
|||
|
|
@ -20,34 +20,34 @@ enum ActivityType: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .walking:
|
||||
return "routes.activitytype.walking".localized
|
||||
return "Walking".localized
|
||||
case .hiking:
|
||||
return "routes.activitytype.hiking".localized
|
||||
return "Hiking".localized
|
||||
case .biking:
|
||||
return "routes.activitytype.biking".localized
|
||||
return "Biking".localized
|
||||
case .driving:
|
||||
return "routes.activitytype.driving".localized
|
||||
return "Driving".localized
|
||||
case .overlanding:
|
||||
return "routes.activitytype.overlanding".localized
|
||||
return "Overlanding".localized
|
||||
case .skiing:
|
||||
return "routes.activitytype.skiing".localized
|
||||
return "Skiing".localized
|
||||
}
|
||||
}
|
||||
|
||||
var fileNameString: String {
|
||||
switch self {
|
||||
case .walking:
|
||||
return "routes.activitytype.filename.walking".localized
|
||||
return "walk".localized
|
||||
case .hiking:
|
||||
return "routes.activitytype.filename.hiking".localized
|
||||
return "hiking".localized
|
||||
case .biking:
|
||||
return "routes.activitytype.filename.biking".localized
|
||||
return "biking".localized
|
||||
case .driving:
|
||||
return "routes.activitytype.filename.driving".localized
|
||||
return "driving".localized
|
||||
case .overlanding:
|
||||
return "routes.activitytype.filename.overlanding".localized
|
||||
return "overlanding".localized
|
||||
case .skiing:
|
||||
return "routes.activitytype.filename.skiing".localized
|
||||
return "skiing".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,31 +32,31 @@ enum RoutingError: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .none:
|
||||
return "routing.acknowledged".localized
|
||||
return "Acknowledged".localized
|
||||
case .noRoute:
|
||||
return "routing.noroute".localized
|
||||
return "No Route".localized
|
||||
case .gotNak:
|
||||
return "routing.gotnak".localized
|
||||
return "Received a negative acknowledgment".localized
|
||||
case .timeout:
|
||||
return "routing.timeout".localized
|
||||
return "Timeout".localized
|
||||
case .noInterface:
|
||||
return "routing.nointerface".localized
|
||||
return "No Interface".localized
|
||||
case .maxRetransmit:
|
||||
return "routing.maxretransmit".localized
|
||||
return "Max Retransmission Reached".localized
|
||||
case .noChannel:
|
||||
return "routing.nochannel".localized
|
||||
return "No Channel".localized
|
||||
case .tooLarge:
|
||||
return "routing.toolarge".localized
|
||||
return "The packet is too large".localized
|
||||
case .noResponse:
|
||||
return "routing.noresponse".localized
|
||||
return "No Response".localized
|
||||
case .dutyCycleLimit:
|
||||
return "routing.dutycyclelimit".localized
|
||||
return "Regional Duty Cycle Limit Reached".localized
|
||||
case .badRequest:
|
||||
return "routing.badRequest".localized
|
||||
return "Bad Request".localized
|
||||
case .notAuthorized:
|
||||
return "routing.notauthorized".localized
|
||||
return "Not Authorized".localized
|
||||
case .pkiFailed:
|
||||
return "routing.pkifailed".localized
|
||||
return "Encrypted Send Failed".localized
|
||||
case .pkiUnknownPubkey:
|
||||
return "Unknown public key".localized
|
||||
case .adminBadSessionKey:
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ enum SerialBaudRates: Int, CaseIterable, Identifiable {
|
|||
switch self {
|
||||
|
||||
case .baudDefault:
|
||||
return "default".localized
|
||||
return "Default".localized
|
||||
case .baud110:
|
||||
return "110 Baud"
|
||||
case .baud300:
|
||||
|
|
@ -118,17 +118,17 @@ enum SerialModeTypes: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .default:
|
||||
return "serial.mode.default".localized
|
||||
return "Default".localized
|
||||
case .simple:
|
||||
return "serial.mode.simple".localized
|
||||
return "Simple".localized
|
||||
case .proto:
|
||||
return "serial.mode.proto".localized
|
||||
return "Protobufs".localized
|
||||
case .txtmsg:
|
||||
return "serial.mode.txtmsg".localized
|
||||
return "Text Message".localized
|
||||
case .nmea:
|
||||
return "serial.mode.nmea".localized
|
||||
return "NMEA Positions".localized
|
||||
case .caltopo:
|
||||
return "serial.mode.caltopo".localized
|
||||
return "CALTOPO".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> ModuleConfig.SerialConfig.Serial_Mode {
|
||||
|
|
@ -166,21 +166,21 @@ enum SerialTimeoutIntervals: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .unset:
|
||||
return "unset".localized
|
||||
return "Unset".localized
|
||||
case .oneSecond:
|
||||
return "interval.one.second".localized
|
||||
return "One Second".localized
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
return "Five Seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
return "Ten Seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
return "Fifteen Seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
return "Thirty Seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
return "One Minute".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
return "Five Minutes".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,17 +20,17 @@ enum Aqi: Int, CaseIterable, Identifiable {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .good:
|
||||
return "telemetry.good".localized
|
||||
return "Good".localized
|
||||
case .moderate:
|
||||
return "telemetry.moderate".localized
|
||||
return "Moderate".localized
|
||||
case .sensitive:
|
||||
return "telemetry.sensitive".localized
|
||||
return "Unhealthy for Sensitive Groups".localized
|
||||
case .unhealthy:
|
||||
return "telementry.unhealthy".localized
|
||||
return "Unhealthy".localized
|
||||
case .veryUnhealthy:
|
||||
return "telementry.veryUnhealthy".localized
|
||||
return "Very Unhealthy".localized
|
||||
case .hazardous:
|
||||
return "telementry.hazardous".localized
|
||||
return "Hazardous".localized
|
||||
}
|
||||
}
|
||||
var color: Color {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,10 @@ struct CsvDocument: FileDocument {
|
|||
}
|
||||
|
||||
init(configuration: ReadConfiguration) throws {
|
||||
|
||||
if let data = configuration.file.regularFileContents {
|
||||
|
||||
csvData = String(decoding: data, as: UTF8.self)
|
||||
csvData = String(data: data, encoding: .utf8) ?? ""
|
||||
|
||||
} else {
|
||||
|
||||
throw CocoaError(.fileReadCorruptFile)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
|
|||
let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "")
|
||||
if metricsType == 0 {
|
||||
// Create Device Metrics Header
|
||||
csvString = "\("battery.level".localized), \("Voltage".localized), \("channel.utilization".localized), \("airtime".localized), \("uptime".localized), \("Timestamp".localized)"
|
||||
csvString = "\("battery.level".localized), \("Voltage".localized), \("Channel Utilization".localized), \("airtime".localized), \("Uptime".localized), \("Timestamp".localized)"
|
||||
for dm in telemetry where dm.metricsType == 0 {
|
||||
csvString += "\n"
|
||||
csvString += dm.batteryLevel?.formatted(.number.grouping(.never)) ?? ""
|
||||
|
|
@ -27,7 +27,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
|
|||
csvString += ", "
|
||||
csvString += dm.uptimeSeconds?.formatted(.number.grouping(.never)) ?? ""
|
||||
csvString += ", "
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
|
||||
}
|
||||
} else if metricsType == 1 {
|
||||
// Create Environment Telemetry Header
|
||||
|
|
@ -44,7 +44,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
|
|||
csvString += ", "
|
||||
csvString += dm.gasResistance?.formatted(.number.grouping(.never)) ?? ""
|
||||
csvString += ", "
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
|
||||
}
|
||||
} else if metricsType == 2 {
|
||||
// Create Power Metrics Header
|
||||
|
|
@ -63,7 +63,7 @@ func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> Strin
|
|||
csvString += ", "
|
||||
csvString += dm.powerCh3Current?.formatted(.number.grouping(.never)) ?? ""
|
||||
csvString += ", "
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
|
||||
csvString += dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
|
||||
}
|
||||
}
|
||||
return csvString
|
||||
|
|
@ -121,7 +121,7 @@ func paxToCsvFile(pax: [PaxCounterEntity]) -> String {
|
|||
csvString += ", "
|
||||
csvString += String(p.uptime)
|
||||
csvString += ", "
|
||||
csvString += p.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
|
||||
csvString += p.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
|
||||
}
|
||||
return csvString
|
||||
}
|
||||
|
|
@ -150,7 +150,7 @@ func positionToCsvFile(positions: [PositionEntity]) -> String {
|
|||
csvString += ", "
|
||||
csvString += String(pos.snr)
|
||||
csvString += ", "
|
||||
csvString += pos.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized
|
||||
csvString += pos.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized
|
||||
}
|
||||
return csvString
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,6 @@ extension PositionEntity {
|
|||
|
||||
extension PositionEntity: MKAnnotation {
|
||||
public var coordinate: CLLocationCoordinate2D { nodeCoordinate ?? LocationsHandler.DefaultLocation }
|
||||
public var title: String? { nodePosition?.user?.shortName ?? "unknown".localized }
|
||||
public var title: String? { nodePosition?.user?.shortName ?? "Unknown".localized }
|
||||
public var subtitle: String? { time?.formatted() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ extension Date {
|
|||
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
|
||||
formatted()
|
||||
} else {
|
||||
"unknown.age".localized
|
||||
"Unknown Age".localized
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,18 +23,18 @@ extension Date {
|
|||
if self.timeIntervalSince1970 > 0 && self < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
|
||||
return dateformat.string(from: self)
|
||||
} else {
|
||||
return "unknown.age".localized
|
||||
return "Unknown Age".localized
|
||||
}
|
||||
}
|
||||
func relativeTimeOfDay() -> String {
|
||||
let hour = Calendar.current.component(.hour, from: self)
|
||||
|
||||
switch hour {
|
||||
case 6..<12: return "relativetimeofday.morning".localized
|
||||
case 12: return "relativetimeofday.midday".localized
|
||||
case 13..<17: return "relativetimeofday.afternoon".localized
|
||||
case 17..<22: return "relativetimeofday.evening".localized
|
||||
default: return "relativetimeofday.nighttime".localized
|
||||
case 6..<12: return "Morning".localized
|
||||
case 12: return "Midday".localized
|
||||
case 13..<17: return "Afternoon".localized
|
||||
case 17..<22: return "Evening".localized
|
||||
default: return "Nighttime".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ extension OSLogEntryLog.Level {
|
|||
case .notice: "⚠️ Notice"
|
||||
case .error: "🚨 Error"
|
||||
case .fault: "💥 Fault"
|
||||
@unknown default: "default"
|
||||
@unknown default: "Default".localized
|
||||
}
|
||||
}
|
||||
var color: Color {
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ struct UserDefault<T: Decodable> {
|
|||
|
||||
var wrappedValue: T {
|
||||
get {
|
||||
if defaultValue as? any RawRepresentable != nil {
|
||||
if defaultValue is any RawRepresentable {
|
||||
let storedValue = UserDefaults.standard.object(forKey: key.rawValue)
|
||||
|
||||
guard let storedValue,
|
||||
let jsonString = (storedValue as? String != nil) ? "\"\(storedValue)\"" : "\(storedValue)",
|
||||
let jsonString = (storedValue is String) ? "\"\(storedValue)\"" : "\(storedValue)",
|
||||
let data = jsonString.data(using: .utf8),
|
||||
let value = (try? JSONDecoder().decode(T.self, from: data)) else { return defaultValue }
|
||||
|
||||
|
|
@ -72,6 +72,7 @@ extension UserDefaults {
|
|||
case firmwareVersion
|
||||
case environmentEnableWeatherKit
|
||||
case enableAdministration
|
||||
case mapReportingOptIn
|
||||
case testIntEnum
|
||||
}
|
||||
|
||||
|
|
@ -97,7 +98,7 @@ extension UserDefaults {
|
|||
@UserDefault(.meshMapDistance, defaultValue: 800000)
|
||||
static var meshMapDistance: Double
|
||||
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: false)
|
||||
@UserDefault(.enableMapWaypoints, defaultValue: true)
|
||||
static var enableMapWaypoints: Bool
|
||||
|
||||
@UserDefault(.enableMapRecentering, defaultValue: false)
|
||||
|
|
@ -118,24 +119,6 @@ extension UserDefaults {
|
|||
@UserDefault(.enableMapPointsOfInterest, defaultValue: false)
|
||||
static var enableMapPointsOfInterest: Bool
|
||||
|
||||
@UserDefault(.enableOfflineMaps, defaultValue: false)
|
||||
static var enableOfflineMaps: Bool
|
||||
|
||||
@UserDefault(.mapTileServer, defaultValue: .openStreetMap)
|
||||
static var mapTileServer: MapTileServer
|
||||
|
||||
@UserDefault(.enableOverlayServer, defaultValue: false)
|
||||
static var enableOverlayServer: Bool
|
||||
|
||||
@UserDefault(.mapOverlayServer, defaultValue: .baseReReflectivityCurrent)
|
||||
static var mapOverlayServer: MapOverlayServer
|
||||
|
||||
@UserDefault(.mapTilesAboveLabels, defaultValue: false)
|
||||
static var mapTilesAboveLabels: Bool
|
||||
|
||||
@UserDefault(.mapUseLegacy, defaultValue: false)
|
||||
static var mapUseLegacy: Bool
|
||||
|
||||
@UserDefault(.enableDetectionNotifications, defaultValue: false)
|
||||
static var enableDetectionNotifications: Bool
|
||||
|
||||
|
|
@ -166,6 +149,9 @@ extension UserDefaults {
|
|||
@UserDefault(.enableAdministration, defaultValue: false)
|
||||
static var enableAdministration: Bool
|
||||
|
||||
@UserDefault(.mapReportingOptIn, defaultValue: false)
|
||||
static var mapReportingOptIn: Bool
|
||||
|
||||
@UserDefault(.testIntEnum, defaultValue: .one)
|
||||
static var testIntEnum: TestIntEnum
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
cancelPeripheralConnection()
|
||||
if errorCode == 14 { // Peer removed pairing information
|
||||
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
|
||||
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re pairing the radio.".localized, e.localizedDescription)
|
||||
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device under Settings > Bluetooth and re pairing the radio.".localized, e.localizedDescription)
|
||||
Logger.services.error("🚨 [BLE] Failed to connect: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)")
|
||||
} else {
|
||||
lastConnectionError = "🚨 \(e.localizedDescription)"
|
||||
|
|
@ -261,7 +261,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
Notification(
|
||||
id: (peripheral.identifier.uuidString),
|
||||
title: "Radio Disconnected".localized,
|
||||
subtitle: "\(peripheral.name ?? "unknown".localized)",
|
||||
subtitle: "\(peripheral.name ?? "Unknown".localized)",
|
||||
content: e.localizedDescription,
|
||||
target: "bluetooth",
|
||||
path: "meshtastic:///bluetooth"
|
||||
|
|
@ -273,7 +273,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized, privacy: .public) Error Code: \(errorCode, privacy: .public) Error: \(e.localizedDescription, privacy: .public)")
|
||||
} else if errorCode == 14 { // Peer removed pairing information
|
||||
// Forgetting and reconnecting seems to be necessary so we need to show the user an error telling them to do that
|
||||
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio.".localized, e.localizedDescription)
|
||||
lastConnectionError = "🚨 " + String.localizedStringWithFormat("%@ This error usually cannot be fixed without forgetting the device under Settings > Bluetooth and re-connecting to the radio.".localized, e.localizedDescription)
|
||||
Logger.services.error("🚨 [BLE] Disconnected: \(peripheral.name ?? "Unknown".localized) Error Code: \(errorCode, privacy: .public) Error: \(self.lastConnectionError, privacy: .public)")
|
||||
} else {
|
||||
if UserDefaults.preferredPeripheralId == peripheral.identifier.uuidString {
|
||||
|
|
@ -281,7 +281,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
Notification(
|
||||
id: (peripheral.identifier.uuidString),
|
||||
title: "Radio Disconnected".localized,
|
||||
subtitle: "\(peripheral.name ?? "unknown".localized)",
|
||||
subtitle: "\(peripheral.name ?? "Unknown".localized)",
|
||||
content: e.localizedDescription,
|
||||
target: "bluetooth",
|
||||
path: "meshtastic:///bluetooth"
|
||||
|
|
@ -428,7 +428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return 0
|
||||
}
|
||||
|
||||
let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser.longName ?? "unknown".localized) by \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ [Device Metadata] Requested for node \(toUser.longName ?? "Unknown".localized) by \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
|
@ -477,14 +477,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
traceRoute.node = receivingNode
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized), privacy: .public)")
|
||||
Logger.data.info("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "Unknown".localized), privacy: .public)")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError, privacy: .public)")
|
||||
}
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, destNum.toHex())
|
||||
let logString = String.localizedStringWithFormat("Sent a Trace Route Request to node: %@".localized, destNum.toHex())
|
||||
Logger.mesh.info("🪧 \(logString, privacy: .public)")
|
||||
|
||||
} catch {
|
||||
|
|
@ -498,13 +498,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
guard connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected else { return }
|
||||
|
||||
if FROMRADIO_characteristic == nil {
|
||||
Logger.mesh.error("🚨 \("firmware.version.unsupported".localized, privacy: .public)")
|
||||
Logger.mesh.error("🚨 \("Unsupported Firmware Version Detected, unable to connect to device.".localized, privacy: .public)")
|
||||
invalidVersion = true
|
||||
return
|
||||
} else {
|
||||
|
||||
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
|
||||
let logString = String.localizedStringWithFormat("mesh.log.wantconfig %@".localized, nodeName)
|
||||
let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized
|
||||
let logString = String.localizedStringWithFormat("Issuing Want Config to %@".localized, nodeName)
|
||||
Logger.mesh.info("🛎️ \(logString, privacy: .public)")
|
||||
// BLE Characteristics discovered, issue wantConfig
|
||||
var toRadio: ToRadio = ToRadio()
|
||||
|
|
@ -579,7 +579,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
|
||||
|
||||
if let error {
|
||||
|
||||
Logger.services.error("🚫 [BLE] didUpdateValueFor Characteristic error \(error.localizedDescription, privacy: .public)")
|
||||
let errorCode = (error as NSError).code
|
||||
if errorCode == 5 || errorCode == 15 {
|
||||
|
|
@ -633,14 +632,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} catch {
|
||||
Logger.services.error("💥 \(error.localizedDescription, privacy: .public) \(characteristic.value!, privacy: .public)")
|
||||
}
|
||||
|
||||
// Publish mqttClientProxyMessages received on the from radio
|
||||
if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.mqttClientProxyMessage(decodedInfo.mqttClientProxyMessage) {
|
||||
let message = CocoaMQTTMessage(
|
||||
topic: decodedInfo.mqttClientProxyMessage.topic,
|
||||
payload: [UInt8](decodedInfo.mqttClientProxyMessage.data),
|
||||
retained: decodedInfo.mqttClientProxyMessage.retained
|
||||
)
|
||||
let message = CocoaMQTTMessage(topic: decodedInfo.mqttClientProxyMessage.topic, payload: [UInt8](decodedInfo.mqttClientProxyMessage.data), retained: decodedInfo.mqttClientProxyMessage.retained)
|
||||
mqttManager.mqttClientProxy?.publish(message)
|
||||
} else if decodedInfo.payloadVariant == FromRadio.OneOf_PayloadVariant.clientNotification(decodedInfo.clientNotification) {
|
||||
if decodedInfo.clientNotification.hasReplyID {
|
||||
|
|
@ -686,8 +680,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if myInfo != nil {
|
||||
UserDefaults.preferredPeripheralNum = Int(myInfo?.myNodeNum ?? 0)
|
||||
connectedPeripheral.num = myInfo?.myNodeNum ?? 0
|
||||
connectedPeripheral.name = myInfo?.bleName ?? "unknown".localized
|
||||
connectedPeripheral.longName = myInfo?.bleName ?? "unknown".localized
|
||||
connectedPeripheral.name = myInfo?.bleName ?? "Unknown".localized
|
||||
connectedPeripheral.longName = myInfo?.bleName ?? "Unknown".localized
|
||||
let newConnection = Int64(UserDefaults.preferredPeripheralNum) != Int64(decodedInfo.myInfo.myNodeNum)
|
||||
if newConnection {
|
||||
// Onboard a new device connection here
|
||||
|
|
@ -702,7 +696,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if self.connectedPeripheral != nil && self.connectedPeripheral.num == nodeInfo.num {
|
||||
if nodeInfo.user != nil {
|
||||
connectedPeripheral.shortName = nodeInfo.user?.shortName ?? "?"
|
||||
connectedPeripheral.longName = nodeInfo.user?.longName ?? "unknown".localized
|
||||
connectedPeripheral.longName = nodeInfo.user?.longName ?? "Unknown".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -745,7 +739,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let supportedVersion = connectedVersion == "0.0.0" || self.minimumVersion.compare(connectedVersion, options: .numeric) == .orderedAscending || minimumVersion.compare(connectedVersion, options: .numeric) == .orderedSame
|
||||
if !supportedVersion {
|
||||
invalidVersion = true
|
||||
lastConnectionError = "🚨" + "update.firmware".localized
|
||||
lastConnectionError = "🚨" + "Update Your Firmware".localized
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -782,13 +776,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
adminAppPacket(packet: decodedInfo.packet, context: context)
|
||||
case .replyApp:
|
||||
Logger.mesh.info("🕸️ MESH PACKET received for Reply App handling as a text message")
|
||||
textMessageAppPacket(
|
||||
packet: decodedInfo.packet,
|
||||
wantRangeTestPackets: wantRangeTestPackets,
|
||||
connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0),
|
||||
context: context,
|
||||
appState: appState
|
||||
)
|
||||
textMessageAppPacket(packet: decodedInfo.packet, wantRangeTestPackets: wantRangeTestPackets, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context, appState: appState)
|
||||
case .ipTunnelApp:
|
||||
Logger.mesh.info("🕸️ MESH PACKET received for IP Tunnel App UNHANDLED UNHANDLED")
|
||||
case .serialApp:
|
||||
|
|
@ -873,13 +861,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
hopNodes.append(traceRouteHop)
|
||||
|
||||
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))
|
||||
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "Unknown".localized))
|
||||
let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : ""
|
||||
let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized
|
||||
routeString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> "
|
||||
}
|
||||
let destinationHop = TraceRouteHopEntity(context: context)
|
||||
destinationHop.name = traceRoute?.node?.user?.longName ?? "unknown".localized
|
||||
destinationHop.name = traceRoute?.node?.user?.longName ?? "Unknown".localized
|
||||
destinationHop.time = Date()
|
||||
// If nil, set to unknown, INT8_MIN (-128) then divide by 4
|
||||
destinationHop.snr = Float(routingMessage.snrTowards.last ?? -128) / 4
|
||||
|
|
@ -892,14 +880,14 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
hopNodes.append(destinationHop)
|
||||
/// Add the destination node to the end of the route towards string and the beginning of the route back string
|
||||
routeString += "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)"
|
||||
routeString += "\(traceRoute?.node?.user?.longName ?? "Unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) (\(destinationHop.snr != -32 ? String(destinationHop.snr) : "unknown ".localized)dB)"
|
||||
traceRoute?.routeText = routeString
|
||||
// Default to -1 only fill in if routeBack is valid below
|
||||
traceRoute?.hopsBack = -1
|
||||
// Only if hopStart is set and there is an SNR entry
|
||||
if decodedInfo.packet.hopStart > 0 && routingMessage.snrBack.count > 0 {
|
||||
traceRoute?.hopsBack = Int32(routingMessage.routeBack.count)
|
||||
var routeBackString = "\(traceRoute?.node?.user?.longName ?? "unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> "
|
||||
var routeBackString = "\(traceRoute?.node?.user?.longName ?? "Unknown".localized) \((traceRoute?.node?.num ?? 0).toHex()) --> "
|
||||
for (index, node) in routingMessage.routeBack.enumerated() {
|
||||
var hopNode = getNodeInfo(id: Int64(node), context: context)
|
||||
if hopNode == nil && hopNode?.num ?? 0 > 0 && node != 4294967295 {
|
||||
|
|
@ -930,7 +918,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
hopNodes.append(traceRouteHop)
|
||||
|
||||
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "unknown".localized))
|
||||
let hopName = hopNode?.user?.longName ?? (node == 4294967295 ? "Repeater" : String(hopNode?.num.toHex() ?? "Unknown".localized))
|
||||
let mqttLabel = hopNode?.viaMqtt ?? false ? "MQTT " : ""
|
||||
let snrLabel = (traceRouteHop.snr != -32) ? String(traceRouteHop.snr) : "unknown ".localized
|
||||
routeBackString += "\(hopName) \(mqttLabel)(\(snrLabel)dB) --> "
|
||||
|
|
@ -950,7 +938,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
id: (UUID().uuidString),
|
||||
title: "Traceroute Complete",
|
||||
subtitle: "TR received back from \(destinationHop.name ?? "unknown")",
|
||||
content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "unknown".localized)\n\(tr.routeBackText ?? "unknown".localized)",
|
||||
content: "Hops from: \(tr.hopsTowards), Hops back: \(tr.hopsBack)\n\(tr.routeText ?? "Unknown".localized)\n\(tr.routeBackText ?? "Unknown".localized)",
|
||||
target: "nodes",
|
||||
path: "meshtastic:///nodes?nodenum=\(connectedNode.user?.num ?? 0)"
|
||||
)
|
||||
|
|
@ -966,7 +954,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let nsError = error as NSError
|
||||
Logger.data.error("Error Updating Core Data TraceRouteHop: \(nsError, privacy: .public)")
|
||||
}
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString)
|
||||
let logString = String.localizedStringWithFormat("Trace Route request returned: %@".localized, routeString)
|
||||
Logger.mesh.info("🪧 \(logString, privacy: .public)")
|
||||
}
|
||||
case .neighborinfoApp:
|
||||
|
|
@ -1069,8 +1057,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if preferredPeripheral != nil && preferredPeripheral?.peripheral != nil {
|
||||
connectTo(peripheral: preferredPeripheral!.peripheral)
|
||||
}
|
||||
let nodeName = connectedPeripheral?.peripheral.name ?? "unknown".localized
|
||||
let logString = String.localizedStringWithFormat("mesh.log.textmessage.send.failed %@".localized, nodeName)
|
||||
let nodeName = connectedPeripheral?.peripheral.name ?? "Unknown".localized
|
||||
let logString = String.localizedStringWithFormat("Message Send Failed, not properly connected to %@".localized, nodeName)
|
||||
Logger.mesh.info("🚫 \(logString, privacy: .public)")
|
||||
|
||||
success = false
|
||||
|
|
@ -1156,7 +1144,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
let logString = String.localizedStringWithFormat("mesh.log.textmessage.sent %@ %@ %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex())
|
||||
let logString = String.localizedStringWithFormat("Sent message %@ from %@ to %@".localized, String(newMessage.messageId), fromUserNum.toHex(), toUserNum.toHex())
|
||||
|
||||
Logger.mesh.info("💬 \(logString, privacy: .public)")
|
||||
do {
|
||||
|
|
@ -1204,7 +1192,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
guard let binaryData: Data = try? toRadio.serializedData() else {
|
||||
return false
|
||||
}
|
||||
let logString = String.localizedStringWithFormat("mesh.log.waypoint.sent %@".localized, String(fromNodeNum))
|
||||
let logString = String.localizedStringWithFormat("Sent a Waypoint Packet from: %@".localized, String(fromNodeNum))
|
||||
Logger.mesh.info("📍 \(logString, privacy: .public)")
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
|
|
@ -1304,7 +1292,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Set Fixed Postion Admin Message to: \(fromUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1329,7 +1317,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Remove Fixed Position Admin Message to: \(fromUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1368,7 +1356,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
let logString = String.localizedStringWithFormat("mesh.log.sharelocation %@".localized, String(fromNodeNum))
|
||||
let logString = String.localizedStringWithFormat("Sent a Position Packet from the Apple device GPS to node: %@".localized, String(fromNodeNum))
|
||||
Logger.services.debug("📍 \(logString, privacy: .public)")
|
||||
return true
|
||||
} else {
|
||||
|
|
@ -1434,7 +1422,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
let messageDescription = "🚀 Sent Shutdown Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Shutdown Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1462,7 +1450,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
let messageDescription = "🚀 Sent Reboot Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Reboot Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1490,7 +1478,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return false
|
||||
}
|
||||
let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Reboot OTA Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1519,7 +1507,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
automaticallyReconnect = false
|
||||
let messageDescription = "🚀 Sent enter DFU mode Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent enter DFU mode Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1547,7 +1535,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
|
||||
let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent Factory Reset Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1574,7 +1562,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "unknown".localized) from: \(fromUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🚀 Sent NodeDB Reset Admin Message to: \(toUser.longName ?? "Unknown".localized) from: \(fromUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -1617,7 +1605,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🎛️ Requested Channel \(channel.index) for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
|
@ -1641,7 +1629,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Channel \(channel.index) for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
|
@ -1723,7 +1711,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
|
||||
let logString = String.localizedStringWithFormat("mesh.log.channel.sent %@ %d".localized, String(connectedPeripheral.num), chan.index)
|
||||
let logString = String.localizedStringWithFormat("Sent a Channel for: %@ Channel Index %d".localized, String(connectedPeripheral.num), chan.index)
|
||||
Logger.mesh.info("🎛️ \(logString, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
|
@ -1752,7 +1740,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
}
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
self.connectedPeripheral.peripheral.writeValue(binaryData, for: self.TORADIO_characteristic, type: .withResponse)
|
||||
let logString = String.localizedStringWithFormat("mesh.log.lora.config.sent %@".localized, String(connectedPeripheral.num))
|
||||
let logString = String.localizedStringWithFormat("Sent a LoRa.Config for: %@".localized, String(connectedPeripheral.num))
|
||||
Logger.mesh.info("📻 \(logString, privacy: .public)")
|
||||
}
|
||||
|
||||
|
|
@ -1790,7 +1778,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
} else {
|
||||
return 0
|
||||
}
|
||||
let messageDescription = "🛟 Saved User Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved User Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
|
@ -1972,7 +1960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.payload = adminData
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🛟 Saved Ham Parameters for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Ham Parameters for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
|
@ -1998,7 +1986,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.payload = adminData
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Bluetooth Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertBluetoothConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
|
||||
|
|
@ -2029,7 +2017,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.payload = adminData
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Device Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertDeviceConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2059,7 +2047,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.payload = adminData
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Display Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertDisplayConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2088,7 +2076,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.payload = adminData
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved LoRa Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertLoRaConfigPacket(config: config, nodeNum: toUser.num, sessionPasskey: toUser.userNode?.sessionPasskey, context: context)
|
||||
|
|
@ -2120,7 +2108,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Position Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertPositionConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2151,7 +2139,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Power Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Power Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertPowerConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2184,7 +2172,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Network Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertNetworkConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2217,7 +2205,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Security Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Security Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertSecurityConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2249,7 +2237,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2281,7 +2269,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Canned Message Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertCannedMessagesModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2314,7 +2302,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Canned Message Module Messages for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
|
||||
|
|
@ -2347,7 +2335,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Detection Sensor Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertDetectionSensorModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2378,7 +2366,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved External Notification Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertExternalNotificationModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2409,7 +2397,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved PAX Counter Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved PAX Counter Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertPaxCounterModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2440,7 +2428,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved RTTTL Ringtone Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved RTTTL Ringtone Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertRtttlConfigPacket(ringtone: ringtone, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2474,7 +2462,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved MQTT Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertMqttModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2504,7 +2492,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Range Test Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Range Test Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertRangeTestModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
|
|
@ -2537,7 +2525,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Serial Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertSerialModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2568,7 +2556,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛟 Saved Store & Forward Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertStoreForwardModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2599,7 +2587,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "Saved Telemetry Module Config for \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
upsertTelemetryModuleConfigPacket(config: config, nodeNum: toUser.num, context: context)
|
||||
return Int64(meshPacket.id)
|
||||
|
|
@ -2629,7 +2617,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Sent a Get Channel \(channelIndex) Request Admin Message for node: \(toUser.longName ?? "unknown".localized))"
|
||||
let messageDescription = "🛎️ Sent a Get Channel \(channelIndex) Request Admin Message for node: \(toUser.longName ?? "Unknown".localized))"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
|
||||
|
|
@ -2672,7 +2660,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
if connectedPeripheral?.peripheral.state ?? CBPeripheralState.disconnected == CBPeripheralState.connected {
|
||||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.get %@".localized, String(connectedPeripheral.num))
|
||||
let logString = String.localizedStringWithFormat("Requested Canned Messages Module Messages for node: %@".localized, String(connectedPeripheral.num))
|
||||
Logger.mesh.info("🥫 \(logString, privacy: .public)")
|
||||
return true
|
||||
}
|
||||
|
|
@ -2735,7 +2723,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Device Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
|
|
@ -2765,7 +2753,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Display Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
|
|
@ -2795,7 +2783,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested LoRa Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
|
||||
|
|
@ -2826,7 +2814,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
dataMessage.wantResponse = true
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Network Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
|
|
@ -2856,7 +2844,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Position Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -2885,7 +2873,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Power Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -2914,7 +2902,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Security Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Security Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -2943,7 +2931,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -2972,7 +2960,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Canned Messages Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3001,7 +2989,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested External Notificaiton Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3030,7 +3018,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested PAX Counter Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3059,7 +3047,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested RTTTL Ringtone Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3088,7 +3076,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Range Test Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3146,7 +3134,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Detection Sensor Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3175,7 +3163,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Serial Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3204,7 +3192,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Store and Forward Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3234,7 +3222,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
let messageDescription = "🛎️ Requested Telemetry Module Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "Unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription) {
|
||||
return true
|
||||
}
|
||||
|
|
@ -3448,7 +3436,7 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
case .unsupported:
|
||||
status = "BLE is unsupported"
|
||||
default:
|
||||
status = "default"
|
||||
status = "Default".localized
|
||||
}
|
||||
Logger.services.info("📜 [BLE] Bluetooth status: \(status, privacy: .public)")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,74 +0,0 @@
|
|||
//
|
||||
// OfflineTileManager.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 4/23/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MapKit
|
||||
import OSLog
|
||||
|
||||
class OfflineTileManager: ObservableObject {
|
||||
static let shared = OfflineTileManager()
|
||||
|
||||
// MARK: - Public properties
|
||||
|
||||
@Published var status: DownloadStatus = .downloaded
|
||||
|
||||
enum DownloadStatus {
|
||||
case downloaded, downloading
|
||||
}
|
||||
|
||||
init() {
|
||||
Logger.services.info("🗂️ Documents Directory = \(self.documentsDirectory.absoluteString, privacy: .public)")
|
||||
createDirectoriesIfNecessary()
|
||||
}
|
||||
|
||||
// MARK: - Private properties
|
||||
|
||||
private var overlay: MKTileOverlay { MKTileOverlay(urlTemplate: UserDefaults.mapTileServer.tileUrl.count > 1 ? UserDefaults.mapTileServer.tileUrl : MapTileServer.openStreetMap.tileUrl) }
|
||||
private var documentsDirectory: URL { fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! }
|
||||
private let fileManager = FileManager.default
|
||||
|
||||
// MARK: - Public methods
|
||||
|
||||
func getAllDownloadedSize() -> String {
|
||||
fileManager.allocatedSizeOfDirectory(at: documentsDirectory.appendingPathComponent("tiles"))
|
||||
}
|
||||
|
||||
func removeAll() {
|
||||
try? fileManager.removeItem(at: documentsDirectory.appendingPathComponent("tiles"))
|
||||
createDirectoriesIfNecessary()
|
||||
}
|
||||
|
||||
func loadAndCacheTileOverlay(for path: MKTileOverlayPath) throws -> Data {
|
||||
guard UserDefaults.enableOfflineMaps, UserDefaults.mapTileServer.zoomRange.contains(path.z) else {
|
||||
return try Data(contentsOf: Bundle.main.url(forResource: "alpha", withExtension: "png")!)
|
||||
}
|
||||
|
||||
let tilesUrl = documentsDirectory
|
||||
.appendingPathComponent("tiles")
|
||||
.appendingPathComponent("\(UserDefaults.mapTileServer.id)-z\(path.z)x\(path.x)y\(path.y)")
|
||||
.appendingPathExtension("png")
|
||||
|
||||
do {
|
||||
return try Data(contentsOf: tilesUrl)
|
||||
} catch let error as NSError where error.code == NSFileReadNoSuchFileError {
|
||||
DispatchQueue.main.async { self.status = .downloading }
|
||||
defer {
|
||||
DispatchQueue.main.async { self.status = .downloaded }
|
||||
}
|
||||
let data = try Data(contentsOf: overlay.url(forTilePath: path))
|
||||
try data.write(to: tilesUrl)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private methods
|
||||
|
||||
private func createDirectoriesIfNecessary() {
|
||||
let tiles = documentsDirectory.appendingPathComponent("tiles")
|
||||
try? fileManager.createDirectory(at: tiles, withIntermediateDirectories: true, attributes: [:])
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// TileOverlay.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 5/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MapKit
|
||||
|
||||
class TileOverlay: MKTileOverlay {
|
||||
override func loadTile(at path: MKTileOverlayPath) async throws -> Data {
|
||||
return try OfflineTileManager.shared.loadAndCacheTileOverlay(for: path)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,7 +103,7 @@ func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNu
|
|||
|
||||
func myInfoPacket (myInfo: MyNodeInfo, peripheralId: String, context: NSManagedObjectContext) -> MyInfoEntity? {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.myinfo %@".localized, String(myInfo.myNodeNum))
|
||||
let logString = String.localizedStringWithFormat("MyInfo received: %@".localized, String(myInfo.myNodeNum))
|
||||
Logger.mesh.info("ℹ️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
|
|
@ -209,7 +209,7 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo
|
|||
func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
if metadata.isInitialized {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.device.metadata.received %@".localized, fromNum.toHex())
|
||||
let logString = String.localizedStringWithFormat("Device Metadata received from: %@".localized, fromNum.toHex())
|
||||
Logger.mesh.info("🏷️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchedNodeRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -261,7 +261,7 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, sessionPass
|
|||
|
||||
func nodeInfoPacket (nodeInfo: NodeInfo, channel: UInt32, context: NSManagedObjectContext) -> NodeInfoEntity? {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, String(nodeInfo.num))
|
||||
let logString = String.localizedStringWithFormat("Node info received for: %@".localized, String(nodeInfo.num))
|
||||
Logger.mesh.info("📟 \(logString, privacy: .public)")
|
||||
|
||||
guard nodeInfo.num > 0 else { return nil }
|
||||
|
|
@ -472,7 +472,7 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
|
||||
if !cmmc.messages.isEmpty {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.cannedmessages.messages.received %@".localized, packet.from.toHex())
|
||||
let logString = String.localizedStringWithFormat("Canned Messages Messages Received For: %@".localized, packet.from.toHex())
|
||||
Logger.mesh.info("🥫 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -582,7 +582,7 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
}
|
||||
func paxCounterPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.paxcounter %@".localized, String(packet.from))
|
||||
let logString = String.localizedStringWithFormat("PAX Counter message received from: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("🧑🤝🧑 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -625,8 +625,8 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
|
||||
let routingError = RoutingError(rawValue: routingMessage.errorReason.rawValue)
|
||||
|
||||
let routingErrorString = routingError?.display ?? "unknown".localized
|
||||
let logString = String.localizedStringWithFormat("mesh.log.routing.message %@ %@".localized, String(packet.decoded.requestID), routingErrorString)
|
||||
let routingErrorString = routingError?.display ?? "Unknown".localized
|
||||
let logString = String.localizedStringWithFormat("Routing received for RequestID: %@ Ack Status: %@".localized, String(packet.decoded.requestID), routingErrorString)
|
||||
Logger.mesh.info("🕸️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchMessageRequest = MessageEntity.fetchRequest()
|
||||
|
|
@ -686,20 +686,15 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
|
|||
func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
if let telemetryMessage = try? Telemetry(serializedBytes: packet.decoded.payload) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.telemetry.received %@".localized, String(packet.from))
|
||||
let logString = String.localizedStringWithFormat("Telemetry received for: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("📈 \(logString, privacy: .public)")
|
||||
|
||||
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) && telemetryMessage.variant != Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
|
||||
/// Other unhandled telemetry packets
|
||||
return
|
||||
}
|
||||
|
||||
let telemetry = TelemetryEntity(context: context)
|
||||
|
||||
let fetchNodeTelemetryRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeTelemetryRequest.predicate = NSPredicate(format: "num == %lld", Int64(packet.from))
|
||||
|
||||
do {
|
||||
let fetchedNode = try context.fetch(fetchNodeTelemetryRequest)
|
||||
if fetchedNode.count == 1 {
|
||||
|
|
@ -756,7 +751,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
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)")
|
||||
} else if telemetryMessage.variant == Telemetry.OneOf_Variant.powerMetrics(telemetryMessage.powerMetrics) {
|
||||
Logger.data.info("📈 [Power Metrics] Received for Node: \(packet.from.toHex(), privacy: .public)")
|
||||
|
||||
telemetry.powerCh1Voltage = telemetryMessage.powerMetrics.hasCh1Voltage.then(telemetryMessage.powerMetrics.ch1Voltage)
|
||||
telemetry.powerCh1Current = telemetryMessage.powerMetrics.hasCh1Current.then(telemetryMessage.powerMetrics.ch1Current)
|
||||
telemetry.powerCh2Voltage = telemetryMessage.powerMetrics.hasCh2Voltage.then(telemetryMessage.powerMetrics.ch2Voltage)
|
||||
|
|
@ -764,7 +758,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
telemetry.powerCh3Voltage = telemetryMessage.powerMetrics.hasCh3Voltage.then(telemetryMessage.powerMetrics.ch3Voltage)
|
||||
telemetry.powerCh3Current = telemetryMessage.powerMetrics.hasCh3Current.then(telemetryMessage.powerMetrics.ch3Current)
|
||||
telemetry.metricsType = 2
|
||||
|
||||
}
|
||||
telemetry.snr = packet.rxSnr
|
||||
telemetry.rssi = packet.rxRssi
|
||||
|
|
@ -781,7 +774,6 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage
|
|||
fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet
|
||||
}
|
||||
try context.save()
|
||||
|
||||
Logger.data.info("💾 [TelemetryEntity] of type \(MetricsTypes(rawValue: Int(telemetry.metricsType))?.name ?? "Unknown Metrics Type", privacy: .public) Saved for Node: \(packet.from.toHex(), privacy: .public)")
|
||||
if telemetry.metricsType == 0 {
|
||||
// Connected Device Metrics
|
||||
|
|
@ -883,7 +875,7 @@ func textMessageAppPacket(
|
|||
}
|
||||
|
||||
if messageText?.count ?? 0 > 0 {
|
||||
Logger.mesh.info("💬 \("mesh.log.textmessage.received".localized, privacy: .public)")
|
||||
Logger.mesh.info("💬 \("Message received from the text message app.".localized, privacy: .public)")
|
||||
let messageUsers = UserEntity.fetchRequest()
|
||||
messageUsers.predicate = NSPredicate(format: "num IN %@", [packet.to, packet.from])
|
||||
do {
|
||||
|
|
@ -980,7 +972,7 @@ func textMessageAppPacket(
|
|||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
|
|
@ -992,7 +984,7 @@ func textMessageAppPacket(
|
|||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized, privacy: .public)")
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
} else if newMessage.fromUser != nil && newMessage.toUser == nil {
|
||||
let fetchMyInfoRequest = MyInfoEntity.fetchRequest()
|
||||
|
|
@ -1011,7 +1003,7 @@ func textMessageAppPacket(
|
|||
manager.notifications = [
|
||||
Notification(
|
||||
id: ("notification.id.\(newMessage.messageId)"),
|
||||
title: "\(newMessage.fromUser?.longName ?? "unknown".localized)",
|
||||
title: "\(newMessage.fromUser?.longName ?? "Unknown".localized)",
|
||||
subtitle: "AKA \(newMessage.fromUser?.shortName ?? "?")",
|
||||
content: messageText!,
|
||||
target: "messages",
|
||||
|
|
@ -1023,7 +1015,7 @@ func textMessageAppPacket(
|
|||
)
|
||||
]
|
||||
manager.schedule()
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "unknown".localized, privacy: .public)")
|
||||
Logger.services.debug("iOS Notification Scheduled for text message from \(newMessage.fromUser?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1045,7 +1037,7 @@ func textMessageAppPacket(
|
|||
|
||||
func waypointPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.waypoint.received %@".localized, String(packet.from))
|
||||
let logString = String.localizedStringWithFormat("Waypoint Packet received from node: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("📍 \(logString, privacy: .public)")
|
||||
|
||||
let fetchWaypointRequest = WaypointEntity.fetchRequest()
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
import Foundation
|
||||
import CocoaMQTT
|
||||
import OSLog
|
||||
import Security
|
||||
|
||||
protocol MqttClientProxyManagerDelegate: AnyObject {
|
||||
func onMqttConnected()
|
||||
|
|
@ -40,20 +41,20 @@ class MqttClientProxyManager {
|
|||
|
||||
if let host = host {
|
||||
let port = defaultServerPort
|
||||
var username = node.mqttConfig?.username
|
||||
var password = node.mqttConfig?.password
|
||||
// if host == defaultServerAddress {
|
||||
//username = ProcessInfo.processInfo.environment["PUBLIC_MQTT_USERNAME"]
|
||||
//password = ProcessInfo.processInfo.environment["PUBLIC_MQTT_PASSWORD"]
|
||||
// }
|
||||
let username = node.mqttConfig?.username
|
||||
let password = node.mqttConfig?.password
|
||||
let root = node.mqttConfig?.root?.count ?? 0 > 0 ? node.mqttConfig?.root : "msh"
|
||||
let prefix = root!
|
||||
topic = prefix + "/2/e" + "/#"
|
||||
let qos = CocoaMQTTQoS(rawValue: UInt8(1))!
|
||||
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic, qos: qos, cleanSession: true)
|
||||
// Require opt in to map report terms to connect
|
||||
if node.mqttConfig?.mapReportingEnabled ?? false && UserDefaults.mapReportingOptIn || !(node.mqttConfig?.mapReportingEnabled ?? false) {
|
||||
connect(host: host, port: port, useSsl: useSsl, username: username, password: password, topic: topic)
|
||||
} else {
|
||||
delegate?.onMqttError(message: "MQTT Map Reporting Terms need to be accepted.")
|
||||
}
|
||||
}
|
||||
}
|
||||
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?, qos: CocoaMQTTQoS, cleanSession: Bool) {
|
||||
func connect(host: String, port: Int, useSsl: Bool, username: String?, password: String?, topic: String?) {
|
||||
guard !host.isEmpty else {
|
||||
delegate?.onMqttDisconnected()
|
||||
return
|
||||
|
|
@ -66,7 +67,7 @@ class MqttClientProxyManager {
|
|||
mqttClient.username = username
|
||||
mqttClient.password = password
|
||||
mqttClient.keepAlive = 60
|
||||
mqttClient.cleanSession = cleanSession
|
||||
mqttClient.cleanSession = true
|
||||
if debugLog {
|
||||
mqttClient.logLevel = .debug
|
||||
}
|
||||
|
|
@ -130,6 +131,16 @@ extension MqttClientProxyManager: CocoaMQTTDelegate {
|
|||
self.disconnect()
|
||||
}
|
||||
}
|
||||
func mqtt(_ mqtt: CocoaMQTT, didReceive trust: SecTrust, completionHandler: @escaping (Bool) -> Void) {
|
||||
let isValid = SecTrustEvaluateWithError(trust, nil)
|
||||
if isValid {
|
||||
Logger.mqtt.info("📲 [MQTT Client Proxy] TLS validation succeeded.")
|
||||
completionHandler(true)
|
||||
} else {
|
||||
Logger.mqtt.warning("📲 [MQTT Client Proxy] TLS validation failed.")
|
||||
completionHandler(true)
|
||||
}
|
||||
}
|
||||
func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
|
||||
Logger.mqtt.debug("📲 [MQTT Client Proxy] disconnected: \(err?.localizedDescription ?? "", privacy: .public)")
|
||||
if let error = err {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ class MetricsChartSeries: ObservableObject {
|
|||
// Used for scaling the Y-axis
|
||||
let initialYAxisRange: ClosedRange<Float>?
|
||||
let minumumYAxisSpan: Float?
|
||||
|
||||
// Main initializer
|
||||
init<Value, ChartBody: ChartContent, ForegroundStyle: ShapeStyle>(
|
||||
id: String,
|
||||
|
|
|
|||
|
|
@ -67,12 +67,12 @@ class MetricsSeriesList: ObservableObject, RandomAccessCollection, RangeReplacea
|
|||
for aSeries in self.visible {
|
||||
var seriesUpper = range[aSeries]?.upperBound ?? -.infinity
|
||||
var seriesLower = range[aSeries]?.lowerBound ?? .infinity
|
||||
|
||||
|
||||
if let value = aSeries.valueFor(te) {
|
||||
// Update the global bounds
|
||||
if value > globalUpper {globalUpper = value}
|
||||
if value < globalLower {globalLower = value}
|
||||
|
||||
|
||||
// Update the series bounds if necessary
|
||||
if value > seriesUpper || value < seriesLower {
|
||||
if value > seriesUpper {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class PersistenceController {
|
|||
// Merge policy that favors in memory data over data in the db
|
||||
self.container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
self.container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
self.container.viewContext.retainsRegisteredObjects = true
|
||||
|
||||
if let error = error as NSError? {
|
||||
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ public func clearCoreDataDatabase(context: NSManagedObjectContext, includeRoutes
|
|||
|
||||
func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.nodeinfo.received %@".localized, packet.from.toHex())
|
||||
let logString = String.localizedStringWithFormat("Node info received for: %@".localized, packet.from.toHex())
|
||||
Logger.mesh.info("📟 \(logString, privacy: .public)")
|
||||
|
||||
guard packet.from > 0 else { return }
|
||||
|
|
@ -200,7 +200,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
Notification(
|
||||
id: (UUID().uuidString),
|
||||
title: "New Node".localized,
|
||||
subtitle: "\(newUser.longName ?? "unknown".localized)",
|
||||
subtitle: "\(newUser.longName ?? "Unknown".localized)",
|
||||
content: "New Node has been discovered".localized,
|
||||
target: "nodes",
|
||||
path: "meshtastic:///nodes?nodenum=\(newUser.num)"
|
||||
|
|
@ -227,6 +227,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
let myInfoEntity = MyInfoEntity(context: context)
|
||||
myInfoEntity.myNodeNum = Int64(packet.from)
|
||||
myInfoEntity.rebootCount = 0
|
||||
newNode.myInfo = myInfoEntity
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("💾 [NodeInfo] Saved a NodeInfo for node number: \(packet.from.toHex(), privacy: .public)")
|
||||
|
|
@ -236,7 +237,6 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
let nsError = error as NSError
|
||||
Logger.data.error("💥 [MyInfoEntity] Error Inserting New Core Data: \(nsError, privacy: .public)")
|
||||
}
|
||||
newNode.myInfo = myInfoEntity
|
||||
|
||||
} else {
|
||||
// Update an existing node
|
||||
|
|
@ -312,7 +312,7 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
|
||||
func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.position.received %@".localized, String(packet.from))
|
||||
let logString = String.localizedStringWithFormat("Position Packet received from node: %@".localized, String(packet.from))
|
||||
Logger.mesh.info("📍 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodePositionRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -406,7 +406,7 @@ func upsertPositionPacket (packet: MeshPacket, context: NSManagedObjectContext)
|
|||
|
||||
func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.bluetooth.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Bluetooth config received: %@".localized, String(nodeNum))
|
||||
Logger.mesh.info("📶 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -450,7 +450,7 @@ func upsertBluetoothConfigPacket(config: Config.BluetoothConfig, nodeNum: Int64,
|
|||
|
||||
func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.device.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Device config received: %@".localized, String(nodeNum))
|
||||
Logger.mesh.info("📟 \(logString, privacy: .public)")
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
|
@ -505,7 +505,7 @@ func upsertDeviceConfigPacket(config: Config.DeviceConfig, nodeNum: Int64, sessi
|
|||
|
||||
func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.display.config %@".localized, nodeNum.toHex())
|
||||
let logString = String.localizedStringWithFormat("Display config received: %@".localized, nodeNum.toHex())
|
||||
Logger.data.info("🖥️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -567,7 +567,7 @@ func upsertDisplayConfigPacket(config: Config.DisplayConfig, nodeNum: Int64, ses
|
|||
|
||||
func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.lora.config %@".localized, nodeNum.toHex())
|
||||
let logString = String.localizedStringWithFormat("LoRa config received: %@".localized, nodeNum.toHex())
|
||||
Logger.data.info("📻 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -638,7 +638,7 @@ func upsertLoRaConfigPacket(config: Config.LoRaConfig, nodeNum: Int64, sessionPa
|
|||
|
||||
func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.network.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Network config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🌐 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -687,7 +687,7 @@ func upsertNetworkConfigPacket(config: Config.NetworkConfig, nodeNum: Int64, ses
|
|||
|
||||
func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.position.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Position config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🗺️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -750,7 +750,7 @@ func upsertPositionConfigPacket(config: Config.PositionConfig, nodeNum: Int64, s
|
|||
}
|
||||
|
||||
func upsertPowerConfigPacket(config: Config.PowerConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.power.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Power config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🗺️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -863,7 +863,7 @@ func upsertSecurityConfigPacket(config: Config.SecurityConfig, nodeNum: Int64, s
|
|||
|
||||
func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightingConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Ambient Lighting module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🏮 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -916,7 +916,7 @@ func upsertAmbientLightingModuleConfigPacket(config: ModuleConfig.AmbientLightin
|
|||
|
||||
func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Canned Message module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🥫 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -975,7 +975,7 @@ func upsertCannedMessagesModuleConfigPacket(config: ModuleConfig.CannedMessageCo
|
|||
|
||||
func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSensorConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.detectionsensor.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Detection Sensor module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🕵️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1032,7 +1032,7 @@ func upsertDetectionSensorModuleConfigPacket(config: ModuleConfig.DetectionSenso
|
|||
|
||||
func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalNotificationConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.externalnotification.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("External Notification module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("📣 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1101,7 +1101,7 @@ func upsertExternalNotificationModuleConfigPacket(config: ModuleConfig.ExternalN
|
|||
|
||||
func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.paxcounter.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("PAX Counter config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🧑🤝🧑 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1143,7 +1143,7 @@ func upsertPaxCounterModuleConfigPacket(config: ModuleConfig.PaxcounterConfig, n
|
|||
|
||||
func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.ringtone.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("RTTTL Ringtone config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("⛰️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1183,7 +1183,7 @@ func upsertRtttlConfigPacket(ringtone: String, nodeNum: Int64, sessionPasskey: D
|
|||
|
||||
func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.mqtt.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("MQTT module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🌉 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1245,7 +1245,7 @@ func upsertMqttModuleConfigPacket(config: ModuleConfig.MQTTConfig, nodeNum: Int6
|
|||
|
||||
func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.rangetest.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Range Test module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("⛰️ \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1289,7 +1289,7 @@ func upsertRangeTestModuleConfigPacket(config: ModuleConfig.RangeTestConfig, nod
|
|||
|
||||
func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.serial.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Serial module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("🤖 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1344,7 +1344,7 @@ func upsertSerialModuleConfigPacket(config: ModuleConfig.SerialConfig, nodeNum:
|
|||
|
||||
func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.storeforward.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Store & Forward module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("📬 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
@ -1393,7 +1393,7 @@ func upsertStoreForwardModuleConfigPacket(config: ModuleConfig.StoreForwardConfi
|
|||
|
||||
func upsertTelemetryModuleConfigPacket(config: ModuleConfig.TelemetryConfig, nodeNum: Int64, sessionPasskey: Data? = Data(), context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.telemetry.config %@".localized, String(nodeNum))
|
||||
let logString = String.localizedStringWithFormat("Telemetry module config received: %@".localized, String(nodeNum))
|
||||
Logger.data.info("📈 \(logString, privacy: .public)")
|
||||
|
||||
let fetchNodeInfoRequest = NodeInfoEntity.fetchRequest()
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@
|
|||
"images": [
|
||||
"t-watch-s3.svg"
|
||||
],
|
||||
"partitionScheme": "16MB"
|
||||
"partitionScheme": "8MB"
|
||||
},
|
||||
{
|
||||
"hwModel": 52,
|
||||
|
|
@ -845,25 +845,32 @@
|
|||
"hwModelSlug": "THINKNODE_M1",
|
||||
"platformioTarget": "thinknode_m1",
|
||||
"architecture": "nrf52840",
|
||||
"activelySupported": false,
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "ThinkNode M1",
|
||||
"tags": [
|
||||
"Elecrow"
|
||||
],
|
||||
"requiresDfu": true
|
||||
"requiresDfu": true,
|
||||
"images": [
|
||||
"thinknode_m1.svg"
|
||||
],
|
||||
"hasInkHud": true
|
||||
},
|
||||
{
|
||||
"hwModel": 90,
|
||||
"hwModelSlug": "THINKNODE_M2",
|
||||
"platformioTarget": "thinknode_m2",
|
||||
"architecture": "esp32-s3",
|
||||
"activelySupported": false,
|
||||
"activelySupported": true,
|
||||
"supportLevel": 1,
|
||||
"displayName": "ThinkNode M2",
|
||||
"tags": [
|
||||
"Elecrow"
|
||||
],
|
||||
"requiresDfu": false
|
||||
"requiresDfu": false,
|
||||
"images": [
|
||||
"thinknode_m2.svg"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@
|
|||
return "tip.channels.share"
|
||||
}
|
||||
var title: Text {
|
||||
Text("tip.channels.share.title")
|
||||
Text("Sharing Meshtastic Channels")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("tip.channels.share.message")
|
||||
Text("A Meshtastic QR code contains the LoRa config and channel values needed for radios to communicate. You can share a complete channel configuration using the Replace Channels option, if you choose Add Channels your shared channels will be added to the channels on the receiving radio.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "qrcode")
|
||||
|
|
@ -29,10 +29,10 @@ struct CreateChannelsTip: Tip {
|
|||
return "tip.channels.create"
|
||||
}
|
||||
var title: Text {
|
||||
Text("tip.channels.create.title")
|
||||
Text("Manage Channels")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("tip.channels.create.message")
|
||||
Text("Most data on your mesh is sent over the primary channel. You can set up secondary channels to create additional messaging groups secured by their own key. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "fibrechannel")
|
||||
|
|
@ -45,10 +45,10 @@ struct AdminChannelTip: Tip {
|
|||
return "tip.channel.admin"
|
||||
}
|
||||
var title: Text {
|
||||
Text("tip.channel.admin.title")
|
||||
Text("Administration Enabled")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("tip.channel.admin.message")
|
||||
Text("Select a node from the drop down to manage connected or remote devices.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "fibrechannel")
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ struct MessagesTip: Tip {
|
|||
return "tip.messages"
|
||||
}
|
||||
var title: Text {
|
||||
Text("tip.messages.title")
|
||||
Text("Messages")
|
||||
}
|
||||
var message: Text? {
|
||||
Text("tip.messages.message")
|
||||
Text("You can send and receive channel (group chats) and direct messages. From any message you can long press to see available actions like copy, reply, tapback and delete as well as delivery details.")
|
||||
}
|
||||
var image: Image? {
|
||||
Image(systemName: "bubble.left.and.bubble.right")
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ struct Connect: View {
|
|||
VStack {
|
||||
List {
|
||||
if bleManager.isSwitchedOn {
|
||||
Section(header: Text("connected.radio").font(.title)) {
|
||||
Section(header: Text("Connected Radio").font(.title)) {
|
||||
if let connectedPeripheral = bleManager.connectedPeripheral, connectedPeripheral.peripheral.state == .connected {
|
||||
TipView(BluetoothConnectionTip(), arrowEdge: .bottom)
|
||||
VStack(alignment: .leading) {
|
||||
|
|
@ -64,10 +64,10 @@ struct Connect: View {
|
|||
if node != nil {
|
||||
Text(connectedPeripheral.longName.addingVariationSelectors).font(.title2)
|
||||
}
|
||||
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "unknown".localized)")
|
||||
Text("BLE Name").font(.callout)+Text(": \(bleManager.connectedPeripheral?.peripheral.name?.addingVariationSelectors ?? "Unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
if node != nil {
|
||||
Text("firmware.version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "unknown".localized)")
|
||||
Text("Firmware Version").font(.callout)+Text(": \(node?.metadata?.firmwareVersion ?? "Unknown".localized)")
|
||||
.font(.callout).foregroundColor(Color.gray)
|
||||
}
|
||||
if bleManager.isSubscribed {
|
||||
|
|
@ -116,12 +116,12 @@ struct Connect: View {
|
|||
#endif
|
||||
}
|
||||
} label: {
|
||||
Label("mesh.live.activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
Label("Mesh Live Activity", systemImage: liveActivityStarted ? "stop" : "play")
|
||||
}
|
||||
#endif
|
||||
Text("Num: \(String(node!.num))")
|
||||
Text("Short Name: \(node?.user?.shortName ?? "?")")
|
||||
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "unknown".localized)")
|
||||
Text("Long Name: \(node?.user?.longName?.addingVariationSelectors ?? "Unknown".localized)")
|
||||
Text("BLE RSSI: \(connectedPeripheral.rssi)")
|
||||
|
||||
Button {
|
||||
|
|
@ -139,7 +139,7 @@ struct Connect: View {
|
|||
NavigationLink {
|
||||
LoRaConfig(node: node)
|
||||
} label: {
|
||||
Label("set.region", systemImage: "globe.americas.fill")
|
||||
Label("Set LoRa Region", systemImage: "globe.americas.fill")
|
||||
.foregroundColor(.red)
|
||||
.font(.title)
|
||||
}
|
||||
|
|
@ -156,7 +156,7 @@ struct Connect: View {
|
|||
.frame(width: 60, height: 60)
|
||||
.padding(.trailing)
|
||||
if bleManager.timeoutTimerCount == 0 {
|
||||
Text("connecting")
|
||||
Text("Connecting . .")
|
||||
.font(.title2)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
|
|
@ -189,7 +189,7 @@ struct Connect: View {
|
|||
.foregroundColor(.red)
|
||||
.frame(width: 60, height: 60)
|
||||
.padding(.trailing)
|
||||
Text("not.connected").font(.title3)
|
||||
Text("No device connected").font(.title3)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ struct InvalidVersion: View {
|
|||
|
||||
VStack {
|
||||
|
||||
Text("update.firmware")
|
||||
Text("Update Your Firmware")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ struct InvalidVersion: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,12 @@ struct ContentView: View {
|
|||
@ObservedObject
|
||||
var router: Router
|
||||
|
||||
init(appState: AppState, router: Router) {
|
||||
self.appState = appState
|
||||
self.router = router
|
||||
UITabBar.appearance().scrollEdgeAppearance = UITabBarAppearance(idiom: .unspecified)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $appState.router.navigationState.selectedTab) {
|
||||
Messages(
|
||||
|
|
@ -19,7 +25,7 @@ struct ContentView: View {
|
|||
unreadDirectMessages: $appState.unreadDirectMessages
|
||||
)
|
||||
.tabItem {
|
||||
Label("messages", systemImage: "message")
|
||||
Label("Messages", systemImage: "message")
|
||||
}
|
||||
.tag(NavigationState.Tab.messages)
|
||||
.badge(appState.totalUnreadMessages)
|
||||
|
|
@ -34,13 +40,13 @@ struct ContentView: View {
|
|||
router: appState.router
|
||||
)
|
||||
.tabItem {
|
||||
Label("nodes", systemImage: "flipphone")
|
||||
Label("Nodes", systemImage: "flipphone")
|
||||
}
|
||||
.tag(NavigationState.Tab.nodes)
|
||||
|
||||
MeshMap(router: appState.router)
|
||||
.tabItem {
|
||||
Label("map", systemImage: "map")
|
||||
Label("Mesh Map", systemImage: "map")
|
||||
}
|
||||
.tag(NavigationState.Tab.map)
|
||||
|
||||
|
|
@ -48,7 +54,7 @@ struct ContentView: View {
|
|||
router: appState.router
|
||||
)
|
||||
.tabItem {
|
||||
Label("settings", systemImage: "gear")
|
||||
Label("Settings", systemImage: "gear")
|
||||
.font(.title)
|
||||
}
|
||||
.tag(NavigationState.Tab.settings)
|
||||
|
|
|
|||
|
|
@ -4,29 +4,43 @@ A view draws a circle in the background of the shortName text
|
|||
*/
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
|
||||
struct CircleText: View {
|
||||
var text: String
|
||||
var color: Color
|
||||
var text: String
|
||||
var color: Color
|
||||
var circleSize: CGFloat = 45
|
||||
var node: NodeInfoEntity?
|
||||
|
||||
var body: some View {
|
||||
var body: some View {
|
||||
if let node = node {
|
||||
NavigationStack {
|
||||
NavigationLink(destination: NodeDetail(node: node)) {
|
||||
circleContent
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
Text(text.addingVariationSelectors)
|
||||
} else {
|
||||
circleContent
|
||||
}
|
||||
}
|
||||
|
||||
var circleContent: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: circleSize, height: circleSize)
|
||||
Text(text)
|
||||
.frame(width: circleSize * 0.9, height: circleSize * 0.9, alignment: .center)
|
||||
.foregroundColor(color.isLight() ? .black : .white)
|
||||
.minimumScaleFactor(0.001)
|
||||
.font(.system(size: 1300))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CircleText_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
CircleText(text: "N1", color: Color.yellow, circleSize: 80)
|
||||
|
|
@ -75,5 +89,5 @@ struct CircleText_Previews: PreviewProvider {
|
|||
.previewLayout(.fixed(width: 300, height: 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import SwiftUI
|
|||
WeightCompactWidget(weight: "123", unit: "kg")
|
||||
SoilTemperatureCompactWidget(temperature: "23", unit: "°C")
|
||||
SoilMoistureCompactWidget(moisture: "23", unit: "%")
|
||||
|
||||
|
||||
let rain: Float = 10.1
|
||||
let locale = NSLocale.current as NSLocale
|
||||
let usesMetricSystem = locale.usesMetricSystem // Returns true for metric (mm), false for imperial (inches)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct RadiationCompactWidget: View {
|
|||
HStack(alignment: .firstTextBaseline) {
|
||||
Text(verbatim: "☢")
|
||||
.font(.system(size: 30, design: .monospaced))
|
||||
.foregroundColor(.accentColor)
|
||||
.tint(.accentColor)
|
||||
Text("Radiation")
|
||||
.textCase(.uppercase)
|
||||
.font(.callout)
|
||||
|
|
|
|||
|
|
@ -39,4 +39,3 @@ struct WeatherConditionsCompactWidget: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct DateTimeText: View {
|
|||
if dateTime != nil && dateTime! >= sixMonthsAgo! {
|
||||
Text(" \(dateTime!.formattedDate(format: dateFormatString))")
|
||||
} else {
|
||||
Text("unknown.age")
|
||||
Text("Unknown Age")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ struct DirectMessagesHelp: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ enum LoRaSignalStrength: Int {
|
|||
var description: String {
|
||||
switch self {
|
||||
case .none:
|
||||
return "lora.signal.strength.none".localized
|
||||
return "None".localized
|
||||
case .bad:
|
||||
return "lora.signal.strength.bad".localized
|
||||
return "Bad".localized
|
||||
case .fair:
|
||||
return "lora.signal.strength.fair".localized
|
||||
return "Fair".localized
|
||||
case .good:
|
||||
return "lora.signal.strength.good".localized
|
||||
return "Good".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ struct MQTTIcon: View {
|
|||
VStack(spacing: 0.5) {
|
||||
Text("Topic: \(topic)".localized)
|
||||
.padding(20)
|
||||
Button("close", action: { self.isPopoverOpen = false }).padding([.bottom], 20)
|
||||
Button("Close", action: { self.isPopoverOpen = false }).padding([.bottom], 20)
|
||||
}
|
||||
.presentationCompactAdaptation(.popover)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ struct PowerMetrics: View {
|
|||
}
|
||||
|
||||
enum PowerMetricType: String {
|
||||
case current = "current"
|
||||
case voltage = "voltage"
|
||||
case current = "Current"
|
||||
case voltage = "Voltage"
|
||||
}
|
||||
|
||||
struct PowerMetricCompactWidget: View {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ struct ChannelList: View {
|
|||
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial"]
|
||||
var restrictedChannels = ["gpio", "mqtt", "serial", "admin"]
|
||||
|
||||
@ViewBuilder
|
||||
private func makeChannelRow(
|
||||
|
|
@ -144,7 +144,7 @@ struct ChannelList: View {
|
|||
context.refresh(myInfo, mergeChanges: true)
|
||||
channelSelection = nil
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +154,6 @@ struct ChannelList: View {
|
|||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
.navigationTitle("channels")
|
||||
.navigationTitle("Channels")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,136 +14,212 @@ struct ChannelMessageList: View {
|
|||
@EnvironmentObject var appState: AppState
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
// Keyboard State
|
||||
@FocusState var messageFieldFocused: Bool
|
||||
|
||||
@ObservedObject var myInfo: MyInfoEntity
|
||||
@ObservedObject var channel: ChannelEntity
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
@AppStorage("preferredPeripheralNum") private var preferredPeripheralNum = -1
|
||||
// Scroll state
|
||||
@State private var showScrollToBottomButton = false
|
||||
@State private var hasReachedBottom = false
|
||||
@State private var gotFirstUnreadMessage: Bool = false
|
||||
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( channel.allPrivateMessages ) { (message: MessageEntity) in
|
||||
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
|
||||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
HStack(alignment: .bottom) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44)
|
||||
.padding(.all, 5)
|
||||
.offset(y: -7)
|
||||
}
|
||||
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
|
||||
|
||||
if !currentUser && message.fromUser != nil {
|
||||
Text("\(message.fromUser?.longName ?? "unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.offset(y: 8)
|
||||
}
|
||||
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach(channel.allPrivateMessages) { (message: MessageEntity) in
|
||||
let currentUser: Bool = (Int64(preferredPeripheralNum) == message.fromUser?.num ? true : false)
|
||||
if message.replyID > 0 {
|
||||
let messageReply = channel.allPrivateMessages.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
MessageText(
|
||||
message: message,
|
||||
tapBackDestination: .channel(channel),
|
||||
isCurrentUser: currentUser
|
||||
) {
|
||||
self.replyMessageId = message.messageId
|
||||
self.messageFieldFocused = true
|
||||
}
|
||||
Button {
|
||||
if let messageNum = messageReply?.messageId {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
if currentUser && message.canRetry {
|
||||
RetryButton(message: message, destination: .channel(channel))
|
||||
}
|
||||
}
|
||||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
if currentUser && message.receivedACK {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(
|
||||
.caption2)
|
||||
.foregroundColor(.orange)
|
||||
} else if currentUser && !isDetectionSensorMessage {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(channel.allPrivateMessages.firstIndex(of: message))
|
||||
HStack(alignment: .bottom) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
if !currentUser {
|
||||
CircleText(text: message.fromUser?.shortName ?? "?", color: Color(UIColor(hex: UInt32(message.fromUser?.num ?? 0))), circleSize: 44, node: getNodeInfo(id: Int64(message.fromUser?.num ?? 0), context: context))
|
||||
.padding(.all, 5)
|
||||
.offset(y: -7)
|
||||
}
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.onAppear {
|
||||
if !message.read {
|
||||
message.read = true
|
||||
do {
|
||||
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
|
||||
unreadMessage.read = true
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
let isDetectionSensorMessage = message.portNum == Int32(PortNum.detectionSensorApp.rawValue)
|
||||
|
||||
if !currentUser && message.fromUser != nil {
|
||||
Text("\(message.fromUser?.longName ?? "Unknown".localized ) (\(message.fromUser?.userId ?? "?"))")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.offset(y: 8)
|
||||
}
|
||||
|
||||
HStack {
|
||||
MessageText(
|
||||
message: message,
|
||||
tapBackDestination: .channel(channel),
|
||||
isCurrentUser: currentUser
|
||||
) {
|
||||
self.replyMessageId = message.messageId
|
||||
self.messageFieldFocused = true
|
||||
}
|
||||
|
||||
if currentUser && message.canRetry {
|
||||
RetryButton(message: message, destination: .channel(channel))
|
||||
}
|
||||
}
|
||||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
}
|
||||
|
||||
HStack {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
if currentUser && message.receivedACK {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(
|
||||
.caption2)
|
||||
.foregroundColor(.orange)
|
||||
} else if currentUser && !isDetectionSensorMessage {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(channel.allPrivateMessages.firstIndex(of: message))
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.onAppear {
|
||||
if gotFirstUnreadMessage {
|
||||
if !message.read {
|
||||
message.read = true
|
||||
do {
|
||||
for unreadMessage in channel.allPrivateMessages.filter({ !$0.read }) {
|
||||
unreadMessage.read = true
|
||||
}
|
||||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
// Check if we've reached the bottom message
|
||||
if message.messageId == channel.allPrivateMessages.last?.messageId {
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
|
||||
appState.unreadChannelMessages = myInfo.unreadMessages
|
||||
context.refresh(myInfo, mergeChanges: true)
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Invisible spacer to detect reaching bottom
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.id("bottomAnchor")
|
||||
.onAppear {
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
// Find first unread message
|
||||
if let firstUnreadMessageId = channel.allPrivateMessages.first(where: { !$0.read })?.messageId {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
} else {
|
||||
// If no unread messages, scroll to bottom
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
}
|
||||
}
|
||||
gotFirstUnreadMessage = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: channel.allPrivateMessages) {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
.onChange(of: channel.allPrivateMessages) {
|
||||
if hasReachedBottom {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(channel.allPrivateMessages.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
}
|
||||
// Scroll to bottom button
|
||||
if showScrollToBottomButton {
|
||||
Button {
|
||||
withAnimation {
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
} label: {
|
||||
ScrollToBottomButtonView()
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.trailing, 16)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -161,7 +237,7 @@ struct ChannelMessageList: View {
|
|||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
CircleText(text: String(channel.index), color: .accentColor, circleSize: 44).fixedSize()
|
||||
Text(String(channel.name ?? "unknown".localized).camelCaseToWords()).font(.headline)
|
||||
Text(String(channel.name ?? "Unknown".localized).camelCaseToWords()).font(.headline)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ struct MessageContextMenuItems: View {
|
|||
Image(systemName: "doc.on.doc")
|
||||
}
|
||||
|
||||
Menu("message.details") {
|
||||
Menu("Message Details") {
|
||||
VStack {
|
||||
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
|
||||
Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray)
|
||||
|
|
@ -69,8 +69,8 @@ struct MessageContextMenuItems: View {
|
|||
}
|
||||
if isCurrentUser && message.receivedACK {
|
||||
VStack {
|
||||
Text("received.ack") + Text(": \(message.receivedACK ? "✔️" : "")")
|
||||
Text("received.ack.real") + Text(": \(message.realACK ? "✔️" : "")")
|
||||
Text("Received Ack") + Text(": \(message.receivedACK ? "✔️" : "")")
|
||||
Text("Recipient Ack") + Text(": \(message.realACK ? "✔️" : "")")
|
||||
}
|
||||
} else if isCurrentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
|
|
@ -104,7 +104,7 @@ struct MessageContextMenuItems: View {
|
|||
Button(role: .destructive) {
|
||||
isShowingDeleteConfirmation = true
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ struct MessageText: View {
|
|||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
}
|
||||
.contextMenu {
|
||||
MessageContextMenuItems(
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ struct Messages: View {
|
|||
List(selection: $router.navigationState.messages) {
|
||||
NavigationLink(value: MessagesNavigationState.channels()) {
|
||||
Label {
|
||||
Text("channels")
|
||||
Text("Channels")
|
||||
.badge(unreadChannelMessages)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
|
|
@ -50,7 +50,7 @@ struct Messages: View {
|
|||
}
|
||||
NavigationLink(value: MessagesNavigationState.directMessages()) {
|
||||
Label {
|
||||
Text("direct.messages")
|
||||
Text("Direct Messages")
|
||||
.badge(unreadDirectMessages)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
|
|
@ -65,7 +65,7 @@ struct Messages: View {
|
|||
|
||||
TipView(MessagesTip(), arrowEdge: .top)
|
||||
}
|
||||
.navigationTitle("messages")
|
||||
.navigationTitle("Messages")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.navigationBarItems(leading: MeshtasticLogo())
|
||||
} content: {
|
||||
|
|
|
|||
|
|
@ -15,78 +15,93 @@ struct TextMessageField: View {
|
|||
@State private var sendPositionWithMessage = false
|
||||
|
||||
var body: some View {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
if destination.showAlertButton {
|
||||
VStack {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
HStack {
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
|
||||
}
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell! \u{7}" }
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes).padding(.trailing)
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
HStack(alignment: .top) {
|
||||
ZStack {
|
||||
TextField("message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage) { _, value in
|
||||
totalBytes = value.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
while totalBytes > Self.maxbytes {
|
||||
typingMessage = String(typingMessage.dropLast())
|
||||
totalBytes = typingMessage.utf8.count
|
||||
}
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss") {
|
||||
isFocused = false
|
||||
HStack(alignment: .top) {
|
||||
if replyMessageId != 0 {
|
||||
HStack {
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
replyMessageId = 0
|
||||
}
|
||||
.font(.subheadline)
|
||||
isFocused = false
|
||||
} label: {
|
||||
Image(systemName: "x.circle.fill")
|
||||
}
|
||||
Text("Replying to a message")
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
TextField("Message", text: $typingMessage, axis: .vertical)
|
||||
.onChange(of: typingMessage) { _, value in
|
||||
totalBytes = value.utf8.count
|
||||
while totalBytes > Self.maxbytes {
|
||||
typingMessage = String(typingMessage.dropLast())
|
||||
totalBytes = typingMessage.utf8.count
|
||||
}
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .keyboard) {
|
||||
Button("Dismiss") {
|
||||
isFocused = false
|
||||
}
|
||||
.font(.subheadline)
|
||||
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
}
|
||||
|
||||
if destination.showAlertButton {
|
||||
Spacer()
|
||||
AlertButton { typingMessage += "🔔 Alert Bell Character! \u{7}" }
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
RequestPositionButton(action: requestPosition)
|
||||
TextMessageSize(maxbytes: Self.maxbytes, totalBytes: totalBytes)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($isFocused)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
sendMessage()
|
||||
#endif
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.focused($isFocused)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(minHeight: 50)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
sendMessage()
|
||||
#endif
|
||||
}
|
||||
|
||||
Text(typingMessage)
|
||||
.opacity(0)
|
||||
.padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
Text(typingMessage)
|
||||
.opacity(0)
|
||||
.padding(.all, 0)
|
||||
}
|
||||
.overlay(RoundedRectangle(cornerRadius: 20).stroke(.tertiary, lineWidth: 1))
|
||||
.padding(.bottom, 15)
|
||||
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
Button(action: sendMessage) {
|
||||
Image(systemName: "arrow.up.circle.fill")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
.padding(.all, 15)
|
||||
}
|
||||
|
||||
private func requestPosition() {
|
||||
let userLongName = bleManager.connectedPeripheral != nil ? bleManager.connectedPeripheral.longName : "Unknown"
|
||||
sendPositionWithMessage = true
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
typingMessage = "📍 " + userLongName + " \(destination.positionShareMessage)."
|
||||
}
|
||||
|
||||
private func sendMessage() {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ struct UserList: View {
|
|||
Image(systemName: "lock.open.fill")
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
Text(user.longName ?? "unknown".localized)
|
||||
Text(user.longName ?? "Unknown".localized)
|
||||
.font(.headline)
|
||||
.allowsTightening(true)
|
||||
Spacer()
|
||||
|
|
@ -187,14 +187,14 @@ struct UserList: View {
|
|||
deleteUserMessages(user: userSelection!, context: context)
|
||||
context.refresh(node!.user!, mergeChanges: true)
|
||||
} label: {
|
||||
Text("delete")
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle(String.localizedStringWithFormat("contacts %@".localized, String(users.count == 0 ? 0 : users.count)))
|
||||
.navigationTitle(String.localizedStringWithFormat("Contacts (%@)".localized, String(users.count == 0 ? 0 : users.count)))
|
||||
.sheet(isPresented: $editingFilters) {
|
||||
NodeListFilter(filterTitle: "Contact Filters", viaLora: $viaLora, viaMqtt: $viaMqtt, isOnline: $isOnline, isPkiEncrypted: $isPkiEncrypted, isFavorite: $isFavorite, isIgnored: $isIgnored, isEnvironment: $isEnvironment, distanceFilter: $distanceFilter, maximumDistance: $maxDistance, hopsAway: $hopsAway, roleFilter: $roleFilter, deviceRoles: $deviceRoles)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,115 +20,194 @@ struct UserMessageList: View {
|
|||
// View State Items
|
||||
@ObservedObject var user: UserEntity
|
||||
@State private var replyMessageId: Int64 = 0
|
||||
// Scroll state
|
||||
@State private var showScrollToBottomButton = false
|
||||
@State private var hasReachedBottom = false
|
||||
@State private var gotFirstUnreadMessage: Bool = false
|
||||
|
||||
@State private var messageToHighlight: Int64 = 0
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ScrollViewReader { scrollView in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
|
||||
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
ForEach( user.messageList ) { (message: MessageEntity) in
|
||||
if user.num != bleManager.connectedPeripheral?.num ?? -1 {
|
||||
let currentUser: Bool = (Int64(UserDefaults.preferredPeripheralNum) == message.fromUser?.num ?? -1 ? true : false)
|
||||
|
||||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
HStack(alignment: .top) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
if message.replyID > 0 {
|
||||
let messageReply = user.messageList.first(where: { $0.messageId == message.replyID })
|
||||
HStack {
|
||||
MessageText(
|
||||
message: message,
|
||||
tapBackDestination: .user(user),
|
||||
isCurrentUser: currentUser
|
||||
) {
|
||||
self.replyMessageId = message.messageId
|
||||
self.messageFieldFocused = true
|
||||
}
|
||||
Button {
|
||||
if let messageNum = messageReply?.messageId {
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = messageNum
|
||||
}
|
||||
scrollView.scrollTo(messageNum, anchor: .center)
|
||||
|
||||
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
|
||||
RetryButton(message: message, destination: .user(user))
|
||||
}
|
||||
}
|
||||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
}
|
||||
|
||||
HStack {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
if message.realACK {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
|
||||
} else {
|
||||
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
|
||||
// Reset highlight after delay
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
|
||||
withAnimation(.easeInOut(duration: 0.5)) {
|
||||
messageToHighlight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
} label: {
|
||||
Text(messageReply?.messagePayload ?? "EMPTY MESSAGE").foregroundColor(.accentColor).font(.caption2)
|
||||
.padding(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 18)
|
||||
.stroke(Color.blue, lineWidth: 0.5)
|
||||
)
|
||||
Image(systemName: "arrowshape.turn.up.left.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.imageScale(.large).foregroundColor(.accentColor)
|
||||
.padding(.trailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(user.messageList.firstIndex(of: message))
|
||||
HStack(alignment: .top) {
|
||||
if currentUser { Spacer(minLength: 50) }
|
||||
VStack(alignment: currentUser ? .trailing : .leading) {
|
||||
HStack {
|
||||
MessageText(
|
||||
message: message,
|
||||
tapBackDestination: .user(user),
|
||||
isCurrentUser: currentUser
|
||||
) {
|
||||
self.replyMessageId = message.messageId
|
||||
self.messageFieldFocused = true
|
||||
}
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
if currentUser && message.canRetry || (message.receivedACK && !message.realACK) {
|
||||
RetryButton(message: message, destination: .user(user))
|
||||
}
|
||||
}
|
||||
|
||||
TapbackResponses(message: message) {
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
}
|
||||
|
||||
HStack {
|
||||
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
|
||||
if currentUser && message.receivedACK {
|
||||
// Ack Received
|
||||
if message.realACK {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.secondary)
|
||||
} else {
|
||||
Text("Acknowledged by another node").font(.caption2).foregroundColor(.orange)
|
||||
}
|
||||
} else if currentUser && message.ackError == 0 {
|
||||
// Empty Error
|
||||
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.yellow)
|
||||
} else if currentUser && message.ackError > 0 {
|
||||
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
|
||||
.foregroundStyle(ackErrorVal?.color ?? Color.red)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.bottom)
|
||||
.id(user.messageList.firstIndex(of: message))
|
||||
|
||||
if !currentUser {
|
||||
Spacer(minLength: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.onAppear {
|
||||
if !message.read {
|
||||
message.read = true
|
||||
do {
|
||||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(.blue, lineWidth: 2)
|
||||
.opacity(((messageToHighlight == message.messageId) || (replyMessageId == message.messageId)) ? 1 : 0)
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.frame(maxWidth: .infinity)
|
||||
.id(message.messageId)
|
||||
.onAppear {
|
||||
if gotFirstUnreadMessage {
|
||||
if !message.read {
|
||||
message.read = true
|
||||
do {
|
||||
for unreadMessage in user.messageList.filter({ !$0.read }) {
|
||||
unreadMessage.read = true
|
||||
}
|
||||
try context.save()
|
||||
Logger.data.info("📖 [App] Read message \(message.messageId, privacy: .public) ")
|
||||
appState.unreadDirectMessages = user.unreadMessages
|
||||
} catch {
|
||||
Logger.data.error("Failed to read message \(message.messageId, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
// Check if we've reached the bottom message
|
||||
if message.messageId == user.messageList.last?.messageId {
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Invisible spacer to detect reaching bottom
|
||||
Color.clear
|
||||
.frame(height: 1)
|
||||
.id("bottomAnchor")
|
||||
.onAppear {
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.onFirstAppear {
|
||||
// Find first unread message
|
||||
if let firstUnreadMessageId = user.messageList.first(where: { !$0.read })?.messageId {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(firstUnreadMessageId, anchor: .top)
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
} else {
|
||||
// If no unread messages, scroll to bottom
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
}
|
||||
}
|
||||
gotFirstUnreadMessage = true
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: user.messageList) {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
.onChange(of: user.messageList) {
|
||||
if hasReachedBottom {
|
||||
withAnimation {
|
||||
scrollView.scrollTo(user.messageList.last?.messageId ?? 0, anchor: .bottom)
|
||||
}
|
||||
} else {
|
||||
showScrollToBottomButton = true
|
||||
}
|
||||
}
|
||||
// Scroll to bottom button
|
||||
if showScrollToBottomButton {
|
||||
Button {
|
||||
withAnimation {
|
||||
scrollView.scrollTo("bottomAnchor", anchor: .bottom)
|
||||
hasReachedBottom = true
|
||||
showScrollToBottomButton = false
|
||||
}
|
||||
} label: {
|
||||
ScrollToBottomButtonView()
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.trailing, 16)
|
||||
.transition(.opacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ struct DetectionSensorLog: View {
|
|||
exportString = detectionsToCsv(detections: chartData)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -118,7 +118,7 @@ struct DetectionSensorLog: View {
|
|||
.padding(.bottom)
|
||||
.padding(.trailing)
|
||||
}
|
||||
.navigationTitle("detection.sensor.log")
|
||||
.navigationTitle("Detection Sensor Log")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
|
|
@ -128,7 +128,7 @@ struct DetectionSensorLog: View {
|
|||
isPresented: $isExporting,
|
||||
document: CsvDocument(emptyCsv: exportString),
|
||||
contentType: .commaSeparatedText,
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("detection.sensor.log".localized)"),
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("Detection Sensor Log".localized)"),
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ struct DeviceMetricsLog: View {
|
|||
Table(deviceMetrics, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("Battery Level") { dm in
|
||||
HStack {
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
Spacer()
|
||||
|
|
@ -165,7 +165,7 @@ struct DeviceMetricsLog: View {
|
|||
// dm.voltage.map { Text("\(String(format: "%.2f", $0))") } ?? Text("--")
|
||||
Text("\(dm.voltage?.formatted(.number.precision(.fractionLength(2))) ?? Constants.nilValueIndicator)")
|
||||
}
|
||||
TableColumn("channel.utilization") { dm in
|
||||
TableColumn("Channel Utilization") { dm in
|
||||
dm.channelUtilization.map { channelUtilization in
|
||||
// Text("\(String(format: "%.2f", channelUtilization))%")
|
||||
Text("\(channelUtilization.formatted(.number.precision(.fractionLength(2))))%")
|
||||
|
|
@ -188,7 +188,7 @@ struct DeviceMetricsLog: View {
|
|||
}
|
||||
.width(min: 100)
|
||||
TableColumn("Timestamp") { dm in
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
}
|
||||
.width(min: 180)
|
||||
}
|
||||
|
|
@ -209,7 +209,7 @@ struct DeviceMetricsLog: View {
|
|||
isPresented: $isPresentingClearLogConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("device.metrics.delete", role: .destructive) {
|
||||
Button("Delete all device metrics?", role: .destructive) {
|
||||
if clearTelemetry(destNum: node.num, metricsType: 0, context: context) {
|
||||
Logger.data.notice("Cleared Device Metrics for \(node.num, privacy: .public)")
|
||||
} else {
|
||||
|
|
@ -222,7 +222,7 @@ struct DeviceMetricsLog: View {
|
|||
exportString = telemetryToCsvFile(telemetry: deviceMetrics, metricsType: 0)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -240,7 +240,7 @@ struct DeviceMetricsLog: View {
|
|||
ContentUnavailableView("No Device Metrics", systemImage: "slash.circle")
|
||||
}
|
||||
}
|
||||
.navigationTitle("device.metrics.log")
|
||||
.navigationTitle("Device Metrics Log")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
|
|
@ -250,7 +250,7 @@ struct DeviceMetricsLog: View {
|
|||
isPresented: $isExporting,
|
||||
document: CsvDocument(emptyCsv: exportString),
|
||||
contentType: .commaSeparatedText,
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("device.metrics.log".localized)"),
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("Device Metrics Log".localized)"),
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ struct EnvironmentMetricsLog: View {
|
|||
exportString = telemetryToCsvFile(telemetry: environmentMetrics, metricsType: 1)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
.imageScale(imageScale)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ struct DeleteNodeButton: View {
|
|||
connectedNodeNum: connectedNode.num
|
||||
)
|
||||
if !success {
|
||||
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "unknown".localized, privacy: .public)")
|
||||
Logger.data.error("Failed to delete node \(deleteNode.user?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,45 +14,43 @@ struct NavigateToButton: View {
|
|||
var node: NodeInfoEntity
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
guard let userNum = node.user?.num else {
|
||||
Logger.services.error("NavigateToAction: Selected node does not exist")
|
||||
Button {
|
||||
guard let userNum = node.user?.num else {
|
||||
Logger.services.error("NavigateToAction: Selected node does not exist")
|
||||
return
|
||||
}
|
||||
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
|
||||
|
||||
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
|
||||
|
||||
do {
|
||||
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
|
||||
guard let nodeInfo = fetchedNodes.first else {
|
||||
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.services.info("Fetching NodeInfoEntity for userNum: \(userNum, privacy: .public)")
|
||||
|
||||
let fetchRequest: NSFetchRequest<NodeInfoEntity> = NSFetchRequest(entityName: "NodeInfoEntity")
|
||||
fetchRequest.predicate = NSPredicate(format: "num == %lld", Int64(userNum))
|
||||
|
||||
do {
|
||||
let fetchedNodes = try PersistenceController.shared.container.viewContext.fetch(fetchRequest)
|
||||
|
||||
guard let nodeInfo = fetchedNodes.first else {
|
||||
Logger.services.error("NavigateToAction: Node with userNum \(userNum, privacy: .public) not found in Core Data")
|
||||
return
|
||||
}
|
||||
|
||||
if let latitude = nodeInfo.latestPosition?.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.longitude {
|
||||
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
} else {
|
||||
Logger.services.error("Failed to create URL for navigation")
|
||||
}
|
||||
if let latitude = nodeInfo.latestPosition?.latitude,
|
||||
let longitude = nodeInfo.latestPosition?.longitude {
|
||||
if let url = URL(string: "maps://?saddr=&daddr=\(latitude),\(longitude)") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
} else {
|
||||
Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
|
||||
Logger.services.error("Failed to create URL for navigation")
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label {
|
||||
Text("Navigate to node")
|
||||
} icon: {
|
||||
Image(systemName: "map")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
} else {
|
||||
Logger.services.warning("NavigateToAction: Node \(userNum, privacy: .public) has invalid or missing coordinates")
|
||||
}
|
||||
} catch {
|
||||
Logger.services.error("NavigateToAction: Failed to fetch node with userNum \(userNum, privacy: .public): \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label {
|
||||
Text("Navigate to node")
|
||||
} icon: {
|
||||
Image(systemName: "map")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ struct MeshMapContent: MapContent {
|
|||
@Binding var selectedMapLayer: MapLayer
|
||||
// Map Configuration
|
||||
@Binding var selectedPosition: PositionEntity?
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = true
|
||||
@Binding var selectedWaypoint: WaypointEntity?
|
||||
|
||||
@FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn)
|
||||
|
|
@ -74,9 +74,9 @@ struct MeshMapContent: MapContent {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { _ in
|
||||
.highPriorityGesture(TapGesture().onEnded { _ in
|
||||
selectedPosition = (selectedPosition == position ? nil : position)
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Node History and Route Lines for favorites
|
||||
if let nodePosition = position.nodePosition,
|
||||
|
|
@ -186,7 +186,7 @@ struct MeshMapContent: MapContent {
|
|||
LazyVStack {
|
||||
ZStack {
|
||||
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 40)
|
||||
.onTapGesture(perform: { _ in
|
||||
.highPriorityGesture(TapGesture().onEnded { _ in
|
||||
selectedWaypoint = (selectedWaypoint == waypoint ? nil : waypoint)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ struct NodeMapContent: MapContent {
|
|||
/// Map State User Defaults
|
||||
@AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false
|
||||
@AppStorage("meshMapShowRouteLines") private var showRouteLines = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = false
|
||||
@AppStorage("enableMapWaypoints") private var showWaypoints = true
|
||||
@AppStorage("enableMapConvexHull") private var showConvexHull = false
|
||||
@AppStorage("enableMapTraffic") private var showTraffic: Bool = false
|
||||
@AppStorage("enableMapPointsOfInterest") private var showPointsOfInterest: Bool = false
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ Spacer()
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ struct NodeMapSwiftUI: View {
|
|||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
}
|
||||
}}
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarTitle(String((node.user?.shortName ?? "Unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ struct PositionPopover: View {
|
|||
var body: some View {
|
||||
// Node Color from node.num
|
||||
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
ZStack {
|
||||
|
||||
if position.nodePosition?.isOnline ?? false {
|
||||
Circle()
|
||||
.fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5)))
|
||||
|
|
@ -34,16 +34,15 @@ struct PositionPopover: View {
|
|||
.scaleEffect(scale)
|
||||
.animation(
|
||||
Animation.easeInOut(duration: 0.6)
|
||||
.repeatForever().delay(delay), value: scale
|
||||
.repeatForever().delay(delay), value: scale
|
||||
)
|
||||
.onAppear {
|
||||
self.scale = 1
|
||||
}
|
||||
.frame(width: 90, height: 90)
|
||||
}
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65)
|
||||
CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 65, node: getNodeInfo(id: Int64(position.nodePosition?.user?.num ?? 0), context: context))
|
||||
}
|
||||
|
||||
Text(position.nodePosition?.user?.longName ?? "Unknown")
|
||||
.font(.largeTitle)
|
||||
}
|
||||
|
|
@ -53,7 +52,7 @@ struct PositionPopover: View {
|
|||
/// Time
|
||||
Label {
|
||||
if idiom != .phone {
|
||||
Text("heard".localized + ":")
|
||||
Text("Heard".localized + ":")
|
||||
}
|
||||
Text(position.time?.lastHeard ?? "unknown")
|
||||
.foregroundColor(.primary)
|
||||
|
|
@ -106,7 +105,6 @@ struct PositionPopover: View {
|
|||
.foregroundColor(.primary)
|
||||
.font(idiom == .phone ? .callout : .body)
|
||||
}
|
||||
|
||||
} icon: {
|
||||
Image(systemName: "mountain.2.fill")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -147,9 +145,9 @@ struct PositionPopover: View {
|
|||
Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))")
|
||||
} icon: {
|
||||
Image(systemName: "location.north")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
/// Distance
|
||||
|
|
@ -181,15 +179,14 @@ struct PositionPopover: View {
|
|||
}
|
||||
.padding(.bottom, 5)
|
||||
if position.nodePosition?.viaMqtt ?? false {
|
||||
|
||||
Label {
|
||||
Text("MQTT")
|
||||
.font(idiom == .phone ? .callout : .body)
|
||||
} icon: {
|
||||
Image(systemName: "network")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
}
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
|
|
@ -235,7 +232,7 @@ struct PositionPopover: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -244,6 +241,7 @@ struct PositionPopover: View {
|
|||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.65), .large])
|
||||
.presentationContentInteraction(.scrolls)
|
||||
.presentationDragIndicator(.visible)
|
||||
|
|
|
|||
|
|
@ -134,40 +134,44 @@ struct WaypointForm: View {
|
|||
.scrollDismissesKeyboard(.immediately)
|
||||
HStack {
|
||||
Button {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
|
||||
if bleManager.isConnected {
|
||||
/// Send a new or exiting waypoint
|
||||
var newWaypoint = Waypoint()
|
||||
if waypoint.id == 0 {
|
||||
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
waypoint.id = Int64(newWaypoint.id)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
newWaypoint.id = UInt32(waypoint.id)
|
||||
}
|
||||
newWaypoint.latitudeI = waypoint.latitudeI
|
||||
newWaypoint.longitudeI = waypoint.longitudeI
|
||||
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
|
||||
newWaypoint.description_p = description
|
||||
// Unicode scalar value for the icon emoji string
|
||||
let unicodeScalers = icon.unicodeScalars
|
||||
// First element as an UInt32
|
||||
let unicode = unicodeScalers[unicodeScalers.startIndex].value
|
||||
newWaypoint.icon = unicode
|
||||
if locked {
|
||||
if lockedTo == 0 {
|
||||
newWaypoint.lockedTo = UInt32(bleManager.connectedPeripheral!.num)
|
||||
} else {
|
||||
newWaypoint.lockedTo = UInt32(lockedTo)
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
}
|
||||
}
|
||||
if expires {
|
||||
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
|
||||
} else {
|
||||
newWaypoint.expire = 0
|
||||
}
|
||||
if bleManager.sendWaypoint(waypoint: newWaypoint) {
|
||||
dismiss()
|
||||
} else {
|
||||
dismiss()
|
||||
Logger.mesh.warning("Send waypoint failed")
|
||||
Logger.mesh.warning("Send waypoint failed, node not connected")
|
||||
}
|
||||
} label: {
|
||||
Label("Send", systemImage: "arrow.up")
|
||||
|
|
@ -235,7 +239,7 @@ struct WaypointForm: View {
|
|||
})
|
||||
}
|
||||
label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -338,7 +342,7 @@ struct WaypointForm: View {
|
|||
if LocationsHandler.currentLocation.distance(from: LocationsHandler.DefaultLocation) > 0.0 {
|
||||
let metersAway = waypoint.coordinate.distance(from: LocationsHandler.currentLocation)
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
|
|
@ -354,7 +358,7 @@ struct WaypointForm: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -364,6 +368,18 @@ struct WaypointForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if waypoint.id == 0 {
|
||||
// New, unsent waypoint created by the user: delete it
|
||||
bleManager.context.delete(waypoint)
|
||||
do {
|
||||
try bleManager.context.save()
|
||||
} catch {
|
||||
bleManager.context.rollback()
|
||||
Logger.mesh.error("Failed to save context on waypoint deletion: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if waypoint.id > 0 {
|
||||
let waypoint = getWaypoint(id: Int64(waypoint.id), context: bleManager.context)
|
||||
|
|
|
|||
|
|
@ -220,7 +220,6 @@ extension MetricsColumnList {
|
|||
)
|
||||
} ?? Text(Constants.nilValueIndicator)
|
||||
}),
|
||||
|
||||
// Rainfall 24-hour
|
||||
MetricsTableColumn(
|
||||
id: "rainfall24H",
|
||||
|
|
@ -334,7 +333,7 @@ extension MetricsColumnList {
|
|||
.replacingOccurrences(of: ",", with: "")
|
||||
Text(
|
||||
time?.formattedDate(format: dateFormatString)
|
||||
?? "unknown.age".localized
|
||||
?? "Unknown Age".localized
|
||||
)
|
||||
})
|
||||
])
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ extension MetricsSeriesList {
|
|||
.alignsMarkStylesWithPlotArea()
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
// Barometric Pressure Series Configuration
|
||||
MetricsChartSeries(
|
||||
id: "barometricPressure",
|
||||
|
|
@ -106,7 +106,7 @@ extension MetricsSeriesList {
|
|||
.alignsMarkStylesWithPlotArea()
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
// Indoor Air Quality Series Configuration
|
||||
MetricsChartSeries(
|
||||
id: "iaq",
|
||||
|
|
@ -134,7 +134,7 @@ extension MetricsSeriesList {
|
|||
.alignsMarkStylesWithPlotArea()
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
// Lux
|
||||
MetricsChartSeries(
|
||||
id: "lux",
|
||||
|
|
@ -460,7 +460,7 @@ extension MetricsSeriesList {
|
|||
.lineStyle(StrokeStyle(lineWidth: 4))
|
||||
.alignsMarkStylesWithPlotArea()
|
||||
}
|
||||
}),
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ struct MetricsColumnDetail: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -120,14 +120,14 @@ struct NodeDetail: View {
|
|||
if let metadata = node.metadata {
|
||||
HStack {
|
||||
Label {
|
||||
Text("firmware.version")
|
||||
Text("Firmware Version")
|
||||
} icon: {
|
||||
Image(systemName: "memorychip")
|
||||
.symbolRenderingMode(.multicolor)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
Text(metadata.firmwareVersion ?? "unknown".localized)
|
||||
Text(metadata.firmwareVersion ?? "Unknown".localized)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ struct NodeDetail: View {
|
|||
if let dm = node.telemetries?.filtered(using: NSPredicate(format: "metricsType == 0")).lastObject as? TelemetryEntity, let uptimeSeconds = dm.uptimeSeconds {
|
||||
HStack {
|
||||
Label {
|
||||
Text("\("uptime".localized)")
|
||||
Text("\("Uptime".localized)")
|
||||
} icon: {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
|
|
@ -195,7 +195,7 @@ struct NodeDetail: View {
|
|||
Spacer()
|
||||
|
||||
if dateFormatRelative, let text = Self.relativeFormatter.string(for: lastHeard) {
|
||||
if lastHeard.formatted() != "unknown.age".localized {
|
||||
if lastHeard.formatted() != "Unknown Age".localized {
|
||||
Text(text)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
|
|
@ -214,7 +214,7 @@ struct NodeDetail: View {
|
|||
// to use with WeatherKit, or has actual data in the most recent EnvironmentMetrics entity
|
||||
// that will be rendered in this section.
|
||||
if node.hasPositions && UserDefaults.environmentEnableWeatherKit
|
||||
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "distance", "soilTemperature", "soilMoisture"]) {
|
||||
|| node.hasDataForLatestEnvironmentMetrics(attributes: ["iaq", "temperature", "relativeHumidity", "barometricPressure", "windSpeed", "radiation", "weight", "Distance", "soilTemperature", "soilMoisture"]) {
|
||||
Section("Environment") {
|
||||
if !node.hasEnvironmentMetrics {
|
||||
LocalWeatherConditions(location: node.latestPosition?.nodeLocation)
|
||||
|
|
@ -499,14 +499,14 @@ struct NodeDetail: View {
|
|||
showingRebootConfirm = true
|
||||
} label: {
|
||||
Label(
|
||||
"reboot",
|
||||
"Reboot",
|
||||
systemImage: "arrow.triangle.2.circlepath"
|
||||
)
|
||||
}.confirmationDialog(
|
||||
"Are you sure?",
|
||||
isPresented: $showingRebootConfirm
|
||||
) {
|
||||
Button("reboot.node", role: .destructive) {
|
||||
Button("Reboot node?", role: .destructive) {
|
||||
if !bleManager.sendReboot(
|
||||
fromUser: connectedNode.user!,
|
||||
toUser: node.user!,
|
||||
|
|
@ -520,6 +520,7 @@ struct NodeDetail: View {
|
|||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "Unknown".localized), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ struct NodeInfoItem: View {
|
|||
}
|
||||
Spacer()
|
||||
if user.hwModel != "UNSET" {
|
||||
Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "unset".localized)))
|
||||
Text(String(node.user?.hwDisplayName ?? (node.user?.hwModel ?? "Unset".localized)))
|
||||
} else {
|
||||
Text(String("incomplete".localized))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,7 +192,7 @@ struct NodeListFilter: View {
|
|||
Button {
|
||||
dismiss()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ struct NodeListItem: View {
|
|||
let (image, color) = userKeyStatus
|
||||
IconAndText(systemName: image,
|
||||
imageColor: color,
|
||||
text: node.user?.longName?.addingVariationSelectors ?? "unknown".localized,
|
||||
text: node.user?.longName?.addingVariationSelectors ?? "Unknown".localized,
|
||||
textColor: .primary)
|
||||
if node.favorite {
|
||||
Spacer()
|
||||
|
|
@ -75,16 +75,16 @@ struct NodeListItem: View {
|
|||
if connected {
|
||||
IconAndText(systemName: "antenna.radiowaves.left.and.right.circle.fill",
|
||||
imageColor: .green,
|
||||
text: "connected".localized)
|
||||
text: "Connected".localized)
|
||||
}
|
||||
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 && node.lastHeard! < Calendar.current.date(byAdding: .year, value: 1, to: Date())!{
|
||||
if node.lastHeard?.timeIntervalSince1970 ?? 0 > 0 && node.lastHeard! < Calendar.current.date(byAdding: .year, value: 1, to: Date())! {
|
||||
IconAndText(systemName: node.isOnline ? "checkmark.circle.fill" : "moon.circle.fill",
|
||||
imageColor: node.isOnline ? .green : .orange,
|
||||
text: node.lastHeard?.formatted() ?? "unknown.age".localized)
|
||||
text: node.lastHeard?.formatted() ?? "Unknown Age".localized)
|
||||
}
|
||||
let role = DeviceRoles(rawValue: Int(node.user?.role ?? 0))
|
||||
IconAndText(systemName: role?.systemName ?? "figure",
|
||||
text: "Role: \(role?.name ?? "unknown".localized)")
|
||||
text: "Role: \(role?.name ?? "Unknown".localized)")
|
||||
if node.isStoreForwardRouter {
|
||||
IconAndText(systemName: "envelope.arrow.triangle.branch",
|
||||
renderingMode: .multicolor,
|
||||
|
|
|
|||
30
Meshtastic/Views/Nodes/Helpers/ScrollToBottomButton.swift
Normal file
30
Meshtastic/Views/Nodes/Helpers/ScrollToBottomButton.swift
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// ScrollToBottomButtonView.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Benjamin Faershtein on 4/2/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ScrollToBottomButtonView: View {
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
Text("Jump to present")
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.cornerRadius(12)
|
||||
Image(systemName: "arrow.down")
|
||||
.font(.title2)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.shadow(radius: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ScrollToBottomButtonView()
|
||||
}
|
||||
|
|
@ -65,10 +65,7 @@ struct NodeList: View {
|
|||
var nodes: FetchedResults<NodeInfoEntity>
|
||||
|
||||
var connectedNode: NodeInfoEntity? {
|
||||
getNodeInfo(
|
||||
id: bleManager.connectedPeripheral?.num ?? 0,
|
||||
context: context
|
||||
)
|
||||
getNodeInfo(id: bleManager.connectedPeripheral?.num ?? 0, context: context)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
|
|
@ -78,19 +75,11 @@ struct NodeList: View {
|
|||
) -> some View {
|
||||
/// Allow users to mute notifications for a node even if they are not connected
|
||||
if let user = node.user {
|
||||
NodeAlertsButton(
|
||||
context: context,
|
||||
node: node,
|
||||
user: user
|
||||
)
|
||||
NodeAlertsButton(context: context, node: node, user: user)
|
||||
}
|
||||
if let connectedNode {
|
||||
/// Favoriting a node requires being connected
|
||||
FavoriteNodeButton(
|
||||
bleManager: bleManager,
|
||||
context: context,
|
||||
node: node
|
||||
)
|
||||
FavoriteNodeButton(bleManager: bleManager, context: context, node: node)
|
||||
/// Don't show message, trace route, position exchange or delete context menu items for the connected node
|
||||
if connectedNode.num != node.num {
|
||||
if !node.viaMqtt || node.viaMqtt && node.hopsAway == 0 {
|
||||
|
|
@ -203,7 +192,7 @@ struct NodeList: View {
|
|||
.searchable(text: $searchText, placement: .automatic, prompt: "Find a node")
|
||||
.disableAutocorrection(true)
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
.navigationTitle(String.localizedStringWithFormat("Nodes (%@)".localized, String(nodes.count)))
|
||||
.listStyle(.plain)
|
||||
.alert(
|
||||
"Position Exchange Requested",
|
||||
|
|
@ -237,7 +226,7 @@ struct NodeList: View {
|
|||
if deleteNode != nil {
|
||||
let success = bleManager.removeNode(node: deleteNode!, connectedNodeNum: Int64(bleManager.connectedPeripheral?.num ?? -1))
|
||||
if !success {
|
||||
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "unknown".localized, privacy: .public)")
|
||||
Logger.data.error("Failed to delete node \(deleteNode?.user?.longName ?? "Unknown".localized, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +253,6 @@ struct NodeList: View {
|
|||
columnVisibility: columnVisibility
|
||||
)
|
||||
.edgesIgnoringSafeArea([.leading, .trailing])
|
||||
.navigationBarTitle(String(node.user?.longName?.addingVariationSelectors ?? "unknown".localized), displayMode: .inline)
|
||||
.navigationBarItems(
|
||||
trailing: ZStack {
|
||||
if UIDevice.current.userInterfaceIdiom != .phone {
|
||||
|
|
@ -284,7 +272,7 @@ struct NodeList: View {
|
|||
)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView("select.node", systemImage: "flipphone")
|
||||
ContentUnavailableView("Select Node", systemImage: "flipphone")
|
||||
}
|
||||
} detail: {
|
||||
ContentUnavailableView("", systemImage: "line.3.horizontal")
|
||||
|
|
|
|||
|
|
@ -1,233 +0,0 @@
|
|||
////
|
||||
//// NodeMap.swift
|
||||
//// MeshtasticApple
|
||||
////
|
||||
//// Created by Garth Vander Houwen on 8/7/21.
|
||||
////
|
||||
//
|
||||
//import SwiftUI
|
||||
//import MapKit
|
||||
//import CoreLocation
|
||||
//import CoreData
|
||||
//
|
||||
//struct NodeMap: View {
|
||||
// @Environment(\.managedObjectContext) var context
|
||||
// @EnvironmentObject var bleManager: BLEManager
|
||||
//
|
||||
// @ObservedObject
|
||||
// var router: Router
|
||||
// @State var selectedMapLayer: MapLayer = UserDefaults.mapLayer
|
||||
// @State var enableMapRecentering: Bool = UserDefaults.enableMapRecentering
|
||||
// @State var enableMapRouteLines: Bool = UserDefaults.enableMapRouteLines
|
||||
// @State var enableMapNodeHistoryPins: Bool = UserDefaults.enableMapNodeHistoryPins
|
||||
// @State var enableOfflineMaps: Bool = UserDefaults.enableOfflineMaps
|
||||
// @State var selectedTileServer: MapTileServer = UserDefaults.mapTileServer
|
||||
// @State var enableOverlayServer: Bool = UserDefaults.enableOverlayServer
|
||||
// @State var selectedOverlayServer: MapOverlayServer = UserDefaults.mapOverlayServer
|
||||
// @State var mapTilesAboveLabels: Bool = UserDefaults.mapTilesAboveLabels
|
||||
// let fromDate: NSDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! as NSDate
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "time", ascending: true)],
|
||||
// predicate: NSPredicate(format: "nodePosition != nil", Calendar.current.date(byAdding: .day, value: -7, to: Date())! as NSDate), animation: .none)
|
||||
// private var positions: FetchedResults<PositionEntity>
|
||||
// @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)],
|
||||
// predicate: NSPredicate(
|
||||
// format: "expire == nil || expire >= %@", Date() as NSDate
|
||||
// ), animation: .none)
|
||||
// private var waypoints: FetchedResults<WaypointEntity>
|
||||
// @State var waypointCoordinate: WaypointCoordinate?
|
||||
// @State var selectedTracking: UserTrackingModes = .none
|
||||
// @State var isPresentingInfoSheet: Bool = false
|
||||
// @State private var customMapOverlay: MapViewSwiftUI.CustomMapOverlay? = MapViewSwiftUI.CustomMapOverlay(
|
||||
// mapName: "offlinemap",
|
||||
// tileType: "png",
|
||||
// canReplaceMapContent: true
|
||||
// )
|
||||
// var body: some View {
|
||||
// NavigationStack {
|
||||
// ZStack {
|
||||
// MapViewSwiftUI(
|
||||
// onLongPress: { coord in
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: coord, waypointId: 0)
|
||||
// }, onWaypointEdit: { wpId in
|
||||
// if wpId > 0 {
|
||||
// waypointCoordinate = WaypointCoordinate(id: .init(), coordinate: nil, waypointId: Int64(wpId))
|
||||
// }
|
||||
// },
|
||||
// selectedMapLayer: selectedMapLayer,
|
||||
// positions: Array(positions),
|
||||
// waypoints: Array(waypoints),
|
||||
// userTrackingMode: selectedTracking.MKUserTrackingModeValue(),
|
||||
// showNodeHistory: enableMapNodeHistoryPins,
|
||||
// showRouteLines: enableMapRouteLines,
|
||||
// customMapOverlay: self.customMapOverlay
|
||||
// )
|
||||
// VStack(alignment: .trailing) {
|
||||
// HStack(alignment: .top) {
|
||||
// Spacer()
|
||||
// MapButtons(tracking: $selectedTracking, isPresentingInfoSheet: $isPresentingInfoSheet)
|
||||
// .padding(.trailing, 8)
|
||||
// .padding(.top, 16)
|
||||
// }
|
||||
// Spacer()
|
||||
// TileDownloadStatus()
|
||||
// .padding(.trailing, 16)
|
||||
// .padding(.bottom, 20)
|
||||
// }
|
||||
// }
|
||||
// .ignoresSafeArea(.all, edges: [.top, .leading, .trailing])
|
||||
// .frame(maxHeight: .infinity)
|
||||
// .sheet(item: $waypointCoordinate, content: { wpc in
|
||||
// WaypointFormMapKit(coordinate: wpc)
|
||||
// .presentationDetents([.medium, .large])
|
||||
// .presentationDragIndicator(.automatic)
|
||||
// })
|
||||
// .sheet(isPresented: $isPresentingInfoSheet) {
|
||||
// VStack {
|
||||
// Form {
|
||||
// Section(header: Text("Map Options")) {
|
||||
// Picker(selection: $selectedMapLayer, label: Text("")) {
|
||||
// ForEach(MapLayer.allCases, id: \.self) { layer in
|
||||
// if layer == MapLayer.offline && enableOfflineMaps {
|
||||
// Text(layer.localized)
|
||||
// } else if layer != MapLayer.offline {
|
||||
// Text(layer.localized)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .pickerStyle(SegmentedPickerStyle())
|
||||
// .onChange(of: selectedMapLayer) { _, newMapLayer in
|
||||
// UserDefaults.mapLayer = newMapLayer
|
||||
// }
|
||||
// .padding(.top, 5)
|
||||
// .padding(.bottom, 5)
|
||||
// Toggle(isOn: $enableMapRecentering) {
|
||||
// Label("map.recentering", systemImage: "camera.metering.center.weighted")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onTapGesture {
|
||||
// self.enableMapRecentering.toggle()
|
||||
// UserDefaults.enableMapRecentering = self.enableMapRecentering
|
||||
// }
|
||||
// Toggle(isOn: $enableMapNodeHistoryPins) {
|
||||
// Label("Show Node History", systemImage: "building.columns.fill")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onTapGesture {
|
||||
// self.enableMapNodeHistoryPins.toggle()
|
||||
// UserDefaults.enableMapNodeHistoryPins = self.enableMapNodeHistoryPins
|
||||
// }
|
||||
// Toggle(isOn: $enableMapRouteLines) {
|
||||
// Label("Show Route Lines", systemImage: "road.lanes")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onTapGesture {
|
||||
// self.enableMapRouteLines.toggle()
|
||||
// UserDefaults.enableMapRouteLines = self.enableMapRouteLines
|
||||
// }
|
||||
// let locale = Locale.current
|
||||
// if locale.region?.identifier ?? "no locale" == "US" {
|
||||
// Toggle(isOn: $enableOverlayServer) {
|
||||
// Label("Show Weather", systemImage: "cloud.fill")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onTapGesture {
|
||||
// self.enableOverlayServer.toggle()
|
||||
// UserDefaults.enableOverlayServer = self.enableOverlayServer
|
||||
// }
|
||||
// if enableOverlayServer {
|
||||
// Picker(selection: $selectedOverlayServer,
|
||||
// label: Text("Radar")) {
|
||||
// ForEach(MapOverlayServer.allCases, id: \.self) { mos in
|
||||
// Text(mos.description)
|
||||
// .font(.footnote)
|
||||
// }
|
||||
// }
|
||||
// .pickerStyle(DefaultPickerStyle())
|
||||
// .onChange(of: (selectedOverlayServer)) { _, newSelectedOverlayServer in
|
||||
// UserDefaults.mapOverlayServer = newSelectedOverlayServer
|
||||
// }
|
||||
// Text(LocalizedStringKey(selectedOverlayServer.attribution))
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.gray)
|
||||
// .padding(0)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// Section(header: Text("Offline Maps")) {
|
||||
// Toggle(isOn: $enableOfflineMaps) {
|
||||
// Text("Enable Offline Maps")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onChange(of: enableOfflineMaps) { _, newEnableOfflineMaps in
|
||||
// UserDefaults.enableOfflineMaps = newEnableOfflineMaps
|
||||
// if !enableOfflineMaps {
|
||||
// if self.selectedMapLayer == .offline {
|
||||
// self.selectedMapLayer = .standard
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if enableOfflineMaps {
|
||||
// VStack(alignment: .leading) {
|
||||
// Picker(selection: $selectedTileServer,
|
||||
// label: Text("Tile Server")) {
|
||||
// ForEach(MapTileServer.allCases, id: \.self) { tsl in
|
||||
// Text(tsl.description)
|
||||
// }
|
||||
// }
|
||||
// .pickerStyle(DefaultPickerStyle())
|
||||
// .onChange(of: (selectedTileServer)) { _, newSelectedTileServer in
|
||||
// UserDefaults.mapTileServer = newSelectedTileServer
|
||||
// }
|
||||
// Text("Attribution:")
|
||||
// .fontWeight(.semibold)
|
||||
// .font(.footnote)
|
||||
// Text(LocalizedStringKey(selectedTileServer.attribution))
|
||||
// .font(.footnote)
|
||||
// .foregroundColor(.gray)
|
||||
// .padding(0)
|
||||
// Divider()
|
||||
// Toggle(isOn: $mapTilesAboveLabels) {
|
||||
// Text("Tiles above Labels")
|
||||
// }
|
||||
// .toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
// .onTapGesture {
|
||||
// self.mapTilesAboveLabels.toggle()
|
||||
// UserDefaults.mapTilesAboveLabels = self.mapTilesAboveLabels
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// #if targetEnvironment(macCatalyst)
|
||||
// Button {
|
||||
// isPresentingInfoSheet = false
|
||||
// } label: {
|
||||
// Label("close", systemImage: "xmark")
|
||||
// }
|
||||
// .buttonStyle(.bordered)
|
||||
// .buttonBorderShape(.capsule)
|
||||
// .controlSize(.large)
|
||||
// .padding(.bottom)
|
||||
// #endif
|
||||
// }
|
||||
// .presentationDetents([enableOfflineMaps || enableOverlayServer ? .large : .medium])
|
||||
// .presentationDragIndicator(.visible)
|
||||
// }
|
||||
// }
|
||||
// .navigationBarItems(leading:
|
||||
// MeshtasticLogo(), trailing:
|
||||
// ZStack {
|
||||
// ConnectedDevice(
|
||||
// bluetoothOn: bleManager.isSwitchedOn,
|
||||
// deviceConnected: bleManager.connectedPeripheral != nil,
|
||||
// name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName :
|
||||
// "?")
|
||||
// })
|
||||
// .onAppear(perform: {
|
||||
// UIApplication.shared.isIdleTimerDisabled = true
|
||||
// })
|
||||
// .onDisappear(perform: {
|
||||
// UIApplication.shared.isIdleTimerDisabled = false
|
||||
// })
|
||||
// }
|
||||
//}
|
||||
|
|
@ -44,7 +44,7 @@ struct PaxCounterLog: View {
|
|||
y: .value("y", (point.wifi + point.ble))
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("paxcounter.total")
|
||||
.accessibilityLabel("Total PAX")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi + point.ble)")
|
||||
.foregroundStyle(paxChartColor)
|
||||
.interpolationMethod(.cardinal)
|
||||
|
|
@ -55,7 +55,7 @@ struct PaxCounterLog: View {
|
|||
y: .value("y", point.wifi)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("paxcounter.wifi")
|
||||
.accessibilityLabel("WiFi")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.wifi)")
|
||||
.foregroundStyle(wifiChartColor)
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ struct PaxCounterLog: View {
|
|||
y: .value("y", point.ble)
|
||||
)
|
||||
}
|
||||
.accessibilityLabel("paxcounter.ble")
|
||||
.accessibilityLabel("BLE")
|
||||
.accessibilityValue("X: \(point.time!), Y: \(point.ble)")
|
||||
.foregroundStyle(bleChartColor)
|
||||
}
|
||||
|
|
@ -76,9 +76,9 @@ struct PaxCounterLog: View {
|
|||
.chartXAxis(.automatic)
|
||||
.chartYScale(domain: 0...maxValue)
|
||||
.chartForegroundStyleScale([
|
||||
"paxcounter.ble".localized: .blue,
|
||||
"paxcounter.wifi".localized: .orange,
|
||||
"paxcounter.total".localized: .green
|
||||
"BLE".localized: .blue,
|
||||
"WiFi".localized: .orange,
|
||||
"Total PAX".localized: .green
|
||||
])
|
||||
.chartLegend(position: .automatic, alignment: .bottom)
|
||||
}
|
||||
|
|
@ -89,23 +89,23 @@ struct PaxCounterLog: View {
|
|||
if UIScreen.main.bounds.size.width > 768 && (UIDevice.current.userInterfaceIdiom == .pad || UIDevice.current.userInterfaceIdiom == .mac) {
|
||||
// Add a table for mac and ipad
|
||||
Table(pax) {
|
||||
TableColumn("paxcounter.ble") { pc in
|
||||
TableColumn("BLE") { pc in
|
||||
Text("\(pc.ble)")
|
||||
}
|
||||
TableColumn("paxcounter.wifi") { pc in
|
||||
TableColumn("WiFi") { pc in
|
||||
Text("\(pc.wifi)")
|
||||
}
|
||||
TableColumn("paxcounter.total") { pc in
|
||||
TableColumn("Total PAX") { pc in
|
||||
Text("\(pc.wifi + pc.ble)")
|
||||
}
|
||||
TableColumn("uptime") { pc in
|
||||
TableColumn("Uptime") { pc in
|
||||
let now = Date.now
|
||||
let later = now + TimeInterval(pc.uptime)
|
||||
let components = (now..<later).formatted(.components(style: .condensedAbbreviated))
|
||||
Text(components)
|
||||
}
|
||||
TableColumn("Timestamp") { pc in
|
||||
Text(pc.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(pc.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
}
|
||||
.width(min: 180)
|
||||
}
|
||||
|
|
@ -120,10 +120,10 @@ struct PaxCounterLog: View {
|
|||
]
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: 1) {
|
||||
GridRow {
|
||||
Text("paxcounter.ble")
|
||||
Text("BLE")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
Text("paxcounter.wifi")
|
||||
Text("WiFi")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
Text("Total")
|
||||
|
|
@ -149,7 +149,7 @@ struct PaxCounterLog: View {
|
|||
let components = (now..<later).formatted(.components(style: .condensedAbbreviated))
|
||||
Text(components)
|
||||
.font(.caption)
|
||||
Text(pc.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(pc.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ struct PaxCounterLog: View {
|
|||
isPresented: $isPresentingClearLogConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("paxcounter.delete", role: .destructive) {
|
||||
Button("Delete all pax data?", role: .destructive) {
|
||||
if clearPax(destNum: node.num, context: context) {
|
||||
Logger.services.info("Cleared Pax Counter for \(node.num, privacy: .public)")
|
||||
} else {
|
||||
|
|
@ -187,7 +187,7 @@ struct PaxCounterLog: View {
|
|||
exportString = paxToCsvFile(pax: pax)
|
||||
isExporting = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -196,10 +196,10 @@ struct PaxCounterLog: View {
|
|||
.padding(.trailing)
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView("paxcounter.content.unavailable", systemImage: "slash.circle")
|
||||
ContentUnavailableView("No PAX Counter Logs", systemImage: "slash.circle")
|
||||
}
|
||||
}
|
||||
.navigationTitle("paxcounter.log")
|
||||
.navigationTitle("PAX Counter Log")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
|
|
|
|||
|
|
@ -60,13 +60,11 @@ struct PositionLog: View {
|
|||
Text("\(String(format: "%.2f", position.snr)) dB")
|
||||
}
|
||||
TableColumn("Time Stamp") { position in
|
||||
Text(position.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(position.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
}
|
||||
.width(min: 180)
|
||||
}
|
||||
.textSelection(.enabled)
|
||||
|
||||
|
||||
} else {
|
||||
ScrollView {
|
||||
// Use a grid on iOS as a table only shows a single column
|
||||
|
|
@ -107,7 +105,7 @@ struct PositionLog: View {
|
|||
.font(.caption2)
|
||||
Text(altitude.formatted())
|
||||
.font(.caption2)
|
||||
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(mappin.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
.font(.caption2)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ struct PowerMetricsLog: View {
|
|||
Table(powerMetrics, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("Timestamp") { m in
|
||||
HStack {
|
||||
Text(m.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(m.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
Spacer()
|
||||
HStack {
|
||||
VStack {
|
||||
|
|
@ -213,7 +213,7 @@ struct PowerMetricsLog: View {
|
|||
}
|
||||
.width(min: 75)
|
||||
TableColumn("Timestamp") { dm in
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "unknown.age".localized)
|
||||
Text(dm.time?.formattedDate(format: dateFormatString) ?? "Unknown Age".localized)
|
||||
}
|
||||
.width(min: 180)
|
||||
|
||||
|
|
@ -282,7 +282,7 @@ struct PowerMetricsLog: View {
|
|||
isPresented: $isExporting,
|
||||
document: CsvDocument(emptyCsv: exportString),
|
||||
contentType: .commaSeparatedText,
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("power.metrics.log".localized)"),
|
||||
defaultFilename: String("\(node.user?.longName ?? "Node") \("Power Metrics Log".localized)"),
|
||||
onCompletion: { result in
|
||||
switch result {
|
||||
case .success:
|
||||
|
|
|
|||
|
|
@ -37,14 +37,14 @@ struct TraceRouteLog: View {
|
|||
VStack {
|
||||
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
|
||||
Label {
|
||||
let routeTime = route.time?.formatted() ?? "unknown".localized
|
||||
let routeTime = route.time?.formatted() ?? "Unknown".localized
|
||||
if route.response && route.hopsTowards == route.hopsBack {
|
||||
let hopString = String(localized: "\(route.hopsTowards) Hops")
|
||||
Text("\(routeTime) - \(hopString)")
|
||||
.font(.caption)
|
||||
} else if route.response {
|
||||
let hopTowardsString = String(localized: "\(route.hopsTowards) Hops")
|
||||
let hopBackString = route.hopsBack >= 0 ? String(localized: "\(route.hopsBack) Hops") : String(localized: "unknown")
|
||||
let hopBackString = route.hopsBack >= 0 ? String(localized: "\(route.hopsBack) Hops") : String(localized: "Unknown")
|
||||
Text("\(routeTime) - \(hopTowardsString) Towards \(hopBackString) Back")
|
||||
.font(.caption)
|
||||
} else if route.sent {
|
||||
|
|
@ -67,7 +67,7 @@ struct TraceRouteLog: View {
|
|||
Logger.data.error("\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,14 +78,14 @@ struct TraceRouteLog: View {
|
|||
if selectedRoute != nil {
|
||||
if selectedRoute?.response ?? false && selectedRoute?.hopsTowards ?? 0 >= 0 {
|
||||
Label {
|
||||
Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)")
|
||||
Text("Route: \(selectedRoute?.routeText ?? "Unknown".localized)")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.font(.title3)
|
||||
Label {
|
||||
Text("Route Back: \(selectedRoute?.routeBackText ?? "unknown".localized)")
|
||||
Text("Route Back: \(selectedRoute?.routeBackText ?? "Unknown".localized)")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
|
|
@ -94,7 +94,7 @@ struct TraceRouteLog: View {
|
|||
} else if !(selectedRoute?.sent ?? true) {
|
||||
Label {
|
||||
VStack {
|
||||
Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "unknown".localized) was not sent.")
|
||||
Text("Trace route to \(selectedRoute?.node?.user?.longName ?? "Unknown".localized) was not sent.")
|
||||
.font(idiom == .phone ? .body : .largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
Text("Trace Route was rate limited. You can send a trace route a maximum of once every thirty seconds.")
|
||||
|
|
@ -109,7 +109,7 @@ struct TraceRouteLog: View {
|
|||
} else {
|
||||
Label {
|
||||
VStack {
|
||||
Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)")
|
||||
Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "Unknown".localized)")
|
||||
.font(idiom == .phone ? .body : .largeTitle)
|
||||
.fontWeight(.semibold)
|
||||
Text("A Trace Route was sent, no response has been received.")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ struct AboutMeshtastic: View {
|
|||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
List {
|
||||
Section(header: Text("What is Meshtastic?")) {
|
||||
Text("An open source, off-grid, decentralized, mesh network that runs on affordable, low-power radios.")
|
||||
|
|
@ -44,12 +43,12 @@ struct AboutMeshtastic: View {
|
|||
Button("Review the app") {
|
||||
if let scene = UIApplication.shared.connectedScenes
|
||||
.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
AppStore.requestReview(in: scene)
|
||||
}
|
||||
}
|
||||
.font(.title2)
|
||||
|
||||
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild)) ")
|
||||
Text("Version: \(Bundle.main.appVersionLong) (\(Bundle.main.appBuild))")
|
||||
}
|
||||
|
||||
Section(header: Text("Project information")) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ struct AppData: View {
|
|||
|
||||
VStack {
|
||||
|
||||
Section(header: Text("phone.gps")) {
|
||||
Section(header: Text("Phone GPS")) {
|
||||
GPSStatus()
|
||||
}
|
||||
Divider()
|
||||
|
|
@ -41,7 +41,7 @@ struct AppData: View {
|
|||
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
|
|
@ -61,7 +61,7 @@ struct AppData: View {
|
|||
Logger.services.error("🗑️ Delete file error: \(error, privacy: .public)")
|
||||
}
|
||||
} label: {
|
||||
Label("delete", systemImage: "trash")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} icon: {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ struct AppLog: View {
|
|||
|
||||
if idiom == .phone {
|
||||
Table(logs, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("log.message", value: \.composedMessage) { value in
|
||||
TableColumn("Message", value: \.composedMessage) { value in
|
||||
Text(value.composedMessage)
|
||||
.foregroundStyle(value.level.color)
|
||||
.font(.caption)
|
||||
|
|
@ -75,18 +75,18 @@ struct AppLog: View {
|
|||
}
|
||||
} else {
|
||||
Table(logs, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("log.time") { value in
|
||||
TableColumn("Time") { value in
|
||||
Text(value.date.formatted(dateFormatStyle))
|
||||
}
|
||||
.width(min: 125, max: 150)
|
||||
TableColumn("log.level") { value in
|
||||
TableColumn("Level") { value in
|
||||
Text(value.level.description)
|
||||
.foregroundStyle(value.level.color)
|
||||
}
|
||||
.width(min: 85, max: 110)
|
||||
TableColumn("log.category", value: \.category)
|
||||
TableColumn("Category", value: \.category)
|
||||
.width(min: 80, max: 130)
|
||||
TableColumn("log.message", value: \.composedMessage) { value in
|
||||
TableColumn("Message", value: \.composedMessage) { value in
|
||||
Text(value.composedMessage)
|
||||
.foregroundStyle(value.level.color)
|
||||
.font(.body)
|
||||
|
|
@ -270,4 +270,4 @@ extension AppLog {
|
|||
}
|
||||
}
|
||||
|
||||
extension OSLogEntry: Identifiable { }
|
||||
extension OSLogEntry: @retroactive Identifiable { }
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import OSLog
|
|||
struct AppSettings: View {
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@ObservedObject var tileManager = OfflineTileManager.shared
|
||||
@State var totalDownloadedTileSize = ""
|
||||
@State private var isPresentingCoreDataResetConfirm = false
|
||||
@State private var isPresentingDeleteMapTilesConfirm = false
|
||||
|
|
@ -44,7 +43,7 @@ struct AppSettings: View {
|
|||
Button {
|
||||
isPresentingCoreDataResetConfirm = true
|
||||
} label: {
|
||||
Label("clear.app.data", systemImage: "trash")
|
||||
Label("Clear App Data", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.confirmationDialog(
|
||||
|
|
@ -85,31 +84,7 @@ struct AppSettings: View {
|
|||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
if totalDownloadedTileSize != "0MB" {
|
||||
Section(header: Text("Map Tile Data")) {
|
||||
Button {
|
||||
isPresentingDeleteMapTilesConfirm = true
|
||||
} label: {
|
||||
Label("\("map.tiles.delete".localized) (\(totalDownloadedTileSize))", systemImage: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.confirmationDialog(
|
||||
"Are you sure?",
|
||||
isPresented: $isPresentingDeleteMapTilesConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button("Delete all map tiles?", role: .destructive) {
|
||||
tileManager.removeAll()
|
||||
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
|
||||
Logger.services.debug("delete all tiles")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear(perform: {
|
||||
totalDownloadedTileSize = tileManager.getAllDownloadedSize()
|
||||
})
|
||||
}
|
||||
.navigationTitle("App Settings")
|
||||
.navigationBarItems(trailing:
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ struct Channels: View {
|
|||
hasChanges = false
|
||||
}
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
Label("Save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(bleManager.connectedPeripheral == nil)// || !hasChanges)// !hasValidKey)
|
||||
.buttonStyle(.bordered)
|
||||
|
|
@ -230,7 +230,7 @@ struct Channels: View {
|
|||
Button {
|
||||
goBack()
|
||||
} label: {
|
||||
Label("close", systemImage: "xmark")
|
||||
Label("Close", systemImage: "xmark")
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
|
@ -279,7 +279,7 @@ struct Channels: View {
|
|||
.padding()
|
||||
}
|
||||
}
|
||||
.navigationTitle("channels")
|
||||
.navigationTitle("Channels")
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
|
|
@ -379,6 +379,6 @@ enum PositionPrecision: Int, CaseIterable, Identifiable {
|
|||
|
||||
var description: String {
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
return String.localizedStringWithFormat("position.precision %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
|
||||
return String.localizedStringWithFormat("Within %@".localized, String(distanceFormatter.string(fromDistance: precisionMeters)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ struct ChannelForm: View {
|
|||
Form {
|
||||
Section(header: Text("channel details")) {
|
||||
HStack {
|
||||
Text("name")
|
||||
Text("Name")
|
||||
Spacer()
|
||||
TextField(
|
||||
"Channel Name",
|
||||
|
|
@ -128,7 +128,7 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("position")) {
|
||||
Section(header: Text("Position")) {
|
||||
VStack(alignment: .leading) {
|
||||
Toggle(isOn: $positionsEnabled) {
|
||||
Label(channelRole == 1 ? "Positions Enabled" : "Allow Position Requests", systemImage: positionsEnabled ? "mappin" : "mappin.slash")
|
||||
|
|
@ -170,7 +170,7 @@ struct ChannelForm: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
Section(header: Text("mqtt")) {
|
||||
Section(header: Text("MQTT")) {
|
||||
Toggle(isOn: $uplink) {
|
||||
Label("Uplink Enabled", systemImage: "arrowshape.up")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,9 @@ struct BluetoothConfig: View {
|
|||
Form {
|
||||
ConfigHeader(title: "Bluetooth", config: \.bluetoothConfig, node: node, onAppear: setBluetoothValues)
|
||||
|
||||
Section(header: Text("options")) {
|
||||
Section(header: Text("Options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "antenna.radiowaves.left.and.right")
|
||||
Label("Enabled", systemImage: "antenna.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Pairing Mode", selection: $mode ) {
|
||||
|
|
|
|||
|
|
@ -30,17 +30,15 @@ struct DeviceConfig: View {
|
|||
@State var ledHeartbeatEnabled = true
|
||||
@State var tripleClickAsAdHocPing = true
|
||||
@State var tzdef = ""
|
||||
|
||||
@State private var showRouterWarning = false
|
||||
@State private var confirmWarning = false
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
ConfigHeader(title: "Device", config: \.deviceConfig, node: node, onAppear: setDeviceValues)
|
||||
|
||||
Section(header: Text("options")) {
|
||||
Section(header: Text("Options")) {
|
||||
VStack(alignment: .leading) {
|
||||
Picker("Device Role", selection: $deviceRole ) {
|
||||
ForEach(DeviceRoles.allCases) { dr in
|
||||
|
|
@ -149,7 +147,7 @@ struct DeviceConfig: View {
|
|||
Picker("Button GPIO", selection: $buttonGPIO) {
|
||||
ForEach(0..<49) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
Text("Unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
|
|
@ -159,7 +157,7 @@ struct DeviceConfig: View {
|
|||
Picker("Buzzer GPIO", selection: $buzzerGPIO) {
|
||||
ForEach(0..<49) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
Text("Unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
|
|
@ -249,7 +247,7 @@ struct DeviceConfig: View {
|
|||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("device.config")
|
||||
.navigationTitle("Device Config")
|
||||
.navigationBarItems(
|
||||
trailing: ZStack {
|
||||
ConnectedDevice(
|
||||
|
|
@ -312,6 +310,9 @@ struct DeviceConfig: View {
|
|||
}
|
||||
}
|
||||
func setDeviceValues() {
|
||||
if node?.deviceConfig?.role ?? 0 == 3 {
|
||||
node?.deviceConfig?.role = 1
|
||||
}
|
||||
self.deviceRole = Int(node?.deviceConfig?.role ?? 0)
|
||||
self.buttonGPIO = Int(node?.deviceConfig?.buttonGpio ?? 0)
|
||||
self.buzzerGPIO = Int(node?.deviceConfig?.buzzerGpio ?? 0)
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ struct DisplayConfig: View {
|
|||
}
|
||||
}
|
||||
|
||||
.navigationTitle("display.config")
|
||||
.navigationTitle("Display Config")
|
||||
.navigationBarItems(
|
||||
trailing: ZStack {
|
||||
ConnectedDevice(
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ struct LoRaConfig: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("lora.config")
|
||||
.navigationTitle("LoRa Config")
|
||||
.navigationBarItems(
|
||||
trailing: ZStack {
|
||||
ConnectedDevice(
|
||||
|
|
@ -304,6 +304,9 @@ struct LoRaConfig: View {
|
|||
}
|
||||
}
|
||||
func setLoRaValues() {
|
||||
if node?.loRaConfig?.modemPreset ?? 0 == 2 {
|
||||
node?.loRaConfig?.modemPreset = 0
|
||||
}
|
||||
self.hopLimit = Int(node?.loRaConfig?.hopLimit ?? 3)
|
||||
self.region = Int(node?.loRaConfig?.regionCode ?? 0)
|
||||
self.usePreset = node?.loRaConfig?.usePreset ?? true
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue