diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 629f0153..7e286dc7 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -1629,41 +1629,6 @@ } } }, - "8" : { - "extractionState" : "stale", - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "8" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "8" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "8" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "8" - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "8" - } - } - } - }, "12 Hour Clock" : { "localizations" : { "ja" : { @@ -9242,34 +9207,6 @@ } } }, - "Currently showing modules that may not be supported by this node." : { - "localizations" : { - "it" : { - "stringUnit" : { - "state" : "translated", - "value" : "Mostra i moduli che potrebbero non essere supportati al momento da questo nodo." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "現在、このノードでサポートされていない可能性のあるモジュールを表示しています。" - } - }, - "sr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Тренутно су приказани модули који можда нису подржани од стране овог чвора." - } - }, - "zh-Hant-TW" : { - "stringUnit" : { - "state" : "translated", - "value" : "目前顯示的模組可能不受此節點支援。" - } - } - } - }, "Currently the recommended way to update ESP32 devices is using the web flasher on a desktop computer from a chrome based browser. It does not work on mobile devices or over BLE." : { "localizations" : { "it" : { diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 9ad98c46..b2cc8a12 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 23D316932E5618D2002FA4FB /* AsyncGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D316922E5618D2002FA4FB /* AsyncGate.swift */; }; 23D9D9392E50DA97005D1C18 /* ResettableTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */; }; 23E23F922E392C2B00919073 /* LogRecord+StringRepresentation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */; }; + 23F061B32E7B056600A1E2EA /* Logger+DataDog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */; }; 23F488122E32980B002C776F /* AccessoryManager+Position.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23F488112E32980B002C776F /* AccessoryManager+Position.swift */; }; 23FF00B62E323C75001DF095 /* AccessoryManager+Connect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23FF00B52E323C75001DF095 /* AccessoryManager+Connect.swift */; }; 251926852C3BA97800249DF5 /* FavoriteNodeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */; }; @@ -203,6 +204,9 @@ DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; DD97E96828EFE9A00056DDA4 /* About.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96728EFE9A00056DDA4 /* About.swift */; }; + DD98EB212E7A31140016320A /* AppIcon_Ham.icon in Resources */ = {isa = PBXBuildFile; fileRef = DD98EB202E7A31140016320A /* AppIcon_Ham.icon */; }; + DD98EB292E7A42CC0016320A /* AppIcon_Chirpy.icon in Resources */ = {isa = PBXBuildFile; fileRef = DD98EB282E7A42CC0016320A /* AppIcon_Chirpy.icon */; }; + DD98EB2B2E7A838D0016320A /* AppIcon_MPowered.icon in Resources */ = {isa = PBXBuildFile; fileRef = DD98EB2A2E7A838D0016320A /* AppIcon_MPowered.icon */; }; DD994B69295F88B60013760A /* IntervalEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD994B68295F88B60013760A /* IntervalEnums.swift */; }; DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA0B6B1294CDC55001356EC /* Channels.swift */; }; DDA1C48E28DB49D3009933EC /* ChannelRoles.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA1C48D28DB49D3009933EC /* ChannelRoles.swift */; }; @@ -210,6 +214,7 @@ DDA9515A2BC6624100CEA535 /* TelemetryWeather.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA951592BC6624100CEA535 /* TelemetryWeather.swift */; }; DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */; }; DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */; }; + DDA9F5E82E77FAC100E70DEB /* AnimatedNodePin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9F5E72E77FAA600E70DEB /* AnimatedNodePin.swift */; }; DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580C2B0DAA9E00147258 /* Routes.swift */; }; DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */; }; DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */; }; @@ -366,6 +371,7 @@ 23D316922E5618D2002FA4FB /* AsyncGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncGate.swift; sourceTree = ""; }; 23D9D9382E50DA97005D1C18 /* ResettableTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResettableTimer.swift; sourceTree = ""; }; 23E23F912E392C2B00919073 /* LogRecord+StringRepresentation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LogRecord+StringRepresentation.swift"; sourceTree = ""; }; + 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+DataDog.swift"; sourceTree = ""; }; 23F488112E32980B002C776F /* AccessoryManager+Position.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Position.swift"; sourceTree = ""; }; 23FF00B52E323C75001DF095 /* AccessoryManager+Connect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccessoryManager+Connect.swift"; sourceTree = ""; }; 251926842C3BA97800249DF5 /* FavoriteNodeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteNodeButton.swift; sourceTree = ""; }; @@ -538,6 +544,9 @@ DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = ""; }; DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticLogo.swift; sourceTree = ""; }; DD97E96728EFE9A00056DDA4 /* About.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = About.swift; sourceTree = ""; }; + DD98EB202E7A31140016320A /* AppIcon_Ham.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon_Ham.icon; sourceTree = ""; }; + DD98EB282E7A42CC0016320A /* AppIcon_Chirpy.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon_Chirpy.icon; sourceTree = ""; }; + DD98EB2A2E7A838D0016320A /* AppIcon_MPowered.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon_MPowered.icon; sourceTree = ""; }; DD994B68295F88B60013760A /* IntervalEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntervalEnums.swift; sourceTree = ""; }; DD9A1A912BA2D2D3001E602E /* MeshtasticDataModelV 30.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 30.xcdatamodel"; sourceTree = ""; }; DDA0B6B1294CDC55001356EC /* Channels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Channels.swift; sourceTree = ""; }; @@ -547,6 +556,7 @@ DDA951592BC6624100CEA535 /* TelemetryWeather.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TelemetryWeather.swift; sourceTree = ""; }; DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TelemetryEnums.swift; sourceTree = ""; }; DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndoorAirQuality.swift; sourceTree = ""; }; + DDA9F5E72E77FAA600E70DEB /* AnimatedNodePin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedNodePin.swift; sourceTree = ""; }; DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV20.xcdatamodel; sourceTree = ""; }; DDAB580C2B0DAA9E00147258 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; @@ -1196,8 +1206,11 @@ DDC2E18926CE24F70042C5E4 /* Resources */ = { isa = PBXGroup; children = ( - 2339EA992E6C65DC0032C234 /* AppIconDebug.icon */, 2339EA972E6C65570032C234 /* AppIcon.icon */, + 2339EA992E6C65DC0032C234 /* AppIconDebug.icon */, + DD98EB282E7A42CC0016320A /* AppIcon_Chirpy.icon */, + DD98EB202E7A31140016320A /* AppIcon_Ham.icon */, + DD98EB2A2E7A838D0016320A /* AppIcon_MPowered.icon */, DDB75A192A05EB67006ED576 /* alpha.png */, DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */, DD0E21002B8A6BC500F2D100 /* DeviceHardware.json */, @@ -1344,6 +1357,7 @@ DDD5BB172C2F9C36007E03CA /* OSLogEntryLog.swift */, DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, DDD5BB0C2C285F00007E03CA /* Logger.swift */, + 23F061B22E7B056600A1E2EA /* Logger+DataDog.swift */, DD6F65732C6CB80A0053C113 /* View.swift */, ); path = Extensions; @@ -1352,6 +1366,7 @@ DDDC22362BA9232C002C44F1 /* MapContent */ = { isa = PBXGroup; children = ( + DDA9F5E72E77FAA600E70DEB /* AnimatedNodePin.swift */, DDDC22372BA92344002C44F1 /* MeshMapContent.swift */, DD93800A2BA3F968008BEC06 /* NodeMapContent.swift */, ); @@ -1514,11 +1529,14 @@ buildActionMask = 2147483647; files = ( DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */, + DD98EB292E7A42CC0016320A /* AppIcon_Chirpy.icon in Resources */, 25AECD4F2C2F723200862C8E /* Localizable.xcstrings in Resources */, DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */, 2339EA9A2E6C65DC0032C234 /* AppIconDebug.icon in Resources */, + DD98EB212E7A31140016320A /* AppIcon_Ham.icon in Resources */, 2339EA982E6C65570032C234 /* AppIcon.icon in Resources */, DDB75A1A2A05EB67006ED576 /* alpha.png in Resources */, + DD98EB2B2E7A838D0016320A /* AppIcon_MPowered.icon in Resources */, DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */, DD0E21012B8A6F1300F2D100 /* DeviceHardware.json in Resources */, DDDBC87B2BC62E4E001E8DF7 /* Settings.bundle in Resources */, @@ -1655,6 +1673,7 @@ DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */, DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */, DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */, + 23F061B32E7B056600A1E2EA /* Logger+DataDog.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, @@ -1763,6 +1782,7 @@ DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, 3D3417D22E2DC260006A988B /* MapDataManager.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, + DDA9F5E82E77FAC100E70DEB /* AnimatedNodePin.swift in Sources */, DDF82CBD2D5BC69200DC25EC /* NavigateToButton.swift in Sources */, 8D3F8A3F2D44BB02009EAAA4 /* PowerMetrics.swift in Sources */, 2519268A2C3BB1B200249DF5 /* ExchangePositionsButton.swift in Sources */, @@ -2052,7 +2072,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.7.2; + MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2086,7 +2106,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.7.2; + MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -2117,7 +2137,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.7.2; + MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2149,7 +2169,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.7.2; + MARKETING_VERSION = 2.7.3; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift index 9f60b1ba..75e9dabb 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Connect.swift @@ -147,6 +147,27 @@ extension AccessoryManager { if transport.requiresPeriodicHeartbeat { await self.setupPeriodicHeartbeat() } + + + + if let device = self.activeConnection?.device { + var version: String? + if let firmwareVersion = device.firmwareVersion { + if let lastDotIndex = firmwareVersion.lastIndex(of: ".") { + version = String(firmwareVersion[...(lastDotIndex)].dropLast()) + } else { + version = firmwareVersion + } + } + + let connectionAttributes: [String: any Encodable] = [ + "firmware_version": version, + "transport_type": device.transportType.rawValue, // e.g., "websocket", "http/2", "quic" + "hardware_model": device.hardwareModel, + "nodes": self.expectedNodeDBSize + ] + Logger.datadog.action(name: "connect", attributes: connectionAttributes ) + } } } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift index 7cc969fe..01cdac79 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+FromRadio.swift @@ -112,6 +112,7 @@ extension AccessoryManager { if let user = nodeInfo.user { updateDevice(deviceId: activeDevice.id, key: \.shortName, value: user.shortName ?? "?") updateDevice(deviceId: activeDevice.id, key: \.longName, value: user.longName ?? "Unknown".localized) + updateDevice(deviceId: activeDevice.id, key: \.hardwareModel, value: user.hwModel) } } } diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Position.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Position.swift index f42a590c..e7c8cd4f 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Position.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+Position.swift @@ -27,7 +27,7 @@ extension AccessoryManager { } } - public func sendPosition(channel: Int32, destNum: Int64, wantResponse: Bool) async throws { + public func sendPosition(channel: Int32, destNum: Int64, hopsAway: Int32 = 0, wantResponse: Bool) async throws { guard let fromNodeNum = activeConnection?.device.num else { throw AccessoryError.ioFailed("Not connected to any device") } @@ -41,6 +41,9 @@ extension AccessoryManager { meshPacket.to = UInt32(destNum) meshPacket.channel = UInt32(channel) meshPacket.from = UInt32(fromNodeNum) + if hopsAway > 0 { + meshPacket.hopLimit = UInt32(truncatingIfNeeded: hopsAway) + } var dataMessage = DataMessage() if let serializedData: Data = try? positionPacket.serializedData() { dataMessage.payload = serializedData diff --git a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift index 2b2735e0..8b42640d 100644 --- a/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift +++ b/Meshtastic/Accessory/Accessory Manager/AccessoryManager+ToRadio.swift @@ -362,6 +362,10 @@ extension AccessoryManager { meshPacket.id = UInt32(newMessage.messageId) if toUserNum > 0 { meshPacket.to = UInt32(toUserNum) + let hopsAway = newMessage.toUser?.userNode?.hopsAway ?? 0 + if hopsAway > Int32(truncatingIfNeeded: newMessage.toUser?.userNode?.loRaConfig?.hopLimit ?? 0) { + meshPacket.hopLimit = UInt32(truncatingIfNeeded: hopsAway) + } } else { meshPacket.to = Constants.maximumNodeNum } diff --git a/Meshtastic/Accessory/Protocols/Device.swift b/Meshtastic/Accessory/Protocols/Device.swift index 1f35d3b1..d24a1eb6 100644 --- a/Meshtastic/Accessory/Protocols/Device.swift +++ b/Meshtastic/Accessory/Protocols/Device.swift @@ -17,6 +17,7 @@ struct Device: Identifiable, Hashable { var shortName: String? var longName: String? var firmwareVersion: String? + var hardwareModel: String? var rssi: Int? var lastUpdate: Date? diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/Contents.json new file mode 100644 index 00000000..537cf6d3 --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "meshtastic_ham.jpg", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "logo-dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "logo-dark 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark 1.png b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark 1.png new file mode 100644 index 00000000..91e94f10 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark 1.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark.png b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark.png new file mode 100644 index 00000000..91e94f10 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/logo-dark.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/meshtastic_ham.jpg b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/meshtastic_ham.jpg new file mode 100644 index 00000000..8a48302a Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_Ham.appiconset/meshtastic_ham.jpg differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/Contents.json new file mode 100644 index 00000000..73c3e1dd --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "logo-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/logo-dark.png b/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/logo-dark.png new file mode 100644 index 00000000..91e94f10 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_Ham_Dark_Thumb.imageset/logo-dark.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/Contents.json new file mode 100644 index 00000000..eb687255 --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "meshtastic_ham.jpg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/meshtastic_ham.jpg b/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/meshtastic_ham.jpg new file mode 100644 index 00000000..8a48302a Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_Ham_Thumb.imageset/meshtastic_ham.jpg differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/Contents.json new file mode 100644 index 00000000..96ecc862 --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "filename" : "MPowered.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "MPowered 1.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "MPowered 2.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 1.png b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 1.png new file mode 100644 index 00000000..d7b3fd09 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 1.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 2.png b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 2.png new file mode 100644 index 00000000..d7b3fd09 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered 2.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered.png b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered.png new file mode 100644 index 00000000..d7b3fd09 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_MPowered.appiconset/MPowered.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/Contents.json new file mode 100644 index 00000000..e77b54b2 --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MPowered.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/MPowered.png b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/MPowered.png new file mode 100644 index 00000000..d7b3fd09 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Dark_Thumb.imageset/MPowered.png differ diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/Contents.json b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/Contents.json new file mode 100644 index 00000000..e77b54b2 --- /dev/null +++ b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "MPowered.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/MPowered.png b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/MPowered.png new file mode 100644 index 00000000..d7b3fd09 Binary files /dev/null and b/Meshtastic/Assets.xcassets/AppIcon_MPowered_Thumb.imageset/MPowered.png differ diff --git a/Meshtastic/Extensions/Logger+DataDog.swift b/Meshtastic/Extensions/Logger+DataDog.swift new file mode 100644 index 00000000..c3c2afdc --- /dev/null +++ b/Meshtastic/Extensions/Logger+DataDog.swift @@ -0,0 +1,69 @@ +// +// Logger+DataDog.swift +// Meshtastic +// +// Created by Jake Bordens on 9/17/25. +// + +import Foundation +import os.log +import DatadogRUM +import DatadogLogs + +struct DatadogLogger { + private let osLogger: os.Logger + private let ddLogger: any DatadogLogs.LoggerProtocol + + // Initialize with a subsystem and category, similar to Logger + fileprivate init(subsystem: String, category: String) { + self.osLogger = Logger(subsystem: subsystem, category: category) + self.ddLogger = DatadogLogs.Logger.create( + with: Logger.Configuration( + name: "gvh.Meshtastic", + networkInfoEnabled: true, + remoteLogThreshold: .debug, + consoleLogFormat: .short + ) + ) + } + + // ✨ os.Logger functions like debug, info, etc., are not normal functions. + // They rely on compiler magic to parse the interpolated string, identify the + // privacy modifiers (.public, .private), and handle the data securely without + // ever creating a potentially sensitive string in your app's memory. + // To do this, the compiler must see the string literal at the point of the call. + // Since this is going to Datadog, care should be taken to only use these functions + // with public debug data. + func debug(_ message: String) { + osLogger.debug("\(message, privacy: .public)") + ddLogger.debug(message) + } + + func info(_ message: String) { + osLogger.info("\(message, privacy: .public)") + ddLogger.info(message) + } + + func warning(_ message: String) { + osLogger.warning("\(message, privacy: .public)") + ddLogger.warn(message) + } + + func error(_ message: String) { + osLogger.error("\(message, privacy: .public)") + ddLogger.error(message) + } + + // MARK: - Methods for RUM actions + func action(name: String, attributes: [String: any Encodable]? = nil) { + RUMMonitor.shared().addAction( + type: .custom, + name: name, + attributes: attributes ?? [:] + ) + } +} + +extension os.Logger { + static let datadog = DatadogLogger(subsystem: "datadog", category: "🐶 DataDog") +} diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 0946c0cb..4c1552a4 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -74,12 +74,12 @@ import OSLog // we'll resume the continuation with .notDetermined to prevent a leak. Task { @MainActor in // Ensure this task runs on the MainActor do { - try await Task.sleep(for: .seconds(10)) // Wait for 10 seconds + try await Task.sleep(for: .seconds(5)) // Wait for 5 seconds if let currentContinuation = self.permissionContinuation { // If the continuation hasn't been nilled out yet, it means // locationManagerDidChangeAuthorization hasn't been called. Logger.services.warning("📍 [App] Location permission request timed out. Resuming continuation with .notDetermined.") - currentContinuation.resume(returning: .notDetermined) + currentContinuation.resume(returning: .denied) self.permissionContinuation = nil // Clear the reference } } catch is CancellationError { diff --git a/Meshtastic/Info.plist b/Meshtastic/Info.plist index 464fba6e..863fb0e9 100644 --- a/Meshtastic/Info.plist +++ b/Meshtastic/Info.plist @@ -100,7 +100,7 @@ NSBluetoothAlwaysUsageDescription We use bluetooth to connect to nearby Meshtastic Devices NSBluetoothPeripheralUsageDescription - Bluetooth is used to connect an iPhone to a user's meshtastic device to allow text messaging and location data for the mesh network. + Bluetooth is used to connect an iPhone to a user's meshtastic device to allow text messaging and location data for the mesh network. NSBonjourServices _meshtastic._tcp @@ -110,13 +110,13 @@ NSLocalNetworkUsageDescription We use local networking to connect to network-based nodes. NSLocationAlwaysAndWhenInUseUsageDescription - We use your location to display it on the mesh map as well as to have GPS coordinates to send to the connected device. Route Recording uses location in the background. + We use your location to display it on the mesh map, show and filter by distance as well as to have GPS coordinates to send to the connected device. Route Recording uses location in the background. NSLocationAlwaysUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinates to send to the connected device. NSLocationUsageDescription We use your location to display it on the mesh map as well as to have GPS coordinates to send to the connected device. NSLocationWhenInUseUsageDescription - We use your location to display it on the mesh map as well as to have GPS coordinates to send to the connected device. + We use your location to display it on the mesh map, show and filter by distance as well as to have GPS coordinates to send to the connected device. Route Recording uses location in the background. NSSupportsLiveActivities Privacy – Bluetooth Always Usage Description diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index f7e07099..62a1d78e 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -75,11 +75,6 @@ struct MeshtasticAppleApp: App { trackBackgroundEvents: true ) ) - let attributes: [String: Encodable] = [ - "firmware_version": UserDefaults.firmwareVersion, - "hardware_model": UserDefaults.hardwareModel - ] - RUMMonitor.shared().addAttributes(attributes) #if DEBUG SessionReplay.enable( with: SessionReplay.Configuration( diff --git a/Meshtastic/Resources/AppIcon_Chirpy.icon/Assets/chirpy-2.svg b/Meshtastic/Resources/AppIcon_Chirpy.icon/Assets/chirpy-2.svg new file mode 100644 index 00000000..d215662d --- /dev/null +++ b/Meshtastic/Resources/AppIcon_Chirpy.icon/Assets/chirpy-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Meshtastic/Resources/AppIcon_Chirpy.icon/icon.json b/Meshtastic/Resources/AppIcon_Chirpy.icon/icon.json new file mode 100644 index 00000000..146d75a9 --- /dev/null +++ b/Meshtastic/Resources/AppIcon_Chirpy.icon/icon.json @@ -0,0 +1,51 @@ +{ + "fill" : { + "automatic-gradient" : "extended-gray:1.00000,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "image-name" : "chirpy-2.svg", + "name" : "chirpy-2", + "position-specializations" : [ + { + "idiom" : "square", + "value" : { + "scale" : 0.48, + "translation-in-points" : [ + -18.390625, + 127.2421875 + ] + } + }, + { + "idiom" : "watchOS", + "value" : { + "scale" : 0.52, + "translation-in-points" : [ + -7, + 145.9296875 + ] + } + } + ] + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/Meshtastic/Resources/AppIcon_Ham.icon/Assets/ham.png b/Meshtastic/Resources/AppIcon_Ham.icon/Assets/ham.png new file mode 100644 index 00000000..59698c56 Binary files /dev/null and b/Meshtastic/Resources/AppIcon_Ham.icon/Assets/ham.png differ diff --git a/Meshtastic/Resources/AppIcon_Ham.icon/icon.json b/Meshtastic/Resources/AppIcon_Ham.icon/icon.json new file mode 100644 index 00000000..0e8257bc --- /dev/null +++ b/Meshtastic/Resources/AppIcon_Ham.icon/icon.json @@ -0,0 +1,39 @@ +{ + "fill" : { + "automatic-gradient" : "display-p3:0.54507,0.90596,0.61177,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "blend-mode" : "normal", + "glass" : false, + "hidden" : false, + "image-name" : "ham.png", + "name" : "ham", + "position" : { + "scale" : 1.65, + "translation-in-points" : [ + -2.0625, + 7.703125 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/Meshtastic/Resources/AppIcon_MPowered.icon/Assets/M-POWERED.svg b/Meshtastic/Resources/AppIcon_MPowered.icon/Assets/M-POWERED.svg new file mode 100644 index 00000000..b8f0fcd1 --- /dev/null +++ b/Meshtastic/Resources/AppIcon_MPowered.icon/Assets/M-POWERED.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Meshtastic/Resources/AppIcon_MPowered.icon/icon.json b/Meshtastic/Resources/AppIcon_MPowered.icon/icon.json new file mode 100644 index 00000000..a6e547c3 --- /dev/null +++ b/Meshtastic/Resources/AppIcon_MPowered.icon/icon.json @@ -0,0 +1,79 @@ +{ + "fill" : { + "solid" : "display-p3:0.17335,0.17642,0.23072,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "glass" : false, + "image-name" : "M-POWERED.svg", + "name" : "M-POWERED", + "position-specializations" : [ + { + "value" : { + "scale" : 1, + "translation-in-points" : [ + 1.6953125, + 12.1875 + ] + } + }, + { + "idiom" : "square", + "value" : { + "scale" : 1.05, + "translation-in-points" : [ + -20.074908088235293, + -22.122702205882355 + ] + } + }, + { + "idiom" : "watchOS", + "value" : { + "scale" : 1.05, + "translation-in-points" : [ + -15.328125, + -35.484375 + ] + } + } + ] + } + ], + "position-specializations" : [ + { + "value" : { + "scale" : 1, + "translation-in-points" : [ + 15.359375, + 7.6484375 + ] + } + }, + { + "idiom" : "watchOS", + "value" : { + "scale" : 1.1, + "translation-in-points" : [ + 7.7109375, + 120.0546875 + ] + } + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "squares" : "shared" + } +} \ No newline at end of file diff --git a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift index 2587550a..05747fa9 100644 --- a/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift +++ b/Meshtastic/Views/Nodes/Helpers/Actions/ExchangePositionsButton.swift @@ -10,12 +10,14 @@ struct ExchangePositionsButton: View { @State private var isPresentingPositionFailedAlert: Bool = false var body: some View { + let hopsAway = Int32(truncatingIfNeeded: node.hopsAway > node.loRaConfig?.hopLimit ?? 0 ? node.hopsAway : node.loRaConfig?.hopLimit ?? 0) Button { Task { do { try await accessoryManager.sendPosition( channel: node.channel, destNum: node.num, + hopsAway: hopsAway, wantResponse: true ) Task { @MainActor in diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift new file mode 100644 index 00000000..2099f6a6 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/AnimatedNodePin.swift @@ -0,0 +1,70 @@ +import SwiftUI +import MapKit +import CoreLocation + +struct AnimatedNodePin: View, Equatable { + let nodeColor: UIColor + let shortName: String? + let hasDetectionSensorMetrics: Bool + let isOnline: Bool + let calculatedDelay: Double + private let swiftUIColor: Color + + init(nodeColor: UIColor, shortName: String?, hasDetectionSensorMetrics: Bool, isOnline: Bool, calculatedDelay: Double) { + self.nodeColor = nodeColor + self.shortName = shortName + self.hasDetectionSensorMetrics = hasDetectionSensorMetrics + self.isOnline = isOnline + self.calculatedDelay = calculatedDelay + self.swiftUIColor = Color(nodeColor) + } + + var body: some View { + ZStack { + // Pass the calculatedDelay to the PulsingCircle view + if isOnline { + PulsingCircle(nodeColor: nodeColor, calculatedDelay: calculatedDelay) + } + + if hasDetectionSensorMetrics { + Image(systemName: "sensor.fill") + .symbolRenderingMode(.palette) + .symbolEffect(.variableColor) + .padding() + .foregroundStyle(.white) + .background(swiftUIColor) + .clipShape(Circle()) + } else { + CircleText(text: shortName ?? "?", color: swiftUIColor, circleSize: 40) + } + } + } + + static func == (lhs: AnimatedNodePin, rhs: AnimatedNodePin) -> Bool { + return lhs.nodeColor == rhs.nodeColor && + lhs.shortName == rhs.shortName && + lhs.hasDetectionSensorMetrics == rhs.hasDetectionSensorMetrics && + lhs.isOnline == rhs.isOnline && + lhs.calculatedDelay == rhs.calculatedDelay // Include calculatedDelay to ensure changes in animation timing trigger UI updates + } +} + +struct PulsingCircle: View { + let nodeColor: UIColor + let calculatedDelay: Double + @State private var isPulsing = false + + var body: some View { + Circle() + .fill(Color(nodeColor.lighter()).opacity(0.4)) + .frame(width: 55, height: 55) + .scaleEffect(isPulsing ? 1.2 : 0.8) + .animation( + .easeInOut(duration: 0.8).repeatForever(autoreverses: true).delay(calculatedDelay), + value: isPulsing + ) + .onAppear { + isPulsing = true + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 649a9edc..4941dd30 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -11,12 +11,12 @@ import CoreLocation import OSLog struct IdentifiableOverlay: Identifiable { - let overlay: MKOverlay - var id: ObjectIdentifier { ObjectIdentifier(overlay as AnyObject) } + let overlay: MKOverlay + var id: ObjectIdentifier { ObjectIdentifier(overlay as AnyObject) } } struct MeshMapContent: MapContent { - + /// Parameters @Binding var showUserLocation: Bool @AppStorage("meshMapShowNodeHistory") private var showNodeHistory = false @@ -30,141 +30,48 @@ struct MeshMapContent: MapContent { @Binding var selectedPosition: PositionEntity? @AppStorage("enableMapWaypoints") private var showWaypoints = true @Binding var selectedWaypoint: WaypointEntity? - // Map overlays @AppStorage("mapOverlaysEnabled") private var showMapOverlays = false @Binding var enabledOverlayConfigs: Set - + @FetchRequest(fetchRequest: PositionEntity.allPositionsFetchRequest(), animation: .easeIn) var positions: FetchedResults - + @FetchRequest(fetchRequest: WaypointEntity.allWaypointssFetchRequest(), animation: .none) var waypoints: FetchedResults - + @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(format: "enabled == true", ""), animation: .none) private var routes: FetchedResults - - var delay: Double = 0 - @State private var scale: CGFloat = 0.5 - + @MapContentBuilder var positionAnnotations: some MapContent { ForEach(positions, id: \.id) { position in - if !showFavorites || (position.nodePosition?.favorite == true) { - /// Node color from node.num + if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) { + let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) let positionName = position.nodePosition?.user?.longName ?? "?" - /// Latest Position Anotations + // Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction. + let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5 + Annotation(positionName, coordinate: position.coordinate) { - LazyVStack { - ZStack { - let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0)) - if position.nodePosition?.isOnline ?? false { - Circle() - .fill(Color(nodeColor.lighter()).opacity(0.4).shadow(.drop(color: Color(nodeColor).isLight() ? .black : .white, radius: 5))) - .foregroundStyle(Color(nodeColor.lighter()).opacity(0.3)) - .scaleEffect(scale) - .animation( - Animation.easeInOut(duration: 0.6) - .repeatForever().delay(delay), value: scale - ) - .onAppear { - self.scale = 1 - } - .onChange(of: showFavorites) { - - scale = 0.5 // Reset to initial state - DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { - scale = 1 - } - } - .frame(width: 60, height: 60) - } - if position.nodePosition?.hasDetectionSensorMetrics ?? false { - Image(systemName: "sensor.fill") - .symbolRenderingMode(.palette) - .symbolEffect(.variableColor) - .padding() - .foregroundStyle(.white) - .background(Color(nodeColor)) - .clipShape(Circle()) - } else { - CircleText(text: position.nodePosition?.user?.shortName ?? "?", color: Color(nodeColor), circleSize: 40) - } + LazyVStack { + AnimatedNodePin( + nodeColor: nodeColor, + shortName: position.nodePosition?.user?.shortName, + hasDetectionSensorMetrics: position.nodePosition?.hasDetectionSensorMetrics ?? false, + isOnline: position.nodePosition?.isOnline ?? false, + calculatedDelay: calculatedDelay + ) } - } - .highPriorityGesture(TapGesture().onEnded { _ in - selectedPosition = (selectedPosition == position ? nil : position) - }) - } - /// Node History and Route Lines for favorites - if let nodePosition = position.nodePosition, - nodePosition.favorite, - let positions = nodePosition.positions, - let nodePositions = Array(positions) as? [PositionEntity] { - if showRouteLines { - let routeCoords = nodePositions.compactMap({(pos) -> CLLocationCoordinate2D in - return pos.nodeCoordinate ?? LocationsHandler.DefaultLocation + .highPriorityGesture(TapGesture().onEnded { _ in + selectedPosition = (selectedPosition == position ? nil : position) }) - let gradient = LinearGradient( - colors: [Color(nodeColor.lighter().lighter()), Color(nodeColor.lighter()), Color(nodeColor)], - startPoint: .leading, endPoint: .trailing - ) - let dashed = StrokeStyle( - lineWidth: 3, - lineCap: .round, lineJoin: .round, dash: [10, 10] - ) - MapPolyline(coordinates: routeCoords) - .stroke(gradient, style: dashed) - } - if showNodeHistory { - ForEach(nodePositions, id: \.self) { (mappin: PositionEntity) in - if mappin.latest == false && mappin.nodePosition?.favorite ?? false { - let pf = PositionFlags(rawValue: Int(mappin.nodePosition?.metadata?.positionFlags ?? 771)) - let headingDegrees = Angle.degrees(Double(mappin.heading)) - Annotation("", coordinate: mappin.coordinate) { - LazyVStack { - if pf.contains(.Heading) { - Image(systemName: "location.north.circle") - .resizable() - .scaledToFit() - .foregroundStyle(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white) - .background(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) - .clipShape(Circle()) - .rotationEffect(headingDegrees) - .frame(width: 16, height: 16) - - } else { - Circle() - .fill(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0)))) - .strokeBorder(Color(UIColor(hex: UInt32(mappin.nodePosition?.num ?? 0))).isLight() ? .black : .white, lineWidth: 2) - .frame(width: 12, height: 12) - } - } - } - .annotationTitles(.hidden) - .annotationSubtitles(.hidden) - } - } - } - } - /// Reduced Precision Map Circles - if 12...15 ~= position.precisionBits { - let pp = PositionPrecision(rawValue: Int(position.precisionBits)) - let radius: CLLocationDistance = pp?.precisionMeters ?? 0 - if radius > 0.0 { - MapCircle(center: position.coordinate, radius: radius) - .foregroundStyle(Color(nodeColor).opacity(0.25)) - .stroke(.white, lineWidth: 2) - .tag(position.nodePosition?.num ?? 0) } } } - } - } - + @MapContentBuilder var routeAnnotations: some MapContent { ForEach(routes) { route in @@ -199,7 +106,7 @@ struct MeshMapContent: MapContent { } } } - + @MapContentBuilder var waypointAnnotations: some MapContent { if waypoints.count > 0, showWaypoints, let waypoints = Array(waypoints) as? [WaypointEntity] { @@ -217,7 +124,7 @@ struct MeshMapContent: MapContent { } } } - + @MapContentBuilder var meshMap: some MapContent { let loraNodes = positions.filter { $0.nodePosition?.viaMqtt ?? true == false } @@ -233,27 +140,27 @@ struct MeshMapContent: MapContent { .foregroundStyle(.indigo.opacity(0.4)) } } - + /// GeoJSON Overlays with embedded styling if showMapOverlays { overlayContent } - + positionAnnotations routeAnnotations waypointAnnotations } - + var overlayContent: some MapContent { // Get all features but filter by enabled configs let allStyledFeatures = GeoJSONOverlayManager.shared.loadStyledFeaturesForConfigs(enabledOverlayConfigs) - + return Group { ForEach(0.. 0 { + Text(position.nodePosition?.user?.longName ?? "Unknown") + .font(.largeTitle) + } + Divider() + HStack(alignment: .center) { + VStack(alignment: .leading) { + /// Time Label { - Text("Hops Away: \(position.nodePosition?.hopsAway ?? 0)") + if idiom != .phone { + Text("Heard".localized + ":") + } + Text(position.time?.lastHeard ?? "unknown") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) + } icon: { + Image(systemName: position.nodePosition?.isOnline ?? false ? "checkmark.circle.fill" : "moon.circle.fill") + .symbolRenderingMode(.hierarchical) + .foregroundColor(position.nodePosition?.isOnline ?? false ? .green : .orange) + .frame(width: 35) + } + .padding(.bottom, 5) + /// Coordinate + Label { + Text("\(String(format: "%.6f", position.coordinate.latitude)), \(String(format: "%.6f", position.coordinate.longitude))") .textSelection(.enabled) .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) + .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/) } icon: { - Image(systemName: "hare") + Image(systemName: "mappin.and.ellipse") .symbolRenderingMode(.hierarchical) .frame(width: 35) } .padding(.bottom, 5) - } - /// Altitude - Label { - let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters) - let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet) - if Locale.current.measurementSystem == .metric { - Text(altitudeFormatter.string(from: distanceInMeters)) - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } else { - Text(altitudeFormatter.string(from: distanceInFeet)) - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } - } icon: { - Image(systemName: "mountain.2.fill") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - .padding(.bottom, 5) - let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) - /// Sats in view - if pf.contains(.Satsinview) { - Label { - Text("Sats in view: \(String(position.satsInView))") - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } icon: { - Image(systemName: "sparkles") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - .padding(.bottom, 5) - } - /// Sequence Number - if pf.contains(.SeqNo) { - Label { - Text("Sequence: \(String(position.seqNo))") - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } icon: { - Image(systemName: "number") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - .padding(.bottom, 5) - } - /// Heading - let degrees = Angle.degrees(Double(position.heading)) - Label { - let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) - Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") - } icon: { - Image(systemName: "location.north") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - .rotationEffect(degrees) - } - .padding(.bottom, 5) - /// Distance - if let lastLocation = locationsHandler.locationsArray.last { - /// Distance - if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { - let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) + /// Hops Away + if position.nodePosition?.hopsAway ?? 0 > 0 { Label { - Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + Text("Hops Away: \(position.nodePosition?.hopsAway ?? 0)") + .textSelection(.enabled) .foregroundColor(.primary) .font(idiom == .phone ? .callout : .body) } icon: { - Image(systemName: "lines.measurement.horizontal") + Image(systemName: "hare") .symbolRenderingMode(.hierarchical) .frame(width: 35) } + .padding(.bottom, 5) } - } - /// Speed - let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) - Label { - Text("Speed: \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))") - .foregroundColor(.primary) - .font(idiom == .phone ? .callout : .body) - } icon: { - Image(systemName: "gauge.with.dots.needle.33percent") - .symbolRenderingMode(.hierarchical) - .frame(width: 35) - } - .padding(.bottom, 5) - if position.nodePosition?.viaMqtt ?? false { + /// Altitude Label { - Text("MQTT") - .font(idiom == .phone ? .callout : .body) + let distanceInMeters = Measurement(value: Double(position.altitude), unit: UnitLength.meters) + let distanceInFeet = distanceInMeters.converted(to: UnitLength.feet) + if Locale.current.measurementSystem == .metric { + Text(altitudeFormatter.string(from: distanceInMeters)) + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } else { + Text(altitudeFormatter.string(from: distanceInFeet)) + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } } icon: { - Image(systemName: "network") + Image(systemName: "mountain.2.fill") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + let pf = PositionFlags(rawValue: Int(position.nodePosition?.metadata?.positionFlags ?? 3)) + /// Sats in view + if pf.contains(.Satsinview) { + Label { + Text("Sats in view: \(String(position.satsInView))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "sparkles") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Sequence Number + if pf.contains(.SeqNo) { + Label { + Text("Sequence: \(String(position.seqNo))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "number") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .padding(.bottom, 5) + } + /// Heading + let degrees = Angle.degrees(Double(position.heading)) + Label { + let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees) + Text("Heading: \(heading.formatted(.measurement(width: .narrow, numberFormatStyle: .number.precision(.fractionLength(0)))))") + } icon: { + Image(systemName: "location.north") .symbolRenderingMode(.hierarchical) .frame(width: 35) .rotationEffect(degrees) } .padding(.bottom, 5) + /// Distance + if let lastLocation = locationsHandler.locationsArray.last { + /// Distance + if lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 { + let metersAway = position.coordinate.distance(from: CLLocationCoordinate2D(latitude: lastLocation.coordinate.latitude, longitude: lastLocation.coordinate.longitude)) + Label { + Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "lines.measurement.horizontal") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + } + } + /// Speed + let speed = Measurement(value: Double(position.speed), unit: UnitSpeed.kilometersPerHour) + Label { + Text("Speed: \(speed.formatted(.measurement(width: .abbreviated, numberFormatStyle: .number.precision(.fractionLength(0)))))") + .foregroundColor(.primary) + .font(idiom == .phone ? .callout : .body) + } icon: { + Image(systemName: "gauge.with.dots.needle.33percent") + .symbolRenderingMode(.hierarchical) + .frame(width: 35) + } + .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) + } + .padding(.bottom, 5) + } + Spacer() } Spacer() - } - Spacer() - VStack(alignment: .center) { - if position.nodePosition != nil { - if position.nodePosition?.favorite ?? false { - Image(systemName: "star.fill") - .foregroundColor(.yellow) - .symbolRenderingMode(.hierarchical) - .font(.largeTitle) - .padding(.bottom, 5) + VStack(alignment: .center) { + if position.nodePosition != nil { + if position.nodePosition?.favorite ?? false { + Image(systemName: "star.fill") + .foregroundColor(.yellow) + .symbolRenderingMode(.hierarchical) + .font(.largeTitle) + .padding(.bottom, 5) + } + if position.nodePosition?.hasEnvironmentMetrics ?? false { + Image(systemName: "cloud.sun.rain") + .foregroundColor(.accentColor) + .symbolRenderingMode(.multicolor) + .font(.largeTitle) + .padding(.bottom) + } + if position.nodePosition?.hasDetectionSensorMetrics ?? false { + Image(systemName: "sensor.fill") + .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) + .symbolRenderingMode(.hierarchical) + .foregroundColor(.accentColor) + .font(.largeTitle) + .padding(.bottom) + } + BatteryGauge(node: position.nodePosition!) } - if position.nodePosition?.hasEnvironmentMetrics ?? false { - Image(systemName: "cloud.sun.rain") - .foregroundColor(.accentColor) - .symbolRenderingMode(.multicolor) - .font(.largeTitle) - .padding(.bottom) + if position.nodePosition?.hopsAway ?? 0 == 0 && !(position.nodePosition?.viaMqtt ?? false) { + LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) } - if position.nodePosition?.hasDetectionSensorMetrics ?? false { - Image(systemName: "sensor.fill") - .symbolEffect(.variableColor.reversing.cumulative, options: .repeat(20).speed(3)) - .symbolRenderingMode(.hierarchical) - .foregroundColor(.accentColor) - .font(.largeTitle) - .padding(.bottom) - } - BatteryGauge(node: position.nodePosition!) + Spacer() } - if position.nodePosition?.hopsAway ?? 0 == 0 && !(position.nodePosition?.viaMqtt ?? false) { - LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) - } - Spacer() } - } - .padding(.top) - if !popover { + .padding(.top) + if !popover { #if targetEnvironment(macCatalyst) - Spacer() - Button { - dismiss() - } label: { - Label("Close", systemImage: "xmark") - } - .buttonStyle(.bordered) - .buttonBorderShape(.capsule) - .controlSize(.large) - .padding(.bottom) + Spacer() + Button { + dismiss() + } label: { + Label("Close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) #endif + } } } - } .presentationDetents([.fraction(0.65), .large]) .presentationContentInteraction(.scrolls) .presentationDragIndicator(.visible) diff --git a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift index 689c1c4c..5fbef989 100644 --- a/Meshtastic/Views/Onboarding/DeviceOnboarding.swift +++ b/Meshtastic/Views/Onboarding/DeviceOnboarding.swift @@ -375,8 +375,8 @@ struct DeviceOnboarding: View { fallthrough } case .location: - let status = LocationsHandler.shared.manager.authorizationStatus - if status != .notDetermined && status != .restricted && status != .denied { + locationStatus = LocationsHandler.shared.manager.authorizationStatus + if locationStatus != .notDetermined && locationStatus != .restricted { navigationPath.append(.localNetwork) } case .localNetwork: diff --git a/Meshtastic/Views/Settings/AppIconPicker.swift b/Meshtastic/Views/Settings/AppIconPicker.swift index 27fbdd75..dd39e549 100644 --- a/Meshtastic/Views/Settings/AppIconPicker.swift +++ b/Meshtastic/Views/Settings/AppIconPicker.swift @@ -6,13 +6,13 @@ struct AppIconPicker: View { @Binding var isPresenting: Bool @State private var didError = false @State private var errorDetails: String? - var iconNames: [String?: String] = [nil: "Default", "AppIcon_Chirpy": "Chirpy"] + var iconNames: [String?: String] = [nil: "Default", "AppIcon_MPowered": "Meshtastic Powered", "AppIcon_Chirpy": "Chirpy", "AppIcon_Ham": "Ham"] // MARK: View var body: some View { List { Section(header: Text("Icons")) { - ForEach(Array(iconNames.sorted(by: { $0.0 ?? "1" < $1.0 ?? "1"}).enumerated()), id: \.offset) { _, icon in + ForEach(Array(iconNames.enumerated()), id: \.offset) { _, icon in AppIconButton(iconDescription: .constant(icon.value), iconName: .constant(icon.key), isPresenting: $isPresenting) } } diff --git a/Meshtastic/Views/Settings/Config/LoRaConfig.swift b/Meshtastic/Views/Settings/Config/LoRaConfig.swift index d3945522..2a145228 100644 --- a/Meshtastic/Views/Settings/Config/LoRaConfig.swift +++ b/Meshtastic/Views/Settings/Config/LoRaConfig.swift @@ -68,7 +68,6 @@ struct LoRaConfig: View { Text(r.description) } } - .fixedSize() Text("The region where you will be using your radios.") .foregroundColor(.gray) .font(.callout) diff --git a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift index 3b15fa77..ceefddaf 100644 --- a/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift +++ b/Meshtastic/Views/Settings/Config/Module/MQTTConfig.swift @@ -20,6 +20,8 @@ struct MQTTConfig: View { @State var enabled = false @State var proxyToClientEnabled = false @State var address = "" + @State var defaultServer = true + @State var showTls = true @State var username = "" @State var password = "" @State var encryptionEnabled = true @@ -196,7 +198,7 @@ struct MQTTConfig: View { .keyboardType(.default) } .autocorrectionDisabled() - if address != "mqtt.meshtastic.org" { + if !defaultServer { HStack { Label("Username", systemImage: "person.text.rectangle") TextField("Username", text: $username) @@ -235,7 +237,7 @@ struct MQTTConfig: View { .keyboardType(.default) .listRowSeparator(/*@START_MENU_TOKEN@*/.visible/*@END_MENU_TOKEN@*/) } - if !address.contains("mqtt.meshtastic.org") && !proxyToClientEnabled { + if showTls { Toggle(isOn: $tlsEnabled) { Label("TLS Enabled", systemImage: "checkmark.shield.fill") Text("Your MQTT Server must support TLS.") @@ -291,6 +293,13 @@ struct MQTTConfig: View { if address.lowercased() == "mqtt.meshtastic.org" { username = "meshdev" password = "large4cats" + defaultServer = true + if proxyToClientEnabled { + showTls = false + } + } else { + defaultServer = false + showTls = true } if newAddress != node?.mqttConfig?.address ?? "" { hasChanges = true } } @@ -316,7 +325,7 @@ struct MQTTConfig: View { if newJsonEnabled != node?.mqttConfig?.jsonEnabled { hasChanges = true } } .onChange(of: tlsEnabled) { _, newTlsEnabled in - if address.lowercased() == "mqtt.meshtastic.org" { + if defaultServer { tlsEnabled = false } else { if newTlsEnabled != node?.mqttConfig?.tlsEnabled { hasChanges = true } @@ -426,6 +435,11 @@ struct MQTTConfig: View { self.enabled = node?.mqttConfig?.enabled ?? false self.proxyToClientEnabled = node?.mqttConfig?.proxyToClientEnabled ?? false self.address = node?.mqttConfig?.address ?? "" + if address.lowercased().contains("mqtt.meshtastic.org") { + defaultServer = true + } else { + defaultServer = false + } self.username = node?.mqttConfig?.username ?? "" self.password = node?.mqttConfig?.password ?? "" self.root = node?.mqttConfig?.root ?? "msh" diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index 2c714b45..d3d15a66 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -27,7 +27,6 @@ struct Settings: View { @State private var selectedNode: Int = 0 @State private var preferredNodeNum: Int = 0 - @State private var moduleOverride: Bool = false @ObservedObject var router: Router @@ -35,7 +34,7 @@ struct Settings: View { // MARK: Helper private func isModuleSupported(_ module: ExcludedModules) -> Bool { - return moduleOverride || Int(nodes.first(where: { $0.num == preferredNodeNum })?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0 + return Int(nodes.first(where: { $0.num == preferredNodeNum })?.metadata?.excludedModules ?? Int32.zero) & module.rawValue == 0 } private func isAnySupported(_ modules: [ExcludedModules]) -> Bool { @@ -288,10 +287,6 @@ struct Settings: View { } } header: { Text("Module Configuration") - } footer: { - if moduleOverride { - Text("Currently showing modules that may not be supported by this node.") - } } } @@ -554,8 +549,6 @@ struct Settings: View { .navigationTitle("Settings") .navigationBarItems( leading: MeshtasticLogo().onLongPressGesture(minimumDuration: 1.0) { - self.moduleOverride.toggle() - UIImpactFeedbackGenerator(style: .medium).impactOccurred() } ) }