diff --git a/gen_protos.sh b/.scripts/gen_protos.sh similarity index 60% rename from gen_protos.sh rename to .scripts/gen_protos.sh index b829095f..ae079a75 100755 --- a/gen_protos.sh +++ b/.scripts/gen_protos.sh @@ -1,15 +1,13 @@ #!/bin/bash # simple sanity checking for repo -if [ ! -d "./protobufs" ]; then - echo 'Please check out the protobuf submodule by running: `git submodule update --init`' - exit +if [ ! -d "./../protobufs" ]; then + git submodule update --init fi # simple sanity checking for executable if [ ! -x "$(which protoc)" ]; then - echo 'Please install swift-protobuf by running: `brew install swift-protobuf`' - exit + brew install swift-protobuf fi protoc --proto_path=./protobufs --swift_out=./Meshtastic/Protobufs ./protobufs/meshtastic/*.proto diff --git a/thebenternify.sh b/.scripts/thebenternify.sh similarity index 100% rename from thebenternify.sh rename to .scripts/thebenternify.sh diff --git a/unthebenternify.sh b/.scripts/unthebenternify.sh similarity index 100% rename from unthebenternify.sh rename to .scripts/unthebenternify.sh diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index cb1f6722..60847f85 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -7,10 +7,24 @@ objects = { /* Begin PBXBuildFile section */ - 25183D462C0A6D97001E31D5 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25183D452C0A6D97001E31D5 /* Logger.swift */; }; + 25A978592C124FA70003AAE7 /* NodeInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */; }; + 25A978A92C12BD3F0003AAE7 /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789A2C12BD3E0003AAE7 /* WaypointEntityExtension.swift */; }; + 25A978AA2C12BD3F0003AAE7 /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789B2C12BD3E0003AAE7 /* ChannelEntityExtension.swift */; }; + 25A978AB2C12BD3F0003AAE7 /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789C2C12BD3E0003AAE7 /* MessageEntityExtension.swift */; }; + 25A978AC2C12BD3F0003AAE7 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789D2C12BD3E0003AAE7 /* TraceRouteEntityExtension.swift */; }; + 25A978AD2C12BD3F0003AAE7 /* LocationEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789E2C12BD3E0003AAE7 /* LocationEntityExtension.swift */; }; + 25A978AE2C12BD3F0003AAE7 /* RangeTestConfigEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A9789F2C12BD3E0003AAE7 /* RangeTestConfigEntityExtension.swift */; }; + 25A978AF2C12BD3F0003AAE7 /* MQTTConfigEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A02C12BD3E0003AAE7 /* MQTTConfigEntityExtension.swift */; }; + 25A978B02C12BD3F0003AAE7 /* ExternalNotificationConfigEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A12C12BD3E0003AAE7 /* ExternalNotificationConfigEntityExtension.swift */; }; + 25A978B12C12BD3F0003AAE7 /* StoreForwardConfigEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A22C12BD3E0003AAE7 /* StoreForwardConfigEntityExtension.swift */; }; + 25A978B22C12BD3F0003AAE7 /* SerialConfigEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A32C12BD3E0003AAE7 /* SerialConfigEntityExtension.swift */; }; + 25A978B32C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A42C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift */; }; + 25A978B42C12BD3F0003AAE7 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A52C12BD3F0003AAE7 /* UserEntityExtension.swift */; }; + 25A978B52C12BD3F0003AAE7 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A62C12BD3F0003AAE7 /* MyInfoEntityExtension.swift */; }; + 25A978B62C12BD3F0003AAE7 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A72C12BD3F0003AAE7 /* PositionEntityExtension.swift */; }; + 25A978B72C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25A978A82C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift */; }; 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; }; 6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; }; - 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; }; B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B399E8A32B6F486400E4488E /* RetryButton.swift */; }; B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E905B02B71F7F300654D07 /* TextMessageField.swift */; }; C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9697F9C279336B700250207 /* LocalMBTileOverlay.swift */; }; @@ -26,8 +40,6 @@ D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */; }; D9C983A02B79D0E800BDBE6A /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */; }; D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */; }; - DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */; }; - DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */; }; DD0D3D222A55CEB10066DB71 /* CocoaMQTT in Frameworks */ = {isa = PBXBuildFile; productRef = DD0D3D212A55CEB10066DB71 /* CocoaMQTT */; }; DD0E20FC2B87090400F2D100 /* atak.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20F92B87090400F2D100 /* atak.pb.swift */; }; DD0E20FD2B87090400F2D100 /* clientonly.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0E20FA2B87090400F2D100 /* clientonly.pb.swift */; }; @@ -69,8 +81,6 @@ DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4A911D2708C65400501B7E /* AppSettings.swift */; }; DD4F23CD28779A3C001D37CB /* EnvironmentMetricsLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */; }; DD5394FC276993AD00AD86B1 /* SwiftProtobuf in Frameworks */ = {isa = PBXBuildFile; productRef = DD5394FB276993AD00AD86B1 /* SwiftProtobuf */; }; - DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */; }; - DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */; }; DD5D0A9C2931B9F200F7EA61 /* EthernetModes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */; }; DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F0298EE33B00D21B61 /* admin.pb.swift */; }; DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5E51F1298EE33B00D21B61 /* config.pb.swift */; }; @@ -117,7 +127,6 @@ DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD94B73F2ACCE3BE00DCD1D1 /* MapSettingsForm.swift */; }; DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */; }; DD964FBF296E76EF007C176F /* WaypointFormMapKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */; }; - DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */; }; DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC32974767D007C176F /* MapViewFitExtension.swift */; }; DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD964FC52975DBFD007C176F /* QueryCoreData.swift */; }; DD97E96628EFD9820056DDA4 /* MeshtasticLogo.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD97E96528EFD9820056DDA4 /* MeshtasticLogo.swift */; }; @@ -130,7 +139,6 @@ DDA9515C2BC6631200CEA535 /* TelemetryEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515B2BC6631200CEA535 /* TelemetryEnums.swift */; }; DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.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 */; }; DDAF8C5326EB1DF10058C060 /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAF8C5226EB1DF10058C060 /* BLEManager.swift */; }; DDB6ABD628AE742000384BA1 /* BluetoothConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB6ABD528AE742000384BA1 /* BluetoothConfig.swift */; }; @@ -156,7 +164,6 @@ DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E15726CE248E0042C5E4 /* MeshtasticApp.swift */; }; DDC2E15C26CE248F0042C5E4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */; }; DDC2E15F26CE248F0042C5E4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */; }; - DDC2E17A26CE248F0042C5E4 /* MeshtasticUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E17926CE248F0042C5E4 /* MeshtasticUITests.swift */; }; DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */; }; DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */; }; DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC3B273283F411B00AC321C /* LastHeardText.swift */; }; @@ -167,10 +174,10 @@ DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC94FCD29CF55310082EA6E /* RtttlConfig.swift */; }; DDCDC6CB29481FCC004C1DDA /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DDCDC6CD29481FCC004C1DDA /* Localizable.strings */; }; DDCE4E2C2869F92900BE9F8F /* UserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */; }; + DDD28D3C2C0EC51D0063CFA3 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD28D3B2C0EC51D0063CFA3 /* Logger.swift */; }; DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */; }; DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD6EEAE29BC024700383354 /* Firmware.swift */; }; DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */; }; - DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */; }; DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB263E2AABEE20003AFCB7 /* NodeList.swift */; }; DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26412AABF655003AFCB7 /* NodeListItem.swift */; }; DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */; }; @@ -203,24 +210,18 @@ DDDE5A1429AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; }; DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */; }; DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */; }; - DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */; }; DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */; }; DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */; }; DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */; }; DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */; }; + DDF8E1F42C10125B0019C87E /* AppLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF8E1F32C10125B0019C87E /* AppLog.swift */; }; + DDF8E1F92C115CCE0019C87E /* LogDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF8E1F82C115CCE0019C87E /* LogDetail.swift */; }; DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; }; DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; }; DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - DDC2E17626CE248F0042C5E4 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = DDC2E14C26CE248E0042C5E4 /* Project object */; - proxyType = 1; - remoteGlobalIDString = DDC2E15326CE248E0042C5E4; - remoteInfo = MeshtasticClient; - }; DDDE5A0129AF163E00490C6C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DDC2E14C26CE248E0042C5E4 /* Project object */; @@ -245,10 +246,25 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 25183D452C0A6D97001E31D5 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; + 258EE1262C0E833D0025A5FB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeInfoExtensions.swift; sourceTree = ""; }; + 25A9789A2C12BD3E0003AAE7 /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; + 25A9789B2C12BD3E0003AAE7 /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = ""; }; + 25A9789C2C12BD3E0003AAE7 /* MessageEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; + 25A9789D2C12BD3E0003AAE7 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = ""; }; + 25A9789E2C12BD3E0003AAE7 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = ""; }; + 25A9789F2C12BD3E0003AAE7 /* RangeTestConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RangeTestConfigEntityExtension.swift; sourceTree = ""; }; + 25A978A02C12BD3E0003AAE7 /* MQTTConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTConfigEntityExtension.swift; sourceTree = ""; }; + 25A978A12C12BD3E0003AAE7 /* ExternalNotificationConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfigEntityExtension.swift; sourceTree = ""; }; + 25A978A22C12BD3E0003AAE7 /* StoreForwardConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreForwardConfigEntityExtension.swift; sourceTree = ""; }; + 25A978A32C12BD3E0003AAE7 /* SerialConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerialConfigEntityExtension.swift; sourceTree = ""; }; + 25A978A42C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMetadataEntityExtension.swift; sourceTree = ""; }; + 25A978A52C12BD3F0003AAE7 /* UserEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; + 25A978A62C12BD3F0003AAE7 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; + 25A978A72C12BD3F0003AAE7 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; + 25A978A82C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = ""; }; 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = ""; }; - 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = ""; }; A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = ""; }; B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = ""; }; @@ -265,8 +281,6 @@ D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = ""; }; D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = ""; }; - DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = ""; }; - DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = ""; }; DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = ""; }; DD0E20F92B87090400F2D100 /* atak.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = atak.pb.swift; sourceTree = ""; }; DD0E20FA2B87090400F2D100 /* clientonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = clientonly.pb.swift; sourceTree = ""; }; @@ -322,8 +336,6 @@ DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmbientLightingConfig.swift; sourceTree = ""; }; DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = ""; }; - DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = ""; }; - DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = ""; }; DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV2.xcdatamodel; sourceTree = ""; }; DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthernetModes.swift; sourceTree = ""; }; DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV7.xcdatamodel; sourceTree = ""; }; @@ -374,7 +386,6 @@ DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = ""; }; DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormMapKit.swift; sourceTree = ""; }; DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = ""; }; - DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = ""; }; DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = ""; }; DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = ""; }; DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = ""; }; @@ -390,7 +401,6 @@ DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndoorAirQuality.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 = ""; }; DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = ""; }; DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = ""; }; DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 24.xcdatamodel"; sourceTree = ""; }; @@ -422,10 +432,6 @@ DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = ""; }; DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DDC2E17026CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DDC2E17526CE248F0042C5E4 /* MeshtasticUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MeshtasticUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - DDC2E17926CE248F0042C5E4 /* MeshtasticUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticUITests.swift; sourceTree = ""; }; - DDC2E17B26CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = ""; }; DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = ""; }; @@ -441,11 +447,10 @@ DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = ""; }; DDD28D362C0CCCD10063CFA3 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = ""; }; DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = ""; }; - DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = ""; }; + DDD28D3B2C0EC51D0063CFA3 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = ""; }; DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = ""; }; DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = ""; }; - DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = ""; }; DDDB263E2AABEE20003AFCB7 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = ""; }; DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = ""; }; DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = ""; }; @@ -483,7 +488,6 @@ DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = ""; }; DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = ""; }; DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = ""; }; - DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = ""; }; DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = ""; }; DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = ""; }; DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTIcon.swift; sourceTree = ""; }; @@ -493,6 +497,8 @@ DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = ""; }; DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForwardConfig.swift; sourceTree = ""; }; DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + DDF8E1F32C10125B0019C87E /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = ""; }; + DDF8E1F82C115CCE0019C87E /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = ""; }; DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = ""; }; DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = ""; }; DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; @@ -509,13 +515,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DDC2E17226CE248F0042C5E4 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DDDE59F129AF163D00490C6C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -528,6 +527,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 25A978582C124FA70003AAE7 /* Protobufs */ = { + isa = PBXGroup; + children = ( + 25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */, + ); + path = Protobufs; + sourceTree = ""; + }; C9483F6B2773016700998F6B /* MapKitMap */ = { isa = PBXGroup; children = ( @@ -564,15 +571,21 @@ DD007BB12AA59B9A00F5FA12 /* CoreData */ = { isa = PBXGroup; children = ( - DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */, - 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */, - DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */, - DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */, - DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */, - DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */, - DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */, - DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */, - DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */, + 25A9789B2C12BD3E0003AAE7 /* ChannelEntityExtension.swift */, + 25A978A42C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift */, + 25A978A12C12BD3E0003AAE7 /* ExternalNotificationConfigEntityExtension.swift */, + 25A9789E2C12BD3E0003AAE7 /* LocationEntityExtension.swift */, + 25A9789C2C12BD3E0003AAE7 /* MessageEntityExtension.swift */, + 25A978A02C12BD3E0003AAE7 /* MQTTConfigEntityExtension.swift */, + 25A978A62C12BD3F0003AAE7 /* MyInfoEntityExtension.swift */, + 25A978A82C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift */, + 25A978A72C12BD3F0003AAE7 /* PositionEntityExtension.swift */, + 25A9789F2C12BD3E0003AAE7 /* RangeTestConfigEntityExtension.swift */, + 25A978A32C12BD3E0003AAE7 /* SerialConfigEntityExtension.swift */, + 25A978A22C12BD3E0003AAE7 /* StoreForwardConfigEntityExtension.swift */, + 25A9789D2C12BD3E0003AAE7 /* TraceRouteEntityExtension.swift */, + 25A978A52C12BD3F0003AAE7 /* UserEntityExtension.swift */, + 25A9789A2C12BD3E0003AAE7 /* WaypointEntityExtension.swift */, ); path = CoreData; sourceTree = ""; @@ -609,6 +622,7 @@ DD93800C2BA74CE3008BEC06 /* Channels */, DD97E96728EFE9A00056DDA4 /* About.swift */, DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */, + DDF8E1F32C10125B0019C87E /* AppLog.swift */, DD4A911D2708C65400501B7E /* AppSettings.swift */, DDAB580C2B0DAA9E00147258 /* Routes.swift */, DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */, @@ -794,12 +808,11 @@ DDC2E14B26CE248E0042C5E4 = { isa = PBXGroup; children = ( + 258EE1262C0E833D0025A5FB /* README.md */, DDDBC87A2BC62E4E001E8DF7 /* Settings.bundle */, DDCDC6CD29481FCC004C1DDA /* Localizable.strings */, DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */, DDC2E15626CE248E0042C5E4 /* Meshtastic */, - DDC2E16D26CE248F0042C5E4 /* MeshtasticTests */, - DDC2E17826CE248F0042C5E4 /* MeshtasticUITests */, DDDE59F729AF163D00490C6C /* Widgets */, DDC2E15526CE248E0042C5E4 /* Products */, DD8EDE9226F97A2B00A5A10B /* Frameworks */, @@ -811,7 +824,6 @@ isa = PBXGroup; children = ( DDC2E15426CE248E0042C5E4 /* Meshtastic.app */, - DDC2E17526CE248F0042C5E4 /* MeshtasticUITests.xctest */, DDDE59F429AF163D00490C6C /* WidgetsExtension.appex */, ); name = Products; @@ -847,24 +859,6 @@ path = "Preview Content"; sourceTree = ""; }; - DDC2E16D26CE248F0042C5E4 /* MeshtasticTests */ = { - isa = PBXGroup; - children = ( - DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */, - DDC2E17026CE248F0042C5E4 /* Info.plist */, - ); - path = MeshtasticTests; - sourceTree = ""; - }; - DDC2E17826CE248F0042C5E4 /* MeshtasticUITests */ = { - isa = PBXGroup; - children = ( - DDC2E17926CE248F0042C5E4 /* MeshtasticUITests.swift */, - DDC2E17B26CE248F0042C5E4 /* Info.plist */, - ); - path = MeshtasticUITests; - sourceTree = ""; - }; DDC2E18726CE24E40042C5E4 /* Views */ = { isa = PBXGroup; children = ( @@ -930,6 +924,7 @@ DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */, DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */, DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */, + DDF8E1F82C115CCE0019C87E /* LogDetail.swift */, ); path = Helpers; sourceTree = ""; @@ -947,7 +942,6 @@ DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */, DDDB443C29F6592F00EE2349 /* NetworkManager.swift */, DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */, - 25183D452C0A6D97001E31D5 /* Logger.swift */, ); path = Helpers; sourceTree = ""; @@ -985,24 +979,26 @@ DDDB443E29F79A9400EE2349 /* Extensions */ = { isa = PBXGroup; children = ( + 25A978582C124FA70003AAE7 /* Protobufs */, DD007BB12AA59B9A00F5FA12 /* CoreData */, + DDFFA7462B3A7F3C004730DB /* Bundle.swift */, DDDB444529F8A96500EE2349 /* Character.swift */, DDDB444929F8AA3A00EE2349 /* CLLocationCoordinate2D.swift */, DDDB444B29F8AAA600EE2349 /* Color.swift */, DDDB445329F8AD1600EE2349 /* Data.swift */, DDDB445129F8ACF900EE2349 /* Date.swift */, DDDB444129F8A88700EE2349 /* Double.swift */, + DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDDB444329F8A8DD00EE2349 /* Float.swift */, DDDB444D29F8AB0E00EE2349 /* Int.swift */, + DDD28D3B2C0EC51D0063CFA3 /* Logger.swift */, + DD1933772B084F4200771CD5 /* Measurement.swift */, DDDB444729F8A9C900EE2349 /* String.swift */, + DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, DD77093E2AA1B146007A8BF0 /* UIColor.swift */, DDDB444F29F8AC9C00EE2349 /* UIImage.swift */, DDDB443F29F79AB000EE2349 /* UserDefaults.swift */, - DDB75A0E2A05920E006ED576 /* FileManager.swift */, DDB75A102A059258006ED576 /* Url.swift */, - DD1933772B084F4200771CD5 /* Measurement.swift */, - DDFFA7462B3A7F3C004730DB /* Bundle.swift */, - DDF45C362BC46A5A005ED5F2 /* TimeZone.swift */, ); path = Extensions; sourceTree = ""; @@ -1037,11 +1033,11 @@ isa = PBXNativeTarget; buildConfigurationList = DDC2E17E26CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "Meshtastic" */; buildPhases = ( - BB450974275599CE00509624 /* ShellScript */, DDC2E15026CE248E0042C5E4 /* Sources */, DDC2E15126CE248E0042C5E4 /* Frameworks */, DDC2E15226CE248E0042C5E4 /* Resources */, DDDE5A0829AF163F00490C6C /* Embed Foundation Extensions */, + DDF8E1F52C10C84D0019C87E /* ShellScript */, ); buildRules = ( ); @@ -1058,24 +1054,6 @@ productReference = DDC2E15426CE248E0042C5E4 /* Meshtastic.app */; productType = "com.apple.product-type.application"; }; - DDC2E17426CE248F0042C5E4 /* MeshtasticUITests */ = { - isa = PBXNativeTarget; - buildConfigurationList = DDC2E18426CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "MeshtasticUITests" */; - buildPhases = ( - DDC2E17126CE248F0042C5E4 /* Sources */, - DDC2E17226CE248F0042C5E4 /* Frameworks */, - DDC2E17326CE248F0042C5E4 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - DDC2E17726CE248F0042C5E4 /* PBXTargetDependency */, - ); - name = MeshtasticUITests; - productName = MeshtasticClientUITests; - productReference = DDC2E17526CE248F0042C5E4 /* MeshtasticUITests.xctest */; - productType = "com.apple.product-type.bundle.ui-testing"; - }; DDDE59F329AF163D00490C6C /* WidgetsExtension */ = { isa = PBXNativeTarget; buildConfigurationList = DDDE5A0529AF163F00490C6C /* Build configuration list for PBXNativeTarget "WidgetsExtension" */; @@ -1107,10 +1085,6 @@ CreatedOnToolsVersion = 12.5.1; LastSwiftMigration = 1340; }; - DDC2E17426CE248F0042C5E4 = { - CreatedOnToolsVersion = 12.5.1; - TestTargetID = DDC2E15326CE248E0042C5E4; - }; DDDE59F329AF163D00490C6C = { CreatedOnToolsVersion = 14.2; }; @@ -1137,13 +1111,13 @@ DD5394FA276993AD00AD86B1 /* XCRemoteSwiftPackageReference "swift-protobuf" */, C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */, DD0D3D202A55CEB10066DB71 /* XCRemoteSwiftPackageReference "CocoaMQTT" */, + 258EE1212C0E81AE0025A5FB /* XCRemoteSwiftPackageReference "SwiftLint" */, ); productRefGroup = DDC2E15526CE248E0042C5E4 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( DDC2E15326CE248E0042C5E4 /* Meshtastic */, - DDC2E17426CE248F0042C5E4 /* MeshtasticUITests */, DDDE59F329AF163D00490C6C /* WidgetsExtension */, ); }; @@ -1164,13 +1138,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DDC2E17326CE248F0042C5E4 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; DDDE59F229AF163D00490C6C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1182,10 +1149,10 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - BB450974275599CE00509624 /* ShellScript */ = { + DDF8E1F52C10C84D0019C87E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 2147483647; + buildActionMask = 8; files = ( ); inputFileListPaths = ( @@ -1196,9 +1163,9 @@ ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]\nthen\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif command -v swiftlint >/dev/null 2>&1\nthen\n swiftlint\nelse\n echo \"warning: `swiftlint` command not found - See https://github.com/realm/SwiftLint#installation for installation instructions.\"\nfi\n"; + shellScript = "defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -1226,7 +1193,6 @@ DD5E5203298EE33B00D21B61 /* config.pb.swift in Sources */, DD798B072915928D005217CD /* ChannelMessageList.swift in Sources */, DDC2E1A726CEB3400042C5E4 /* LocationHelper.swift in Sources */, - DD5394FE276BA0EF00AD86B1 /* PositionEntityExtension.swift in Sources */, DD77093D2AA1AFA3007A8BF0 /* ChannelTips.swift in Sources */, DD913639270DFF4C00D7ACF3 /* LocalNotificationManager.swift in Sources */, DDDB444C29F8AAA600EE2349 /* Color.swift in Sources */, @@ -1244,12 +1210,15 @@ DDDB445229F8ACF900EE2349 /* Date.swift in Sources */, DDC4D568275499A500A4208E /* Persistence.swift in Sources */, DDD6EEAF29BC024700383354 /* Firmware.swift in Sources */, + 25A978B72C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift in Sources */, DD77093B2AA1ABB8007A8BF0 /* BluetoothTips.swift in Sources */, + 25A978B22C12BD3F0003AAE7 /* SerialConfigEntityExtension.swift in Sources */, DD90860E26F69BAE00DC5189 /* NodeMap.swift in Sources */, D9C9839D2B79CFD700BDBE6A /* TextMessageSize.swift in Sources */, DDC94FCE29CF55310082EA6E /* RtttlConfig.swift in Sources */, DD964FBD296E6B01007C176F /* EmojiOnlyTextField.swift in Sources */, DD8169FF272476C700F4AB02 /* LogDocument.swift in Sources */, + 25A978B32C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift in Sources */, DDC94FC129CE063B0082EA6E /* BatteryLevel.swift in Sources */, DD354FD92BD96A0B0061A25F /* IAQScale.swift in Sources */, DDDB445429F8AD1600EE2349 /* Data.swift in Sources */, @@ -1257,9 +1226,12 @@ DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */, DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */, DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */, + 25A978B02C12BD3F0003AAE7 /* ExternalNotificationConfigEntityExtension.swift in Sources */, + 25A978AA2C12BD3F0003AAE7 /* ChannelEntityExtension.swift in Sources */, DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */, DD6193792863875F00E59241 /* SerialConfig.swift in Sources */, DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */, + DDF8E1F92C115CCE0019C87E /* LogDetail.swift in Sources */, DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */, DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */, B399E8A42B6F486400E4488E /* RetryButton.swift in Sources */, @@ -1269,7 +1241,7 @@ DD5E5209298EE33B00D21B61 /* module_config.pb.swift in Sources */, DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */, DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */, - 6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */, + 25A978B52C12BD3F0003AAE7 /* MyInfoEntityExtension.swift in Sources */, DDDB444229F8A88700EE2349 /* Double.swift in Sources */, DD5E520F298EE33B00D21B61 /* cannedmessages.pb.swift in Sources */, DDF45C342BC1A48E005ED5F2 /* MQTTIcon.swift in Sources */, @@ -1283,17 +1255,18 @@ DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */, DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */, DDF45C372BC46A5A005ED5F2 /* TimeZone.swift in Sources */, - DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */, DD33DB622B3D27C7003E1EA0 /* FirmwareApi.swift in Sources */, DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */, + 25A978AC2C12BD3F0003AAE7 /* TraceRouteEntityExtension.swift in Sources */, DD1BF2F92776FE2E008C8D2F /* UserMessageList.swift in Sources */, DD5E5207298EE33B00D21B61 /* connection_status.pb.swift in Sources */, DD3CC6C228EB9D4900FA9159 /* UpdateCoreData.swift in Sources */, DDE0F7C5295F77B700B8AAB3 /* AppSettingsEnums.swift in Sources */, + 25A978AD2C12BD3F0003AAE7 /* LocationEntityExtension.swift in Sources */, DDB6ABE628B1406100384BA1 /* LoraConfigEnums.swift in Sources */, DDB8F4142A9EE5F000230ECE /* ChannelList.swift in Sources */, DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */, - DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */, + 25A978B12C12BD3F0003AAE7 /* StoreForwardConfigEntityExtension.swift in Sources */, DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */, DD0E20FC2B87090400F2D100 /* atak.pb.swift in Sources */, DDDB444629F8A96500EE2349 /* Character.swift in Sources */, @@ -1301,20 +1274,18 @@ DDB6ABDB28B0AC6000384BA1 /* DistanceText.swift in Sources */, DD5E520D298EE33B00D21B61 /* storeforward.pb.swift in Sources */, DD94B7402ACCE3BE00DCD1D1 /* MapSettingsForm.swift in Sources */, - DD964FC2297272AE007C176F /* WaypointEntityExtension.swift in Sources */, + 25A978AF2C12BD3F0003AAE7 /* MQTTConfigEntityExtension.swift in Sources */, 6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */, D93068DB2B81C85E0066FBC8 /* PowerConfig.swift in Sources */, D93068D32B8129510066FBC8 /* MessageContextMenuItems.swift in Sources */, DD0E20FE2B87090400F2D100 /* paxcount.pb.swift in Sources */, DD5E520A298EE33B00D21B61 /* channel.pb.swift in Sources */, - 25183D462C0A6D97001E31D5 /* Logger.swift in Sources */, DD8EBF43285058FA00426DCA /* DisplayConfig.swift in Sources */, DD964FC42974767D007C176F /* MapViewFitExtension.swift in Sources */, DD47E3D626F17ED900029299 /* CircleText.swift in Sources */, DDC2E18F26CE25FE0042C5E4 /* ContentView.swift in Sources */, DD2553572855B02500E55709 /* LoRaConfig.swift in Sources */, DDB6ABD928B0A4BA00384BA1 /* BluetoothModes.swift in Sources */, - DDD9E4E4284B208E003777C5 /* UserEntityExtension.swift in Sources */, DD2553592855B52700E55709 /* PositionConfig.swift in Sources */, DD97E96828EFE9A00056DDA4 /* About.swift in Sources */, DDDB444029F79AB000EE2349 /* UserDefaults.swift in Sources */, @@ -1326,11 +1297,15 @@ DD8ED9C8289CE4B900B3B0AB /* RoutingError.swift in Sources */, DD5E5202298EE33B00D21B61 /* admin.pb.swift in Sources */, DDC1B81A2AB5377B00C71E39 /* MessagesTips.swift in Sources */, + DDF8E1F42C10125B0019C87E /* AppLog.swift in Sources */, DD964FC62975DBFD007C176F /* QueryCoreData.swift in Sources */, DDB75A112A059258006ED576 /* Url.swift in Sources */, DDAD49ED2AFB39DC00B4425D /* MeshMap.swift in Sources */, + 25A978A92C12BD3F0003AAE7 /* WaypointEntityExtension.swift in Sources */, DD8169FB271F1F3A00F4AB02 /* MeshLog.swift in Sources */, + 25A978AB2C12BD3F0003AAE7 /* MessageEntityExtension.swift in Sources */, DD1B8F402B35E2F10022AABC /* GPSStatus.swift in Sources */, + 25A978B42C12BD3F0003AAE7 /* UserEntityExtension.swift in Sources */, DD8ED9C52898D51F00B3B0AB /* NetworkConfig.swift in Sources */, DDC3B274283F411B00AC321C /* LastHeardText.swift in Sources */, DDDE5A1029AFE69700490C6C /* MeshActivityAttributes.swift in Sources */, @@ -1350,6 +1325,7 @@ DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */, D93068D92B81509C0066FBC8 /* TapbackResponses.swift in Sources */, DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */, + DDD28D3C2C0EC51D0063CFA3 /* Logger.swift in Sources */, DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */, DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */, DDB6ABE228B13FB500384BA1 /* PositionConfigEnums.swift in Sources */, @@ -1368,12 +1344,12 @@ DDAB580D2B0DAA9E00147258 /* Routes.swift in Sources */, C9697F9D279336B700250207 /* LocalMBTileOverlay.swift in Sources */, D93068D52B812B700066FBC8 /* MessageDestination.swift in Sources */, - DD58C5F22919AD3C00D5BEFB /* ChannelEntityExtension.swift in Sources */, DDA9515E2BC6F56F00CEA535 /* IndoorAirQuality.swift in Sources */, DDDB444E29F8AB0E00EE2349 /* Int.swift in Sources */, DD0F791B28713C8A00A6FDAD /* AdminMessageList.swift in Sources */, DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */, DDC4C9FF2A8D982900CE201C /* DetectionSensorConfig.swift in Sources */, + 25A978592C124FA70003AAE7 /* NodeInfoExtensions.swift in Sources */, D9C983A22B79D1A600BDBE6A /* RequestPositionButton.swift in Sources */, DDDB26442AAC0206003AFCB7 /* NodeDetail.swift in Sources */, DD5E5210298EE33B00D21B61 /* telemetry.pb.swift in Sources */, @@ -1382,27 +1358,19 @@ DDF6B2482A9AEBF500BA6931 /* StoreForwardConfig.swift in Sources */, DD8169F9271F1A6100F4AB02 /* MeshLogger.swift in Sources */, DD93800B2BA3F968008BEC06 /* NodeMapContent.swift in Sources */, + 25A978AE2C12BD3F0003AAE7 /* RangeTestConfigEntityExtension.swift in Sources */, DD41582A28585C32009B0E59 /* RangeTestConfig.swift in Sources */, DD1925B728CDA5A400720036 /* CannedMessagesConfigEnums.swift in Sources */, DDDB444429F8A8DD00EE2349 /* Float.swift in Sources */, - DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */, + 25A978B62C12BD3F0003AAE7 /* PositionEntityExtension.swift in Sources */, B3E905B12B71F7F300654D07 /* TextMessageField.swift in Sources */, DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */, D93068D72B8146690066FBC8 /* MessageText.swift in Sources */, DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */, - DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */, DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - DDC2E17126CE248F0042C5E4 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DDC2E17A26CE248F0042C5E4 /* MeshtasticUITests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; DDDE59F029AF163D00490C6C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1417,11 +1385,6 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - DDC2E17726CE248F0042C5E4 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = DDC2E15326CE248E0042C5E4 /* Meshtastic */; - targetProxy = DDC2E17626CE248F0042C5E4 /* PBXContainerItemProxy */; - }; DDDE5A0229AF163E00490C6C /* PBXTargetDependency */ = { isa = PBXTargetDependency; platformFilter = ios; @@ -1507,6 +1470,7 @@ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -1564,6 +1528,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=200"; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -1583,7 +1548,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 959; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1595,7 +1560,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1617,7 +1582,7 @@ CODE_SIGN_ENTITLEMENTS = Meshtastic/Meshtastic.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 959; DEVELOPMENT_ASSET_PATHS = "\"Meshtastic/Preview Content\""; DEVELOPMENT_TEAM = GCH7VS5Y9R; ENABLE_PREVIEWS = YES; @@ -1629,7 +1594,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.11; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; @@ -1639,48 +1604,6 @@ }; name = Release; }; - DDC2E18526CE248F0042C5E4 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = GCH7VS5Y9R; - INFOPLIST_FILE = MeshtasticUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Meshtastic; - }; - name = Debug; - }; - DDC2E18626CE248F0042C5E4 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = GCH7VS5Y9R; - INFOPLIST_FILE = MeshtasticUITests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 16.2; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticUITests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_TARGET_NAME = Meshtastic; - }; - name = Release; - }; DDDE5A0629AF163F00490C6C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1702,7 +1625,8 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.11; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1735,7 +1659,8 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 2.3.10; + MARKETING_VERSION = 2.3.11; + OTHER_SWIFT_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1768,15 +1693,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DDC2E18426CE248F0042C5E4 /* Build configuration list for PBXNativeTarget "MeshtasticUITests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DDC2E18526CE248F0042C5E4 /* Debug */, - DDC2E18626CE248F0042C5E4 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; DDDE5A0529AF163F00490C6C /* Build configuration list for PBXNativeTarget "WidgetsExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1789,6 +1705,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 258EE1212C0E81AE0025A5FB /* XCRemoteSwiftPackageReference "SwiftLint" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/realm/SwiftLint"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.55.1; + }; + }; C9697FA327933B8C00250207 /* XCRemoteSwiftPackageReference "SQLite.swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stephencelis/SQLite.swift.git"; diff --git a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a62998be..0a15919c 100644 --- a/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Meshtastic.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "e9855e3a299c14a10f11ee0b8f29e4170b09548533939361223a0f50e7caac8c", + "originHash" : "32ea1ad7873163554215310b8eea710c94c63f855b5b01c0b790e7b537747ceb", "pins" : [ { "identity" : "cocoamqtt", @@ -10,6 +10,24 @@ "version" : "2.1.5" } }, + { + "identity" : "collectionconcurrencykit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", + "state" : { + "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", + "version" : "0.2.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", + "version" : "1.8.2" + } + }, { "identity" : "mqttcocoaasyncsocket", "kind" : "remoteSourceControl", @@ -19,6 +37,15 @@ "version" : "1.0.8" } }, + { + "identity" : "sourcekitten", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/SourceKitten.git", + "state" : { + "revision" : "fd4df99170f5e9d7cf9aa8312aa8506e0e7a44e7", + "version" : "0.35.0" + } + }, { "identity" : "sqlite.swift", "kind" : "remoteSourceControl", @@ -37,6 +64,15 @@ "version" : "3.1.2" } }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" + } + }, { "identity" : "swift-protobuf", "kind" : "remoteSourceControl", @@ -45,6 +81,51 @@ "revision" : "ce20dc083ee485524b802669890291c0d8090170", "version" : "1.22.1" } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" + } + }, + { + "identity" : "swiftlint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/realm/SwiftLint", + "state" : { + "revision" : "b515723b16eba33f15c4677ee65f3fef2ce8c255", + "version" : "0.55.1" + } + }, + { + "identity" : "swiftytexttable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", + "state" : { + "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", + "version" : "0.9.0" + } + }, + { + "identity" : "swxmlhash", + "kind" : "remoteSourceControl", + "location" : "https://github.com/drmohundro/SWXMLHash.git", + "state" : { + "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", + "version" : "7.0.2" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", + "version" : "5.1.2" + } } ], "version" : 3 diff --git a/Meshtastic/Export/LogDocument.swift b/Meshtastic/Export/LogDocument.swift index 0732d189..fb098f07 100644 --- a/Meshtastic/Export/LogDocument.swift +++ b/Meshtastic/Export/LogDocument.swift @@ -11,12 +11,10 @@ struct LogDocument: FileDocument { } init(configuration: ReadConfiguration) throws { - guard let data = configuration.file.regularFileContents, - let string = String(data: data, encoding: .utf8) - else { + guard let data = configuration.file.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } - logFile = string + logFile = String(decoding: data, as: UTF8.self) } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { diff --git a/Meshtastic/Export/WriteCsvFile.swift b/Meshtastic/Export/WriteCsvFile.swift index 521dc937..56776554 100644 --- a/Meshtastic/Export/WriteCsvFile.swift +++ b/Meshtastic/Export/WriteCsvFile.swift @@ -6,6 +6,7 @@ // import SwiftUI +import OSLog func telemetryToCsvFile(telemetry: [TelemetryEntity], metricsType: Int) -> String { var csvString: String = "" @@ -64,6 +65,27 @@ func detectionsToCsv(detections: [MessageEntity]) -> String { return csvString } +func logToCsvFile(log: [OSLogEntryLog]) -> String { + var csvString: String = "" + let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) + let dateFormatString = (localeDateFormat ?? "MM/dd/YY j:mma").replacingOccurrences(of: ",", with: "") + // Create PAX Header + csvString = "Process, Category, Level, Message, \("timestamp".localized)" + for l in log { + csvString += "\n" + csvString += String(l.process) + csvString += ", " + csvString += String(l.category) + csvString += ", " + csvString += String(l.level.description) + csvString += ", " + csvString += String(l.composedMessage) + csvString += ", " + csvString += l.date.formattedDate(format: dateFormatString) + } + return csvString +} + func paxToCsvFile(pax: [PaxCounterEntity]) -> String { var csvString: String = "" let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMddjmma", options: 0, locale: Locale.current) diff --git a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift index 2568943a..84ef3ece 100644 --- a/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/ChannelEntityExtension.swift @@ -5,9 +5,50 @@ // Copyright(c) Garth Vander Houwen 11/7/22. // import Foundation +import CoreData extension ChannelEntity { + convenience init( + context: NSManagedObjectContext, + id: Int32, + index: Int32, + uplinkEnabled: Bool, + downlinkEnabled: Bool, + name: String?, + role: Int32, + psk: Data, + positionPrecision: Int32 + ) { + self.init(context: context) + self.id = id + self.index = index + self.uplinkEnabled = uplinkEnabled + self.downlinkEnabled = downlinkEnabled + self.name = name + self.role = role + self.psk = psk + self.positionPrecision = positionPrecision + } + + convenience init( + context: NSManagedObjectContext, + channel: Channel + ) { + self.init(context: context) + self.id = Int32(channel.index) + self.index = Int32(channel.index) + self.uplinkEnabled = channel.settings.uplinkEnabled + self.downlinkEnabled = channel.settings.downlinkEnabled + self.name = channel.settings.name + self.role = Int32(channel.role.rawValue) + self.psk = channel.settings.psk + if channel.settings.hasModuleSettings { + self.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) + self.mute = channel.settings.moduleSettings.isClientMuted + } + } + var allPrivateMessages: [MessageEntity] { self.value(forKey: "allPrivateMessages") as? [MessageEntity] ?? [MessageEntity]() diff --git a/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift b/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift new file mode 100644 index 00000000..cc0bd37f --- /dev/null +++ b/Meshtastic/Extensions/CoreData/DeviceMetadataEntityExtension.swift @@ -0,0 +1,24 @@ +import Foundation +import CoreData + +extension DeviceMetadataEntity { + convenience init( + context: NSManagedObjectContext, + metadata: DeviceMetadata + ) { + self.init(context: context) + self.time = Date() + self.deviceStateVersion = Int32(metadata.deviceStateVersion) + self.canShutdown = metadata.canShutdown + self.hasWifi = metadata.hasWifi_p + self.hasBluetooth = metadata.hasBluetooth_p + self.hasEthernet = metadata.hasEthernet_p + self.role = Int32(metadata.role.rawValue) + self.positionFlags = Int32(metadata.positionFlags) + // Swift does strings weird, this does work to get the version without the github hash + let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") + var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] + version = version.dropLast() + self.firmwareVersion = String(version) + } +} diff --git a/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift new file mode 100644 index 00000000..6baa3213 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/ExternalNotificationConfigEntityExtension.swift @@ -0,0 +1,43 @@ +import CoreData + +extension ExternalNotificationConfigEntity { + convenience init( + context: NSManagedObjectContext, + config: ModuleConfig.ExternalNotificationConfig + ) { + self.init(context: context) + self.enabled = config.enabled + self.usePWM = config.usePwm + self.alertBell = config.alertBell + self.alertBellBuzzer = config.alertBellBuzzer + self.alertBellVibra = config.alertBellVibra + self.alertMessage = config.alertMessage + self.alertMessageBuzzer = config.alertMessageBuzzer + self.alertMessageVibra = config.alertMessageVibra + self.active = config.active + self.output = Int32(config.output) + self.outputBuzzer = Int32(config.outputBuzzer) + self.outputVibra = Int32(config.outputVibra) + self.outputMilliseconds = Int32(config.outputMs) + self.nagTimeout = Int32(config.nagTimeout) + self.useI2SAsBuzzer = config.useI2SAsBuzzer + } + + func update(with config: ModuleConfig.ExternalNotificationConfig) { + enabled = config.enabled + usePWM = config.usePwm + alertBell = config.alertBell + alertBellBuzzer = config.alertBellBuzzer + alertBellVibra = config.alertBellVibra + alertMessage = config.alertMessage + alertMessageBuzzer = config.alertMessageBuzzer + alertMessageVibra = config.alertMessageVibra + active = config.active + output = Int32(config.output) + outputBuzzer = Int32(config.outputBuzzer) + outputVibra = Int32(config.outputVibra) + outputMilliseconds = Int32(config.outputMs) + nagTimeout = Int32(config.nagTimeout) + useI2SAsBuzzer = config.useI2SAsBuzzer + } +} diff --git a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift index 5b35f620..d66ec017 100644 --- a/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/LocationEntityExtension.swift @@ -11,6 +11,22 @@ import MapKit import SwiftUI extension LocationEntity { + + convenience init( + context: NSManagedObjectContext, + route: RouteEntity, + id: Int32, + location: CLLocation + ) { + self.init(context: context) + self.routeLocation = route + self.id = id + self.altitude = Int32(location.altitude) + self.heading = Int32(location.course) + self.speed = Int32(location.speed) + self.latitudeI = Int32(location.coordinate.latitude * 1e7) + self.longitudeI = Int32(location.coordinate.longitude * 1e7) + } var latitude: Double? { diff --git a/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift new file mode 100644 index 00000000..32744db5 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/MQTTConfigEntityExtension.swift @@ -0,0 +1,37 @@ +import CoreData + +extension MQTTConfigEntity { + convenience init( + context: NSManagedObjectContext, + config: ModuleConfig.MQTTConfig + ) { + self.init(context: context) + self.enabled = config.enabled + self.proxyToClientEnabled = config.proxyToClientEnabled + self.address = config.address + self.username = config.username + self.password = config.password + self.root = config.root + self.encryptionEnabled = config.encryptionEnabled + self.jsonEnabled = config.jsonEnabled + self.tlsEnabled = config.tlsEnabled + self.mapReportingEnabled = config.mapReportingEnabled + self.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + self.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) + } + + func update(with config: ModuleConfig.MQTTConfig) { + enabled = config.enabled + proxyToClientEnabled = config.proxyToClientEnabled + address = config.address + username = config.username + password = config.password + root = config.root + encryptionEnabled = config.encryptionEnabled + jsonEnabled = config.jsonEnabled + tlsEnabled = config.tlsEnabled + mapReportingEnabled = config.mapReportingEnabled + mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) + mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) + } +} diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index 9258b424..935e35a8 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -9,6 +9,15 @@ import Foundation import CoreData extension NodeInfoEntity { + convenience init( + context: NSManagedObjectContext, + num: Int + ) { + self.init(context: context) + self.id = Int64(num) + self.num = Int64(num) + self.user = UserEntity(context: context, num: num) + } var hasPositions: Bool { return positions?.count ?? 0 > 0 @@ -47,20 +56,3 @@ extension NodeInfoEntity { return false } } - -public func createNodeInfo(num: Int64, context: NSManagedObjectContext) -> NodeInfoEntity { - - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(num) - newNode.num = Int64(num) - let newUser = UserEntity(context: context) - newUser.num = Int64(num) - let userId = String(format: "%2X", num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - newNode.user = newUser - return newNode -} diff --git a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift index 0c634779..37261ed6 100644 --- a/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/PositionEntityExtension.swift @@ -11,7 +11,22 @@ import MapKit import SwiftUI extension PositionEntity { - + convenience init( + context: NSManagedObjectContext, + nodeInfo: NodeInfo + ) { + self.init(context: context) + self.latest = true + self.seqNo = Int32(nodeInfo.position.seqNumber) + self.latitudeI = nodeInfo.position.latitudeI + self.longitudeI = nodeInfo.position.longitudeI + self.altitude = nodeInfo.position.altitude + self.satsInView = Int32(nodeInfo.position.satsInView) + self.speed = Int32(nodeInfo.position.groundSpeed) + self.heading = Int32(nodeInfo.position.groundTrack) + self.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) + } + static func allPositionsFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = PositionEntity.fetchRequest() request.fetchLimit = 1000 diff --git a/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift new file mode 100644 index 00000000..9be459be --- /dev/null +++ b/Meshtastic/Extensions/CoreData/RangeTestConfigEntityExtension.swift @@ -0,0 +1,19 @@ +import CoreData + +extension RangeTestConfigEntity { + convenience init( + context: NSManagedObjectContext, + config: ModuleConfig.RangeTestConfig + ) { + self.init(context: context) + self.sender = Int32(config.sender) + self.enabled = config.enabled + self.save = config.save + } + + func update(with config: ModuleConfig.RangeTestConfig) { + sender = Int32(config.sender) + enabled = config.enabled + save = config.save + } +} diff --git a/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift new file mode 100644 index 00000000..ac114909 --- /dev/null +++ b/Meshtastic/Extensions/CoreData/SerialConfigEntityExtension.swift @@ -0,0 +1,27 @@ +import CoreData + +extension SerialConfigEntity { + convenience init( + context: NSManagedObjectContext, + config: ModuleConfig.SerialConfig + ) { + self.init(context: context) + self.enabled = config.enabled + self.echo = config.echo + self.rxd = Int32(config.rxd) + self.txd = Int32(config.txd) + self.baudRate = Int32(config.baud.rawValue) + self.timeout = Int32(config.timeout) + self.mode = Int32(config.mode.rawValue) + } + + func update(with config: ModuleConfig.SerialConfig) { + enabled = config.enabled + echo = config.echo + rxd = Int32(config.rxd) + txd = Int32(config.txd) + baudRate = Int32(config.baud.rawValue) + timeout = Int32(config.timeout) + mode = Int32(config.mode.rawValue) + } +} diff --git a/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift b/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift new file mode 100644 index 00000000..2951bb8a --- /dev/null +++ b/Meshtastic/Extensions/CoreData/StoreForwardConfigEntityExtension.swift @@ -0,0 +1,23 @@ +import CoreData + +extension StoreForwardConfigEntity { + convenience init( + context: NSManagedObjectContext, + config: ModuleConfig.StoreForwardConfig + ) { + self.init(context: context) + self.enabled = config.enabled + self.heartbeat = config.heartbeat + self.records = Int32(config.records) + self.historyReturnMax = Int32(config.historyReturnMax) + self.historyReturnWindow = Int32(config.historyReturnWindow) + } + + func update(with config: ModuleConfig.StoreForwardConfig) { + enabled = config.enabled + heartbeat = config.heartbeat + records = Int32(config.records) + historyReturnMax = Int32(config.historyReturnMax) + historyReturnWindow = Int32(config.historyReturnWindow) + } +} diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index a8adeaaf..886f90ca 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -9,6 +9,31 @@ import Foundation import CoreData extension UserEntity { + convenience init( + context: NSManagedObjectContext, + user: User, + num: Int + ) { + self.init(context: context) + self.userId = user.id + self.num = Int64(num) + self.longName = user.longName + self.shortName = user.shortName + self.hwModel = String(describing: user.hwModel).uppercased() + self.isLicensed = user.isLicensed + self.role = Int32(user.role.rawValue) + } + + convenience init(context: NSManagedObjectContext, num: Int) { + self.init(context: context) + self.num = Int64(num) + let userId = String(format: "!%2X", num) + self.userId = userId + let last4 = String(userId.suffix(4)) + self.longName = "Meshtastic \(last4)" + self.shortName = last4 + self.hwModel = "UNSET" + } var messageList: [MessageEntity] { self.value(forKey: "allMessages") as? [MessageEntity] ?? [MessageEntity]() @@ -27,15 +52,3 @@ extension UserEntity { return unreadMessages.count } } - -public func createUser(num: Int64, context: NSManagedObjectContext) -> UserEntity { - let newUser = UserEntity(context: context) - newUser.num = Int64(num) - let userId = String(format: "%2X", num) - newUser.userId = "!\(userId)" - let last4 = String(userId.suffix(4)) - newUser.longName = "Meshtastic \(last4)" - newUser.shortName = last4 - newUser.hwModel = "UNSET" - return newUser -} diff --git a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift index b17ccecf..96b5ed5f 100644 --- a/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/WaypointEntityExtension.swift @@ -10,6 +10,19 @@ import MapKit import SwiftUI extension WaypointEntity { + + convenience init( + context: NSManagedObjectContext, + coordinate: CLLocationCoordinate2D + ) { + self.init(context: context) + self.id = 0 + self.name = "Waypoint Pin" + self.expire = Date.now.addingTimeInterval(60 * 480) + self.latitudeI = Int32(coordinate.latitude * 1e7) + self.longitudeI = Int32(coordinate.longitude * 1e7) + self.expire = Date.now.addingTimeInterval(60 * 480) + } static func allWaypointssFetchRequest() -> NSFetchRequest { let request: NSFetchRequest = WaypointEntity.fetchRequest() diff --git a/Meshtastic/Extensions/Logger.swift b/Meshtastic/Extensions/Logger.swift new file mode 100644 index 00000000..c932d4df --- /dev/null +++ b/Meshtastic/Extensions/Logger.swift @@ -0,0 +1,68 @@ +// +// Logger.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/3/24. +// + +import OSLog + +extension Logger { + + /// The logger's subsystem. + private static var subsystem = Bundle.main.bundleIdentifier! + + /// All logs related to data such as decoding error, parsing issues, etc. + static let data = Logger(subsystem: subsystem, category: "🗄️ Data") + + /// All logs related to the mesh + static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh") + + /// All admin messages + static let admin = Logger(subsystem: subsystem, category: "🏛 Admin") + + /// All logs related to services such as network calls, location, etc. + static let services = Logger(subsystem: subsystem, category: "🍏 Services") + + /// All logs related to tracking and analytics. + static let statistics = Logger(subsystem: subsystem, category: "📊 Stats") + + /// Fetch from the logstore + static public func fetch(predicateFormat: String) async throws -> [OSLogEntryLog] { + + let store = try OSLogStore(scope: .currentProcessIdentifier) + let position = store.position(timeIntervalSinceLatestBoot: 0) + let calendar = Calendar.current + //let dayAgo = calendar.date(byAdding: .day, value: -1, to: Date.now) + //let position = store.position(date: dayAgo!) + let predicate = NSPredicate(format: predicateFormat) + let entries = try store.getEntries(at: position, matching: predicate) + + var logs: [OSLogEntryLog] = [] + for entry in entries { + + try Task.checkCancellation() + + if let log = entry as? OSLogEntryLog { + logs.append(log) + } + } + + if logs.isEmpty { logs = [] } + return logs + } +} + +extension OSLogEntryLog.Level { + var description: String { + switch self { + case .undefined: "undefined" + case .debug: "🩺 Debug" + case .info: "ℹ️ Info" + case .notice: "⚠️ Notice" + case .error: "🚨 Error" + case .fault: "💥 Fault" + @unknown default: "default" + } + } +} diff --git a/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift b/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift new file mode 100644 index 00000000..ff20c78a --- /dev/null +++ b/Meshtastic/Extensions/Protobufs/NodeInfoExtensions.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NodeInfo { + var isValidPosition: Bool { + hasPosition && + position.longitudeI != 0 && + position.latitudeI != 0 && + position.latitudeI != 373346000 && + position.longitudeI != -1220090000 + } +} diff --git a/Meshtastic/Helpers/BLEManager.swift b/Meshtastic/Helpers/BLEManager.swift index caab235d..3ac78995 100644 --- a/Meshtastic/Helpers/BLEManager.swift +++ b/Meshtastic/Helpers/BLEManager.swift @@ -62,7 +62,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func startScanning() { if isSwitchedOn { centralManager.scanForPeripherals(withServices: [meshtasticServiceCBUUID], options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]) - Logger.services.info("✅ Scanning Started") + Logger.services.info("🟢 Started Scanning for BLE Devices") } } @@ -70,7 +70,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate func stopScanning() { if centralManager.isScanning { centralManager.stopScan() - Logger.services.info("🛑 Stopped Scanning") + Logger.services.info("🛑 Stopped Scanning for BLE Devices") } } @@ -130,7 +130,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate let context = ["name": "\(peripheral.name ?? "Unknown")"] timeoutTimer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(timeoutTimerFired), userInfo: context, repeats: true) RunLoop.current.add(timeoutTimer!, forMode: .common) - Logger.services.info("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown")") + Logger.services.info("ℹ️ BLE Connecting: \(peripheral.name ?? "Unknown", privacy: .public)") } // Disconnect Connected Peripheral @@ -259,12 +259,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate manager.schedule() } lastConnectionError = "🚨 \(e.localizedDescription)" - Logger.services.error("🚨 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") + Logger.services.error("🚫 BLE Disconnected: \(peripheral.name ?? "Unknown") Error Code: \(errorCode) Error: \(e.localizedDescription)") } } else { // Disconnected without error which indicates user intent to disconnect // Happens when swiping to disconnect - Logger.services.info("ℹ️ BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") + Logger.services.info("🚫 BLE Disconnected: \(peripheral.name ?? "Unknown"): User Initiated Disconnect") } // Start a scan so the disconnected peripheral is moved to the peripherals[] if it is awake self.startScanning() @@ -452,7 +452,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } catch { context!.rollback() let nsError = error as NSError - Logger.data.error("Error Updating Core Data BluetoothConfigEntity: \(nsError)") + Logger.data.error("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)") } let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum)) @@ -492,7 +492,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { - Logger.services.error("didUpdateNotificationStateFor error: \(error?.localizedDescription ?? "Unknown")") + Logger.services.error("💥 BLE didUpdateNotificationStateFor error: \(error?.localizedDescription ?? "Unknown")") } // MARK: Data Read / Update Characteristic Event @@ -500,14 +500,13 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate if let error { - Logger.services.error("🚫 didUpdateValueFor Characteristic error \(error.localizedDescription)") + Logger.services.error("🚫 BLE didUpdateValueFor Characteristic error \(error.localizedDescription)") let errorCode = (error as NSError).code if errorCode == 5 || errorCode == 15 { // BLE PIN connection errors // 5 CBATTErrorDomain Code=5 "Authentication is insufficient." // 15 CBATTErrorDomain Code=15 "Encryption is insufficient." lastConnectionError = "🚨" + String.localizedStringWithFormat("ble.errorcode.pin %@".localized, error.localizedDescription) - Logger.services.error("\(error.localizedDescription) Please try connecting again and check the PIN carefully.") self.disconnectPeripheral(reconnect: false) } return @@ -686,8 +685,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate var hopNodes: [TraceRouteHopEntity] = [] for node in routingMessage.route { var hopNode = getNodeInfo(id: Int64(node), context: context!) - if hopNode == nil && hopNode?.num ?? 0 > 0 { - hopNode = createNodeInfo(num: Int64(node), context: context!) + if hopNode == nil { + hopNode = NodeInfoEntity(context: context!, num: Int(node)) } let traceRouteHop = TraceRouteHopEntity(context: context!) traceRouteHop.time = Date() @@ -803,7 +802,8 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate } case FROMNUM_UUID: - Logger.services.info("🗞️ BLE (Notify) characteristic, value will be read next") + return + // Logger.services.info("🗞️ BLE (Notify) characteristic, value will be read next") default: Logger.services.error("Unhandled Characteristic UUID: \(characteristic.uuid)") } @@ -2824,12 +2824,12 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate do { connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse) try context!.save() - Logger.mesh.debug("\(adminDescription)") + Logger.admin.debug("\(adminDescription)") return true } catch { context!.rollback() let nsError = error as NSError - Logger.data.error("Error inserting new core data MessageEntity: \(nsError)") + Logger.admin.error("Error inserting new core data admin MessageEntity: \(nsError)") } } return false @@ -3013,7 +3013,7 @@ extension BLEManager: CBCentralManagerDelegate { default: status = "default" } - Logger.services.debug("📜 BLEManager status: \(status)") + Logger.services.debug("📜 BLE status: \(status)") } // Called each time a peripheral is discovered @@ -3021,7 +3021,7 @@ extension BLEManager: CBCentralManagerDelegate { if self.automaticallyReconnect && peripheral.identifier.uuidString == UserDefaults.standard.object(forKey: "preferredPeripheralId") as? String ?? "" { self.connectTo(peripheral: peripheral) - Logger.services.info("BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") + Logger.services.info("🔄 BLE Reconnecting to prefered peripheral: \(peripheral.name ?? "Unknown")") } let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String let device = Peripheral(id: peripheral.identifier.uuidString, num: 0, name: name ?? "Unknown", shortName: "?", longName: name ?? "Unknown", firmwareVersion: "Unknown", rssi: RSSI.intValue, lastUpdate: Date(), peripheral: peripheral) diff --git a/Meshtastic/Helpers/LocationsHandler.swift b/Meshtastic/Helpers/LocationsHandler.swift index 32fc88d9..3f9cda69 100644 --- a/Meshtastic/Helpers/LocationsHandler.swift +++ b/Meshtastic/Helpers/LocationsHandler.swift @@ -85,15 +85,15 @@ import OSLog if smartPostion { let age = -location.timestamp.timeIntervalSinceNow if age > 10 { - Logger.services.warning("📍 Bad Location \(self.count): Too Old \(age) seconds ago \(location)") + Logger.services.warning("📍 Bad Location \(self.count): Too Old \(age) seconds ago \(location, privacy: .private)") return false } if location.horizontalAccuracy < 0 { - Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") + Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } if location.horizontalAccuracy > 5 { - Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location)") + Logger.services.warning("📍 Bad Location \(self.count): Horizontal Accuracy: \(location.horizontalAccuracy) \(location, privacy: .private)") return false } } diff --git a/Meshtastic/Helpers/Logger.swift b/Meshtastic/Helpers/Logger.swift deleted file mode 100644 index bf9ad575..00000000 --- a/Meshtastic/Helpers/Logger.swift +++ /dev/null @@ -1,19 +0,0 @@ -import OSLog - -extension Logger { - - /// The logger's subsystem. - private static var subsystem = Bundle.main.bundleIdentifier! - - /// All logs related to data such as decoding error, parsing issues, etc. - static let data = Logger(subsystem: subsystem, category: "🗄️ Data") - - /// All logs related to the mesh - static let mesh = Logger(subsystem: subsystem, category: "🕸️ Mesh") - - /// All logs related to services such as network calls, location, etc. - static let services = Logger(subsystem: subsystem, category: "🍏 Services") - - /// All logs related to tracking and analytics. - static let statistics = Logger(subsystem: subsystem, category: "📈 Stats") -} diff --git a/Meshtastic/Helpers/Map/OfflineTileManager.swift b/Meshtastic/Helpers/Map/OfflineTileManager.swift index 89e9bc16..12b159ba 100644 --- a/Meshtastic/Helpers/Map/OfflineTileManager.swift +++ b/Meshtastic/Helpers/Map/OfflineTileManager.swift @@ -21,7 +21,7 @@ class OfflineTileManager: ObservableObject { } init() { - Logger.services.debug("Documents Directory = \(self.documentsDirectory.absoluteString)") + Logger.services.debug("🗄️ Documents Directory = \(self.documentsDirectory.absoluteString)") createDirectoriesIfNecessary() } diff --git a/Meshtastic/Helpers/MeshLogger.swift b/Meshtastic/Helpers/MeshLogger.swift index 25dbc8f1..a706b45e 100644 --- a/Meshtastic/Helpers/MeshLogger.swift +++ b/Meshtastic/Helpers/MeshLogger.swift @@ -32,7 +32,7 @@ class MeshLogger { fileHandle.closeFile() } else { try data.write(to: logFile, options: .atomicWrite) - let log = String(data: data, encoding: .utf8) ?? "unknown".localized + let log = String(decoding: data, as: UTF8.self) Logger.mesh.notice("\(log)") } } catch { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index 3250334b..bb4c2e2e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -156,30 +156,29 @@ func channelPacket (channel: Channel, fromNum: Int64, context: NSManagedObjectCo return } if fetchedMyInfo.count == 1 { - let newChannel = ChannelEntity(context: context) - newChannel.id = Int32(channel.index) - newChannel.index = Int32(channel.index) - newChannel.uplinkEnabled = channel.settings.uplinkEnabled - newChannel.downlinkEnabled = channel.settings.downlinkEnabled - newChannel.name = channel.settings.name - newChannel.role = Int32(channel.role.rawValue) - newChannel.psk = channel.settings.psk - if channel.settings.hasModuleSettings { - newChannel.positionPrecision = Int32(truncatingIfNeeded: channel.settings.moduleSettings.positionPrecision) - newChannel.mute = channel.settings.moduleSettings.isClientMuted - } - guard let mutableChannels = fetchedMyInfo[0].channels!.mutableCopy() as? NSMutableOrderedSet else { + let newChannel = ChannelEntity( + context: context, + channel: channel + ) + guard let mutableChannels = fetchedMyInfo.first?.channels?.mutableCopy() as? NSMutableOrderedSet else { return } - if let oldChannel = mutableChannels.first(where: {($0 as AnyObject).index == newChannel.index }) as? ChannelEntity { - let index = mutableChannels.index(of: oldChannel as Any) + let oldChannel = mutableChannels.first(where: { + if let channel = $0 as? ChannelEntity { + return channel.index == newChannel.index + } + return false + }) as? ChannelEntity + + if let oldChannel { + let index = mutableChannels.index(of: oldChannel) mutableChannels.replaceObject(at: index, with: newChannel) } else { mutableChannels.add(newChannel) } - fetchedMyInfo[0].channels = mutableChannels.copy() as? NSOrderedSet + fetchedMyInfo.first?.channels = mutableChannels.copy() as? NSOrderedSet if newChannel.name?.lowercased() == "admin" { - fetchedMyInfo[0].adminIndex = newChannel.index + fetchedMyInfo.first?.adminIndex = newChannel.index } context.refresh(newChannel, mergeChanges: true) do { @@ -212,29 +211,18 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS guard let fetchedNode = try context.fetch(fetchedNodeRequest) as? [NodeInfoEntity] else { return } - let newMetadata = DeviceMetadataEntity(context: context) - newMetadata.time = Date() - newMetadata.deviceStateVersion = Int32(metadata.deviceStateVersion) - newMetadata.canShutdown = metadata.canShutdown - newMetadata.hasWifi = metadata.hasWifi_p - newMetadata.hasBluetooth = metadata.hasBluetooth_p - newMetadata.hasEthernet = metadata.hasEthernet_p - newMetadata.role = Int32(metadata.role.rawValue) - newMetadata.positionFlags = Int32(metadata.positionFlags) - // Swift does strings weird, this does work to get the version without the github hash - let lastDotIndex = metadata.firmwareVersion.lastIndex(of: ".") - var version = metadata.firmwareVersion[...(lastDotIndex ?? String.Index(utf16Offset: 6, in: metadata.firmwareVersion))] - version = version.dropLast() - newMetadata.firmwareVersion = String(version) - if fetchedNode.count > 0 { - fetchedNode[0].metadata = newMetadata - } else { - - if fromNum > 0 { - let newNode = createNodeInfo(num: Int64(fromNum), context: context) - newNode.metadata = newMetadata - } + let newMetadata = DeviceMetadataEntity( + context: context, + metadata: metadata + ) + + if let node = fetchedNode.first { + node.metadata = newMetadata + } else if fromNum > 0 { + let node = NodeInfoEntity(context: context, num: Int(fromNum)) + node.metadata = newMetadata } + do { try context.save() } catch { @@ -251,180 +239,111 @@ func deviceMetadataPacket (metadata: DeviceMetadata, fromNum: Int64, context: NS 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( + "mesh.log.nodeinfo.received %@ %@".localized, + String(nodeInfo.num), + String(nodeInfo.viaMqtt) + ) MeshLogger.log("📟 \(logString)") - guard nodeInfo.num > 0 else { return nil } + guard nodeInfo.num > 0 else { + Logger.data.error("nodeInfo \(nodeInfo.num) invalid") + return nil + } let fetchNodeInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "NodeInfoEntity") fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeInfo.num)) do { - guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { + guard let fetchedNodes = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return nil } - // Not Found Insert - if fetchedNode.isEmpty && nodeInfo.num > 0 { - - let newNode = NodeInfoEntity(context: context) - newNode.id = Int64(nodeInfo.num) - newNode.num = Int64(nodeInfo.num) - newNode.channel = Int32(nodeInfo.channel) - newNode.favorite = nodeInfo.isFavorite - newNode.hopsAway = Int32(nodeInfo.hopsAway) - - if nodeInfo.hasDeviceMetrics { - let telemetry = TelemetryEntity(context: context) - telemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) - telemetry.voltage = nodeInfo.deviceMetrics.voltage - telemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization - telemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - var newTelemetries = [TelemetryEntity]() - newTelemetries.append(telemetry) - newNode.telemetries? = NSOrderedSet(array: newTelemetries) + let node: NodeInfoEntity + if let update = fetchedNodes.first { + node = update + } else { + node = NodeInfoEntity(context: context) + } + + node.id = Int64(nodeInfo.num) + node.num = Int64(nodeInfo.num) + node.channel = Int32(nodeInfo.channel) + node.favorite = nodeInfo.isFavorite + node.hopsAway = Int32(nodeInfo.hopsAway) + node.viaMqtt = nodeInfo.viaMqtt + + if nodeInfo.hasDeviceMetrics { + let newTelemetry = TelemetryEntity(context: context) + newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) + newTelemetry.voltage = nodeInfo.deviceMetrics.voltage + newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization + newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx + + var telemetries: [TelemetryEntity] + if let tele = node.telemetries?.array as? [TelemetryEntity] { + telemetries = tele + telemetries.append(newTelemetry) + } else { + telemetries = [newTelemetry] } - - newNode.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - newNode.snr = nodeInfo.snr - if nodeInfo.hasUser { - - let newUser = UserEntity(context: context) - newUser.userId = nodeInfo.user.id - newUser.num = Int64(nodeInfo.num) - newUser.longName = nodeInfo.user.longName - newUser.shortName = nodeInfo.user.shortName - newUser.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - newUser.isLicensed = nodeInfo.user.isLicensed - newUser.role = Int32(nodeInfo.user.role.rawValue) - newNode.user = newUser - } else if nodeInfo.num > Int16.max { - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - newNode.user = newUser + node.telemetries = NSOrderedSet(array: telemetries) + } + + + node.lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) + node.snr = nodeInfo.snr + + // User + var user: UserEntity? + if nodeInfo.hasUser { + user = UserEntity( + context: context, + user: nodeInfo.user, + num: Int(nodeInfo.num) + ) + } else if nodeInfo.num > Int16.max { + user = UserEntity( + context: context, + num: Int(nodeInfo.num) + ) + } + node.user = user + + // Position + if nodeInfo.isValidPosition { + let position = PositionEntity( + context: context, + nodeInfo: nodeInfo + ) + + if let positions = node.positions?.mutableCopy() as? NSMutableOrderedSet { + positions.add(position) + node.positions = positions + } else { + node.positions = NSOrderedSet(object: position) } + } - if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - let position = PositionEntity(context: context) - position.latest = true - position.seqNo = Int32(nodeInfo.position.seqNumber) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.speed = Int32(nodeInfo.position.groundSpeed) - position.heading = Int32(nodeInfo.position.groundTrack) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - var newPostions = [PositionEntity]() - newPostions.append(position) - newNode.positions? = NSOrderedSet(array: newPostions) - } - - // Look for a MyInfo - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { - return nil - } - if fetchedMyInfo.count > 0 { - newNode.myInfo = fetchedMyInfo[0] - } + // MyInfo + do { + let fetchMyInfoRequest = MyInfoEntity.fetchRequest() + fetchMyInfoRequest.predicate = NSPredicate( + format: "myNodeNum == %lld", Int64(nodeInfo.num) + ) + let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) + if let myInfo = fetchedMyInfo.first { + node.myInfo = myInfo do { try context.save() Logger.data.info("💾 Saved a new Node Info For: \(String(nodeInfo.num))") - return newNode + return node } catch { context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") - } - } catch { - Logger.data.error("Fetch MyInfo Error") - } - } else if nodeInfo.num > 0 { - - fetchedNode[0].id = Int64(nodeInfo.num) - fetchedNode[0].num = Int64(nodeInfo.num) - fetchedNode[0].lastHeard = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.lastHeard))) - fetchedNode[0].snr = nodeInfo.snr - fetchedNode[0].channel = Int32(nodeInfo.channel) - fetchedNode[0].favorite = nodeInfo.isFavorite - fetchedNode[0].hopsAway = Int32(nodeInfo.hopsAway) - - if nodeInfo.hasUser { - if fetchedNode[0].user == nil { - fetchedNode[0].user = UserEntity(context: context) - } - fetchedNode[0].user!.userId = nodeInfo.user.id - fetchedNode[0].user!.num = Int64(nodeInfo.num) - fetchedNode[0].user!.numString = String(nodeInfo.num) - fetchedNode[0].user!.longName = nodeInfo.user.longName - fetchedNode[0].user!.shortName = nodeInfo.user.shortName - fetchedNode[0].user!.isLicensed = nodeInfo.user.isLicensed - fetchedNode[0].user!.role = Int32(nodeInfo.user.role.rawValue) - fetchedNode[0].user!.hwModel = String(describing: nodeInfo.user.hwModel).uppercased() - } else { - if fetchedNode[0].user == nil && nodeInfo.num > Int16.max { - - let newUser = createUser(num: Int64(nodeInfo.num), context: context) - fetchedNode[0].user = newUser + Logger.data.error("Error Saving Core Data NodeInfoEntity: \(error.localizedDescription)") } } - - if nodeInfo.hasDeviceMetrics { - - let newTelemetry = TelemetryEntity(context: context) - newTelemetry.batteryLevel = Int32(nodeInfo.deviceMetrics.batteryLevel) - newTelemetry.voltage = nodeInfo.deviceMetrics.voltage - newTelemetry.channelUtilization = nodeInfo.deviceMetrics.channelUtilization - newTelemetry.airUtilTx = nodeInfo.deviceMetrics.airUtilTx - guard let mutableTelemetries = fetchedNode[0].telemetries!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].telemetries = mutableTelemetries.copy() as? NSOrderedSet - } - - if nodeInfo.hasPosition { - - if (nodeInfo.position.longitudeI != 0 && nodeInfo.position.latitudeI != 0) && (nodeInfo.position.latitudeI != 373346000 && nodeInfo.position.longitudeI != -1220090000) { - - let position = PositionEntity(context: context) - position.latitudeI = nodeInfo.position.latitudeI - position.longitudeI = nodeInfo.position.longitudeI - position.altitude = nodeInfo.position.altitude - position.satsInView = Int32(nodeInfo.position.satsInView) - position.time = Date(timeIntervalSince1970: TimeInterval(Int64(nodeInfo.position.time))) - guard let mutablePositions = fetchedNode[0].positions!.mutableCopy() as? NSMutableOrderedSet else { - return nil - } - fetchedNode[0].positions = mutablePositions.copy() as? NSOrderedSet - } - - } - - // Look for a MyInfo - let fetchMyInfoRequest: NSFetchRequest = NSFetchRequest.init(entityName: "MyInfoEntity") - fetchMyInfoRequest.predicate = NSPredicate(format: "myNodeNum == %lld", Int64(nodeInfo.num)) - - do { - guard let fetchedMyInfo = try context.fetch(fetchMyInfoRequest) as? [MyInfoEntity] else { - return nil - } - if fetchedMyInfo.count > 0 { - fetchedNode[0].myInfo = fetchedMyInfo[0] - } - do { - try context.save() - Logger.data.info("💾 NodeInfo saved for \(nodeInfo.num)") - return fetchedNode[0] - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving Core Data NodeInfoEntity: \(nsError)") - } - } catch { - Logger.data.error("Fetch MyInfo Error") - } + } catch { + Logger.data.error("Fetch MyInfo Error: \(error.localizedDescription)") } } catch { Logger.data.error("Fetch NodeInfoEntity Error") @@ -619,6 +538,8 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana if routingMessage.errorReason == Routing.Error.none { fetchedMessage![0].receivedACK = true + } else { + Logger.statistics.error("❗ Routing Error: \(routingErrorString) for a text message packet from Node: \(packet.from)") } fetchedMessage![0].ackSNR = packet.rxSnr fetchedMessage![0].ackTimestamp = Int32(truncatingIfNeeded: packet.rxTime) @@ -683,6 +604,7 @@ func telemetryPacket(packet: MeshPacket, connectedNode: Int64, context: NSManage telemetry.voltage = telemetryMessage.deviceMetrics.voltage telemetry.uptimeSeconds = Int32(telemetryMessage.deviceMetrics.uptimeSeconds) telemetry.metricsType = 0 + Logger.statistics.info("📈 Channel Utilization: \(telemetryMessage.deviceMetrics.channelUtilization) Airtime: \(telemetryMessage.deviceMetrics.airUtilTx) for Node: \(packet.from)") } else if telemetryMessage.variant == Telemetry.OneOf_Variant.environmentMetrics(telemetryMessage.environmentMetrics) { // Environment Metrics telemetry.barometricPressure = telemetryMessage.environmentMetrics.barometricPressure diff --git a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift index a2e363d0..29a3548a 100644 --- a/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift +++ b/Meshtastic/Helpers/Mqtt/MqttClientProxyManager.swift @@ -130,8 +130,7 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { } } func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) { - Logger.services.debug("mqttDidDisconnect: \(err?.localizedDescription ?? "")") - + Logger.services.debug("📲 MQTT Client Proxy mqttDidDisconnect: \(err?.localizedDescription ?? "")") if let error = err { delegate?.onMqttError(message: error.localizedDescription) } @@ -152,7 +151,7 @@ extension MqttClientProxyManager: CocoaMQTTDelegate { Logger.services.info("📲 MQTT Client Proxy didSubscribeTopics: \(success.allKeys.count) topics. failed: \(failed.count) topics") } func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) { - Logger.services.info("didUnsubscribeTopics: \(topics.joined(separator: ", "))") + Logger.services.info("📲 MQTT Client Proxy didUnsubscribeTopics: \(topics.joined(separator: ", "))") } func mqttDidPing(_ mqtt: CocoaMQTT) { Logger.services.info("📲 MQTT Client Proxy mqttDidPing") diff --git a/Meshtastic/MeshtasticApp.swift b/Meshtastic/MeshtasticApp.swift index da3383d2..b6cb96c1 100644 --- a/Meshtastic/MeshtasticApp.swift +++ b/Meshtastic/MeshtasticApp.swift @@ -48,7 +48,7 @@ struct MeshtasticAppleApp: App { Logger.services.debug("Add Channel \(self.addChannels)") } self.saveChannels = true - Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") + Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .private)") } if self.saveChannels { Logger.mesh.debug("User wants to open Channel Settings URL: \(String(describing: self.incomingUrl!.relativeString))") @@ -56,14 +56,14 @@ struct MeshtasticAppleApp: App { } .onOpenURL(perform: { (url) in - Logger.mesh.debug("Some sort of URL was received \(url)") + Logger.mesh.debug("Some sort of URL was received \(url, privacy: .private)") self.incomingUrl = url if url.absoluteString.lowercased().contains("meshtastic.org/e/#") { if let components = self.incomingUrl?.absoluteString.components(separatedBy: "#") { self.channelSettings = components.last! } self.saveChannels = true - Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link")") + Logger.mesh.debug("User wants to open a Channel Settings URL: \(self.incomingUrl?.absoluteString ?? "No QR Code Link", privacy: .private)") } else if url.absoluteString.lowercased().contains("meshtastic://") { appState.navigationPath = url.absoluteString let path = appState.navigationPath ?? "" @@ -140,7 +140,7 @@ struct MeshtasticAppleApp: App { .onChange(of: scenePhase) { (newScenePhase) in switch newScenePhase { case .background: - Logger.services.info("🍏 Scene is in the background") + Logger.services.info("🎬 Scene is in the background") do { try persistenceController.container.viewContext.save() @@ -151,9 +151,9 @@ struct MeshtasticAppleApp: App { Logger.services.error("💥 Failed to save viewContext when the app goes to the background.") } case .inactive: - Logger.services.info("🍏 Scene is inactive") + Logger.services.info("🎬 Scene is inactive") case .active: - Logger.services.info("🍏 Scene is active") + Logger.services.info("🎬 Scene is active") @unknown default: Logger.services.error("🍎 Apple must have changed something") } diff --git a/Meshtastic/MeshtasticAppDelegate.swift b/Meshtastic/MeshtasticAppDelegate.swift index 1bfa73ed..cc5dc397 100644 --- a/Meshtastic/MeshtasticAppDelegate.swift +++ b/Meshtastic/MeshtasticAppDelegate.swift @@ -10,7 +10,7 @@ import OSLog class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - Logger.services.info("🚀 Meshtstic Apple App launched!") + Logger.services.info("🚀 Meshtastic Apple App launched!") // Default User Default Values UserDefaults.standard.register(defaults: ["meshMapRecentering": true]) UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory": true]) diff --git a/Meshtastic/Persistence/UpdateCoreData.swift b/Meshtastic/Persistence/UpdateCoreData.swift index 1ef04ff7..cb36435c 100644 --- a/Meshtastic/Persistence/UpdateCoreData.swift +++ b/Meshtastic/Persistence/UpdateCoreData.swift @@ -166,20 +166,14 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) if let newUserMessage = try? User(serializedData: packet.decoded.payload) { - if newUserMessage.id.isEmpty { - if packet.from > Int16.max { - let newUser = createUser(num: Int64(packet.from), context: context) - newNode.user = newUser - } + if newUserMessage.id.isEmpty, packet.from > Int16.max { + newNode.user = UserEntity(context: context, num: Int(packet.from)) } else { - - let newUser = UserEntity(context: context) - newUser.userId = newUserMessage.id - newUser.num = Int64(packet.from) - newUser.longName = newUserMessage.longName - newUser.shortName = newUserMessage.shortName - newUser.role = Int32(newUserMessage.role.rawValue) - newUser.hwModel = String(describing: newUserMessage.hwModel).uppercased() + let newUser = UserEntity( + context: context, + user: newUserMessage, + num: Int(packet.from) + ) newNode.user = newUser if UserDefaults.newNodeNotifications { @@ -199,13 +193,13 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) } } else { if packet.from > Int16.max { - let newUser = createUser(num: Int64(packet.from), context: context) + let newUser = UserEntity(context: context, num: Int(packet.from)) fetchedNode[0].user = newUser } } if newNode.user == nil && packet.from > Int16.max { - newNode.user = createUser(num: Int64(packet.from), context: context) + newNode.user = UserEntity(context: context, num: Int(packet.from)) } let myInfoEntity = MyInfoEntity(context: context) @@ -265,8 +259,8 @@ func upsertNodeInfoPacket (packet: MeshPacket, context: NSManagedObjectContext) fetchedNode[0].hopsAway = Int32(packet.hopStart - packet.hopLimit) } if fetchedNode[0].user == nil { - let newUser = createUser(num: Int64(packet.from), context: context) - fetchedNode[0].user! = newUser + let newUser = UserEntity(context: context, num: Int(packet.from)) + fetchedNode[0].user = newUser } do { try context.save() @@ -966,56 +960,28 @@ func upsertExternalNotificationModuleConfigPacket(config: Meshtastic.ModuleConfi guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return } + + guard let node = fetchedNode.first else { + return Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save External Notification Module Config") + } + // Found a node, save External Notificaitone Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].externalNotificationConfig == nil { - let newExternalNotificationConfig = ExternalNotificationConfigEntity(context: context) - newExternalNotificationConfig.enabled = config.enabled - newExternalNotificationConfig.usePWM = config.usePwm - newExternalNotificationConfig.alertBell = config.alertBell - newExternalNotificationConfig.alertBellBuzzer = config.alertBellBuzzer - newExternalNotificationConfig.alertBellVibra = config.alertBellVibra - newExternalNotificationConfig.alertMessage = config.alertMessage - newExternalNotificationConfig.alertMessageBuzzer = config.alertMessageBuzzer - newExternalNotificationConfig.alertMessageVibra = config.alertMessageVibra - newExternalNotificationConfig.active = config.active - newExternalNotificationConfig.output = Int32(config.output) - newExternalNotificationConfig.outputBuzzer = Int32(config.outputBuzzer) - newExternalNotificationConfig.outputVibra = Int32(config.outputVibra) - newExternalNotificationConfig.outputMilliseconds = Int32(config.outputMs) - newExternalNotificationConfig.nagTimeout = Int32(config.nagTimeout) - newExternalNotificationConfig.useI2SAsBuzzer = config.useI2SAsBuzzer - fetchedNode[0].externalNotificationConfig = newExternalNotificationConfig - - } else { - fetchedNode[0].externalNotificationConfig?.enabled = config.enabled - fetchedNode[0].externalNotificationConfig?.usePWM = config.usePwm - fetchedNode[0].externalNotificationConfig?.alertBell = config.alertBell - fetchedNode[0].externalNotificationConfig?.alertBellBuzzer = config.alertBellBuzzer - fetchedNode[0].externalNotificationConfig?.alertBellVibra = config.alertBellVibra - fetchedNode[0].externalNotificationConfig?.alertMessage = config.alertMessage - fetchedNode[0].externalNotificationConfig?.alertMessageBuzzer = config.alertMessageBuzzer - fetchedNode[0].externalNotificationConfig?.alertMessageVibra = config.alertMessageVibra - fetchedNode[0].externalNotificationConfig?.active = config.active - fetchedNode[0].externalNotificationConfig?.output = Int32(config.output) - fetchedNode[0].externalNotificationConfig?.outputBuzzer = Int32(config.outputBuzzer) - fetchedNode[0].externalNotificationConfig?.outputVibra = Int32(config.outputVibra) - fetchedNode[0].externalNotificationConfig?.outputMilliseconds = Int32(config.outputMs) - fetchedNode[0].externalNotificationConfig?.nagTimeout = Int32(config.nagTimeout) - fetchedNode[0].externalNotificationConfig?.useI2SAsBuzzer = config.useI2SAsBuzzer - } - - do { - try context.save() - Logger.data.info("💾 Updated External Notification Module Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)") - } + if let externalNotificationConfig = node.externalNotificationConfig { + externalNotificationConfig.update(with: config) } else { - Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save External Notification Module Config") + node.externalNotificationConfig = ExternalNotificationConfigEntity( + context: context, + config: config + ) + } + + do { + try context.save() + Logger.data.info("💾 Updated External Notification Module Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data ExternalNotificationConfigEntity: \(nsError)") } } catch { let nsError = error as NSError @@ -1120,48 +1086,29 @@ func upsertMqttModuleConfigPacket(config: Meshtastic.ModuleConfig.MQTTConfig, no guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return } - // Found a node, save MQTT Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].mqttConfig == nil { - let newMQTTConfig = MQTTConfigEntity(context: context) - newMQTTConfig.enabled = config.enabled - newMQTTConfig.proxyToClientEnabled = config.proxyToClientEnabled - newMQTTConfig.address = config.address - newMQTTConfig.username = config.username - newMQTTConfig.password = config.password - newMQTTConfig.root = config.root - newMQTTConfig.encryptionEnabled = config.encryptionEnabled - newMQTTConfig.jsonEnabled = config.jsonEnabled - newMQTTConfig.tlsEnabled = config.tlsEnabled - newMQTTConfig.mapReportingEnabled = config.mapReportingEnabled - newMQTTConfig.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) - newMQTTConfig.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) - fetchedNode[0].mqttConfig = newMQTTConfig - } else { - fetchedNode[0].mqttConfig?.enabled = config.enabled - fetchedNode[0].mqttConfig?.proxyToClientEnabled = config.proxyToClientEnabled - fetchedNode[0].mqttConfig?.address = config.address - fetchedNode[0].mqttConfig?.username = config.username - fetchedNode[0].mqttConfig?.password = config.password - fetchedNode[0].mqttConfig?.root = config.root - fetchedNode[0].mqttConfig?.encryptionEnabled = config.encryptionEnabled - fetchedNode[0].mqttConfig?.jsonEnabled = config.jsonEnabled - fetchedNode[0].mqttConfig?.tlsEnabled = config.tlsEnabled - fetchedNode[0].mqttConfig?.mapReportingEnabled = config.mapReportingEnabled - fetchedNode[0].mqttConfig?.mapPositionPrecision = Int32(config.mapReportSettings.positionPrecision) - fetchedNode[0].mqttConfig?.mapPublishIntervalSecs = Int32(config.mapReportSettings.publishIntervalSecs) - } - do { - try context.save() - Logger.data.info("💾 Updated MQTT Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Updating Core Data MQTTConfigEntity: \(nsError)") - } - } else { + + guard let node = fetchedNode.first else { Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save MQTT Module Config") + return + } + // Found a node, save MQTT Config + + if let mqttConfig = node.mqttConfig { + mqttConfig.update(with: config) + } else { + node.mqttConfig = MQTTConfigEntity( + context: context, + config: config + ) + } + + do { + try context.save() + Logger.data.info("💾 Updated MQTT Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data MQTTConfigEntity: \(nsError)") } } catch { let nsError = error as NSError @@ -1183,32 +1130,28 @@ func upsertRangeTestModuleConfigPacket(config: Meshtastic.ModuleConfig.RangeTest return } // Found a node, save Device Config - if !fetchedNode.isEmpty { - if fetchedNode[0].rangeTestConfig == nil { - let newRangeTestConfig = RangeTestConfigEntity(context: context) - newRangeTestConfig.sender = Int32(config.sender) - newRangeTestConfig.enabled = config.enabled - newRangeTestConfig.save = config.save - fetchedNode[0].rangeTestConfig = newRangeTestConfig + if let node = fetchedNode.first { + if let rangeTestConfig = node.rangeTestConfig { + rangeTestConfig.update(with: config) } else { - fetchedNode[0].rangeTestConfig?.sender = Int32(config.sender) - fetchedNode[0].rangeTestConfig?.enabled = config.enabled - fetchedNode[0].rangeTestConfig?.save = config.save + node.rangeTestConfig = RangeTestConfigEntity( + context: context, + config: config + ) } + do { try context.save() Logger.data.info("💾 Updated Range Test Config for node number: \(String(nodeNum))") } catch { context.rollback() - let nsError = error as NSError - Logger.data.error("Error Updating Core Data RangeTestConfigEntity: \(nsError)") + Logger.data.error("Error Updating Core Data RangeTestConfigEntity: \(error.localizedDescription)") } } else { Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Range Test Module Config") } } catch { - let nsError = error as NSError - Logger.data.error("Fetching node for core data RangeTestConfigEntity failed: \(nsError)") + Logger.data.error("Fetching node for core data RangeTestConfigEntity failed: \(error.localizedDescription)") } } @@ -1221,55 +1164,32 @@ func upsertSerialModuleConfigPacket(config: Meshtastic.ModuleConfig.SerialConfig fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum)) do { - guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return } - + guard let node = fetchedNode.first else { + return Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Serial Module Config") + } + // Found a node, save Device Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].serialConfig == nil { - - let newSerialConfig = SerialConfigEntity(context: context) - newSerialConfig.enabled = config.enabled - newSerialConfig.echo = config.echo - newSerialConfig.rxd = Int32(config.rxd) - newSerialConfig.txd = Int32(config.txd) - newSerialConfig.baudRate = Int32(config.baud.rawValue) - newSerialConfig.timeout = Int32(config.timeout) - newSerialConfig.mode = Int32(config.mode.rawValue) - fetchedNode[0].serialConfig = newSerialConfig - - } else { - fetchedNode[0].serialConfig?.enabled = config.enabled - fetchedNode[0].serialConfig?.echo = config.echo - fetchedNode[0].serialConfig?.rxd = Int32(config.rxd) - fetchedNode[0].serialConfig?.txd = Int32(config.txd) - fetchedNode[0].serialConfig?.baudRate = Int32(config.baud.rawValue) - fetchedNode[0].serialConfig?.timeout = Int32(config.timeout) - fetchedNode[0].serialConfig?.mode = Int32(config.mode.rawValue) - } - - do { - try context.save() - Logger.data.info("💾 Updated Serial Module Config for node number: \(String(nodeNum))") - - } catch { - - context.rollback() - - let nsError = error as NSError - Logger.data.error("Error Updating Core Data SerialConfigEntity: \(nsError)") - } - + if let serialConfig = node.serialConfig { + node.serialConfig?.update(with: config) } else { - - Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Serial Module Config") + node.serialConfig = SerialConfigEntity( + context: context, + config: config + ) } + do { + try context.save() + Logger.data.info("💾 Updated Serial Module Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data SerialConfigEntity: \(nsError)") + } } catch { - let nsError = error as NSError Logger.data.error("Fetching node for core data SerialConfigEntity failed: \(nsError)") } @@ -1288,36 +1208,26 @@ func upsertStoreForwardModuleConfigPacket(config: Meshtastic.ModuleConfig.StoreF guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else { return } - // Found a node, save Store & Forward Sensor Config - if !fetchedNode.isEmpty { - - if fetchedNode[0].storeForwardConfig == nil { - - let newConfig = StoreForwardConfigEntity(context: context) - newConfig.enabled = config.enabled - newConfig.heartbeat = config.heartbeat - newConfig.records = Int32(config.records) - newConfig.historyReturnMax = Int32(config.historyReturnMax) - newConfig.historyReturnWindow = Int32(config.historyReturnWindow) - fetchedNode[0].storeForwardConfig = newConfig - - } else { - fetchedNode[0].storeForwardConfig?.enabled = config.enabled - fetchedNode[0].storeForwardConfig?.heartbeat = config.heartbeat - fetchedNode[0].storeForwardConfig?.records = Int32(config.records) - fetchedNode[0].storeForwardConfig?.historyReturnMax = Int32(config.historyReturnMax) - fetchedNode[0].storeForwardConfig?.historyReturnWindow = Int32(config.historyReturnWindow) - } - do { - try context.save() - Logger.data.info("💾 Updated Store & Forward Module Config for node number: \(String(nodeNum))") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Updating Core Data StoreForwardConfigEntity: \(nsError)") - } - } else { + guard let node = fetchedNode.first else { Logger.data.error("No Nodes found in local database matching node number \(nodeNum) unable to save Store & Forward Module Config") + return + } + // Found a node, save Store & Forward Sensor Config + if let storeForwardConfig = node.storeForwardConfig { + storeForwardConfig.update(with: config) + } else { + node.storeForwardConfig = StoreForwardConfigEntity( + context: context, + config: config + ) + } + do { + try context.save() + Logger.data.info("💾 Updated Store & Forward Module Config for node number: \(String(nodeNum))") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Updating Core Data StoreForwardConfigEntity: \(nsError)") } } catch { let nsError = error as NSError diff --git a/Meshtastic/Views/ContentView.swift b/Meshtastic/Views/ContentView.swift index 46f7f903..d9239dfe 100644 --- a/Meshtastic/Views/ContentView.swift +++ b/Meshtastic/Views/ContentView.swift @@ -8,13 +8,24 @@ import SwiftUI struct ContentView: View { @StateObject var appState = AppState.shared + + var meshMap: some View { + SwiftUI.Group { + if #available(iOS 17.0, macOS 14.0, *), !UserDefaults.mapUseLegacy { + MeshMap() + } else { + NodeMap() + } + } + } + var body: some View { TabView(selection: $appState.tabSelection) { Messages() .tabItem { Label("messages", systemImage: "message") } - .tag(Tab.contacts) + .tag(Tab.messages) .badge(appState.unreadDirectMessages + appState.unreadChannelMessages) Connect() .tabItem { @@ -26,27 +37,11 @@ struct ContentView: View { Label("nodes", systemImage: "flipphone") } .tag(Tab.nodes) - if #available(iOS 17.0, macOS 14.0, *) { - if UserDefaults.mapUseLegacy { - NodeMap() - .tabItem { - Label("map", systemImage: "map") - } - .tag(Tab.map) - } else { - MeshMap() - .tabItem { - Label("map", systemImage: "map") - } - .tag(Tab.map) + meshMap + .tabItem { + Label("map", systemImage: "map") } - } else { - NodeMap() - .tabItem { - Label("map", systemImage: "map") - } - .tag(Tab.map) - } + .tag(Tab.map) Settings() .tabItem { Label("settings", systemImage: "gear") @@ -56,22 +51,8 @@ struct ContentView: View { } } } -// #Preview { -// if #available(iOS 17.0, *) { -// // ContentView(deepLinkManager: .init()) -// } else { -// // Fallback on earlier versions -// } -// } - -// struct ContentView_Previews: PreviewProvider { -// static var previews: some View { -// ContentView() -// } -// } enum Tab: Hashable { - case contacts case messages case map case ble diff --git a/Meshtastic/Views/Helpers/LogDetail.swift b/Meshtastic/Views/Helpers/LogDetail.swift new file mode 100644 index 00000000..69cf87a2 --- /dev/null +++ b/Meshtastic/Views/Helpers/LogDetail.swift @@ -0,0 +1,142 @@ +// +// LogDetail.swift +// Meshtastic +// +// Copyright(c) Garth Vander Houwen 6/5/24. +// + +import SwiftUI +import MapKit +import OSLog + +@available(iOS 17.0, macOS 14.0, *) +struct LogDetail: View { + + @Environment(\.dismiss) private var dismiss + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + var log: OSLogEntryLog + + var body: some View { + + VStack { + HStack { + Text("OS Log Entry Details") + .font(.largeTitle) + } + Divider() + HStack(alignment: .top) { + VStack(alignment: .leading) { + List { + /// Time + Label { + Text("log.time".localized + ":") + .font(idiom == .phone ? .callout : .title) + LastHeardText(lastHeard: log.date) + .font(idiom == .phone ? .callout : .title) + } icon: { + Image(systemName: "timer") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .padding(.bottom, 5) + .listSectionSeparator(.hidden, edges: .top) + .listSectionSeparator(.visible, edges: .bottom) + /// Subsystem + Label { + Text("log.subsystem".localized + ":") + .font(idiom == .phone ? .callout : .title) + Text(log.subsystem) + .font(idiom == .phone ? .callout : .title) + } icon: { + Image(systemName: "gear") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .padding(.bottom, 5) + .listRowSeparator(.visible) + /// Process + Label { + Text("log.process".localized + ":") + .font(idiom == .phone ? .callout : .title) + Text(log.process) + .font(idiom == .phone ? .callout : .title) + } icon: { + Image(systemName: "tag") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .padding(.bottom, 5) + .listRowSeparator(.visible) + /// Category + Label { + Text("log.category".localized + ":") + .font(idiom == .phone ? .callout : .title) + Text(log.category) + .font(idiom == .phone ? .callout : .title) + } icon: { + Image(systemName: "square.grid.2x2") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .padding(.bottom, 5) + .listRowSeparator(.visible) + /// Level + Label { + Text("log.level".localized + ":") + .font(idiom == .phone ? .callout : .title) + Text(log.level.description) + .font(idiom == .phone ? .callout : .title) + } icon: { + Image(systemName: "stairs") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .padding(.bottom, 5) + .listRowSeparator(.visible) + /// message + Label { + Text("log.message".localized + ":") + .font(idiom == .phone ? .callout : .title) + Text(log.composedMessage) + .textSelection(.enabled) + .font(idiom == .phone ? .callout : .title) + .padding(.bottom, 5) + + } icon: { + Image(systemName: "text.bubble") + .symbolRenderingMode(.hierarchical) + .font(idiom == .phone ? .callout : .title) + .frame(width: 35) + } + .listRowSeparator(.hidden) + + } + .listStyle(.plain) + + } + Spacer() + } + .padding(.top) +#if targetEnvironment(macCatalyst) + Spacer() + Button { + dismiss() + } label: { + Label("close", systemImage: "xmark") + } + .buttonStyle(.bordered) + .buttonBorderShape(.capsule) + .controlSize(.large) + .padding(.bottom) +#endif + } + .monospaced() + .presentationDetents([.fraction(0.65), .fraction(0.75), .fraction(0.85)]) + .presentationDragIndicator(.visible) + } +} diff --git a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift index 5c7291a5..78d66cf2 100644 --- a/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift +++ b/Meshtastic/Views/MapKitMap/Custom/MapViewSwiftUI.swift @@ -183,7 +183,11 @@ struct MapViewSwiftUI: UIViewRepresentable { } var lineIndex = 0 for position in latest { - let nodePositions = positions.filter { $0.nodeCoordinate != nil && $0.nodePosition?.num ?? 0 == position.nodePosition?.num ?? -1 } + let nodePositions = positions.filter { + $0.nodeCoordinate != nil && + $0.nodePosition != nil && + $0.nodePosition?.num == position.nodePosition?.num + } let lineCoords = nodePositions.compactMap({(position) -> CLLocationCoordinate2D in return position.nodeCoordinate ?? LocationHelper.DefaultLocation }) diff --git a/Meshtastic/Views/Messages/Messages.swift b/Meshtastic/Views/Messages/Messages.swift index 13ecbfe7..00042799 100644 --- a/Meshtastic/Views/Messages/Messages.swift +++ b/Meshtastic/Views/Messages/Messages.swift @@ -73,7 +73,6 @@ struct Messages: View { if let urlComponent = URLComponents(string: newPath ?? "") { let queryItems = urlComponent.queryItems - let messageId = queryItems?.first(where: { $0.name == "messageId" })?.value let channel = queryItems?.first(where: { $0.name == "channel" })?.value if let channel { diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 2c943be9..527a896e 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -76,7 +76,7 @@ struct MeshMapContent: MapContent { } } } - .onTapGesture { location in + .onTapGesture { _ in selectedPosition = (selectedPosition == position ? nil : position) } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift index f8ee5165..27f6b7e6 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/NodeMapSwiftUI.swift @@ -35,172 +35,203 @@ struct NodeMapSwiftUI: View { @State private var mapRegion = MKCoordinateRegion.init() - @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], - predicate: NSPredicate( - format: "expire == nil || expire >= %@", Date() as NSDate - ), animation: .none) + @FetchRequest( + sortDescriptors: [NSSortDescriptor(key: "name", ascending: false)], + predicate: NSPredicate( + format: "expire == nil || expire >= %@", Date() as NSDate + ), + animation: .none + ) private var waypoints: FetchedResults - var body: some View { - var mostRecent = node.positions?.lastObject as? PositionEntity + private var title: String { + String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")) + } + + private var mostRecent: PositionEntity? { + node.positions?.lastObject as? PositionEntity + } - if node.hasPositions { - ZStack { - MapReader { _ in - Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) { - NodeMapContent(node: node) - } - .mapScope(mapScope) - .mapStyle(mapStyle) - .mapControls { - MapScaleView(scope: mapScope) - .mapControlVisibility(.visible) - if showUserLocation { - MapUserLocationButton(scope: mapScope) - .mapControlVisibility(.visible) - } - MapPitchToggle(scope: mapScope) - .mapControlVisibility(.visible) - MapCompass(scope: mapScope) - .mapControlVisibility(.visible) - } - .controlSize(.regular) - .overlay(alignment: .bottom) { - if scene != nil && isLookingAround { - LookAroundPreview(initialScene: scene) - .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .padding(.horizontal, 20) - } - } - .overlay(alignment: .bottom) { - if !isLookingAround && isShowingAltitude { - PositionAltitudeChart(node: node) - .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) - .clipShape(RoundedRectangle(cornerRadius: 12)) - .padding(.horizontal, 20) - } - } - .sheet(isPresented: $isEditingSettings) { - MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap) - .onChange(of: (selectedMapLayer)) { newMapLayer in - switch selectedMapLayer { - case .standard: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .hybrid: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .satellite: - UserDefaults.mapLayer = newMapLayer - mapStyle = MapStyle.imagery(elevation: .flat) - case .offline: - return - } - } - } - .onChange(of: node) { - isLookingAround = false + + + private var mapControls: some View { + HStack { + MapScaleView(scope: mapScope) + .mapControlVisibility(.visible) + if showUserLocation { + MapUserLocationButton(scope: mapScope) + .mapControlVisibility(.visible) + } + MapPitchToggle(scope: mapScope) + .mapControlVisibility(.visible) + MapCompass(scope: mapScope) + .mapControlVisibility(.visible) + } + } + + private var mapAccessoryControls: some View { + HStack { + Button(action: { + withAnimation { + isEditingSettings = !isEditingSettings + } + }) { + Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + /// Look Around Button + if self.scene != nil { + Button(action: { + if isShowingAltitude { isShowingAltitude = false - mostRecent = node.positions?.lastObject as? PositionEntity - if node.positions?.count ?? 0 > 1 { - position = .automatic - } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) - } - if let mostRecent { - Task { - scene = try? await fetchScene(for: mostRecent.coordinate) - } - } } - .onAppear { - UIApplication.shared.isIdleTimerDisabled = true - switch selectedMapLayer { - case .standard: - mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .hybrid: - mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - case .satellite: - mapStyle = MapStyle.imagery(elevation: .flat) - case .offline: - mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) - } - mostRecent = node.positions?.lastObject as? PositionEntity - if node.positions?.count ?? 0 > 1 { - position = .automatic - } else { - position = .camera(MapCamera(centerCoordinate: mostRecent!.coordinate, distance: 8000, heading: 0, pitch: 60)) - } - if self.scene == nil { - Task { - scene = try? await fetchScene(for: mostRecent!.coordinate) - } - } + isLookingAround = !isLookingAround + }) { + Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + /// Altitude Button + if node.positions?.count ?? 0 > 1 { + Button(action: { + if isLookingAround { + isLookingAround = false } - .safeAreaInset(edge: .bottom, alignment: .trailing) { - HStack { - Button(action: { - withAnimation { - isEditingSettings = !isEditingSettings - } - }) { - Image(systemName: isEditingSettings ? "info.circle.fill" : "info.circle") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - /// Look Around Button - if self.scene != nil { - Button(action: { - if isShowingAltitude { - isShowingAltitude = false - } - isLookingAround = !isLookingAround - }) { - Image(systemName: isLookingAround ? "binoculars.fill" : "binoculars") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - } - /// Altitude Button - if node.positions?.count ?? 0 > 1 { - Button(action: { - if isLookingAround { - isLookingAround = false - } - isShowingAltitude = !isShowingAltitude - }) { - Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2") - .padding(.vertical, 5) - } - .tint(Color(UIColor.secondarySystemBackground)) - .foregroundColor(.accentColor) - .buttonStyle(.borderedProminent) - } - } - .controlSize(.regular) - .padding(5) + isShowingAltitude = !isShowingAltitude + }) { + Image(systemName: isShowingAltitude ? "mountain.2.fill" : "mountain.2") + .padding(.vertical, 5) + } + .tint(Color(UIColor.secondarySystemBackground)) + .foregroundColor(.accentColor) + .buttonStyle(.borderedProminent) + } + } + .controlSize(.regular) + .padding(5) + } + + private var connectedDeviceIndicator: some View { + ConnectedDevice( + bluetoothOn: bleManager.isSwitchedOn, + deviceConnected: bleManager.connectedPeripheral != nil, + name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?" + ) + } + + var body: some View { + if node.hasPositions { + Map( + position: $position, + bounds: MapCameraBounds( + minimumDistance: 1, + maximumDistance: .infinity + ), + scope: mapScope + ) { + NodeMapContent(node: node) + } + .mapScope(mapScope) + .mapStyle(mapStyle) + .mapControls { + mapControls + } + .controlSize(.regular) + .overlay(alignment: .bottom) { + if scene != nil, isLookingAround { + LookAroundPreview(initialScene: scene) + .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal, 20) + } else if isShowingAltitude { + PositionAltitudeChart(node: node) + .frame(height: UIDevice.current.userInterfaceIdiom == .phone ? 250 : 400) + .clipShape(RoundedRectangle(cornerRadius: 12)) + .padding(.horizontal, 20) + } + } + .sheet(isPresented: $isEditingSettings) { + MapSettingsForm( + traffic: $showTraffic, + pointsOfInterest: $showPointsOfInterest, + mapLayer: $selectedMapLayer, + meshMap: $isMeshMap + ).onChange(of: (selectedMapLayer)) { newMapLayer in + switch selectedMapLayer { + case .standard: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .hybrid: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .satellite: + UserDefaults.mapLayer = newMapLayer + mapStyle = MapStyle.imagery(elevation: .flat) + case .offline: + return } - .onDisappear { - UIApplication.shared.isIdleTimerDisabled = false + } + } + .onChange(of: node) { + isLookingAround = false + isShowingAltitude = false + if node.positions?.count ?? 0 > 1 { + position = .automatic + } else if let coordinate = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: coordinate, distance: 8000, heading: 0, pitch: 60)) + } + if let coordinate = mostRecent?.coordinate { + Task { + scene = try await fetchScene(for: coordinate) } - }} - .navigationBarTitle(String((node.user?.shortName ?? "unknown".localized) + (" \(node.positions?.count ?? 0) points")), displayMode: .inline) - .navigationBarItems(trailing: - ZStack { - ConnectedDevice( - bluetoothOn: bleManager.isSwitchedOn, - deviceConnected: bleManager.connectedPeripheral != nil, - name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?") - }) + } + } + .onAppear { + setupMapView() + } + .safeAreaInset(edge: .bottom, alignment: .trailing) { + mapAccessoryControls + } + .onDisappear { + UIApplication.shared.isIdleTimerDisabled = false + } + .navigationBarTitle(title, displayMode: .inline) + .navigationBarItems(trailing: connectedDeviceIndicator) } else { ContentUnavailableView("No Positions", systemImage: "mappin.slash") } } + + private func setupMapView() { + UIApplication.shared.isIdleTimerDisabled = true + switch selectedMapLayer { + case .standard: + mapStyle = MapStyle.standard(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .hybrid: + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + case .satellite: + mapStyle = MapStyle.imagery(elevation: .flat) + case .offline: + mapStyle = MapStyle.hybrid(elevation: .flat, pointsOfInterest: showPointsOfInterest ? .all : .excludingAll, showsTraffic: showTraffic) + } + if node.positions?.count ?? 0 > 1 { + position = .automatic + } else if let coordinate = mostRecent?.coordinate { + position = .camera(MapCamera(centerCoordinate: coordinate, distance: 8000, heading: 0, pitch: 60)) + } + if scene == nil, let coordinate = mostRecent?.coordinate { + Task { + scene = try await fetchScene(for: coordinate) + } + } + } + /// Get the look around scene private func fetchScene(for coordinate: CLLocationCoordinate2D) async throws -> MKLookAroundScene? { let lookAroundScene = MKLookAroundSceneRequest(coordinate: coordinate) diff --git a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift index 221bf24f..737020a7 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/PositionPopover.swift @@ -18,6 +18,7 @@ struct PositionPopover: View { var popover: Bool = true let distanceFormatter = MKDistanceFormatter() var delay: Double = 0 + @State private var scale: CGFloat = 0.5 var body: some View { // Node Color from node.num @@ -74,7 +75,8 @@ struct PositionPopover: View { .padding(.bottom, 5) /// Altitude Label { - Text("Altitude: \(distanceFormatter.string(fromDistance: Double(position.altitude)))") + let altitude = Measurement(value: Double(position.altitude), unit: UnitLength.meters) + Text("Altitude: \(altitude.formatted())") .foregroundColor(.primary) } icon: { Image(systemName: "mountain.2.fill") @@ -193,7 +195,9 @@ struct PositionPopover: View { } BatteryGauge(node: position.nodePosition!) } - LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) + if !(position.nodePosition?.viaMqtt ?? true) && position.nodePosition?.hopsAway == 0 { + LoRaSignalStrengthMeter(snr: position.nodePosition?.snr ?? 0.0, rssi: position.nodePosition?.rssi ?? 0, preset: ModemPresets(rawValue: UserDefaults.modemPreset) ?? ModemPresets.longFast, compact: false) + } Spacer() } } diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index caa3f5d6..eff15a39 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -78,13 +78,10 @@ struct MeshMap: View { } newWaypointCoord = coordinate - editingWaypoint = WaypointEntity(context: context) - editingWaypoint!.name = "Waypoint Pin" - editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) - editingWaypoint!.latitudeI = Int32((newWaypointCoord?.latitude ?? 0) * 1e7) - editingWaypoint!.longitudeI = Int32((newWaypointCoord?.longitude ?? 0) * 1e7) - editingWaypoint!.expire = Date.now.addingTimeInterval(60 * 480) - editingWaypoint!.id = 0 + editingWaypoint = WaypointEntity( + context: context, + coordinate: coordinate + ) Logger.services.debug("Long press occured at Lat: \(coordinate.latitude) Long: \(coordinate.longitude)") default: return } @@ -133,7 +130,7 @@ struct MeshMap: View { // //position = .camera(MapCamera(centerCoordinate: waypoint.coordinate, distance: 1000, heading: 0, pitch: 60)) // } // } - .onChange(of: (selectedMapLayer)) { newMapLayer in + .onChange(of: selectedMapLayer) { newMapLayer in switch selectedMapLayer { case .standard: UserDefaults.mapLayer = newMapLayer diff --git a/Meshtastic/Views/Settings/AppLog.swift b/Meshtastic/Views/Settings/AppLog.swift new file mode 100644 index 00000000..22402ce5 --- /dev/null +++ b/Meshtastic/Views/Settings/AppLog.swift @@ -0,0 +1,142 @@ +// +// AppLog.swift +// Meshtastic +// +// Created by Garth Vander Houwen on 6/4/24. +// + +import SwiftUI +import OSLog + +@available(iOS 17.4, *) +struct AppLog: View { + + @State private var logs: [OSLogEntryLog] = [] + @State private var sortOrder = [KeyPathComparator(\OSLogEntryLog.date, order: .reverse)] + @State private var selection: OSLogEntry.ID? + @State private var selectedLog: OSLogEntryLog? + @State private var presentingErrorDetails: Bool = false + @State private var searchTerm = "" + @State var isExporting = false + @State var exportString = "" + private var idiom: UIUserInterfaceIdiom { UIDevice.current.userInterfaceIdiom } + private let dateFormatStyle = Date.FormatStyle() + .hour(.twoDigits(amPM: .omitted)) + .minute() + .second() + .secondFraction(.fractional(3)) + + private var searchResults: [OSLogEntryLog] { + if searchTerm.isEmpty { + return logs.filter { _ in true } + } else { + return logs.filter { $0.composedMessage.lowercased().contains(searchTerm.lowercased) } + } + } + + var body: some View { + + Table(searchResults, selection: $selection, sortOrder: $sortOrder) { + if idiom != .phone { + TableColumn("log.time", value: \.date) { value in + Text(value.date.formatted(dateFormatStyle)) + } + .width(min: 125, max: 150) + TableColumn("log.category", value: \.category) + .width(min: 125, max: 150) + TableColumn("log.level") { value in + Text(value.level.description) + } + .width(min: 75, max: 100) + } + TableColumn("log.message", value: \.composedMessage) { value in + Text(value.composedMessage) + } + .width(ideal: 200, max: .infinity) + + + } + .monospaced() + .searchable(text: $searchTerm, placement: .navigationBarDrawer, prompt: "Search") + .disabled(selection != nil) + .overlay { + if logs.isEmpty { + ContentUnavailableView("Getting Logs . . .", systemImage: "scroll") + } + } + .onChange(of: sortOrder) { _, sortOrder in + withAnimation { + logs.sort(using: sortOrder) + } + } + .onChange(of: selection) { newSelection in + presentingErrorDetails = true + let log = logs.first { + $0.id == newSelection + } + selectedLog = log + } + .sheet(item: $selectedLog, onDismiss: didDismiss) { log in + LogDetail(log: log) + .padding() + } + .task { + logs = await fetchLogs() + logs.sort(using: sortOrder) + } + .fileExporter( + isPresented: $isExporting, + document: CsvDocument(emptyCsv: exportString), + contentType: .commaSeparatedText, + defaultFilename: String("Meshtastic Application Logs"), + onCompletion: { result in + switch result { + case .success: + self.isExporting = false + Logger.services.info("Application log download succeeded.") + case .failure(let error): + Logger.services.error("Application log download failed: \(error.localizedDescription)") + } + } + ) + .navigationBarTitle("Debug Logs\(logs.isEmpty ? "" : " (\(logs.count))")", displayMode: .inline) + .toolbar { + if !logs.isEmpty { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + exportString = logToCsvFile(log: logs) + isExporting = true + }) { + Image(systemName: "square.and.arrow.down") + } + } + } + } + } + + func didDismiss() { + selection = nil + selectedLog = nil + } +} + +@available(iOS 17.4, *) +extension AppLog { + static private let template = NSPredicate(format: "(subsystem BEGINSWITH $PREFIX) || ((subsystem IN $SYSTEM) && ((messageType == error) || (messageType == fault)))") + + @MainActor + private func fetchLogs() async -> [OSLogEntryLog] { + do { + let predicate = NSPredicate(format: "subsystem IN %@", [ + "com.apple.coredata", + "gvh.MeshtasticClient" + ]) + let logs = try await Logger.fetch(predicateFormat: predicate.predicateFormat) + return logs + } catch { + return [] + } + } +} + +extension OSLogEntry: Identifiable { } diff --git a/Meshtastic/Views/Settings/Channels.swift b/Meshtastic/Views/Settings/Channels.swift index 689a009e..18a9890d 100644 --- a/Meshtastic/Views/Settings/Channels.swift +++ b/Meshtastic/Views/Settings/Channels.swift @@ -259,18 +259,18 @@ struct Channels: View { uplink = false downlink = false hasChanges = true - - let newChannel = ChannelEntity(context: context) - newChannel.id = channelIndex - newChannel.index = channelIndex - newChannel.uplinkEnabled = uplink - newChannel.downlinkEnabled = downlink - newChannel.name = channelName - newChannel.role = Int32(channelRole) - newChannel.psk = Data(base64Encoded: channelKey) ?? Data() - newChannel.positionPrecision = Int32(positionPrecision) - selectedChannel = newChannel - + + selectedChannel = ChannelEntity( + context: context, + id: channelIndex, + index: channelIndex, + uplinkEnabled: uplink, + downlinkEnabled: downlink, + name: channelName, + role: Int32(channelRole), + psk: Data(base64Encoded: channelKey) ?? Data(), + positionPrecision: Int32(positionPrecision) + ) } label: { Label("Add Channel", systemImage: "plus.square") } diff --git a/Meshtastic/Views/Settings/RouteRecorder.swift b/Meshtastic/Views/Settings/RouteRecorder.swift index c19dbd9f..38685a23 100644 --- a/Meshtastic/Views/Settings/RouteRecorder.swift +++ b/Meshtastic/Views/Settings/RouteRecorder.swift @@ -284,29 +284,22 @@ struct RouteRecorder: View { .onDisappear(perform: { UIApplication.shared.isIdleTimerDisabled = false }) - .onChange(of: locationsHandler.locationsArray.last) { newLoc in - if locationsHandler.isRecording { - if let loc = newLoc { - if recording != nil { - let locationEntity = LocationEntity(context: context) - locationEntity.routeLocation = recording - locationEntity.id = Int32(locationsHandler.count) - locationEntity.altitude = Int32(loc.altitude) - locationEntity.heading = Int32(loc.course) - locationEntity.speed = Int32(loc.speed) - locationEntity.latitudeI = Int32(loc.coordinate.latitude * 1e7) - locationEntity.longitudeI = Int32(loc.coordinate.longitude * 1e7) - do { - try context.save() - Logger.data.info("💾 Saved a new route location") - // logger.info("💾 Updated Canned Messages Messages For: \(fetchedNode[0].num)") - } catch { - context.rollback() - let nsError = error as NSError - Logger.data.error("Error Saving LocationEntity from the Route Recorder \(nsError)") - } - } - } + .onChange(of: locationsHandler.locationsArray.last) { location in + guard locationsHandler.isRecording, let location, let recording else { return } + let locationEntity = LocationEntity( + context: context, + route: recording, + id: Int32(locationsHandler.count), + location: location + ) + + do { + try context.save() + Logger.data.info("💾 Saved a new route location") + } catch { + context.rollback() + let nsError = error as NSError + Logger.data.error("Error Saving LocationEntity from the Route Recorder \(nsError)") } } } diff --git a/Meshtastic/Views/Settings/Routes.swift b/Meshtastic/Views/Settings/Routes.swift index cc70de3b..35e4a894 100644 --- a/Meshtastic/Views/Settings/Routes.swift +++ b/Meshtastic/Views/Settings/Routes.swift @@ -58,21 +58,22 @@ struct Routes: View { } do { - guard let fileContent = String(data: try Data(contentsOf: selectedFile), encoding: .utf8) else { return } + let data = try Data(contentsOf: selectedFile) + let fileContent = String(decoding: data, as: UTF8.self) let routeName = selectedFile.lastPathComponent.dropLast(4) let lines = fileContent.components(separatedBy: "\n") - let headers = lines.first?.components(separatedBy: ",") - var latIndex = -1 - var longIndex = -1 - for index in headers!.indices { - Logger.services.debug("\(index): \( headers![index])") - if headers![index].trimmingCharacters(in: .whitespaces) == "Latitude" { + guard let headers = lines.first?.components(separatedBy: ",") else { return } + var latIndex: Int? + var longIndex: Int? + for index in headers.indices { + Logger.services.debug("\(index): \(headers[index])") + if headers[index].trimmingCharacters(in: .whitespaces) == "Latitude" { latIndex = index - } else if headers![index].trimmingCharacters(in: .whitespaces) == "Longitude" { + } else if headers[index].trimmingCharacters(in: .whitespaces) == "Longitude" { longIndex = index } } - if latIndex >= 0 && longIndex >= 0 { + if let latIndex, let longIndex { let newRoute = RouteEntity(context: context) newRoute.name = String(routeName) newRoute.id = Int32.random(in: Int32(Int8.max) ... Int32.max) @@ -83,8 +84,8 @@ struct Routes: View { lines.dropFirst().forEach { line in let data = line.components(separatedBy: ",") if data.count > 1 { - let latitude = latIndex >= 0 ? data[latIndex].trimmingCharacters(in: .whitespaces) : "0" - let longitude = longIndex >= 0 ? data[longIndex].trimmingCharacters(in: .whitespaces) : "0" + let latitude = data[latIndex].trimmingCharacters(in: .whitespaces) + let longitude = data[longIndex].trimmingCharacters(in: .whitespaces) let loc = LocationEntity(context: context) loc.latitudeI = Int32((Double(latitude) ?? 0) * 1e7) loc.longitudeI = Int32((Double(longitude) ?? 0) * 1e7) diff --git a/Meshtastic/Views/Settings/Settings.swift b/Meshtastic/Views/Settings/Settings.swift index f2a38030..690ebdaa 100644 --- a/Meshtastic/Views/Settings/Settings.swift +++ b/Meshtastic/Views/Settings/Settings.swift @@ -47,6 +47,7 @@ struct Settings: View { case telemetryConfig case meshLog case adminMessageLog + case appLog case about } var body: some View { @@ -412,17 +413,18 @@ struct Settings: View { } } .tag(SettingsSidebar.meshLog) - NavigationLink { - let connectedNode = nodes.first(where: { $0.num == preferredNodeNum }) - AdminMessageList(user: connectedNode?.user) - } label: { - Label { - Text("admin.log") - } icon: { - Image(systemName: "building.columns") + if #available (iOS 17.4, *) { + NavigationLink { + AppLog() + } label: { + Label { + Text("Debug Logs") + } icon: { + Image(systemName: "stethoscope") + } } + .tag(SettingsSidebar.appLog) } - .tag(SettingsSidebar.adminMessageLog) } Section(header: Text("Firmware")) { NavigationLink { diff --git a/MeshtasticTests/Info.plist b/MeshtasticTests/Info.plist deleted file mode 100644 index 64d65ca4..00000000 --- a/MeshtasticTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/MeshtasticTests/MeshtasticTests.swift b/MeshtasticTests/MeshtasticTests.swift deleted file mode 100644 index bc75d0da..00000000 --- a/MeshtasticTests/MeshtasticTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// MeshtasticAppleTests.swift -// MeshtasticAppleTests -// -// Created by Garth Vander Houwen on 8/18/21. -// - -import XCTest - -@testable import Meshtastic - -class MeshtasticTests: XCTestCase { - - override func setUpWithError() throws { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/MeshtasticUITests/Info.plist b/MeshtasticUITests/Info.plist deleted file mode 100644 index 64d65ca4..00000000 --- a/MeshtasticUITests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/MeshtasticUITests/MeshtasticUITests.swift b/MeshtasticUITests/MeshtasticUITests.swift deleted file mode 100644 index ae140de9..00000000 --- a/MeshtasticUITests/MeshtasticUITests.swift +++ /dev/null @@ -1,42 +0,0 @@ -// -// MeshtasticUITests.swift -// MeshtasticUITests -// -// Copyright(c) Garth Vander Houwen 8/18/21. -// - -import XCTest - -class MeshtasticUITests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - continueAfterFailure = false - - // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // UI tests must launch the application that they test. - let app = XCUIApplication() - app.launch() - - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testLaunchPerformance() throws { - if #available(macOS 13, iOS 16.0, watchOS 8.0, *) { - // This measures how long it takes to launch your application. - measure(metrics: [XCTApplicationLaunchMetric()]) { - XCUIApplication().launch() - } - } - } -} diff --git a/README.md b/README.md index 9b888e90..43dd88aa 100644 --- a/README.md +++ b/README.md @@ -8,35 +8,41 @@ SwiftUI client applications for iOS, iPadOS and macOS. -## OS Requirements +## Getting Started -* iOS App Requires iOS 16 + -* iPadOS App Requires iPadOS 16 + -* Mac App Reguires macOS 13 + +This project is currently using **Xcode 15.4**. -## Code Standards +1. Clone the repo. +2. Open `Meshtastic.xcodeproj` +2. Build and run the `Meshtastic` target. -- Use SwiftUI (Maps are the exception) -- Use Hierarchical icons +```sh +git clone git@github.com:meshtastic/Meshtastic-Apple.git +cd Meshtastic-Apple +open Meshtastic.xcodeproj +``` + +## Technical Standards + +### Supported Operating Systems + +* iOS 16+ +* iPadOS 16+ +* macOS 13+ + +### Code Standards + +- Use SwiftUI +- Use SFSymbols for icons - Use Core Data for persistence -- Requires SwiftLint - see https://github.com/realm/SwiftLint -## To update protobufs: - -- install swift-protobuf: - ```bash - brew install swift-protobuf - ``` -- check out the latest protobuf commit from the master branch - ```bash - git submodule update --init - ``` +## Updating Protobufs: - run: ```bash - ./gen_protos.sh + .scripts/gen_protos.sh ``` - build, test, commit changes -- You may need to run: - ```bash - swiftlint --fix - ``` + +## License + +This project is licensed under the GPL v3. See the [LICENSE](LICENSE) file for details. diff --git a/Widgets/Info.plist b/Widgets/Info.plist index 0f118fb7..b3dac3d4 100644 --- a/Widgets/Info.plist +++ b/Widgets/Info.plist @@ -2,6 +2,8 @@ + CFBundleVersion + $(CURRENT_PROJECT_VERSION) NSExtension NSExtensionPointIdentifier diff --git a/de.lproj/Localizable.strings b/de.lproj/Localizable.strings index 511ecdaa..83fe06d3 100644 --- a/de.lproj/Localizable.strings +++ b/de.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.seventytwo.hours"="Seventy Two Hours"; "keyboard.type"="Keyboard Typ"; "logging"="Logging"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Einstellungen"; "map"="Mesh Karte"; diff --git a/en.lproj/Localizable.strings b/en.lproj/Localizable.strings index 4c507879..fb3af6f6 100644 --- a/en.lproj/Localizable.strings +++ b/en.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="Seventy Two Hours"; "keyboard.type"="Keyboard Type"; "logging"="Logging"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Config"; "map"="Mesh Map"; diff --git a/fr.lproj/Localizable.strings b/fr.lproj/Localizable.strings index 785a2012..3c517ac3 100644 --- a/fr.lproj/Localizable.strings +++ b/fr.lproj/Localizable.strings @@ -154,6 +154,12 @@ "interval.seventytwo.hours"="Soixante douze heures"; "keyboard.type"="Type de clavier"; "logging"="Enregistrement"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="Configuration LoRa"; "map"="Carte de maillage"; diff --git a/he.lproj/Localizable.strings b/he.lproj/Localizable.strings index 154c2391..fd844980 100644 --- a/he.lproj/Localizable.strings +++ b/he.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="שבעים ושתיים שעות"; "keyboard.type"="סוג מקלדת"; "logging"="רישום"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="לורה"; "lora.config"="הגדרות לורה"; "map"="מפת מש"; diff --git a/pl.lproj/Localizable.strings b/pl.lproj/Localizable.strings index eb8372b0..e09d745b 100644 --- a/pl.lproj/Localizable.strings +++ b/pl.lproj/Localizable.strings @@ -175,6 +175,12 @@ "interval.seventytwo.hours"="Siedemdziesiąt Dwie Godziny"; "keyboard.type"="Typ Klawiatury"; "logging"="Rejestracja"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="Konfiguracja LoRa"; "map"="Mapa Sieci"; diff --git a/pt-PT.lproj/Localizable.strings b/pt-PT.lproj/Localizable.strings index 4c507879..f5131a0c 100644 --- a/pt-PT.lproj/Localizable.strings +++ b/pt-PT.lproj/Localizable.strings @@ -2,376 +2,382 @@ Localizable.strings Meshtastic - Copyright(c) Garth Vander Houwen on 12/12/22. + Copyright(c) Garth Vander Houwen on 12/12/22. Translated from English to Portuguese by Philip Rosa-Leeke 2024 */ -"about"="About"; -"about.meshtastic"="About Meshtastic"; -"activity"="Activity"; +"about"="Sobre"; +"about.meshtastic"="Sobre Meshtastic"; +"activity"="Actividade"; "admin"="Admin"; -"admin.log"="Admin Message Log"; -"ago"="ago"; -"airtime"="Airtime"; -"always.on"="Always On"; -"ambient.lighting"="Ambient Lighting"; -"ambient.lighting.config"="Ambient Lighting Config"; -"appsettings"="App Settings"; -"appsettings.provide.location"="Share location"; -"appsettings.smartposition"="Smart Position"; -"are.you.sure"="Are you sure?"; -"ascii.capable"="ASCII Capable"; -"available.radios"="Available Radios"; -"automatic.detection"="Automatic Detection"; -"battery.level"="Battery Level"; -"ble.name"="BLE Name"; -"ble.connection.timeout %d %@"="Connection failed after %d attempts to connect to %@. You may need to forget your device under Settings > Bluetooth."; -"ble.errorcode.6 %@"="%@ The app will automatically reconnect to the preferred radio if it comes back in range."; -"ble.errorcode.14 %@"="%@ This error usually cannot be fixed without forgetting the device unders Settings > Bluetooth and re-connecting to the radio."; -"ble.errorcode.pin %@"="%@ Please try connecting again and check the PIN carefully."; +"admin.log"="Log das Mensagens do Admin"; +"ago"="há"; +"airtime"="Tempo ao Ár"; +"always.on"="Sempre Ligado"; +"ambient.lighting"="Iluminação Ambiental"; +"ambient.lighting.config"="Configuração Iluminação Ambiental"; +"appsettings"="Definições do App"; +"appsettings.provide.location"="Partilha localização"; +"appsettings.smartposition"="Posição Inteligente"; +"are.you.sure"="Tem a certeza?"; +"ascii.capable"="Capacidade ASCII"; +"available.radios"="Rádios Disponíveis"; +"automatic.detection"="Deteção Automático"; +"battery.level"="Nível de Bataria"; +"ble.name"="Nome BLE"; +"ble.connection.timeout %d %@"="Falha de conexão após %d tentativas de conectar a %@. Você pode precisar esquecer seu dispositivo em Configurações > Bluetooth."; +"ble.errorcode.6 %@"="%@ O App vai reconetar automaticamente ao rádio preferido se ele voltar ao alcance."; +"ble.errorcode.14 %@"="%@ Esse erro geralmente não pode ser corrigido sem esquecer o dispositivo em Configurações > Bluetooth e reconetar ao rádio."; +"ble.errorcode.pin %@"="%@ Por favor, tente conectar novamente e verifique cuidadosamente o PIN."; "bluetooth"="Bluetooth"; -"bluetooth.off"="Bluetooth is off"; -"bluetooth.config"="Bluetooth Config"; -"bluetooth.mode.randompin"="Random PIN"; -"bluetooth.mode.fixedpin"="Fixed PIN"; -"bluetooth.mode.nopin"="No PIN (Just Works)"; -"bluetooth.pairingmode"="Pairing Mode"; -"bluetooth.pin.validation"="BLE Pin must be 6 digits long."; +"bluetooth.off"="Bluetooth está desligado"; +"bluetooth.config"="Configuração Bluetooth"; +"bluetooth.mode.randompin"="PIN Aleatório"; +"bluetooth.mode.fixedpin"="PIN fixo"; +"bluetooth.mode.nopin"="Sem PIN (Simplesmente Funciona)"; +"bluetooth.pairingmode"="Modo Pairing"; +"bluetooth.pin.validation"="O Pin do BLE deve ter 6 dígitos."; "bytes"="Bytes"; -"cancel"="Cancel"; -"canned.messages"="Canned Messages"; -"canned.messages.config"="Canned Messages Config"; -"canned.messages.preset.manual"="Manual Configuration"; -"canned.messages.preset.rakrotary"="RAK Rotary Encoder Module"; -"canned.messages.preset.cardkb"="M5 Stack Card KB / RAK Keypad"; -"channel"="Channel"; -"channel.role.disabled"="Disabled"; -"channel.role.primary"="Primary"; -"channel.role.secondary"="Secondary"; -"channel.utilization"="Channel Utilization"; -"channels"="Channels"; -"clear.app.data"="Clear App Data"; -"clear.log"="Clear"; -"close"="Close"; -"config.power.settings"="Power"; -"config.power.title"="Power Config"; -"config.power.section.battery"="Battery"; -"config.power.section.sleep"="Sleep"; -"config.power.adc.override"="ADC Override"; -"config.power.adc.multiplier"="Multiplier"; -"config.power.ls.secs"="Light Sleep Interval"; -"config.power.min.wake.secs"="Minimum Wake Interval"; -"config.power.saving"="Power Saving"; -"config.power.saving.description"="Will sleep everything as much as possible, for the tracker and sensor role this will also include the lora radio. Don't use this setting if you want to use your device with the phone apps or are using a device without a user button."; -"config.power.shutdown.on.power.loss"="Shutdown on Power Loss"; -"config.power.shutdown.after.secs"="After"; -"config.power.wait.bluetooth.secs"="Bluetooth Off After"; -"config.ringtone"="RTTTL Ringtone"; -"config.ringtone.title"="Ringtone Config"; -"config.ringtone.label"="Ringtone Transfer Language"; -"config.ringtone.description"="Ringtone Transfer Language(RTTTL) Ringtone String used by supported buzzers in external notifications."; -"config.module.paxcounter.settings"="PAX Counter"; -"config.module.paxcounter.title"="PAX Counter Config"; -"config.module.paxcounter.enabled.description"="When enabled the PAX Counter module counts the number of people passing by using WiFi and Bluetooth. Both WiFI and Bluetooth must be disabled for PAX counter to work."; -"config.module.paxcounter.updateinterval"="Update Interval"; -"config.module.paxcounter.updateinterval.description"="How often we can send a message to the mesh when people are detected."; -"config.save.confirm"="After config values save the node will reboot."; -"communicating"="Communicating with device. ."; -"connected.radio"="Connected Radio"; -"connected"="Bluetooth Connected"; -"connecting"="Connecting . ."; -"contacts"="Contacts"; -"contacts %@"="Contacts (%@)"; -"copy"="Copy"; -"current"="Current"; -"default"="Default"; -"delete"="Delete"; -"detection.sensor"="Detection Sensor"; -"detection.sensor.config"="Detection Sensor Config"; -"detection.sensor.log"="Detection Sensor Log"; -"device"="Device"; -"device.config"="Device Config"; -"device.configuration"="Device Configuration"; -"device.metrics.delete"="Delete all device metrics?"; -"device.metrics.log"="Device Metrics Log"; -"device.role.client"="App connected or stand alone messaging device."; -"device.role.clientmute"="Device that does not forward packets from other devices."; -"device.role.clienthidden"="Device that only broadcasts as needed for stealth or power savings."; -"device.role.tracker"="Broadcasts GPS position packets as priority."; -"device.role.lostandfound"="Broadcasts location as message to default channel regularly for to assist with device recovery."; -"device.role.sensor"="Broadcasts telemetry packets as priority."; -"device.role.tak"="Optimized for ATAK system communication, reduces routine broadcasts."; -"device.role.taktracker"="Enables automatic TAK PLI broadcasts and reduces routine broadcasts."; -"device.role.repeater"="Infrastructure node for extending network coverage by relaying messages with minimal overhead. Not visible in Nodes list."; -"device.role.router"="Infrastructure node for extending network coverage by relaying messages. Visible in Nodes list."; -"device.role.routerclient"="Combination of both ROUTER and CLIENT. Not for mobile devices."; -"direct.messages"="Direct Messages"; -"dismiss.keyboard"="Dismiss"; -"display"="Display"; -"display.config"="Display Config"; -"distance"="Distance"; -"disconnect"="Disconnect"; -"echo"="Echo"; -"email.address"="Email Address"; -"enabled"="Enabled"; -"encrypted"="Encrypted"; -"export"="Export"; -"external.notification"="External Notification"; -"external.notification.config"="External Notification Config"; -"finish"="Finish"; -"firmware.version"="Firmware Version"; -"firmware.version.unsupported"="Unsupported Firmware Version Detected, unable to connect to device."; +"cancel"="Cancelar"; +"canned.messages"="Mensagens Enlatados"; +"canned.messages.config"="Configuração dos Mensagens Enlatados"; +"canned.messages.preset.manual"="Configuração Manual"; +"canned.messages.preset.rakrotary"="Module Codificador do RAK Rotary"; +"canned.messages.preset.cardkb"="M5 Stack Card KB / Teclado RAK"; +"channel"="Canal"; +"channel.role.disabled"="Desativado"; +"channel.role.primary"="Primário"; +"channel.role.secondary"="Secundária"; +"channel.utilization"="Utilização do Canal"; +"channels"="Canais"; +"clear.app.data"="Apagar os dados do App"; +"clear.log"="Apagar"; +"close"="Fechar"; +"config.power.settings"="Energia"; +"config.power.title"="Configuração de Energia"; +"config.power.section.battery"="Bataria"; +"config.power.section.sleep"="Dormir"; +"config.power.adc.override"="Substituir ADC"; +"config.power.adc.multiplier"="Multiplicador"; +"config.power.ls.secs"="Intervalo de Dormir Leve"; +"config.power.min.wake.secs"="Intervalo Mínimo de Despertar"; +"config.power.saving"="Poupar a Energia"; +"config.power.saving.description"="Vai por dormir o máximo possível, para o papel do rastreador e o papel do sensor isso incluirá também o rádio lora. Não use essa configuração se deseja usar seu dispositivo com os aplicativos do telefone ou está usando um dispositivo sem um botão do usuário."; +"config.power.shutdown.on.power.loss"="Desligar em caso de Perda de Energia"; +"config.power.shutdown.after.secs"="Após"; +"config.power.wait.bluetooth.secs"="Desligar o Bluetooth Após"; +"config.ringtone"="Toque RTTTL"; +"config.ringtone.title"="Configuração de Toque"; +"config.ringtone.label"="Idioma de Transferência de Toque"; +"config.ringtone.description"="Idioma de Transferência de Toque (RTTTL) Sequência de Toque usada por campainhas suportadas em notificações externas."; +"config.module.paxcounter.settings"="Contador de PAX"; +"config.module.paxcounter.title"="Configuração do Contador de PAX"; +"config.module.paxcounter.enabled.description"="Quando ativado, o módulo de Contador de PAX conta o número de pessoas que passam usando Wi-Fi e Bluetooth. Tanto o Wi-Fi quanto o Bluetooth devem estar desativados para que o contador de PAX funcione."; +"config.module.paxcounter.updateinterval"="Intervalo de Atualização"; +"config.module.paxcounter.updateinterval.description"="Com que frequência podemos enviar uma mensagem para a malha quando as pessoas são detectadas."; +"config.save.confirm"="Após salvar os valores de configuração, o nó reiniciará"; +"communicating"="Comunicando com dispositivo. ."; +"connected.radio"="Rádio Conectado"; +"connected"="Bluetooth Connectado"; +"connecting"="Conectando . ."; +"contacts"="Contactos"; +"contacts %@"="Contactos (%@)"; +"copy"="Copiar"; +"current"="Atual"; +"default"="Padrão"; +"delete"="Apagar"; +"detection.sensor"="Sensor de Detecção"; +"detection.sensor.config"="Configuração do Sensor de Detecção"; +"detection.sensor.log"="Log Sensor de Detecção"; +"device"="Dispositivo"; +"device.config"="Configuração do Dispositivo"; +"device.configuration"="Configuração do Dispositivo"; +"device.metrics.delete"="Apagar todas as métricas do dispositivo?"; +"device.metrics.log"="Log g de Métricas do Dispositivo"; +"device.role.client"="Dispositivo conectado ao App ou independente para mensagens."; +"device.role.clientmute"="Dispositivo que não encaminha pacotes de outros dispositivos."; +"device.role.clienthidden"="Dispositivo que apenas transmite conforme necessário em modo furtivo ou economia de energia."; +"device.role.tracker"="Transmite pacotes de posição GPS como prioridade."; +"device.role.lostandfound"="Transmite a localização como mensagem para o canal padrão regularmente para auxiliar na recuperação do dispositivo."; +"device.role.sensor"="Transmite pacotes de telemetria como prioridade."; +"device.role.tak"="Otimizado para comunicação do sistema ATAK, reduz transmissões rotineiras."; +"device.role.taktracker"="Permite transmissões automáticas de TAK PLI e reduz transmissões rotineiras."; +"device.role.repeater"="Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens com sobrecarga mínima. Não visível na lista de Nós."; +"device.role.router"="Nó de infraestrutura para ampliar a cobertura da rede transmitindo mensagens. Visível na lista de Nós."; +"device.role.routerclient"="Combinação de ROTEADOR e CLIENTE. Não para dispositivos móveis."; +"direct.messages"="Mensagens Directas"; +"dismiss.keyboard"="Dispensar"; +"display"="Icrã"; +"display.config"="Configuração do Icrã"; +"distance"="Distância"; +"disconnect"="Desconectar"; +"echo"="Eco"; +"email.address"="Endereço de Email"; +"enabled"="Activado"; +"encrypted"="Encriptado"; +"export"="Exportar"; +"external.notification"="Notificação Externa"; +"external.notification.config"="Configuração de Notificação Externa"; +"finish"="Terminar"; +"firmware.version"="Versão do Firmware"; +"firmware.version.unsupported"="Versão de Firmware não suportada detetada, impossível conectar ao dispositivo."; "gas"="Gas"; -"gas.resistance"="Gas Resistance"; -"generate.qr.code"="Generate QR Code"; -"gpsformat.dec"="Decimal Degrees Format"; -"gpsformat.dms"="Degrees Minutes Seconds"; +"gas.resistance"="Resistência ao Gas"; +"generate.qr.code"="Gerar Código QR"; +"gpsformat.dec"="Formato de Graus Decimais"; +"gpsformat.dms"="Graus Minutos Segundos"; "gpsformat.utm"="Universal Transverse Mercator"; -"gpsformat.mgrs"="Military Grid Reference System"; -"gpsformat.olc"="Open Location Code (aka Plus Codes)"; -"gpsformat.osgr"="Ordnance Survey Grid Reference"; -"gpsmode.disabled"="Disabled"; -"gpsmode.enabled"="Enabled"; -"gpsmode.notPresent"="Not Present"; -"heard"="Heard"; -"heard.last"="Last Heard"; -"hybrid"="Hybrid"; -"hybrid.flyover"="Hybrid Flyover"; -"include"="Include"; -"incomplete"="Incomplete"; -"inputevent.none"="None"; -"inputevent.up"="Up"; -"inputevent.down"="Down"; -"inputevent.left"="Left"; -"inputevent.right"="Right"; -"inputevent.select"="Select"; -"inputevent.back"="Back"; -"inputevent.cancel"="Cancel"; -"interval.one.second"="One Second"; -"interval.two.seconds"="Two Seconds"; -"interval.three.seconds"="Three Seconds"; -"interval.four.seconds"="Four Seconds"; -"interval.five.seconds"="Five Seconds"; -"interval.ten.seconds"="Ten Seconds"; -"interval.fifteen.seconds"="Fifteen Seconds"; -"interval.twenty.seconds"="Twenty Seconds"; -"interval.twentyfive.seconds"="Twenty Five Seconds"; -"interval.thirty.seconds"="Thirty Seconds"; -"interval.fortyfive.seconds"="Forty Five Seconds"; -"interval.one.minute"="One Minute"; -"interval.two.minutes"="Two Minutes"; -"interval.five.minutes"="Five Minutes"; -"interval.ten.minutes"="Ten Minutes"; -"interval.fifteen.minutes"="Fifteen Minutes"; -"interval.thirty.minutes"="Thirty Minutes"; -"interval.one.hour"="One Hour"; -"interval.two.hours"="Two Hours"; -"interval.three.hours"="Three Hours"; -"interval.four.hours"="Four Hours"; -"interval.five.hours"="Five Hours"; -"interval.six.hours"="Six Hours"; -"interval.twelve.hours"="Twelve Hours"; -"interval.eighteen.hours"="Eighteen Hours"; -"interval.twentyfour.hours"="Twenty Four Hours"; -"interval.thirtysix.hours"="Thirty Six Hours"; -"interval.fortyeight.hours"="Forty Eight Hours"; -"interval.seventytwo.hours"="Seventy Two Hours"; -"keyboard.type"="Keyboard Type"; -"logging"="Logging"; +"gpsformat.mgrs"="Sistema de Referência de Grelha Militar"; +"gpsformat.olc"="Código de Localização Aberto (também conhecido como Plus Codes)"; +"gpsformat.osgr"="Referência de Grelha da Ordnance Survey"; +"gpsmode.disabled"="Desativado"; +"gpsmode.enabled"="Ativado"; +"gpsmode.notPresent"="Não Presente"; +"heard"="Ouvido"; +"heard.last"="Último Ouvido"; +"hybrid"="Híbrido"; +"hybrid.flyover"="Híbrido o de Sobrevoo"; +"include"="Incluir"; +"incomplete"="Incompleto"; +"inputevent.none"="Nenhum"; +"inputevent.up"="Para Cima"; +"inputevent.down"="Para Baixo"; +"inputevent.left"="Esquerda"; +"inputevent.right"="Direita"; +"inputevent.select"="Selecionar"; +"inputevent.back"="Voltar"; +"inputevent.cancel"="Cancelar"; +"interval.one.second"="Um Segundo"; +"interval.two.seconds"="Dois Segundos"; +"interval.three.seconds"="Três Segundos"; +"interval.four.seconds"="Quatro Segundos"; +"interval.five.seconds"="Cinco Segundos"; +"interval.ten.seconds"="Dez Segundos"; +"interval.fifteen.seconds"="Quinze Segundos"; +"interval.twenty.seconds"="Vinte Segundos"; +"interval.twentyfive.seconds"="Vinte e Cinco Segundos"; +"interval.thirty.seconds"="Trinta Segundos"; +"interval.fortyfive.seconds"="Quarenta e Cinco Segundos"; +"interval.one.minute"="Um Minuto"; +"interval.two.minutes"="Dois Minutos"; +"interval.five.minutes"="Cinco Minutos"; +"interval.ten.minutes"="Dez Minutos"; +"interval.fifteen.minutes"="Quinze Minutos"; +"interval.thirty.minutes"="Trinta Minutos"; +"interval.one.hour"="Uma Hora"; +"interval.two.hours"="Duas Horas"; +"interval.three.hours"="Três Horas"; +"interval.four.hours"="Quatro Horas"; +"interval.five.hours"="Cinco Horas"; +"interval.six.hours"="Seis Horas"; +"interval.twelve.hours"="Doze Horas"; +"interval.eighteen.hours"="Dezoito Horas"; +"interval.twentyfour.hours"="Vinte e Quatro Horas"; +"interval.thirtysix.hours"="Trinta e Seis Horas"; +"interval.fortyeight.hours"="Quarenta e Oito Horas"; +"interval.seventytwo.hours"="Setenta e Duas Horas"; +"keyboard.type"="Tipo de Teclado"; +"logging"="Registo"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; -"lora.config"="LoRa Config"; -"map"="Mesh Map"; -"map.type"="Default Type"; -"map.centering"="Centering Mode"; -"map.tiles.delete"="Delete All Map Tiles"; -"map.recentering"="Automatic Re-centering"; -"map.use.legacy"="Use Legacy Mesh Map"; -"map.usertrackingmode"="User tracking mode"; -"map.usertrackingmode.follow"="Follow"; -"map.usertrackingmode.followwithheading"="Follow with heading"; -"map.usertrackingmode.none"="None"; -"mesh.live.activity"="Mesh Live Activity"; -"mesh.log"="Mesh Log"; -"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@"; -"mesh.log.bluetooth.config %@"="Bluetooth config received: %@"; -"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@"; -"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@"; -"mesh.log.cannedmessages.messages.received %@"="Canned Messages Messages Received For: %@"; -"mesh.log.channel.sent %@ %d"="Sent a Channel for: %@ Channel Index %d"; -"mesh.log.channel.received %d %@"="Channel %d received from: %@"; -"mesh.log.device.config %@"="Device config received: %@"; -"mesh.log.display.config %@"="Display config received: %@"; -"mesh.log.devicemetadata %@"="Requesting Device Metadata for %@"; -"mesh.log.device.metadata.received %@"="Device Metadata received from: %@"; -"mesh.log.detectionsensor.config %@"="Detection Sensor module config received: %@"; -"mesh.log.externalnotification.config %@"="External Notification module config received: %@"; -"mesh.log.lora.config %@"="LoRa config received: %@"; -"mesh.log.lora.config.sent %@"="Sent a LoRa.Config for: %@"; -"mesh.log.mqtt.config %@"="MQTT module config received: %@"; -"mesh.log.myinfo %@"="MyInfo received: %@"; -"mesh.log.network.config %@"="Network config received: %@"; -"mesh.log.nodeinfo.received %@"="Node info received for: %@"; -"mesh.log.paxcounter %@"="PAX Counter message received from: %@"; -"mesh.log.paxcounter.config %@"="PAX Counter config received: %@"; -"mesh.log.position.config %@"="Positon config received: %@"; -"mesh.log.position.received %@"="Position Packet received from node: %@"; -"mesh.log.power.config %@"="Power config received: %@"; -"mesh.log.rangetest.config %@"="Range Test module config received: %@"; -"mesh.log.ringtone.config %@"="RTTTL Ringtone config received: %@"; -"mesh.log.routing.message %@ %@"="Routing received for RequestID: %@ Ack Status: %@"; -"mesh.log.serial.config %@"="Serial module config received: %@"; -"mesh.log.sharelocation %@"="Sent a Position Packet from the Apple device GPS to node: %@"; -"mesh.log.storeforward.config %@"="Store & Forward module config received: %@"; -"mesh.log.telemetry.config %@"="Telemetry module config received: %@"; -"mesh.log.telemetry.received %@"="Telemetry received for: %@"; -"mesh.log.textmessage.received"="Message received from the text message app."; -"mesh.log.textmessage.send.failed %@"="Message Send Failed, not properly connected to %@"; -"mesh.log.textmessage.sent %@ %@ %@"="Sent message %@ from %@ to %@"; -"mesh.log.traceroute.received.direct %@"="Trace Route request sent to node: %@ was recieived directly."; -"mesh.log.traceroute.received.route %@"="Trace Route request returned: %@"; -"mesh.log.traceroute.sent %@"="Sent a Trace Route Request to node: %@"; -"mesh.log.wantconfig %@"="Issuing Want Config to %@"; -"mesh.log.waypoint.sent %@"="Sent a Waypoint Packet from: %@"; -"mesh.log.waypoint.received %@"="Waypoint Packet received from node: %@"; -"message"="Message"; -"message.details"="Message Details"; -"messages"="Messages"; -"mode"="Mode"; -"module.configuration"="Module Configuration"; +"lora.config"="Configuração LoRa"; +"map"="Mapa do Mesh"; +"map.type"="Tipo Padrão"; +"map.centering"="Modo de Centralização"; +"map.tiles.delete"="Apagar Todas as Imagens da Mapa"; +"map.recentering"="Re-centralização Automática"; +"map.use.legacy"="Utilizar Mapa do Mesh Antigo"; +"map.usertrackingmode"="Modo de Rastreamento do Utilizador"; +"map.usertrackingmode.follow"="Seguir"; +"map.usertrackingmode.followwithheading"="Seguir com Direção"; +"map.usertrackingmode.none"="Nenhum"; +"mesh.live.activity"="Atividade Ao Vivo do Mesh"; +"mesh.log"="Log do Mesh"; +"mesh.log.ambientlighting.config %@"="Configuração do módulo de Iluminação Ambiente recebida: %@"; +"mesh.log.bluetooth.config %@"="Configuração Bluetooth recebida: %@"; +"mesh.log.cannedmessage.config %@"="Configuração do módulo de Mensagens Padrão recebida: %@"; +"mesh.log.cannedmessages.messages.get %@"="Mensagens Padrão solicitadas para o módulo de mensagens para o nó: %@"; +"mesh.log.cannedmessages.messages.received %@"="Mensagens Padrão recebidas para: %@"; +"mesh.log.channel.sent %@ %d"="Um Canal Enviado para: %@ Índice do Canal %d"; +"mesh.log.channel.received %d %@"="Canal %d recebido de: %@"; +"mesh.log.device.config %@"="Configuração do dispositivo recebida: %@"; +"mesh.log.display.config %@"="Configuração do icrãn recebida: %@"; +"mesh.log.devicemetadata %@"="Solicitando os Metadados do Dispositivo para %@"; +"mesh.log.device.metadata.received %@"="Os Metadados do dispositivo recebidos de: %@"; +"mesh.log.detectionsensor.config %@"="Configuração do módulo de sensor de detecção recebida: %@"; +"mesh.log.externalnotification.config %@"="Configuração do módulo de notificação externa recebida: %@"; +"mesh.log.lora.config %@"="Configuração LoRa recebida: %@"; +"mesh.log.lora.config.sent %@"="Configuração do LoRa Enviado para: %@"; +"mesh.log.mqtt.config %@"="Configuração do módulo MQTT recebida: %@"; +"mesh.log.myinfo %@"="MyInfo recebido: %@"; +"mesh.log.network.config %@"="Configuração de rede recebida: %@"; +"mesh.log.nodeinfo.received %@"="Informações do nó recebidas para: %@"; +"mesh.log.paxcounter %@"="Mensagem do Contador PAX recebida de: %@"; +"mesh.log.paxcounter.config %@"="Configuração do Contador PAX recebida: %@"; +"mesh.log.position.config %@"="Configuração de posição recebida: %@"; +"mesh.log.position.received %@"="Pacote de posição recebido do nó: %@"; +"mesh.log.power.config %@"="Configuração de energia recebida: %@"; +"mesh.log.rangetest.config %@"="Configuração do módulo de teste de alcance recebida: %@"; +"mesh.log.ringtone.config %@"="Configuração de toque RTTTL recebida: %@"; +"mesh.log.routing.message %@ %@"="Roteamento recebido para RequestID: %@ Estado de Ack: %@"; +"mesh.log.serial.config %@"="Configuração do módulo serial recebida: %@"; +"mesh.log.sharelocation %@"="Enviado um Pacote de Posição do GPS do dispositivo Apple para o nó: %@"; +"mesh.log.storeforward.config %@"="Configuração do módulo Store & Forward recebida: %@"; +"mesh.log.telemetry.config %@"="Configuração do módulo de telemetria recebida: %@"; +"mesh.log.telemetry.received %@"="Telemetria recebida para: %@"; +"mesh.log.textmessage.received"="Mensagem recebida do App de mensagem de texto."; +"mesh.log.textmessage.send.failed %@"="Falha no envio da mensagem, não conectado corretamente a %@"; +"mesh.log.textmessage.sent %@ %@ %@"="Mensagem enviada %@ de %@ para %@"; +"mesh.log.traceroute.received.direct %@"="Solicitação de Rastreamento enviada para o nó: %@ foi recebida diretamente."; +"mesh.log.traceroute.received.route %@"="Solicitação de Rastreamento retornada: %@"; +"mesh.log.traceroute.sent %@"="Enviei uma solicitação de Rastreamento para o nó: %@"; +"mesh.log.wantconfig %@"="Emitindo Configuração Desejada para %@"; +"mesh.log.waypoint.sent %@"="Enviado um Pacote de Ponto de Referência de: %@"; +"mesh.log.waypoint.received %@"="Pacote de Ponto de Referência recebido do nó: %@"; +"message"="Mensagem"; +"message.details"="Dados de Mensagem"; +"messages"="Mensagens"; +"mode"="Modo"; +"module.configuration"="Configuração do Módulo"; "mqtt"="MQTT"; -"mqtt.connect"="Connect to MQTT"; -"mqtt.config"="MQTT Config"; -"mqtt.clientproxy"="MQTT Client Proxy"; -"mqtt.disconnect"="Disconnect from MQTT"; -"mqtt.username"="Username"; -"name"="Name"; -"network"="Network"; -"network.config"="Network Config"; -"nodes"="Nodes"; -"nodes %@"="Nodes (%@)"; -"nodelist.filter.distance %@"="up to %@ away"; -"save.config %@"="Save Config for %@"; -"no.nodes"="No Meshtastic Nodes Found"; -"not.connected"="No device connected"; -"numbers.punctuation"="Numbers and Punctuation"; -"off"="Off"; +"mqtt.connect"="Conectar ao MQTT"; +"mqtt.config"="Configuração MQTT"; +"mqtt.clientproxy"="Proxy do Cliente MQTT"; +"mqtt.disconnect"="Desconectar do MQTT"; +"mqtt.username"="Nome de Utilizador"; +"name"="Nome"; +"network"="Rede"; +"network.config"="Configuração de Rede"; +"nodes"="Nós"; +"nodes %@"="Nós (%@)"; +"nodelist.filter.distance %@"="até %@ de distância"; +"save.config %@"="Salvar Configuração para %@"; +"no.nodes"="Nenhum Nó Meshtastic Encontrado"; +"not.connected"="Nenhum dispositivo conectado"; +"numbers.punctuation"="Números e Pontuação"; +"off"="Desligado"; "offline"="Offline"; -"on.boot"="On Boot Only"; -"options"="Options"; -"password"="Password"; -"pause"="Pause"; +"on.boot"="No arranque"; +"options"="Opções"; +"password"="Senha"; +"pause"="Pausa"; "paxcounter.ble"="BLE"; -"paxcounter.delete"="Delete all pax data?"; +"paxcounter.delete"="Apagar todos os dados de pax?"; "paxcounter.wifi"="WiFi"; -"paxcounter.content.unavailable"="No PAX Counter Logs"; -"paxcounter.log"="PAX Counter Log"; -"paxcounter.total"="Total PAX"; -"phone.gps"="Phone GPS"; -"phone.gps.interval.description"="How frequently your phone will send your location to the device, location updates to the mesh are managed by the device."; -"position"="Position"; -"position.config"="Position Config"; -"position.precision %@"="Within %@"; -"preferred.radio"="Preferred Radio"; -"radio.configuration"="Radio Configuration"; -"range.test"="Range Test"; -"range.test.blocked"="Block Range Test"; -"range.test.config"="Range Test Config"; -"reply"="Reply"; -"reboot"="Reboot"; -"reboot.node"="Reboot node?"; -"received.ack"="Received Ack"; -"received.ack.real"="Recipient Ack"; -"relativetimeofday.morning"="Morning"; -"relativetimeofday.midday"="Midday"; -"relativetimeofday.afternoon"="Afternoon"; -"relativetimeofday.evening"="Evening"; -"relativetimeofday.nighttime"="Nighttime"; -"resume"="Resume"; -"ringtone"="Ringtone"; -"ringtone.config"="Ringtone Config"; -"route.recorder"="Route Recorder"; -"routes"="Routes"; -"routes.activitytype.walking"="Walking"; -"routes.activitytype.hiking"="Hiking"; -"routes.activitytype.biking"="Biking"; -"routes.activitytype.driving"="Driving"; +"paxcounter.content.unavailable"="Nenhum Log do Contador PAX Disponível"; +"paxcounter.log"="Log do Contador PAX"; +"paxcounter.total"="Total de PAX"; +"phone.gps"="GPS do Telefone"; +"phone.gps.interval.description"="Com que frequência seu telefone enviará sua localização para o dispositivo, as atualizações de localização no mesh são geridas pelo dispositivo."; +"position"="Posição"; +"position.config"="Configuração de Posição"; +"position.precision %@"="Dentro de %@"; +"preferred.radio"="Rádio Preferido"; +"radio.configuration"="Configuração de Rádio"; +"range.test"="Teste de Alcance"; +"range.test.blocked"="Bloquear Teste de Alcance"; +"range.test.config"="Configuração do teste de Alcance"; +"reply"="Responder"; +"reboot"="Reiniciar"; +"reboot.node"="Reiniciar nó?"; +"received.ack"="Ack Recebido"; +"received.ack.real"="Ack do Destinário"; +"relativetimeofday.morning"="Manhã"; +"relativetimeofday.midday"="Meio-dia"; +"relativetimeofday.afternoon"="Tarde"; +"relativetimeofday.evening"="Noite"; +"relativetimeofday.nighttime"="Noite"; +"resume"="Continuar"; +"ringtone"="Toque"; +"ringtone.config"="Configuração de Toque"; +"route.recorder"="Gravador de Rotas"; +"routes"="Rotas"; +"routes.activitytype.walking"="Caminhada"; +"routes.activitytype.hiking"="Caminhada na Montanha"; +"routes.activitytype.biking"="Passeio de Bicicleta"; +"routes.activitytype.driving"="Conduzir"; "routes.activitytype.overlanding"="Overlanding"; -"routes.activitytype.skiing"="Skiing"; -"routes.activitytype.filename.walking"="walk"; -"routes.activitytype.filename.hiking"="hike"; -"routes.activitytype.filename.biking"="bike tour"; -"routes.activitytype.filename.driving"="drive"; -"routes.activitytype.filename.overlanding"="overland drive"; -"routes.activitytype.filename.skiing"="ski tour"; -"routing.acknowledged"="Acknowledged"; -"routing.noroute"="No Route"; -"routing.gotnak"="Received a negative acknowledgment"; -"routing.timeout"="Timeout"; -"routing.nointerface"="No Interface"; -"routing.maxretransmit"="Max Retransmission Reached"; -"routing.nochannel"="No Channel"; -"routing.toolarge"="The packet is too large"; -"routing.noresponse"="No Response"; -"routing.dutycyclelimit"="Regional Duty Cycle Limit Reached"; -"routing.badRequest"="Bad Request"; -"routing.notauthorized"="Not Authorized"; -"satellite"="Satellite"; -"satellite.flyover"="Satellite Flyover"; -"save"="Save"; -"save.config %@"="Save Config for %@"; +"routes.activitytype.skiing"="Esqui"; +"routes.activitytype.filename.walking"="Caminhar"; +"routes.activitytype.filename.hiking"="Caminhar na Montanha"; +"routes.activitytype.filename.biking"="Passeio de Bicicleta"; +"routes.activitytype.filename.driving"="Conduzir"; +"routes.activitytype.filename.overlanding"="Caminhar overland"; +"routes.activitytype.filename.skiing"="Passeio de esqui"; +"routing.acknowledged"="Reconhecido"; +"routing.noroute"="Sem Rota"; +"routing.gotnak"="Recebido um reconhecimento negativo"; +"routing.timeout"="Tempo Esgotado"; +"routing.nointerface"="Sem Interface"; +"routing.maxretransmit"="Máximo de Retransmissão Alcançado"; +"routing.nochannel"="Sem Canal"; +"routing.toolarge"="O pacote é grande de mais"; +"routing.noresponse"="Sem Resposta"; +"routing.dutycyclelimit"="O limite do Regional Duty Cycle foi abrangido"; +"routing.badRequest"="Pedido Ruim"; +"routing.notauthorized"="Não Autorizado"; +"satellite"="Satéllite"; +"satellite.flyover"="Passagem de Satélite"; +"save"="Salvar"; +"save.config %@"="Salvar a Configuração para %@"; "serial"="Serial"; -"serial.config"="Serial Config"; -"serial.mode.default"="Default"; -"serial.mode.simple"="Simple"; +"serial.config"="Configuração Serial"; +"serial.mode.default"="Padrão"; +"serial.mode.simple"="Simples"; "serial.mode.proto"="Protobufs"; -"serial.mode.txtmsg"="Text Message"; -"serial.mode.nmea"="NMEA Positions"; -"settings"="Settings"; -"share.channels"="Share QR Code"; -"share.position"="Share Position"; -"subscribed"="Subscribed to mesh"; -"select.contact"="Select a Contact"; -"select.node"="Select a Node"; -"select.menu.item"="Select an item from the menu"; -"set.region"="Set LoRa Region"; -"standard"="Standard"; -"standard.muted"="Standard Muted"; -"start"="Start"; -"storeforward"="Store & Forward"; -"storeforward.config"="Store & Forward Config"; -"storeforward.heartbeat"="Send Heartbeat"; +"serial.mode.txtmsg"="Mensagem de Texto"; +"serial.mode.nmea"="Posições NMEA"; +"settings"="Definições"; +"share.channels"="Partilhar o Código do QR"; +"share.position"="Partilhar o Posição"; +"subscribed"="Inscrito no mesh"; +"select.contact"="Seleciona a Contacto"; +"select.node"="Seleciona a Nó"; +"select.menu.item"="Seleciona um opção do menu"; +"set.region"="Seleciona o Região da LoRa"; +"standard"="Padrão"; +"standard.muted"="Padrão Silenciado"; +"start"="Iniciar"; +"storeforward"="Armazenar e Encaminhar"; +"storeforward.config"="Configuração de Armazenar e Encaminhar"; +"storeforward.heartbeat"="Enviar Batimento Cardíaco"; "ssid"="SSID"; -"tapback"="Tapback Response"; -"tapback.heart"="Heart"; -"tapback.thumbsup"="Thumbs Up"; -"tapback.thumbsdown"="Thumbs Down"; +"tapback"="Resposta Tapback"; +"tapback.heart"="Coração"; +"tapback.thumbsup"="Polegar para Cima"; +"tapback.thumbsdown"="Polegar para Baixo"; "tapback.haha"="HaHa"; -"tapback.exclamation"="Exclamation Mark"; -"tapback.question"="Question Mark"; -"tapback.poop"="Poop"; -"tapback.wave"="Wave"; -"telemetry"="Telemetry (Sensors)"; -"telemetry.config"="Telemetry Config"; -"timeout"="Timeout"; -"timestamp"="Timestamp"; -"tip.bluetooth.connect.title"="Connected Radio"; -"tip.bluetooth.connect.message"="Shows information for the Lora radio connected via bluetooth. You can swipe left to disconnect the radio and long press to view stats or start the live activity."; -"tip.channel.admin.title"="Admin Channel"; -"tip.channel.admin.message"="Admin channel detected: Select a node from the drop down to manage connected or remote devices."; -"tip.channels.create.title"="Manage Channels"; -"tip.channels.create.message"="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/)"; -"tip.channels.share.title"="Sharing Meshtastic Channels"; -"tip.channels.share.message"="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."; -"tip.messages.title"="Messages"; -"tip.messages.message"="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."; +"tapback.exclamation"="Ponto de Exclamação"; +"tapback.question"="Ponto de Interrogação"; +"tapback.poop"="Cocó"; +"tapback.wave"="Adeus"; +"telemetry"="Telemetria (Sensores)"; +"telemetry.config"="Configuração Telemetria"; +"timeout"="Tempo Limite"; +"timestamp"="Carimbo de Data/Hora"; +"tip.bluetooth.connect.title"="Rádio Conectado"; +"tip.bluetooth.connect.message"="Mostra informações para o rádio LoRa conectado via bluetooth. Você pode deslizar para a esquerda para desconectar o rádio e pressionar por um longo período para ver estatísticas ou iniciar a atividade ao vivo."; +"tip.channel.admin.title"="Canal de Administração"; +"tip.channel.admin.message"="Canal de administração detectado: Selecione um nó do menu suspenso para gerir dispositivos conectados ou remotos."; +"tip.channels.create.title"="Gerir Canais"; +"tip.channels.create.message"="A maioria dos dados na sua malha é enviada pelo canal principal. Você pode configurar canais secundários para criar grupos de mensagens adicionais protegidos por sua própria chave. [Channel config tips](https://meshtastic.org/docs/configuration/tips/)"; +"tip.channels.share.title"="Compartilhando Canais Meshtastis"; +"tip.channels.share.message"="Um código QR Meshtastic contém a configuração LoRa e os valores do canal necessários para os rádios se comunicarem. Você pode compartilhar uma configuração completa do canal usando a opção Substituir Canais; se você escolher Adicionar Canais, seus canais compartilhados serão adicionados aos canais no rádio receptor."; +"tip.messages.title"="Mensagens"; +"tip.messages.message"="Você pode enviar e receber mensagens de canal (conversas em grupo) e mensagens diretas. De qualquer mensagem, você pode pressionar por um longo período para ver ações disponíveis como copiar, responder, tapback e excluir, bem como detalhes de entrega."; "twitter"="Twitter"; -"unknown"="Unknown"; -"unknown.age"="Unknown Age"; -"unset"="Unset"; -"update.firmware"="Update Your Firmware"; -"update.interval"="Update Interval"; -"uptime"="Uptime"; -"user"="User"; -"user.details"="User Details"; -"voltage"="Voltage"; -"waiting"="Waiting. . ."; -"appsettings.newNodeNotifications"="New Node Notifications"; +"unknown"="Desconhecido"; +"unknown.age"="Idade Desconhecido"; +"unset"="Não Definido"; +"update.firmware"="Atualiza o Seu Firmware"; +"update.interval"="Intervalo de Atualização"; +"uptime"="Tempo No Ár"; +"user"="Utilizador"; +"user.details"="Dados do Utilizador"; +"voltage"="Tensão"; +"waiting"="À Espara. . ."; +"appsettings.newNodeNotifications"="Notificações de Nó Novo"; diff --git a/se.lproj/Localizable.strings b/se.lproj/Localizable.strings index e45ca2fe..81d00170 100644 --- a/se.lproj/Localizable.strings +++ b/se.lproj/Localizable.strings @@ -177,6 +177,12 @@ "interval.seventytwo.hours"="Sjuttiotvå Timmar"; "keyboard.type"="Tangentbordstyp"; "logging"="Loggning"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa Konfiguration"; "map"="Mesh Karta"; diff --git a/zh-Hans.lproj/Localizable.strings b/zh-Hans.lproj/Localizable.strings index 5606f44b..3a241f3f 100644 --- a/zh-Hans.lproj/Localizable.strings +++ b/zh-Hans.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.eventytwo.hours"="七十二小时"; "keyboard.type"="键盘类型"; "logging"="加载中"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa 配置"; "map"="Mesh 地图"; diff --git a/zh-Hant-TW.lproj/Localizable.strings b/zh-Hant-TW.lproj/Localizable.strings index 1665f916..fdc02df2 100644 --- a/zh-Hant-TW.lproj/Localizable.strings +++ b/zh-Hant-TW.lproj/Localizable.strings @@ -173,6 +173,12 @@ "interval.eventytwo.hours"="七十二小時"; "keyboard.type"="鍵盤類型"; "logging"="加載中"; +"log.time"="Time"; +"log.subsystem"="Subsystem"; +"log.process"="Process"; +"log.category"="Category"; +"log.level"="Level"; +"log.message"="Message"; "lora"="LoRa"; "lora.config"="LoRa 設定"; "map"="Mesh 地圖";