Merge pull request #662 from meshtastic/2.3.11_Working_Changes

2.3.11 Working Changes
This commit is contained in:
Garth Vander Houwen 2024-06-07 10:30:24 -07:00 committed by GitHub
commit 9c8dadddef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
59 changed files with 1834 additions and 1422 deletions

View file

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

View file

@ -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 = "<group>"; };
258EE1262C0E833D0025A5FB /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
25A978572C124FA70003AAE7 /* NodeInfoExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeInfoExtensions.swift; sourceTree = "<group>"; };
25A9789A2C12BD3E0003AAE7 /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
25A9789B2C12BD3E0003AAE7 /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = "<group>"; };
25A9789C2C12BD3E0003AAE7 /* MessageEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
25A9789D2C12BD3E0003AAE7 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = "<group>"; };
25A9789E2C12BD3E0003AAE7 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = "<group>"; };
25A9789F2C12BD3E0003AAE7 /* RangeTestConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RangeTestConfigEntityExtension.swift; sourceTree = "<group>"; };
25A978A02C12BD3E0003AAE7 /* MQTTConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTConfigEntityExtension.swift; sourceTree = "<group>"; };
25A978A12C12BD3E0003AAE7 /* ExternalNotificationConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExternalNotificationConfigEntityExtension.swift; sourceTree = "<group>"; };
25A978A22C12BD3E0003AAE7 /* StoreForwardConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreForwardConfigEntityExtension.swift; sourceTree = "<group>"; };
25A978A32C12BD3E0003AAE7 /* SerialConfigEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerialConfigEntityExtension.swift; sourceTree = "<group>"; };
25A978A42C12BD3F0003AAE7 /* DeviceMetadataEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceMetadataEntityExtension.swift; sourceTree = "<group>"; };
25A978A52C12BD3F0003AAE7 /* UserEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
25A978A62C12BD3F0003AAE7 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = "<group>"; };
25A978A72C12BD3F0003AAE7 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
25A978A82C12BD3F0003AAE7 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
A65FA974296876BF00A97686 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
B399E8A32B6F486400E4488E /* RetryButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryButton.swift; sourceTree = "<group>"; };
B3E905B02B71F7F300654D07 /* TextMessageField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageField.swift; sourceTree = "<group>"; };
@ -265,8 +281,6 @@
D9C9839C2B79CFD700BDBE6A /* TextMessageSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextMessageSize.swift; sourceTree = "<group>"; };
D9C9839F2B79D0E800BDBE6A /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
D9C983A12B79D1A600BDBE6A /* RequestPositionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestPositionButton.swift; sourceTree = "<group>"; };
DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoEntityExtension.swift; sourceTree = "<group>"; };
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntityExtension.swift; sourceTree = "<group>"; };
DD05296F2B77F454008E44CD /* MeshtasticDataModelV 26.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 26.xcdatamodel"; sourceTree = "<group>"; };
DD0E20F92B87090400F2D100 /* atak.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = atak.pb.swift; sourceTree = "<group>"; };
DD0E20FA2B87090400F2D100 /* clientonly.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = clientonly.pb.swift; sourceTree = "<group>"; };
@ -322,8 +336,6 @@
DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmbientLightingConfig.swift; sourceTree = "<group>"; };
DD4A911D2708C65400501B7E /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentMetricsLog.swift; sourceTree = "<group>"; };
DD5394FD276BA0EF00AD86B1 /* PositionEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PositionEntityExtension.swift; sourceTree = "<group>"; };
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelEntityExtension.swift; sourceTree = "<group>"; };
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV2.xcdatamodel; sourceTree = "<group>"; };
DD5D0A9B2931B9F200F7EA61 /* EthernetModes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthernetModes.swift; sourceTree = "<group>"; };
DD5E51CC2986643400D21B61 /* MeshtasticDataModelV7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV7.xcdatamodel; sourceTree = "<group>"; };
@ -374,7 +386,6 @@
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiOnlyTextField.swift; sourceTree = "<group>"; };
DD964FBE296E76EF007C176F /* WaypointFormMapKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointFormMapKit.swift; sourceTree = "<group>"; };
DD964FC029724F6D007C176F /* MeshtasticDataModelV6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV6.xcdatamodel; sourceTree = "<group>"; };
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointEntityExtension.swift; sourceTree = "<group>"; };
DD964FC32974767D007C176F /* MapViewFitExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewFitExtension.swift; sourceTree = "<group>"; };
DD964FC52975DBFD007C176F /* QueryCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryCoreData.swift; sourceTree = "<group>"; };
DD9681A22BBB22BE00FD2C47 /* MeshtasticDataModelV32.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV32.xcdatamodel; sourceTree = "<group>"; };
@ -390,7 +401,6 @@
DDA9515D2BC6F56F00CEA535 /* IndoorAirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndoorAirQuality.swift; sourceTree = "<group>"; };
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV20.xcdatamodel; sourceTree = "<group>"; };
DDAB580C2B0DAA9E00147258 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = "<group>"; };
DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEntityExtension.swift; sourceTree = "<group>"; };
DDAD49EC2AFB39DC00B4425D /* MeshMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshMap.swift; sourceTree = "<group>"; };
DDAF8C5226EB1DF10058C060 /* BLEManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEManager.swift; sourceTree = "<group>"; };
DDB234392B5CA9B000DA6FB1 /* MeshtasticDataModelV 24.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 24.xcdatamodel"; sourceTree = "<group>"; };
@ -422,10 +432,6 @@
DDC2E15B26CE248F0042C5E4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = ../Assets.xcassets; sourceTree = "<group>"; };
DDC2E15E26CE248F0042C5E4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
DDC2E16526CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DDC2E17026CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
DDC2E17B26CE248F0042C5E4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DDC2E18E26CE25FE0042C5E4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
DDC2E1A626CEB3400042C5E4 /* LocationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationHelper.swift; sourceTree = "<group>"; };
DDC3B273283F411B00AC321C /* LastHeardText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastHeardText.swift; sourceTree = "<group>"; };
@ -441,11 +447,10 @@
DDCE4E2B2869F92900BE9F8F /* UserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserConfig.swift; sourceTree = "<group>"; };
DDD28D362C0CCCD10063CFA3 /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
DDD28D372C0CD2670063CFA3 /* MeshtasticDataModelV 37.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 37.xcdatamodel"; sourceTree = "<group>"; };
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MeshtasticTests.swift; sourceTree = "<group>"; };
DDD28D3B2C0EC51D0063CFA3 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
DDD43FE22A78C8900083A3E9 /* MqttClientProxyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MqttClientProxyManager.swift; sourceTree = "<group>"; };
DDD6EEAE29BC024700383354 /* Firmware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Firmware.swift; sourceTree = "<group>"; };
DDD94A4F2845C8F5004A87A0 /* DateTimeText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeText.swift; sourceTree = "<group>"; };
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserEntityExtension.swift; sourceTree = "<group>"; };
DDDB263E2AABEE20003AFCB7 /* NodeList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeList.swift; sourceTree = "<group>"; };
DDDB26412AABF655003AFCB7 /* NodeListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeListItem.swift; sourceTree = "<group>"; };
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDetail.swift; sourceTree = "<group>"; };
@ -483,7 +488,6 @@
DDDEE5E229DBE43E00A8E078 /* MeshtasticDataModelV11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV11.xcdatamodel; sourceTree = "<group>"; };
DDE0F7C4295F77B700B8AAB3 /* AppSettingsEnums.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettingsEnums.swift; sourceTree = "<group>"; };
DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteLog.swift; sourceTree = "<group>"; };
DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TraceRouteEntityExtension.swift; sourceTree = "<group>"; };
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteRecorder.swift; sourceTree = "<group>"; };
DDEE03EC29544A1000FCAD57 /* MeshtasticDataModelV4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV4.xcdatamodel; sourceTree = "<group>"; };
DDF45C332BC1A48E005ED5F2 /* MQTTIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MQTTIcon.swift; sourceTree = "<group>"; };
@ -493,6 +497,8 @@
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
DDF6B2472A9AEBF500BA6931 /* StoreForwardConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForwardConfig.swift; sourceTree = "<group>"; };
DDF6B24B2A9C2FC800BA6931 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
DDF8E1F32C10125B0019C87E /* AppLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLog.swift; sourceTree = "<group>"; };
DDF8E1F82C115CCE0019C87E /* LogDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogDetail.swift; sourceTree = "<group>"; };
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = "<group>"; };
DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 = "<group>";
@ -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 = "<group>";
};
DDC2E16D26CE248F0042C5E4 /* MeshtasticTests */ = {
isa = PBXGroup;
children = (
DDD3BBD4292D763200D609B3 /* MeshtasticTests.swift */,
DDC2E17026CE248F0042C5E4 /* Info.plist */,
);
path = MeshtasticTests;
sourceTree = "<group>";
};
DDC2E17826CE248F0042C5E4 /* MeshtasticUITests */ = {
isa = PBXGroup;
children = (
DDC2E17926CE248F0042C5E4 /* MeshtasticUITests.swift */,
DDC2E17B26CE248F0042C5E4 /* Info.plist */,
);
path = MeshtasticUITests;
sourceTree = "<group>";
};
DDC2E18726CE24E40042C5E4 /* Views */ = {
isa = PBXGroup;
children = (
@ -930,6 +924,7 @@
DDB75A1D2A0B0CD0006ED576 /* LoRaSignalStrengthIndicator.swift */,
DDB75A202A12B954006ED576 /* LoRaSignalStrength.swift */,
DDB75A222A13CDA9006ED576 /* BatteryLevelCompact.swift */,
DDF8E1F82C115CCE0019C87E /* LogDetail.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -947,7 +942,6 @@
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
25183D452C0A6D97001E31D5 /* Logger.swift */,
);
path = Helpers;
sourceTree = "<group>";
@ -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 = "<group>";
@ -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";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<PositionEntity> {
let request: NSFetchRequest<PositionEntity> = PositionEntity.fetchRequest()
request.fetchLimit = 1000

View file

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

View file

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

View file

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

View file

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

View file

@ -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<WaypointEntity> {
let request: NSFetchRequest<WaypointEntity> = WaypointEntity.fetchRequest()

View file

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

View file

@ -0,0 +1,11 @@
import Foundation
extension NodeInfo {
var isValidPosition: Bool {
hasPosition &&
position.longitudeI != 0 &&
position.latitudeI != 0 &&
position.latitudeI != 373346000 &&
position.longitudeI != -1220090000
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -76,7 +76,7 @@ struct MeshMapContent: MapContent {
}
}
}
.onTapGesture { location in
.onTapGesture { _ in
selectedPosition = (selectedPosition == position ? nil : position)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

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

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

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

View file

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

View file

@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View file

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

View file

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

View file

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

View file

@ -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"="מפת מש";

View file

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

View file

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

View file

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

View file

@ -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 地图";

View file

@ -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 地圖";