mirror of
https://github.com/meshtastic/Meshtastic-Apple.git
synced 2026-04-20 22:13:56 +00:00
Merge pull request #429 from meshtastic/2.2.14_Working_Changes
2.2.14 working changes
This commit is contained in:
commit
1fb0d781f1
45 changed files with 2428 additions and 1018 deletions
|
|
@ -29,6 +29,7 @@
|
|||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2AD8A7296D2DF9001FF0E7 /* MapViewSwiftUI.swift */; };
|
||||
DD2DC2C029BCD8AB003B383C /* HardwareModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */; };
|
||||
DD3501892852FC3B000FC853 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3501882852FC3B000FC853 /* Settings.swift */; };
|
||||
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */; };
|
||||
DD3CC6B528E33FD100FA9159 /* ShareChannels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */; };
|
||||
DD3CC6BC28E366DF00FA9159 /* Meshtastic.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */; };
|
||||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */; };
|
||||
|
|
@ -41,6 +42,7 @@
|
|||
DD457188293C7E63000C49FB /* BLESignalStrengthIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD457187293C7E63000C49FB /* BLESignalStrengthIndicator.swift */; };
|
||||
DD4640202AFF10F4002A5ECB /* WaypointForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */; };
|
||||
DD47E3D626F17ED900029299 /* CircleText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD47E3D526F17ED900029299 /* CircleText.swift */; };
|
||||
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */; };
|
||||
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 */; };
|
||||
|
|
@ -171,6 +173,9 @@
|
|||
DDDE5A1329AFEAB900490C6C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDDE5A1229AFEAB900490C6C /* Assets.xcassets */; };
|
||||
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 */; };
|
||||
DDF6B2482A9AEBF500BA6931 /* StoreForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */; };
|
||||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDF924C926FBB953009FE055 /* ConnectedDevice.swift */; };
|
||||
DDFEB3BB29900C1200EE7472 /* CurrentConditionsCompact.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */; };
|
||||
|
|
@ -239,6 +244,8 @@
|
|||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV19.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD2DC2BF29BCD8AB003B383C /* HardwareModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HardwareModels.swift; sourceTree = "<group>"; };
|
||||
DD3501882852FC3B000FC853 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
|
||||
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV21.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationsHandler.swift; sourceTree = "<group>"; };
|
||||
DD3CC6B428E33FD100FA9159 /* ShareChannels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareChannels.swift; sourceTree = "<group>"; };
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModel.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD3CC6BD28E4CD9800FA9159 /* BatteryGauge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatteryGauge.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -254,6 +261,7 @@
|
|||
DD457BC4295D5E35004BCE4D /* MeshtasticDataModelV5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV5.xcdatamodel; sourceTree = "<group>"; };
|
||||
DD46401F2AFF10F4002A5ECB /* WaypointForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaypointForm.swift; sourceTree = "<group>"; };
|
||||
DD47E3D526F17ED900029299 /* CircleText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircleText.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
|
|
@ -401,6 +409,9 @@
|
|||
DDDE5A1229AFEAB900490C6C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
DDF6B2462A9AEB9E00BA6931 /* MeshtasticDataModelV17.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MeshtasticDataModelV17.xcdatamodel; sourceTree = "<group>"; };
|
||||
DDF6B2472A9AEBF500BA6931 /* StoreForward.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreForward.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -478,6 +489,7 @@
|
|||
DDD9E4E3284B208E003777C5 /* UserEntityExtension.swift */,
|
||||
DD964FC1297272AE007C176F /* WaypointEntityExtension.swift */,
|
||||
DDAB580E2B0DAFBC00147258 /* LocationEntityExtension.swift */,
|
||||
DDE5B4052B227E3200FCDD05 /* TraceRouteEntityExtension.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -508,6 +520,7 @@
|
|||
DD73FD1028750779000852D6 /* PositionLog.swift */,
|
||||
DD4F23CC28779A3C001D37CB /* EnvironmentMetricsLog.swift */,
|
||||
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */,
|
||||
DDE5B4032B2279A700FCDD05 /* TraceRouteLog.swift */,
|
||||
);
|
||||
path = Nodes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -529,6 +542,7 @@
|
|||
DD0F791A28713C8A00A6FDAD /* AdminMessageList.swift */,
|
||||
DD4A911D2708C65400501B7E /* AppSettings.swift */,
|
||||
DDAB580C2B0DAA9E00147258 /* Routes.swift */,
|
||||
DDE9659B2B1C3B6A00531070 /* RouteRecorder.swift */,
|
||||
DDA0B6B1294CDC55001356EC /* Channels.swift */,
|
||||
DDD6EEAE29BC024700383354 /* Firmware.swift */,
|
||||
DD8169FA271F1F3A00F4AB02 /* MeshLog.swift */,
|
||||
|
|
@ -592,6 +606,7 @@
|
|||
DD61937B2863877A00E59241 /* Module */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DD4975A42B147BA90026544E /* AmbientLightingConfig.swift */,
|
||||
DD6193762862F90F00E59241 /* CannedMessagesConfig.swift */,
|
||||
DDC4C9FE2A8D982900CE201C /* DetectionSensorConfig.swift */,
|
||||
DD6193742862F6E600E59241 /* ExternalNotificationConfig.swift */,
|
||||
|
|
@ -833,6 +848,7 @@
|
|||
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
|
||||
DD964FBC296E6B01007C176F /* EmojiOnlyTextField.swift */,
|
||||
DDDB443C29F6592F00EE2349 /* NetworkManager.swift */,
|
||||
DD3619142B1EF9F900C41C8C /* LocationsHandler.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -1142,10 +1158,12 @@
|
|||
DDDB26462AACC0B7003AFCB7 /* NodeInfoItem.swift in Sources */,
|
||||
DD2AD8A8296D2DF9001FF0E7 /* MapViewSwiftUI.swift in Sources */,
|
||||
DD5E5213298EE33B00D21B61 /* deviceonly.pb.swift in Sources */,
|
||||
DDE5B4042B2279A700FCDD05 /* TraceRouteLog.swift in Sources */,
|
||||
DD5E5208298EE33B00D21B61 /* rtttl.pb.swift in Sources */,
|
||||
DD6193792863875F00E59241 /* SerialConfig.swift in Sources */,
|
||||
DDDB263F2AABEE20003AFCB7 /* NodeList.swift in Sources */,
|
||||
DDA0B6B2294CDC55001356EC /* Channels.swift in Sources */,
|
||||
DDE9659C2B1C3B6A00531070 /* RouteRecorder.swift in Sources */,
|
||||
DDB8F4102A9EE5B400230ECE /* Messages.swift in Sources */,
|
||||
DDDB26482AACD6D1003AFCB7 /* NodeMapMapkit.swift in Sources */,
|
||||
DD4A911E2708C65400501B7E /* AppSettings.swift in Sources */,
|
||||
|
|
@ -1160,6 +1178,7 @@
|
|||
DDF924CA26FBB953009FE055 /* ConnectedDevice.swift in Sources */,
|
||||
DD3CC6BE28E4CD9800FA9159 /* BatteryGauge.swift in Sources */,
|
||||
DD6193772862F90F00E59241 /* CannedMessagesConfig.swift in Sources */,
|
||||
DD3619152B1EF9F900C41C8C /* LocationsHandler.swift in Sources */,
|
||||
DDDB444A29F8AA3A00EE2349 /* CLLocationCoordinate2D.swift in Sources */,
|
||||
DD41582628582E9B009B0E59 /* DeviceConfig.swift in Sources */,
|
||||
DD007BAE2AA4E91200F5FA12 /* MyInfoEntityExtension.swift in Sources */,
|
||||
|
|
@ -1216,6 +1235,7 @@
|
|||
DDB75A212A12B954006ED576 /* LoRaSignalStrength.swift in Sources */,
|
||||
DD6193752862F6E600E59241 /* ExternalNotificationConfig.swift in Sources */,
|
||||
DDB6ABE428B13FFF00384BA1 /* DisplayEnums.swift in Sources */,
|
||||
DD4975A52B147BA90026544E /* AmbientLightingConfig.swift in Sources */,
|
||||
DD86D40A287F04F100BAEB7A /* InvalidVersion.swift in Sources */,
|
||||
DDD94A502845C8F5004A87A0 /* DateTimeText.swift in Sources */,
|
||||
DD5E5212298EE33B00D21B61 /* apponly.pb.swift in Sources */,
|
||||
|
|
@ -1250,6 +1270,7 @@
|
|||
DDAB580F2B0DAFBC00147258 /* LocationEntityExtension.swift in Sources */,
|
||||
DD5E5211298EE33B00D21B61 /* remote_hardware.pb.swift in Sources */,
|
||||
DD5E5204298EE33B00D21B61 /* xmodem.pb.swift in Sources */,
|
||||
DDE5B4062B227E3200FCDD05 /* TraceRouteEntityExtension.swift in Sources */,
|
||||
DDC2E15826CE248E0042C5E4 /* MeshtasticApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -1460,7 +1481,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
MARKETING_VERSION = 2.2.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1494,7 +1515,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
MARKETING_VERSION = 2.2.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
|
|
@ -1616,7 +1637,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
MARKETING_VERSION = 2.2.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1649,7 +1670,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 2.2.13;
|
||||
MARKETING_VERSION = 2.2.14;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = gvh.MeshtasticClient.Widgets;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -1760,6 +1781,7 @@
|
|||
DD3CC6BA28E366DF00FA9159 /* Meshtastic.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */,
|
||||
DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */,
|
||||
DD2CC2E52ABFE04E00EDFDA7 /* MeshtasticDataModelV19.xcdatamodel */,
|
||||
DDDB26492AAD743E003AFCB7 /* MeshtasticDataModelV18.xcdatamodel */,
|
||||
|
|
@ -1781,7 +1803,7 @@
|
|||
DD5D0A9A2931AD6B00F7EA61 /* MeshtasticDataModelV2.xcdatamodel */,
|
||||
DD3CC6BB28E366DF00FA9159 /* MeshtasticDataModel.xcdatamodel */,
|
||||
);
|
||||
currentVersion = DDAB580B2B0D913500147258 /* MeshtasticDataModelV20.xcdatamodel */;
|
||||
currentVersion = DD3619132B1EE20700C41C8C /* MeshtasticDataModelV21.xcdatamodel */;
|
||||
name = Meshtastic.xcdatamodeld;
|
||||
path = Meshtastic/Meshtastic.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
case repeater = 4
|
||||
case tracker = 5
|
||||
case sensor = 6
|
||||
case tak = 7
|
||||
case clientHidden = 8
|
||||
case lostAndFound = 9
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var name: String {
|
||||
|
|
@ -35,6 +38,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "Tracker"
|
||||
case .sensor:
|
||||
return "Sensor"
|
||||
case .tak:
|
||||
return "TAK"
|
||||
case .clientHidden:
|
||||
return "Client Hidden"
|
||||
case .lostAndFound:
|
||||
return "Lost and Found"
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
|
|
@ -53,6 +62,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return "device.role.tracker".localized
|
||||
case .sensor:
|
||||
return "device.role.sensor".localized
|
||||
case .tak:
|
||||
return "device.role.tak".localized
|
||||
case .clientHidden:
|
||||
return "device.role.clienthidden".localized
|
||||
case .lostAndFound:
|
||||
return "device.role.lostandfound".localized
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DeviceConfig.Role {
|
||||
|
|
@ -72,6 +87,12 @@ enum DeviceRoles: Int, CaseIterable, Identifiable {
|
|||
return Config.DeviceConfig.Role.tracker
|
||||
case .sensor:
|
||||
return Config.DeviceConfig.Role.sensor
|
||||
case .tak:
|
||||
return Config.DeviceConfig.Role.tak
|
||||
case .clientHidden:
|
||||
return Config.DeviceConfig.Role.clientHidden
|
||||
case .lostAndFound:
|
||||
return Config.DeviceConfig.Role.lostAndFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -81,6 +102,7 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
case all = 0
|
||||
case allSkipDecoding = 1
|
||||
case localOnly = 2
|
||||
case knownOnly = 3
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
|
||||
|
|
@ -92,6 +114,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
return "All Skip Decoding"
|
||||
case .localOnly:
|
||||
return "Local Only"
|
||||
case .knownOnly:
|
||||
return "Known Only"
|
||||
}
|
||||
}
|
||||
var description: String {
|
||||
|
|
@ -102,6 +126,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
return "Same as behavior as ALL but skips packet decoding and simply rebroadcasts them. Only available in Repeater role. Setting this on any other roles will result in ALL behavior."
|
||||
case .localOnly:
|
||||
return "Ignores observed messages from foreign meshes that are open or those which it cannot decrypt. Only rebroadcasts message on the nodes local primary / secondary channels."
|
||||
case .knownOnly:
|
||||
return "Ignores observed messages from foreign meshes like Local Only, but takes it step further by also ignoring messages from nodes not already in the node's known list."
|
||||
}
|
||||
}
|
||||
func protoEnumValue() -> Config.DeviceConfig.RebroadcastMode {
|
||||
|
|
@ -113,6 +139,8 @@ enum RebroadcastModes: Int, CaseIterable, Identifiable {
|
|||
return Config.DeviceConfig.RebroadcastMode.allSkipDecoding
|
||||
case .localOnly:
|
||||
return Config.DeviceConfig.RebroadcastMode.localOnly
|
||||
case .knownOnly:
|
||||
return Config.DeviceConfig.RebroadcastMode.knownOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,116 +52,3 @@ enum GpsFormats: Int, CaseIterable, Identifiable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GpsUpdateIntervals: Int, CaseIterable, Identifiable {
|
||||
|
||||
case fiveSeconds = 5
|
||||
case tenSeconds = 10
|
||||
case fifteenSeconds = 15
|
||||
case twentySeconds = 20
|
||||
case twentyFiveSeconds = 25
|
||||
case thirtySeconds = 30
|
||||
case fortyFiveSeconds = 45
|
||||
case oneMinute = 60
|
||||
case twoMinutes = 120
|
||||
case fiveMinutes = 300
|
||||
case tenMinutes = 600
|
||||
case fifteenMinutes = 900
|
||||
case thirtyMinutes = 1800
|
||||
case oneHour = 3600
|
||||
case sixHours = 21600
|
||||
case twelveHours = 43200
|
||||
case twentyFourHours = 86400
|
||||
case maxInt32 = 2147483647
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
case .twentySeconds:
|
||||
return "interval.twenty.seconds".localized
|
||||
case .twentyFiveSeconds:
|
||||
return "interval.twentyfive.seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
case .fortyFiveSeconds:
|
||||
return "interval.fortyfive.seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
case .twoMinutes:
|
||||
return "interval.two.minutes".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
case .thirtyMinutes:
|
||||
return "interval.thirty.minutes".localized
|
||||
case .oneHour:
|
||||
return "interval.one.hour".localized
|
||||
case .sixHours:
|
||||
return "interval.six.hours".localized
|
||||
case .twelveHours:
|
||||
return "interval.twelve.hours".localized
|
||||
case .twentyFourHours:
|
||||
return "interval.twentyfour.hours".localized
|
||||
case .maxInt32:
|
||||
return "on.boot"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum GpsAttemptTimes: Int, CaseIterable, Identifiable {
|
||||
|
||||
case twoSeconds = 2
|
||||
case fiveSeconds = 5
|
||||
case tenSeconds = 10
|
||||
case fifteenSeconds = 15
|
||||
case twentySeconds = 20
|
||||
case twentyFiveSeconds = 25
|
||||
case thirtySeconds = 30
|
||||
case fortyFiveSeconds = 45
|
||||
case oneMinute = 60
|
||||
case twoMinutes = 120
|
||||
case fiveMinutes = 300
|
||||
case tenMinutes = 600
|
||||
case fifteenMinutes = 900
|
||||
|
||||
var id: Int { self.rawValue }
|
||||
var description: String {
|
||||
switch self {
|
||||
case .twoSeconds:
|
||||
return "interval.two.seconds".localized
|
||||
case .fiveSeconds:
|
||||
return "interval.five.seconds".localized
|
||||
case .tenSeconds:
|
||||
return "interval.ten.seconds".localized
|
||||
case .fifteenSeconds:
|
||||
return "interval.fifteen.seconds".localized
|
||||
case .twentySeconds:
|
||||
return "interval.twenty.seconds".localized
|
||||
case .twentyFiveSeconds:
|
||||
return "interval.twentyfive.seconds".localized
|
||||
case .thirtySeconds:
|
||||
return "interval.thirty.seconds".localized
|
||||
case .fortyFiveSeconds:
|
||||
return "interval.fortyfive.seconds".localized
|
||||
case .oneMinute:
|
||||
return "interval.one.minute".localized
|
||||
case .twoMinutes:
|
||||
return "interval.two.minutes".localized
|
||||
case .fiveMinutes:
|
||||
return "interval.five.minutes".localized
|
||||
case .tenMinutes:
|
||||
return "interval.ten.minutes".localized
|
||||
case .fifteenMinutes:
|
||||
return "interval.fifteen.minutes".localized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// TraceRouteEntityExtension.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 12/7/23.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import CoreLocation
|
||||
import MapKit
|
||||
import SwiftUI
|
||||
|
||||
extension TraceRouteEntity {
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
let d = Double(latitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var longitude: Double? {
|
||||
|
||||
let d = Double(longitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var coordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TraceRouteHopEntity {
|
||||
|
||||
var latitude: Double? {
|
||||
|
||||
let d = Double(latitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var longitude: Double? {
|
||||
|
||||
let d = Double(longitudeI)
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return d / 1e7
|
||||
}
|
||||
|
||||
var coordinate: CLLocationCoordinate2D? {
|
||||
if latitudeI != 0 && longitudeI != 0 {
|
||||
let coord = CLLocationCoordinate2D(latitude: latitude!, longitude: longitude!)
|
||||
return coord
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,8 @@ extension UserDefaults {
|
|||
case mapTileServer
|
||||
case mapTilesAboveLabels
|
||||
case mapUseLegacy
|
||||
case enableDetectionNotifications
|
||||
case detectionSensorRole
|
||||
}
|
||||
|
||||
func reset() {
|
||||
|
|
@ -190,4 +192,22 @@ extension UserDefaults {
|
|||
UserDefaults.standard.set(newValue, forKey: "mapUseLegacy")
|
||||
}
|
||||
}
|
||||
|
||||
static var enableDetectionNotifications: Bool {
|
||||
get {
|
||||
UserDefaults.standard.bool(forKey: "enableDetectionNotifications")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "enableDetectionNotifications")
|
||||
}
|
||||
}
|
||||
|
||||
static var detectionSensorRole: DetectionSensorRole {
|
||||
get {
|
||||
DetectionSensorRole(rawValue: UserDefaults.standard.string(forKey: "detectionSensorRole") ?? DetectionSensorRole.sensor.rawValue) ?? DetectionSensorRole.sensor
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: "detectionSensorRole")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
var context: NSManagedObjectContext?
|
||||
//var userSettings: UserSettings?
|
||||
private var centralManager: CBCentralManager!
|
||||
private let restoreKey = "Meshtastic.BLE.Manager"
|
||||
@Published var peripherals: [Peripheral] = []
|
||||
@Published var connectedPeripheral: Peripheral!
|
||||
@Published var lastConnectionError: String
|
||||
|
|
@ -42,6 +41,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let emptyNodeNum: UInt32 = 4294967295
|
||||
let mqttManager = MqttClientProxyManager.shared
|
||||
var wantRangeTestPackets = false
|
||||
var wantStoreAndForwardPackets = false
|
||||
/* Meshtastic Service Details */
|
||||
var TORADIO_characteristic: CBCharacteristic!
|
||||
var FROMRADIO_characteristic: CBCharacteristic!
|
||||
|
|
@ -377,6 +377,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
let fromNodeNum = connectedPeripheral.num
|
||||
let routePacket = RouteDiscovery()
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.to = UInt32(destNum)
|
||||
meshPacket.from = UInt32(fromNodeNum)
|
||||
meshPacket.wantAck = true
|
||||
|
|
@ -395,8 +396,43 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
connectedPeripheral.peripheral.writeValue(binaryData, for: TORADIO_characteristic, type: .withResponse)
|
||||
success = true
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum))
|
||||
MeshLogger.log("🪧 \(logString)")
|
||||
let traceRoute = TraceRouteEntity(context: context!)
|
||||
let nodes: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
nodes.predicate = NSPredicate(format: "num IN %@", [destNum, self.connectedPeripheral.num])
|
||||
do {
|
||||
guard let fetchedNodes = try context!.fetch(nodes) as? [NodeInfoEntity] else {
|
||||
return false
|
||||
}
|
||||
let receivingNode = fetchedNodes.first(where: { $0.num == destNum })
|
||||
let connectedNode = fetchedNodes.first(where: { $0.num == self.connectedPeripheral.num })
|
||||
|
||||
traceRoute.id = Int64(meshPacket.id)
|
||||
traceRoute.time = Date()
|
||||
traceRoute.node = receivingNode
|
||||
// Grab the most recent postion, within the last hour
|
||||
if connectedNode?.positions?.count ?? 0 > 0 {
|
||||
let mostRecent = connectedNode?.positions?.lastObject as! PositionEntity
|
||||
if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! {
|
||||
traceRoute.altitude = mostRecent.altitude
|
||||
traceRoute.latitudeI = mostRecent.latitudeI
|
||||
traceRoute.longitudeI = mostRecent.longitudeI
|
||||
}
|
||||
}
|
||||
do {
|
||||
try context!.save()
|
||||
print("💾 Saved TraceRoute sent to node: \(String(receivingNode?.user?.longName ?? "unknown".localized))")
|
||||
} catch {
|
||||
context!.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data BluetoothConfigEntity: \(nsError)")
|
||||
}
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.sent %@".localized, String(destNum))
|
||||
MeshLogger.log("🪧 \(logString)")
|
||||
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
|
@ -572,13 +608,17 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
case .serialApp:
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Serial App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
case .storeForwardApp:
|
||||
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
if wantStoreAndForwardPackets {
|
||||
storeAndForwardPacket(packet: decodedInfo.packet, connectedNodeNum: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
} else {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Store and Forward App - Store and Forward is disabled.")
|
||||
}
|
||||
case .rangeTestApp:
|
||||
if wantRangeTestPackets && !UserDefaults.blockRangeTest {
|
||||
textMessageAppPacket(packet: decodedInfo.packet, blockRangeTest: false, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!)
|
||||
}
|
||||
else {
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Range Test App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
MeshLogger.log("🕸️ MESH PACKET received for Range Test App Range testing is disabled.")
|
||||
}
|
||||
case .telemetryApp:
|
||||
if !invalidVersion { telemetryPacket(packet: decodedInfo.packet, connectedNode: (self.connectedPeripheral != nil ? connectedPeripheral.num : 0), context: context!) }
|
||||
|
|
@ -596,16 +636,44 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
MeshLogger.log("🕸️ MESH PACKET received for Audio App UNHANDLED \((try? decodedInfo.packet.jsonString()) ?? "JSON Decode Failure")")
|
||||
case .tracerouteApp:
|
||||
if let routingMessage = try? RouteDiscovery(serializedData: decodedInfo.packet.decoded.payload) {
|
||||
|
||||
let traceRoute = getTraceRoute(id: Int64(decodedInfo.packet.decoded.requestID), context: context!)
|
||||
traceRoute?.response = true
|
||||
traceRoute?.route = routingMessage.route
|
||||
if routingMessage.route.count == 0 {
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.direct %@".localized, String(decodedInfo.packet.from))
|
||||
MeshLogger.log("🪧 \(logString)")
|
||||
} else {
|
||||
|
||||
var routeString = "\(decodedInfo.packet.to) --> "
|
||||
for node in routingMessage.route {
|
||||
routeString += "\(node) --> "
|
||||
} else {
|
||||
var routeString = "You --> "
|
||||
var hopNodes: [TraceRouteHopEntity] = []
|
||||
// for node in routingMessage.route {
|
||||
// let hopNode = getNodeInfo(id: Int64(node), context: context!)
|
||||
// let traceRouteHop = TraceRouteHopEntity(context: context!)
|
||||
// traceRouteHop.time = Date()
|
||||
// let mostRecent = hopNode?.positions?.lastObject as! PositionEntity
|
||||
// if mostRecent.time! >= Calendar.current.date(byAdding: .minute, value: -60, to: Date())! {
|
||||
// traceRouteHop.altitude = mostRecent.altitude
|
||||
// traceRouteHop.latitudeI = mostRecent.latitudeI
|
||||
// traceRouteHop.longitudeI = mostRecent.longitudeI
|
||||
// traceRouteHop.name = hopNode?.user?.longName ?? "unknown".localized
|
||||
// }
|
||||
// traceRouteHop.num = hopNode?.num ?? 0
|
||||
// if hopNode != nil {
|
||||
// hopNodes.append(traceRouteHop)
|
||||
// }
|
||||
// routeString += "\(hopNode?.user?.longName ?? "unknown".localized) --> "
|
||||
// }
|
||||
traceRoute?.routeText = routeString
|
||||
traceRoute?.hops = NSOrderedSet(array: hopNodes)
|
||||
do {
|
||||
try context!.save()
|
||||
print("💾 Saved Trace Route")
|
||||
} catch {
|
||||
context!.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data TraceRouteHOp: \(nsError)")
|
||||
}
|
||||
|
||||
routeString += "\(decodedInfo.packet.from)"
|
||||
let logString = String.localizedStringWithFormat("mesh.log.traceroute.received.route %@".localized, routeString)
|
||||
MeshLogger.log("🪧 \(logString)")
|
||||
|
|
@ -656,6 +724,9 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].rangeTestConfig?.enabled == true {
|
||||
wantRangeTestPackets = true;
|
||||
}
|
||||
if fetchedNodeInfo.count == 1 && fetchedNodeInfo[0].storeForwardConfig?.enabled == true {
|
||||
wantStoreAndForwardPackets = true;
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Failed to find a node info for the connected node")
|
||||
|
|
@ -874,23 +945,48 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
public func sendPosition(destNum: Int64, wantResponse: Bool) -> Bool {
|
||||
var success = false
|
||||
let fromNodeNum = connectedPeripheral.num
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
}
|
||||
var positionPacket = Position()
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
positionPacket.time = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(LocationHelper.currentTimestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationHelper.currentAltitude)
|
||||
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
|
||||
if LocationHelper.currentSpeed > 0 && (!LocationHelper.currentSpeed.isNaN || !LocationHelper.currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(LocationHelper.currentSpeed * 3.6)
|
||||
}
|
||||
if LocationHelper.currentHeading > 0 && (!LocationHelper.currentHeading.isNaN || !LocationHelper.currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(LocationHelper.currentHeading)
|
||||
}
|
||||
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
if fromNodeNum <= 0 {
|
||||
return false
|
||||
}
|
||||
positionPacket.latitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationsHandler.shared.lastLocation.coordinate.longitude * 1e7)
|
||||
let timestamp = LocationsHandler.shared.lastLocation.timestamp
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationsHandler.shared.lastLocation.altitude)
|
||||
positionPacket.satsInView = UInt32(LocationsHandler.satsInView)
|
||||
let currentSpeed = LocationsHandler.shared.lastLocation.speed
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
|
||||
}
|
||||
let currentHeading = LocationsHandler.shared.lastLocation.course
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
} else {
|
||||
if fromNodeNum <= 0 || LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) == 0.0 {
|
||||
return false
|
||||
}
|
||||
positionPacket.latitudeI = Int32(LocationHelper.currentLocation.latitude * 1e7)
|
||||
positionPacket.longitudeI = Int32(LocationHelper.currentLocation.longitude * 1e7)
|
||||
let timestamp = LocationHelper.shared.locationManager.location?.timestamp ?? Date()
|
||||
positionPacket.time = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.timestamp = UInt32(timestamp.timeIntervalSince1970)
|
||||
positionPacket.altitude = Int32(LocationHelper.shared.locationManager.location?.altitude ?? 0)
|
||||
positionPacket.satsInView = UInt32(LocationHelper.satsInView)
|
||||
let currentSpeed = LocationHelper.shared.locationManager.location?.speed ?? 0
|
||||
if currentSpeed > 0 && (!currentSpeed.isNaN || !currentSpeed.isInfinite) {
|
||||
positionPacket.groundSpeed = UInt32(currentSpeed * 3.6)
|
||||
}
|
||||
let currentHeading = LocationHelper.shared.locationManager.location?.course ?? 0
|
||||
if currentHeading > 0 && (!currentHeading.isNaN || !currentHeading.isInfinite) {
|
||||
positionPacket.groundTrack = UInt32(currentHeading)
|
||||
}
|
||||
}
|
||||
|
||||
var meshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(destNum)
|
||||
meshPacket.from = UInt32(fromNodeNum)
|
||||
|
|
@ -1359,6 +1455,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return 0
|
||||
}
|
||||
|
||||
public func saveAmbientLightingModuleConfig(config: ModuleConfig.AmbientLightingConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.setModuleConfig.ambientLighting = config
|
||||
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.channel = UInt32(adminIndex)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.wantAck = true
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛟 Saved Ambient Lighting Module Config for \(toUser.longName ?? "unknown".localized)"
|
||||
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
upsertAmbientLightingModuleConfigPacket(config: config, nodeNum: toUser.num, context: context!)
|
||||
return Int64(meshPacket.id)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
public func saveCannedMessageModuleConfig(config: ModuleConfig.CannedMessageConfig, fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Int64 {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
|
|
@ -1858,6 +1981,33 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
return false
|
||||
}
|
||||
|
||||
public func requestAmbientLightingConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
adminPacket.getModuleConfigRequest = AdminMessage.ModuleConfigType.ambientlightingConfig
|
||||
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(toUser.num)
|
||||
meshPacket.from = UInt32(fromUser.num)
|
||||
meshPacket.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
|
||||
meshPacket.priority = MeshPacket.Priority.reliable
|
||||
meshPacket.channel = UInt32(adminIndex)
|
||||
meshPacket.wantAck = true
|
||||
|
||||
var dataMessage = DataMessage()
|
||||
dataMessage.payload = try! adminPacket.serializedData()
|
||||
dataMessage.portnum = PortNum.adminApp
|
||||
dataMessage.wantResponse = true
|
||||
|
||||
meshPacket.decoded = dataMessage
|
||||
|
||||
let messageDescription = "🛎️ Requested Ambient Lighting Config on admin channel \(adminIndex) for node: \(toUser.longName ?? "unknown".localized)"
|
||||
if sendAdminMessageToRadio(meshPacket: meshPacket, adminDescription: messageDescription, fromUser: fromUser, toUser: toUser) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func requestCannedMessagesModuleConfig(fromUser: UserEntity, toUser: UserEntity, adminIndex: Int32) -> Bool {
|
||||
|
||||
var adminPacket = AdminMessage()
|
||||
|
|
@ -2121,7 +2271,7 @@ class BLEManager: NSObject, CBPeripheralDelegate, MqttClientProxyManagerDelegate
|
|||
/// send a request for ClientHistory with a time period matching the heartbeat
|
||||
var sfPacket = StoreAndForward()
|
||||
sfPacket.rr = StoreAndForward.RequestResponse.clientHistory
|
||||
sfPacket.history.window = storeAndForwardMessage.heartbeat.period
|
||||
sfPacket.history.window = 18000000 // storeAndForwardMessage.heartbeat.period
|
||||
var meshPacket: MeshPacket = MeshPacket()
|
||||
meshPacket.to = UInt32(packet.from)
|
||||
meshPacket.from = UInt32(connectedNodeNum)
|
||||
|
|
@ -2249,29 +2399,4 @@ extension BLEManager: CBCentralManagerDelegate {
|
|||
let visibleDuration = Calendar.current.date(byAdding: .second, value: -5, to: today)!
|
||||
self.peripherals.removeAll(where: { $0.lastUpdate < visibleDuration})
|
||||
}
|
||||
|
||||
// func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
|
||||
//
|
||||
// guard let peripherals = dict[CBCentralManagerRestoredStatePeripheralsKey] as? [CBPeripheral] else {
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// if peripherals.count > 0 {
|
||||
//
|
||||
// for peripheral in peripherals {
|
||||
// print(peripheral)
|
||||
// switch peripheral.state {
|
||||
// case .connecting: // I've only seen this happen when
|
||||
// // re-launching attached to Xcode.
|
||||
// print("Xcode Restore")
|
||||
//
|
||||
// case .connected:
|
||||
// connectTo(peripheral: peripheral)
|
||||
// print("Restore BLE State")
|
||||
// default: break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// print("willRestoreState Hit!")
|
||||
// }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,43 +14,16 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||
locationManager.pausesLocationUpdatesAutomatically = true
|
||||
locationManager.allowsBackgroundLocationUpdates = true
|
||||
locationManager.activityType = .otherNavigation
|
||||
locationManager.activityType = .other
|
||||
}
|
||||
// Apple Park
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
static let DefaultAltitude = CLLocationDistance(integerLiteral: 0)
|
||||
static let DefaultSpeed = CLLocationSpeed(integerLiteral: 0)
|
||||
static let DefaultHeading = CLLocationDirection(integerLiteral: 0)
|
||||
static var currentLocation: CLLocationCoordinate2D {
|
||||
guard let location = shared.locationManager.location else {
|
||||
return DefaultLocation
|
||||
}
|
||||
return location.coordinate
|
||||
}
|
||||
static var currentAltitude: CLLocationDistance {
|
||||
guard let altitude = shared.locationManager.location?.altitude else {
|
||||
return DefaultAltitude
|
||||
}
|
||||
return altitude
|
||||
}
|
||||
static var currentSpeed: CLLocationSpeed {
|
||||
guard let speed = shared.locationManager.location?.speed else {
|
||||
return DefaultSpeed
|
||||
}
|
||||
return speed
|
||||
}
|
||||
static var currentHeading: CLLocationDirection {
|
||||
guard let heading = shared.locationManager.location?.course else {
|
||||
return DefaultHeading
|
||||
}
|
||||
return heading
|
||||
}
|
||||
static var currentTimestamp: Date {
|
||||
guard let timestamp = shared.locationManager.location?.timestamp else {
|
||||
return Date.now
|
||||
}
|
||||
return timestamp
|
||||
}
|
||||
static var satsInView: Int {
|
||||
// If we have a position we have a sat
|
||||
var sats = 1
|
||||
|
|
@ -74,9 +47,11 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
}
|
||||
return sats
|
||||
}
|
||||
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
switch manager.authorizationStatus {
|
||||
case .authorizedAlways:
|
||||
authorizationStatus = .authorizedAlways
|
||||
case .authorizedWhenInUse:
|
||||
authorizationStatus = .authorizedWhenInUse
|
||||
locationManager.requestLocation()
|
||||
|
|
@ -86,19 +61,13 @@ class LocationHelper: NSObject, ObservableObject, CLLocationManagerDelegate {
|
|||
authorizationStatus = .denied
|
||||
case .notDetermined:
|
||||
authorizationStatus = .notDetermined
|
||||
locationManager.requestWhenInUseAuthorization()
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
// locationManager.stopUpdatingLocation()
|
||||
// locations.last.map {
|
||||
// region = MKCoordinateRegion(
|
||||
// center: $0.coordinate,
|
||||
// span: .init(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
||||
// )
|
||||
// }
|
||||
|
||||
}
|
||||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
print("Location manager error: \(error.localizedDescription)")
|
||||
|
|
|
|||
96
Meshtastic/Helpers/LocationsHandler.swift
Normal file
96
Meshtastic/Helpers/LocationsHandler.swift
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// LocationsHandler.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 12/4/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
|
||||
// Shared state that manages the `CLLocationManager` and `CLBackgroundActivitySession`.
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
@MainActor class LocationsHandler: ObservableObject {
|
||||
|
||||
static let shared = LocationsHandler() // Create a single, shared instance of the object.
|
||||
private let manager: CLLocationManager
|
||||
private var background: CLBackgroundActivitySession?
|
||||
|
||||
@Published var lastLocation = CLLocation()
|
||||
@Published var isStationary = false
|
||||
@Published var count = 0
|
||||
|
||||
@Published
|
||||
var updatesStarted: Bool = UserDefaults.standard.bool(forKey: "liveUpdatesStarted") {
|
||||
didSet { UserDefaults.standard.set(updatesStarted, forKey: "liveUpdatesStarted") }
|
||||
}
|
||||
|
||||
@Published
|
||||
var backgroundActivity: Bool = UserDefaults.standard.bool(forKey: "BGActivitySessionStarted") {
|
||||
didSet {
|
||||
backgroundActivity ? self.background = CLBackgroundActivitySession() : self.background?.invalidate()
|
||||
UserDefaults.standard.set(backgroundActivity, forKey: "BGActivitySessionStarted")
|
||||
}
|
||||
}
|
||||
|
||||
private init() {
|
||||
self.manager = CLLocationManager() // Creating a location manager instance is safe to call here in `MainActor`.
|
||||
}
|
||||
|
||||
func startLocationUpdates() {
|
||||
if self.manager.authorizationStatus == .notDetermined {
|
||||
self.manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
print("Starting location updates")
|
||||
Task() {
|
||||
do {
|
||||
self.updatesStarted = true
|
||||
let updates = CLLocationUpdate.liveUpdates()
|
||||
for try await update in updates {
|
||||
if !self.updatesStarted { break } // End location updates by breaking out of the loop.
|
||||
if let loc = update.location {
|
||||
self.lastLocation = loc
|
||||
self.isStationary = update.isStationary
|
||||
self.count += 1
|
||||
//print("Location \(self.count): \(self.lastLocation)")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
print("Could not start location updates")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func stopLocationUpdates() {
|
||||
print("Stopping location updates")
|
||||
self.updatesStarted = false
|
||||
}
|
||||
|
||||
static let DefaultLocation = CLLocationCoordinate2D(latitude: 37.3346, longitude: -122.0090)
|
||||
|
||||
static var satsInView: Int {
|
||||
// If we have a position we have a sat
|
||||
var sats = 1
|
||||
if shared.lastLocation.verticalAccuracy > 0 {
|
||||
sats = 4
|
||||
if 0...5 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 12
|
||||
} else if 6...15 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 10
|
||||
} else if 16...30 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 9
|
||||
} else if 31...45 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 7
|
||||
} else if 46...60 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 5
|
||||
}
|
||||
} else if shared.lastLocation.verticalAccuracy < 0 && 60...300 ~= shared.lastLocation.horizontalAccuracy {
|
||||
sats = 3
|
||||
} else if shared.lastLocation.verticalAccuracy < 0 && shared.lastLocation.horizontalAccuracy > 300 {
|
||||
sats = 2
|
||||
}
|
||||
return sats
|
||||
}
|
||||
}
|
||||
|
|
@ -56,7 +56,9 @@ func localConfig (config: Config, context: NSManagedObjectContext, nodeNum: Int6
|
|||
|
||||
func moduleConfig (config: ModuleConfig, context: NSManagedObjectContext, nodeNum: Int64, nodeLongName: String) {
|
||||
|
||||
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) {
|
||||
if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(config.ambientLighting) {
|
||||
upsertAmbientLightingModuleConfigPacket(config: config.ambientLighting, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(config.cannedMessage) {
|
||||
upsertCannedMessagesModuleConfigPacket(config: config.cannedMessage, nodeNum: nodeNum, context: context)
|
||||
} else if config.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(config.detectionSensor) {
|
||||
upsertDetectionSensorModuleConfigPacket(config: config.detectionSensor, nodeNum: nodeNum, context: context)
|
||||
|
|
@ -472,7 +474,9 @@ func adminAppPacket (packet: MeshPacket, context: NSManagedObjectContext) {
|
|||
}
|
||||
} else if adminMessage.payloadVariant == AdminMessage.OneOf_PayloadVariant.getModuleConfigResponse(adminMessage.getModuleConfigResponse) {
|
||||
let moduleConfig = adminMessage.getModuleConfigResponse
|
||||
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
|
||||
if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.ambientLighting(moduleConfig.ambientLighting) {
|
||||
upsertAmbientLightingModuleConfigPacket(config: moduleConfig.ambientLighting, nodeNum: Int64(packet.from), context: context)
|
||||
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.cannedMessage(moduleConfig.cannedMessage) {
|
||||
upsertCannedMessagesModuleConfigPacket(config: moduleConfig.cannedMessage, nodeNum: Int64(packet.from), context: context)
|
||||
} else if moduleConfig.payloadVariant == ModuleConfig.OneOf_PayloadVariant.detectionSensor(moduleConfig.detectionSensor) {
|
||||
upsertDetectionSensorModuleConfigPacket(config: moduleConfig.detectionSensor, nodeNum: Int64(packet.from), context: context)
|
||||
|
|
@ -726,7 +730,11 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod
|
|||
newMessage.isEmoji = packet.decoded.emoji == 1
|
||||
newMessage.channel = Int32(packet.channel)
|
||||
newMessage.portNum = Int32(packet.decoded.portnum.rawValue)
|
||||
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp {
|
||||
if !UserDefaults.enableDetectionNotifications {
|
||||
newMessage.read = true
|
||||
}
|
||||
}
|
||||
if packet.decoded.replyID > 0 {
|
||||
newMessage.replyID = Int64(packet.decoded.replyID)
|
||||
}
|
||||
|
|
@ -751,6 +759,10 @@ func textMessageAppPacket(packet: MeshPacket, blockRangeTest: Bool, connectedNod
|
|||
messageSaved = true
|
||||
|
||||
if messageSaved {
|
||||
|
||||
if packet.decoded.portnum == PortNum.detectionSensorApp && !UserDefaults.enableDetectionNotifications {
|
||||
return
|
||||
}
|
||||
let appState = AppState.shared
|
||||
if newMessage.fromUser != nil && newMessage.toUser != nil && !(newMessage.fromUser?.mute ?? false) {
|
||||
// Set Unread Message Indicators
|
||||
|
|
|
|||
|
|
@ -55,6 +55,10 @@
|
|||
<string>We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device.</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>We use your location to display it on the mesh map as well as to have GPS coordinatess to send to the connected device. Route Recording uses location in the background.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>Privacy – Bluetooth Always Usage Description</key>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>MeshtasticDataModelV20.xcdatamodel</string>
|
||||
<string>MeshtasticDataModelV21.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5047e" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C5055b" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
|
|
@ -116,10 +116,12 @@
|
|||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="horizontalAccuracy" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="verticalAccuracy" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,404 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23C64" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="green" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ledState" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="red" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="ambientLightingConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="BluetoothConfigEntity" representedClassName="BluetoothConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPin" optional="YES" attributeType="Integer 32" defaultValueString="123456" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="bluetoothConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="bluetoothConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="CannedMessageConfigEntity" representedClassName="CannedMessageConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCcw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventCw" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerEventPress" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinA" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinB" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="inputbrokerPinPress" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="messages" optional="YES" attributeType="String" minValueString="0" maxValueString="198"/>
|
||||
<attribute name="rotary1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="updown1Enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="cannedMessagesConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="cannedMessageConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ChannelEntity" representedClassName="ChannelEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="downlinkEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="index" attributeType="Integer 32" minValueString="0" maxValueString="13" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="psk" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="role" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="uplinkEnabled" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="myInfoChannel" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="channels" inverseEntity="MyInfoEntity"/>
|
||||
<fetchedProperty name="allPrivateMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="channel == $FETCH_SOURCE.index && toUser == nil AND isEmoji == false"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="index"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="DetectionSensorConfigEntity" representedClassName="DetectionSensorConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="detectionTriggeredHigh" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="minimumBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="monitorPin" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sendBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="stateBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePullup" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<relationship name="detectionSensorConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="detectionSensorConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceConfigEntity" representedClassName="DeviceConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="buttonGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="buzzerGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="debugLogEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="disableTripleClick" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="doubleTapAsButtonPress" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="isManaged" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="nodeInfoBroadcastSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rebroadcastMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="serialEnabled" optional="YES" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="deviceConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="deviceConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DeviceMetadataEntity" representedClassName="DeviceMetadataEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="canShutdown" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceStateVersion" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="firmwareVersion" optional="YES" attributeType="String"/>
|
||||
<attribute name="hasBluetooth" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasEthernet" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hasWifi" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="hwModel" optional="YES" attributeType="String"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="role" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="metadataNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="metadata" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="DisplayConfigEntity" representedClassName="DisplayConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="compassNorthTop" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="displayMode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="flipScreen" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsFormat" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="headingBold" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="oledType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenCarouselInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="screenOnSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="units" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wakeOnTapOrMotion" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="displayConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="displayConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="ExternalNotificationConfigEntity" representedClassName="ExternalNotificationConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="active" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBell" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertBellVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessage" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageBuzzer" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="alertMessageVibra" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="nagTimeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="output" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputBuzzer" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputMilliseconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="outputVibra" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePWM" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="externalNotificationConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="externalNotificationConfig" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="fetchedProperty" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="ExternalNotificationConfigEntity"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="LocationEntity" representedClassName="LocationEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="routeLocation" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RouteEntity" inverseName="locations" inverseEntity="RouteEntity"/>
|
||||
</entity>
|
||||
<entity name="LoRaConfigEntity" representedClassName="LoRaConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bandwidth" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="codingRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="frequencyOffset" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="hopLimit" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="modemPreset" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideDutyCycle" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="overrideFrequency" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="regionCode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="spreadFactor" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sx126xRxBoostedGain" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="txPower" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="usePreset" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="loRaConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="loRaConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MessageEntity" representedClassName="MessageEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ackError" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackSNR" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="ackTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="admin" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="adminDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="isEmoji" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="messageId" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="messagePayload" optional="YES" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="messagePayloadMarkdown" optional="YES" attributeType="String"/>
|
||||
<attribute name="messageTimestamp" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="portNum" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="receivedTimestamp" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="fromUser" optional="YES" maxCount="1" deletionRule="Nullify" ordered="YES" destinationEntity="UserEntity" inverseName="sentMessages" inverseEntity="UserEntity"/>
|
||||
<relationship name="toUser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="receivedMessages" inverseEntity="UserEntity"/>
|
||||
<fetchedProperty name="tapbacks" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="replyID == $FETCH_SOURCE.messageId AND isEmoji == true"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="messageId"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="MQTTConfigEntity" representedClassName="MQTTConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="encryptionEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="jsonEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="password" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<attribute name="proxyToClientEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="root" optional="YES" attributeType="String" defaultValueString="msh"/>
|
||||
<attribute name="tlsEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="username" optional="YES" attributeType="String" maxValueString="30"/>
|
||||
<relationship name="mqttConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="mqttConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="MyInfoEntity" representedClassName="MyInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="adminIndex" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="minAppVersion" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="myNodeNum" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rebootCount" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="channels" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="ChannelEntity" inverseName="myInfoChannel" inverseEntity="ChannelEntity"/>
|
||||
<relationship name="myInfoNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="myInfo" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="toUser == nil"/>
|
||||
</fetchedProperty>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="myNodeNum"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="NetworkConfigEntity" representedClassName="NetworkConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="dns" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ethEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="gateway" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ip" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="ntpServer" optional="YES" attributeType="String"/>
|
||||
<attribute name="subnet" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiEnabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiMode" optional="YES" attributeType="Integer 32" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="wifiPsk" optional="YES" attributeType="String" minValueString="0" maxValueString="60"/>
|
||||
<attribute name="wifiSsid" optional="YES" attributeType="String" minValueString="0" maxValueString="30"/>
|
||||
<relationship name="networkConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="networkConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="NodeInfoEntity" representedClassName="NodeInfoEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bleName" optional="YES" attributeType="String"/>
|
||||
<attribute name="channel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="detection" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="environment" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastHeard" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="peripheralId" optional="YES" attributeType="String"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="ambientLightingConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AmbientLightingConfigEntity" inverseName="ambientLightingConfigNode" inverseEntity="AmbientLightingConfigEntity"/>
|
||||
<relationship name="bluetoothConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="BluetoothConfigEntity" inverseName="bluetoothConfigNode" inverseEntity="BluetoothConfigEntity"/>
|
||||
<relationship name="cannedMessageConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CannedMessageConfigEntity" inverseName="cannedMessagesConfigNode" inverseEntity="CannedMessageConfigEntity"/>
|
||||
<relationship name="detectionSensorConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DetectionSensorConfigEntity" inverseName="detectionSensorConfigNode" inverseEntity="DetectionSensorConfigEntity"/>
|
||||
<relationship name="deviceConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceConfigEntity" inverseName="deviceConfigNode" inverseEntity="DeviceConfigEntity"/>
|
||||
<relationship name="displayConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DisplayConfigEntity" inverseName="displayConfigNode" inverseEntity="DisplayConfigEntity"/>
|
||||
<relationship name="externalNotificationConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ExternalNotificationConfigEntity" inverseName="externalNotificationConfigNode" inverseEntity="ExternalNotificationConfigEntity"/>
|
||||
<relationship name="loRaConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LoRaConfigEntity" inverseName="loRaConfigNode" inverseEntity="LoRaConfigEntity"/>
|
||||
<relationship name="metadata" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="DeviceMetadataEntity" inverseName="metadataNode" inverseEntity="DeviceMetadataEntity"/>
|
||||
<relationship name="mqttConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MQTTConfigEntity" inverseName="mqttConfigNode" inverseEntity="MQTTConfigEntity"/>
|
||||
<relationship name="myInfo" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MyInfoEntity" inverseName="myInfoNode" inverseEntity="MyInfoEntity"/>
|
||||
<relationship name="networkConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NetworkConfigEntity" inverseName="networkConfigNode" inverseEntity="NetworkConfigEntity"/>
|
||||
<relationship name="positionConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PositionConfigEntity" inverseName="positionConfigNode" inverseEntity="PositionConfigEntity"/>
|
||||
<relationship name="positions" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PositionEntity" inverseName="nodePosition" inverseEntity="PositionEntity"/>
|
||||
<relationship name="rangeTestConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RangeTestConfigEntity" inverseName="rangeTestConfigNode" inverseEntity="RangeTestConfigEntity"/>
|
||||
<relationship name="rtttlConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="RTTTLConfigEntity" inverseName="rtttlConfigNode" inverseEntity="RTTTLConfigEntity"/>
|
||||
<relationship name="serialConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SerialConfigEntity" inverseName="serialConfigNode" inverseEntity="SerialConfigEntity"/>
|
||||
<relationship name="storeForwardConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreForwardConfigEntity" inverseName="storeForwardConfigNode" inverseEntity="StoreForwardConfigEntity"/>
|
||||
<relationship name="telemetries" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TelemetryEntity" inverseName="nodeTelemetry" inverseEntity="TelemetryEntity"/>
|
||||
<relationship name="telemetryConfig" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TelemetryConfigEntity" inverseName="telemetryConfigNode" inverseEntity="TelemetryConfigEntity"/>
|
||||
<relationship name="traceRoutes" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteEntity" inverseName="node" inverseEntity="TraceRouteEntity"/>
|
||||
<relationship name="user" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="UserEntity" inverseName="userNode" inverseEntity="UserEntity"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="num"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="PositionConfigEntity" representedClassName="PositionConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="broadcastSmartMinimumDistance" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="broadcastSmartMinimumIntervalSecs" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="deviceGpsEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="fixedPosition" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsAttemptTime" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="gpsUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionBroadcastSeconds" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="positionFlags" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="smartPositionEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="txGpio" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="positionConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positionConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="PositionEntity" representedClassName="PositionEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="heading" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latest" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="satsInView" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seqNo" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="speed" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="nodePosition" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="positions" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RangeTestConfigEntity" representedClassName="RangeTestConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="save" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="sender" optional="YES" attributeType="Integer 32" usesScalarValueType="YES"/>
|
||||
<relationship name="rangeTestConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rangeTestConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="RouteEntity" representedClassName="RouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="color" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="notes" optional="YES" attributeType="String"/>
|
||||
<relationship name="locations" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="LocationEntity" inverseName="routeLocation" inverseEntity="LocationEntity"/>
|
||||
</entity>
|
||||
<entity name="RTTTLConfigEntity" representedClassName="RTTTLConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="ringtone" optional="YES" attributeType="String" maxValueString="228" defaultValueString=""/>
|
||||
<relationship name="rtttlConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="rtttlConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="SerialConfigEntity" representedClassName="SerialConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="baudRate" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="echo" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="enabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="mode" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="rxd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="timeout" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="txd" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="serialConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="serialConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="StoreForwardConfigEntity" representedClassName="StoreForwardConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="heartbeat" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnMax" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="historyReturnWindow" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="records" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="storeForwardConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="storeForwardConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryConfigEntity" representedClassName="TelemetryConfigEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="deviceUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentDisplayFahrenheit" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentMeasurementEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentScreenEnabled" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="environmentUpdateInterval" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<relationship name="telemetryConfigNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetryConfig" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TelemetryEntity" representedClassName="TelemetryEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="airUtilTx" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="barometricPressure" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="batteryLevel" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="channelUtilization" optional="YES" attributeType="Float" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="current" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="gasResistance" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="metricsType" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="relativeHumidity" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="temperature" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="voltage" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<relationship name="nodeTelemetry" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="telemetries" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteEntity" representedClassName="TraceRouteEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="response" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="route" optional="YES" attributeType="Transformable" customClassName="[UInt32]"/>
|
||||
<attribute name="routeText" optional="YES" attributeType="String"/>
|
||||
<attribute name="time" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="hops" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="TraceRouteHopEntity" inverseName="traceRoute" inverseEntity="TraceRouteHopEntity"/>
|
||||
<relationship name="node" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="traceRoutes" inverseEntity="NodeInfoEntity"/>
|
||||
</entity>
|
||||
<entity name="TraceRouteHopEntity" representedClassName="TraceRouteHopEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="altitude" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="latitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="longitudeI" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="num" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="time" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<relationship name="traceRoute" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="TraceRouteEntity" inverseName="hops" inverseEntity="TraceRouteEntity"/>
|
||||
</entity>
|
||||
<entity name="UserEntity" representedClassName="UserEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hwModel" attributeType="String"/>
|
||||
<attribute name="isLicensed" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="lastMessage" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="longName" attributeType="String"/>
|
||||
<attribute name="mute" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="num" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="shortName" attributeType="String"/>
|
||||
<attribute name="userId" attributeType="String"/>
|
||||
<attribute name="vip" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="receivedMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="toUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="sentMessages" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="MessageEntity" inverseName="fromUser" inverseEntity="MessageEntity"/>
|
||||
<relationship name="userNode" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="NodeInfoEntity" inverseName="user" inverseEntity="NodeInfoEntity"/>
|
||||
<fetchedProperty name="adminMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND isEmoji == false AND admin = true"/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="allMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="((toUser.num == $FETCH_SOURCE.num) OR (fromUser.num == $FETCH_SOURCE.num)) AND toUser != nil AND fromUser != nil AND isEmoji == false AND admin = false AND portNum != 10 "/>
|
||||
</fetchedProperty>
|
||||
<fetchedProperty name="detectionSensorMessages" optional="YES">
|
||||
<fetchRequest name="fetchedPropertyFetchRequest" entity="MessageEntity" predicateString="(fromUser.num == $FETCH_SOURCE.num) AND portNum = 10"/>
|
||||
</fetchedProperty>
|
||||
</entity>
|
||||
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>
|
||||
<attribute name="longitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="name" attributeType="String" minValueString="1" maxValueString="30"/>
|
||||
</entity>
|
||||
</model>
|
||||
|
|
@ -59,40 +59,42 @@ struct MeshtasticAppleApp: App {
|
|||
saveChannels = false
|
||||
print("User wants to import a MBTILES offline map file: \(self.incomingUrl?.absoluteString ?? "No Tiles link")")
|
||||
}
|
||||
|
||||
if UserDefaults.mapUseLegacy {
|
||||
/// we are expecting a .mbtiles map file that contains raster data
|
||||
/// save it to the documents directory, and name it offline_map.mbtiles
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false)
|
||||
|
||||
// we are expecting a .mbtiles map file that contains raster data
|
||||
// save it to the documents directory, and name it offline_map.mbtiles
|
||||
let fileManager = FileManager.default
|
||||
let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let destination = documentsDirectory.appendingPathComponent("offline_map.mbtiles", isDirectory: false)
|
||||
|
||||
if !self.saveChannels {
|
||||
|
||||
// tell the system we want the file please
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
return
|
||||
}
|
||||
|
||||
// do we need to delete an old one?
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Found an old map file. Deleting it")
|
||||
try? fileManager.removeItem(atPath: destination.path)
|
||||
}
|
||||
|
||||
do {
|
||||
try fileManager.copyItem(at: url, to: destination)
|
||||
} catch {
|
||||
print("Copy MB Tile file failed. Error: \(error)")
|
||||
}
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Saved the map file")
|
||||
|
||||
// need to tell the map view that it needs to update and try loading the new overlay
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile")
|
||||
|
||||
} else {
|
||||
print("💥 Didn't save the map file")
|
||||
if !self.saveChannels {
|
||||
|
||||
// tell the system we want the file please
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
return
|
||||
}
|
||||
|
||||
// do we need to delete an old one?
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Found an old map file. Deleting it")
|
||||
try? fileManager.removeItem(atPath: destination.path)
|
||||
}
|
||||
|
||||
do {
|
||||
try fileManager.copyItem(at: url, to: destination)
|
||||
} catch {
|
||||
print("Copy MB Tile file failed. Error: \(error)")
|
||||
}
|
||||
|
||||
if fileManager.fileExists(atPath: destination.path) {
|
||||
print("ℹ️ Saved the map file")
|
||||
|
||||
// need to tell the map view that it needs to update and try loading the new overlay
|
||||
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "lastUpdatedLocalMapFile")
|
||||
|
||||
} else {
|
||||
print("💥 Didn't save the map file")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
print("🚀 Meshtstic Apple App launched!")
|
||||
// Default User Default Values
|
||||
|
|
@ -16,6 +16,18 @@ class MeshtasticAppDelegate: NSObject, UIApplicationDelegate, UNUserNotification
|
|||
UserDefaults.standard.register(defaults: ["meshMapShowNodeHistory" : true])
|
||||
UserDefaults.standard.register(defaults: ["meshMapShowRouteLines" : true])
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
let locationsHandler = LocationsHandler.shared
|
||||
|
||||
// If location updates were previously active, restart them after the background launch.
|
||||
if locationsHandler.updatesStarted {
|
||||
locationsHandler.startLocationUpdates()
|
||||
}
|
||||
// If a background activity session was previously active, reinstantiate it after the background launch.
|
||||
if locationsHandler.backgroundActivity {
|
||||
locationsHandler.backgroundActivity = true
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,23 @@ public func getStoreAndForwardMessageIds(seconds: Int, context: NSManagedObjectC
|
|||
return []
|
||||
}
|
||||
|
||||
public func getTraceRoute(id: Int64, context: NSManagedObjectContext) -> TraceRouteEntity? {
|
||||
|
||||
let fetchTraceRouteRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "TraceRouteEntity")
|
||||
fetchTraceRouteRequest.predicate = NSPredicate(format: "id == %lld", Int64(id))
|
||||
|
||||
do {
|
||||
guard let fetchedTraceRoute = try context.fetch(fetchTraceRouteRequest) as? [TraceRouteEntity] else {
|
||||
return nil
|
||||
}
|
||||
if fetchedTraceRoute.count == 1 {
|
||||
return fetchedTraceRoute[0]
|
||||
}
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func getUser(id: Int64, context: NSManagedObjectContext) -> UserEntity {
|
||||
|
||||
|
|
@ -82,21 +99,3 @@ public func getWaypoint(id: Int64, context: NSManagedObjectContext) -> WaypointE
|
|||
}
|
||||
return WaypointEntity(context: context)
|
||||
}
|
||||
|
||||
public func getDetectionSensorMessages(nodeNum: Int64?, context: NSManagedObjectContext) -> [MessageEntity] {
|
||||
|
||||
let fetchDetectionMessagesPredicate: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "MessageEntity")
|
||||
fetchDetectionMessagesPredicate.predicate = NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue))
|
||||
|
||||
do {
|
||||
let fetched = try context.fetch(fetchDetectionMessagesPredicate) as? [MessageEntity] ?? []
|
||||
if nodeNum == nil {
|
||||
return fetched.reversed()
|
||||
}
|
||||
return fetched.filter { message in
|
||||
return message.fromUser?.num == nodeNum!
|
||||
}.reversed()
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -624,6 +624,62 @@ func upsertPositionConfigPacket(config: Meshtastic.Config.PositionConfig, nodeNu
|
|||
}
|
||||
}
|
||||
|
||||
func upsertAmbientLightingModuleConfigPacket(config: Meshtastic.ModuleConfig.AmbientLightingConfig, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.ambientlighting.config %@".localized, String(nodeNum))
|
||||
MeshLogger.log("🏮 \(logString)")
|
||||
|
||||
let fetchNodeInfoRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest.init(entityName: "NodeInfoEntity")
|
||||
fetchNodeInfoRequest.predicate = NSPredicate(format: "num == %lld", Int64(nodeNum))
|
||||
|
||||
do {
|
||||
|
||||
guard let fetchedNode = try context.fetch(fetchNodeInfoRequest) as? [NodeInfoEntity] else {
|
||||
return
|
||||
}
|
||||
// Found a node, save Ambient Lighting Config
|
||||
if !fetchedNode.isEmpty {
|
||||
|
||||
if fetchedNode[0].cannedMessageConfig == nil {
|
||||
|
||||
let newAmbientLightingConfig = AmbientLightingConfigEntity(context: context)
|
||||
|
||||
newAmbientLightingConfig.ledState = config.ledState
|
||||
newAmbientLightingConfig.current = Int32(config.current)
|
||||
newAmbientLightingConfig.red = Int32(config.red)
|
||||
newAmbientLightingConfig.green = Int32(config.green)
|
||||
newAmbientLightingConfig.blue = Int32(config.blue)
|
||||
fetchedNode[0].ambientLightingConfig = newAmbientLightingConfig
|
||||
|
||||
} else {
|
||||
|
||||
if fetchedNode[0].ambientLightingConfig == nil {
|
||||
fetchedNode[0].ambientLightingConfig = AmbientLightingConfigEntity(context: context)
|
||||
}
|
||||
fetchedNode[0].ambientLightingConfig?.ledState = config.ledState
|
||||
fetchedNode[0].ambientLightingConfig?.current = Int32(config.current)
|
||||
fetchedNode[0].ambientLightingConfig?.red = Int32(config.red)
|
||||
fetchedNode[0].ambientLightingConfig?.green = Int32(config.green)
|
||||
fetchedNode[0].ambientLightingConfig?.blue = Int32(config.blue)
|
||||
}
|
||||
|
||||
do {
|
||||
try context.save()
|
||||
print("💾 Updated Ambient Lighting Module Config for node number: \(String(nodeNum))")
|
||||
} catch {
|
||||
context.rollback()
|
||||
let nsError = error as NSError
|
||||
print("💥 Error Updating Core Data AmbientLightingConfigEntity: \(nsError)")
|
||||
}
|
||||
} else {
|
||||
print("💥 No Nodes found in local database matching node number \(nodeNum) unable to save Ambient Lighting Module Config")
|
||||
}
|
||||
} catch {
|
||||
let nsError = error as NSError
|
||||
print("💥 Fetching node for core data AmbientLightingConfigEntity failed: \(nsError)")
|
||||
}
|
||||
}
|
||||
|
||||
func upsertCannedMessagesModuleConfigPacket(config: Meshtastic.ModuleConfig.CannedMessageConfig, nodeNum: Int64, context: NSManagedObjectContext) {
|
||||
|
||||
let logString = String.localizedStringWithFormat("mesh.log.cannedmessage.config %@".localized, String(nodeNum))
|
||||
|
|
|
|||
|
|
@ -244,6 +244,21 @@ struct Config {
|
|||
/// Turns off many of the routine broadcasts to favor CoT packet stream
|
||||
/// from the Meshtastic ATAK plugin -> IMeshService -> Node
|
||||
case tak // = 7
|
||||
|
||||
///
|
||||
/// Client Hidden device role
|
||||
/// Used for nodes that "only speak when spoken to"
|
||||
/// Turns all of the routine broadcasts but allows for ad-hoc communication
|
||||
/// Still rebroadcasts, but with local only rebroadcast mode (known meshes only)
|
||||
/// Can be used for clandestine operation or to dramatically reduce airtime / power consumption
|
||||
case clientHidden // = 8
|
||||
|
||||
///
|
||||
/// Lost and Found device role
|
||||
/// Used to automatically send a text message to the mesh
|
||||
/// with the current position of the device on a frequent interval:
|
||||
/// "I'm lost! Position: lat / long"
|
||||
case lostAndFound // = 9
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -260,6 +275,8 @@ struct Config {
|
|||
case 5: self = .tracker
|
||||
case 6: self = .sensor
|
||||
case 7: self = .tak
|
||||
case 8: self = .clientHidden
|
||||
case 9: self = .lostAndFound
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -274,6 +291,8 @@ struct Config {
|
|||
case .tracker: return 5
|
||||
case .sensor: return 6
|
||||
case .tak: return 7
|
||||
case .clientHidden: return 8
|
||||
case .lostAndFound: return 9
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -299,6 +318,11 @@ struct Config {
|
|||
/// Ignores observed messages from foreign meshes that are open or those which it cannot decrypt.
|
||||
/// Only rebroadcasts message on the nodes local primary / secondary channels.
|
||||
case localOnly // = 2
|
||||
|
||||
///
|
||||
/// Ignores observed messages from foreign meshes like LOCAL_ONLY,
|
||||
/// but takes it step further by also ignoring messages from nodenums not in the node's known list (NodeDB)
|
||||
case knownOnly // = 3
|
||||
case UNRECOGNIZED(Int)
|
||||
|
||||
init() {
|
||||
|
|
@ -310,6 +334,7 @@ struct Config {
|
|||
case 0: self = .all
|
||||
case 1: self = .allSkipDecoding
|
||||
case 2: self = .localOnly
|
||||
case 3: self = .knownOnly
|
||||
default: self = .UNRECOGNIZED(rawValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -319,6 +344,7 @@ struct Config {
|
|||
case .all: return 0
|
||||
case .allSkipDecoding: return 1
|
||||
case .localOnly: return 2
|
||||
case .knownOnly: return 3
|
||||
case .UNRECOGNIZED(let i): return i
|
||||
}
|
||||
}
|
||||
|
|
@ -1295,6 +1321,8 @@ extension Config.DeviceConfig.Role: CaseIterable {
|
|||
.tracker,
|
||||
.sensor,
|
||||
.tak,
|
||||
.clientHidden,
|
||||
.lostAndFound,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1304,6 +1332,7 @@ extension Config.DeviceConfig.RebroadcastMode: CaseIterable {
|
|||
.all,
|
||||
.allSkipDecoding,
|
||||
.localOnly,
|
||||
.knownOnly,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1703,6 +1732,8 @@ extension Config.DeviceConfig.Role: SwiftProtobuf._ProtoNameProviding {
|
|||
5: .same(proto: "TRACKER"),
|
||||
6: .same(proto: "SENSOR"),
|
||||
7: .same(proto: "TAK"),
|
||||
8: .same(proto: "CLIENT_HIDDEN"),
|
||||
9: .same(proto: "LOST_AND_FOUND"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1711,6 +1742,7 @@ extension Config.DeviceConfig.RebroadcastMode: SwiftProtobuf._ProtoNameProviding
|
|||
0: .same(proto: "ALL"),
|
||||
1: .same(proto: "ALL_SKIP_DECODING"),
|
||||
2: .same(proto: "LOCAL_ONLY"),
|
||||
3: .same(proto: "KNOWN_ONLY"),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,14 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
/// RAK11310 (RP2040 + SX1262)
|
||||
case rak11310 // = 26
|
||||
|
||||
///
|
||||
/// Makerfabs SenseLoRA Receiver (RP2040 + RFM96)
|
||||
case senseloraRp2040 // = 27
|
||||
|
||||
///
|
||||
/// Makerfabs SenseLoRA Industrial Monitor (ESP32-S3 + RFM96)
|
||||
case senseloraS3 // = 28
|
||||
|
||||
///
|
||||
/// ---------------------------------------------------------------------------
|
||||
/// Less common/prototype boards listed here (needs one more byte over the air)
|
||||
|
|
@ -247,6 +255,8 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case 19: self = .loraType
|
||||
case 25: self = .stationG1
|
||||
case 26: self = .rak11310
|
||||
case 27: self = .senseloraRp2040
|
||||
case 28: self = .senseloraS3
|
||||
case 32: self = .loraRelayV1
|
||||
case 33: self = .nrf52840Dk
|
||||
case 34: self = .ppr
|
||||
|
|
@ -299,6 +309,8 @@ enum HardwareModel: SwiftProtobuf.Enum {
|
|||
case .loraType: return 19
|
||||
case .stationG1: return 25
|
||||
case .rak11310: return 26
|
||||
case .senseloraRp2040: return 27
|
||||
case .senseloraS3: return 28
|
||||
case .loraRelayV1: return 32
|
||||
case .nrf52840Dk: return 33
|
||||
case .ppr: return 34
|
||||
|
|
@ -356,6 +368,8 @@ extension HardwareModel: CaseIterable {
|
|||
.loraType,
|
||||
.stationG1,
|
||||
.rak11310,
|
||||
.senseloraRp2040,
|
||||
.senseloraS3,
|
||||
.loraRelayV1,
|
||||
.nrf52840Dk,
|
||||
.ppr,
|
||||
|
|
@ -935,6 +949,10 @@ struct User {
|
|||
/// Also, "long_name" should be their licence number.
|
||||
var isLicensed: Bool = false
|
||||
|
||||
///
|
||||
/// Indicates that the user's role in the mesh
|
||||
var role: Config.DeviceConfig.Role = .client
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
||||
init() {}
|
||||
|
|
@ -1930,20 +1948,26 @@ struct FromRadio {
|
|||
///
|
||||
/// The packet id, used to allow the phone to request missing read packets from the FIFO,
|
||||
/// see our bluetooth docs
|
||||
var id: UInt32 = 0
|
||||
var id: UInt32 {
|
||||
get {return _storage._id}
|
||||
set {_uniqueStorage()._id = newValue}
|
||||
}
|
||||
|
||||
///
|
||||
/// Log levels, chosen to match python logging conventions.
|
||||
var payloadVariant: FromRadio.OneOf_PayloadVariant? = nil
|
||||
var payloadVariant: OneOf_PayloadVariant? {
|
||||
get {return _storage._payloadVariant}
|
||||
set {_uniqueStorage()._payloadVariant = newValue}
|
||||
}
|
||||
|
||||
///
|
||||
/// Log levels, chosen to match python logging conventions.
|
||||
var packet: MeshPacket {
|
||||
get {
|
||||
if case .packet(let v)? = payloadVariant {return v}
|
||||
if case .packet(let v)? = _storage._payloadVariant {return v}
|
||||
return MeshPacket()
|
||||
}
|
||||
set {payloadVariant = .packet(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .packet(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -1951,10 +1975,10 @@ struct FromRadio {
|
|||
/// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
|
||||
var myInfo: MyNodeInfo {
|
||||
get {
|
||||
if case .myInfo(let v)? = payloadVariant {return v}
|
||||
if case .myInfo(let v)? = _storage._payloadVariant {return v}
|
||||
return MyNodeInfo()
|
||||
}
|
||||
set {payloadVariant = .myInfo(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .myInfo(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -1962,30 +1986,30 @@ struct FromRadio {
|
|||
/// starts over with the first node in our DB
|
||||
var nodeInfo: NodeInfo {
|
||||
get {
|
||||
if case .nodeInfo(let v)? = payloadVariant {return v}
|
||||
if case .nodeInfo(let v)? = _storage._payloadVariant {return v}
|
||||
return NodeInfo()
|
||||
}
|
||||
set {payloadVariant = .nodeInfo(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .nodeInfo(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Include a part of the config (was: RadioConfig radio)
|
||||
var config: Config {
|
||||
get {
|
||||
if case .config(let v)? = payloadVariant {return v}
|
||||
if case .config(let v)? = _storage._payloadVariant {return v}
|
||||
return Config()
|
||||
}
|
||||
set {payloadVariant = .config(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .config(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Set to send debug console output over our protobuf stream
|
||||
var logRecord: LogRecord {
|
||||
get {
|
||||
if case .logRecord(let v)? = payloadVariant {return v}
|
||||
if case .logRecord(let v)? = _storage._payloadVariant {return v}
|
||||
return LogRecord()
|
||||
}
|
||||
set {payloadVariant = .logRecord(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .logRecord(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -1995,10 +2019,10 @@ struct FromRadio {
|
|||
/// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
|
||||
var configCompleteID: UInt32 {
|
||||
get {
|
||||
if case .configCompleteID(let v)? = payloadVariant {return v}
|
||||
if case .configCompleteID(let v)? = _storage._payloadVariant {return v}
|
||||
return 0
|
||||
}
|
||||
set {payloadVariant = .configCompleteID(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .configCompleteID(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -2008,70 +2032,70 @@ struct FromRadio {
|
|||
/// NOTE: This ID must not change - to keep (minimal) compatibility with <1.2 version of android apps.
|
||||
var rebooted: Bool {
|
||||
get {
|
||||
if case .rebooted(let v)? = payloadVariant {return v}
|
||||
if case .rebooted(let v)? = _storage._payloadVariant {return v}
|
||||
return false
|
||||
}
|
||||
set {payloadVariant = .rebooted(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .rebooted(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Include module config
|
||||
var moduleConfig: ModuleConfig {
|
||||
get {
|
||||
if case .moduleConfig(let v)? = payloadVariant {return v}
|
||||
if case .moduleConfig(let v)? = _storage._payloadVariant {return v}
|
||||
return ModuleConfig()
|
||||
}
|
||||
set {payloadVariant = .moduleConfig(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .moduleConfig(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// One packet is sent for each channel
|
||||
var channel: Channel {
|
||||
get {
|
||||
if case .channel(let v)? = payloadVariant {return v}
|
||||
if case .channel(let v)? = _storage._payloadVariant {return v}
|
||||
return Channel()
|
||||
}
|
||||
set {payloadVariant = .channel(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .channel(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Queue status info
|
||||
var queueStatus: QueueStatus {
|
||||
get {
|
||||
if case .queueStatus(let v)? = payloadVariant {return v}
|
||||
if case .queueStatus(let v)? = _storage._payloadVariant {return v}
|
||||
return QueueStatus()
|
||||
}
|
||||
set {payloadVariant = .queueStatus(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .queueStatus(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// File Transfer Chunk
|
||||
var xmodemPacket: XModem {
|
||||
get {
|
||||
if case .xmodemPacket(let v)? = payloadVariant {return v}
|
||||
if case .xmodemPacket(let v)? = _storage._payloadVariant {return v}
|
||||
return XModem()
|
||||
}
|
||||
set {payloadVariant = .xmodemPacket(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .xmodemPacket(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// Device metadata message
|
||||
var metadata: DeviceMetadata {
|
||||
get {
|
||||
if case .metadata(let v)? = payloadVariant {return v}
|
||||
if case .metadata(let v)? = _storage._payloadVariant {return v}
|
||||
return DeviceMetadata()
|
||||
}
|
||||
set {payloadVariant = .metadata(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .metadata(newValue)}
|
||||
}
|
||||
|
||||
///
|
||||
/// MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT)
|
||||
var mqttClientProxyMessage: MqttClientProxyMessage {
|
||||
get {
|
||||
if case .mqttClientProxyMessage(let v)? = payloadVariant {return v}
|
||||
if case .mqttClientProxyMessage(let v)? = _storage._payloadVariant {return v}
|
||||
return MqttClientProxyMessage()
|
||||
}
|
||||
set {payloadVariant = .mqttClientProxyMessage(newValue)}
|
||||
set {_uniqueStorage()._payloadVariant = .mqttClientProxyMessage(newValue)}
|
||||
}
|
||||
|
||||
var unknownFields = SwiftProtobuf.UnknownStorage()
|
||||
|
|
@ -2192,6 +2216,8 @@ struct FromRadio {
|
|||
}
|
||||
|
||||
init() {}
|
||||
|
||||
fileprivate var _storage = _StorageClass.defaultInstance
|
||||
}
|
||||
|
||||
///
|
||||
|
|
@ -2519,6 +2545,8 @@ extension HardwareModel: SwiftProtobuf._ProtoNameProviding {
|
|||
19: .same(proto: "LORA_TYPE"),
|
||||
25: .same(proto: "STATION_G1"),
|
||||
26: .same(proto: "RAK11310"),
|
||||
27: .same(proto: "SENSELORA_RP2040"),
|
||||
28: .same(proto: "SENSELORA_S3"),
|
||||
32: .same(proto: "LORA_RELAY_V1"),
|
||||
33: .same(proto: "NRF52840DK"),
|
||||
34: .same(proto: "PPR"),
|
||||
|
|
@ -2830,6 +2858,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase,
|
|||
4: .same(proto: "macaddr"),
|
||||
5: .standard(proto: "hw_model"),
|
||||
6: .standard(proto: "is_licensed"),
|
||||
7: .same(proto: "role"),
|
||||
]
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
|
|
@ -2844,6 +2873,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase,
|
|||
case 4: try { try decoder.decodeSingularBytesField(value: &self.macaddr) }()
|
||||
case 5: try { try decoder.decodeSingularEnumField(value: &self.hwModel) }()
|
||||
case 6: try { try decoder.decodeSingularBoolField(value: &self.isLicensed) }()
|
||||
case 7: try { try decoder.decodeSingularEnumField(value: &self.role) }()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
|
@ -2868,6 +2898,9 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase,
|
|||
if self.isLicensed != false {
|
||||
try visitor.visitSingularBoolField(value: self.isLicensed, fieldNumber: 6)
|
||||
}
|
||||
if self.role != .client {
|
||||
try visitor.visitSingularEnumField(value: self.role, fieldNumber: 7)
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
|
|
@ -2878,6 +2911,7 @@ extension User: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase,
|
|||
if lhs.macaddr != rhs.macaddr {return false}
|
||||
if lhs.hwModel != rhs.hwModel {return false}
|
||||
if lhs.isLicensed != rhs.isLicensed {return false}
|
||||
if lhs.role != rhs.role {return false}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
@ -3687,246 +3721,280 @@ extension FromRadio: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementation
|
|||
14: .same(proto: "mqttClientProxyMessage"),
|
||||
]
|
||||
|
||||
fileprivate class _StorageClass {
|
||||
var _id: UInt32 = 0
|
||||
var _payloadVariant: FromRadio.OneOf_PayloadVariant?
|
||||
|
||||
static let defaultInstance = _StorageClass()
|
||||
|
||||
private init() {}
|
||||
|
||||
init(copying source: _StorageClass) {
|
||||
_id = source._id
|
||||
_payloadVariant = source._payloadVariant
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate mutating func _uniqueStorage() -> _StorageClass {
|
||||
if !isKnownUniquelyReferenced(&_storage) {
|
||||
_storage = _StorageClass(copying: _storage)
|
||||
}
|
||||
return _storage
|
||||
}
|
||||
|
||||
mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularUInt32Field(value: &self.id) }()
|
||||
case 2: try {
|
||||
var v: MeshPacket?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .packet(let m) = current {v = m}
|
||||
_ = _uniqueStorage()
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
while let fieldNumber = try decoder.nextFieldNumber() {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every case branch when no optimizations are
|
||||
// enabled. https://github.com/apple/swift-protobuf/issues/1034
|
||||
switch fieldNumber {
|
||||
case 1: try { try decoder.decodeSingularUInt32Field(value: &_storage._id) }()
|
||||
case 2: try {
|
||||
var v: MeshPacket?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .packet(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .packet(v)
|
||||
}
|
||||
}()
|
||||
case 3: try {
|
||||
var v: MyNodeInfo?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .myInfo(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .myInfo(v)
|
||||
}
|
||||
}()
|
||||
case 4: try {
|
||||
var v: NodeInfo?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .nodeInfo(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .nodeInfo(v)
|
||||
}
|
||||
}()
|
||||
case 5: try {
|
||||
var v: Config?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .config(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .config(v)
|
||||
}
|
||||
}()
|
||||
case 6: try {
|
||||
var v: LogRecord?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .logRecord(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .logRecord(v)
|
||||
}
|
||||
}()
|
||||
case 7: try {
|
||||
var v: UInt32?
|
||||
try decoder.decodeSingularUInt32Field(value: &v)
|
||||
if let v = v {
|
||||
if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .configCompleteID(v)
|
||||
}
|
||||
}()
|
||||
case 8: try {
|
||||
var v: Bool?
|
||||
try decoder.decodeSingularBoolField(value: &v)
|
||||
if let v = v {
|
||||
if _storage._payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .rebooted(v)
|
||||
}
|
||||
}()
|
||||
case 9: try {
|
||||
var v: ModuleConfig?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .moduleConfig(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .moduleConfig(v)
|
||||
}
|
||||
}()
|
||||
case 10: try {
|
||||
var v: Channel?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .channel(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .channel(v)
|
||||
}
|
||||
}()
|
||||
case 11: try {
|
||||
var v: QueueStatus?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .queueStatus(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .queueStatus(v)
|
||||
}
|
||||
}()
|
||||
case 12: try {
|
||||
var v: XModem?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .xmodemPacket(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .xmodemPacket(v)
|
||||
}
|
||||
}()
|
||||
case 13: try {
|
||||
var v: DeviceMetadata?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .metadata(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .metadata(v)
|
||||
}
|
||||
}()
|
||||
case 14: try {
|
||||
var v: MqttClientProxyMessage?
|
||||
var hadOneofValue = false
|
||||
if let current = _storage._payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .mqttClientProxyMessage(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
_storage._payloadVariant = .mqttClientProxyMessage(v)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .packet(v)
|
||||
}
|
||||
}()
|
||||
case 3: try {
|
||||
var v: MyNodeInfo?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .myInfo(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .myInfo(v)
|
||||
}
|
||||
}()
|
||||
case 4: try {
|
||||
var v: NodeInfo?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .nodeInfo(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .nodeInfo(v)
|
||||
}
|
||||
}()
|
||||
case 5: try {
|
||||
var v: Config?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .config(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .config(v)
|
||||
}
|
||||
}()
|
||||
case 6: try {
|
||||
var v: LogRecord?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .logRecord(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .logRecord(v)
|
||||
}
|
||||
}()
|
||||
case 7: try {
|
||||
var v: UInt32?
|
||||
try decoder.decodeSingularUInt32Field(value: &v)
|
||||
if let v = v {
|
||||
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .configCompleteID(v)
|
||||
}
|
||||
}()
|
||||
case 8: try {
|
||||
var v: Bool?
|
||||
try decoder.decodeSingularBoolField(value: &v)
|
||||
if let v = v {
|
||||
if self.payloadVariant != nil {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .rebooted(v)
|
||||
}
|
||||
}()
|
||||
case 9: try {
|
||||
var v: ModuleConfig?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .moduleConfig(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .moduleConfig(v)
|
||||
}
|
||||
}()
|
||||
case 10: try {
|
||||
var v: Channel?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .channel(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .channel(v)
|
||||
}
|
||||
}()
|
||||
case 11: try {
|
||||
var v: QueueStatus?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .queueStatus(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .queueStatus(v)
|
||||
}
|
||||
}()
|
||||
case 12: try {
|
||||
var v: XModem?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .xmodemPacket(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .xmodemPacket(v)
|
||||
}
|
||||
}()
|
||||
case 13: try {
|
||||
var v: DeviceMetadata?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .metadata(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .metadata(v)
|
||||
}
|
||||
}()
|
||||
case 14: try {
|
||||
var v: MqttClientProxyMessage?
|
||||
var hadOneofValue = false
|
||||
if let current = self.payloadVariant {
|
||||
hadOneofValue = true
|
||||
if case .mqttClientProxyMessage(let m) = current {v = m}
|
||||
}
|
||||
try decoder.decodeSingularMessageField(value: &v)
|
||||
if let v = v {
|
||||
if hadOneofValue {try decoder.handleConflictingOneOf()}
|
||||
self.payloadVariant = .mqttClientProxyMessage(v)
|
||||
}
|
||||
}()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if self.id != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: self.id, fieldNumber: 1)
|
||||
}
|
||||
switch self.payloadVariant {
|
||||
case .packet?: try {
|
||||
guard case .packet(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||
}()
|
||||
case .myInfo?: try {
|
||||
guard case .myInfo(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}()
|
||||
case .nodeInfo?: try {
|
||||
guard case .nodeInfo(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||
}()
|
||||
case .config?: try {
|
||||
guard case .config(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}()
|
||||
case .logRecord?: try {
|
||||
guard case .logRecord(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||
}()
|
||||
case .configCompleteID?: try {
|
||||
guard case .configCompleteID(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7)
|
||||
}()
|
||||
case .rebooted?: try {
|
||||
guard case .rebooted(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 8)
|
||||
}()
|
||||
case .moduleConfig?: try {
|
||||
guard case .moduleConfig(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
|
||||
}()
|
||||
case .channel?: try {
|
||||
guard case .channel(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
|
||||
}()
|
||||
case .queueStatus?: try {
|
||||
guard case .queueStatus(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
|
||||
}()
|
||||
case .xmodemPacket?: try {
|
||||
guard case .xmodemPacket(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
|
||||
}()
|
||||
case .metadata?: try {
|
||||
guard case .metadata(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
|
||||
}()
|
||||
case .mqttClientProxyMessage?: try {
|
||||
guard case .mqttClientProxyMessage(let v)? = self.payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 14)
|
||||
}()
|
||||
case nil: break
|
||||
try withExtendedLifetime(_storage) { (_storage: _StorageClass) in
|
||||
// The use of inline closures is to circumvent an issue where the compiler
|
||||
// allocates stack space for every if/case branch local when no optimizations
|
||||
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
|
||||
// https://github.com/apple/swift-protobuf/issues/1182
|
||||
if _storage._id != 0 {
|
||||
try visitor.visitSingularUInt32Field(value: _storage._id, fieldNumber: 1)
|
||||
}
|
||||
switch _storage._payloadVariant {
|
||||
case .packet?: try {
|
||||
guard case .packet(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
|
||||
}()
|
||||
case .myInfo?: try {
|
||||
guard case .myInfo(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
|
||||
}()
|
||||
case .nodeInfo?: try {
|
||||
guard case .nodeInfo(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 4)
|
||||
}()
|
||||
case .config?: try {
|
||||
guard case .config(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 5)
|
||||
}()
|
||||
case .logRecord?: try {
|
||||
guard case .logRecord(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 6)
|
||||
}()
|
||||
case .configCompleteID?: try {
|
||||
guard case .configCompleteID(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 7)
|
||||
}()
|
||||
case .rebooted?: try {
|
||||
guard case .rebooted(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularBoolField(value: v, fieldNumber: 8)
|
||||
}()
|
||||
case .moduleConfig?: try {
|
||||
guard case .moduleConfig(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 9)
|
||||
}()
|
||||
case .channel?: try {
|
||||
guard case .channel(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 10)
|
||||
}()
|
||||
case .queueStatus?: try {
|
||||
guard case .queueStatus(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 11)
|
||||
}()
|
||||
case .xmodemPacket?: try {
|
||||
guard case .xmodemPacket(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 12)
|
||||
}()
|
||||
case .metadata?: try {
|
||||
guard case .metadata(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 13)
|
||||
}()
|
||||
case .mqttClientProxyMessage?: try {
|
||||
guard case .mqttClientProxyMessage(let v)? = _storage._payloadVariant else { preconditionFailure() }
|
||||
try visitor.visitSingularMessageField(value: v, fieldNumber: 14)
|
||||
}()
|
||||
case nil: break
|
||||
}
|
||||
}
|
||||
try unknownFields.traverse(visitor: &visitor)
|
||||
}
|
||||
|
||||
static func ==(lhs: FromRadio, rhs: FromRadio) -> Bool {
|
||||
if lhs.id != rhs.id {return false}
|
||||
if lhs.payloadVariant != rhs.payloadVariant {return false}
|
||||
if lhs._storage !== rhs._storage {
|
||||
let storagesAreEqual: Bool = withExtendedLifetime((lhs._storage, rhs._storage)) { (_args: (_StorageClass, _StorageClass)) in
|
||||
let _storage = _args.0
|
||||
let rhs_storage = _args.1
|
||||
if _storage._id != rhs_storage._id {return false}
|
||||
if _storage._payloadVariant != rhs_storage._payloadVariant {return false}
|
||||
return true
|
||||
}
|
||||
if !storagesAreEqual {return false}
|
||||
}
|
||||
if lhs.unknownFields != rhs.unknownFields {return false}
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class LocalMBTileOverlay: MKTileOverlay {
|
|||
|
||||
// make sure it's raster
|
||||
let formatQuery = try mb.pluck(metadata.select(value).filter(name == "format"))
|
||||
if formatQuery?[value] == nil || (formatQuery![value] != "jpg" && formatQuery![value] != "png") {
|
||||
if formatQuery?[value] == nil || (formatQuery![value] != "jpeg" && formatQuery![value] != "jpg" && formatQuery![value] != "png") {
|
||||
throw MapTileError.invalidFormat
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@ struct UserList: View {
|
|||
@State var node: NodeInfoEntity?
|
||||
@State private var userSelection: UserEntity? // Nothing selected by default.
|
||||
@State private var isPresentingDeleteUserMessagesConfirm: Bool = false
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
|
||||
var body: some View {
|
||||
let localeDateFormat = DateFormatter.dateFormat(fromTemplate: "yyMMdd", options: 0, locale: Locale.current)
|
||||
|
|
@ -126,14 +125,6 @@ struct UserList: View {
|
|||
} label: {
|
||||
Label(user.mute ? "Show Alerts" : "Hide Alerts", systemImage: user.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: user.num, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
if user.messageList.count > 0 {
|
||||
Button(role: .destructive) {
|
||||
isPresentingDeleteUserMessagesConfirm = true
|
||||
|
|
@ -143,14 +134,6 @@ struct UserList: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("This could take a while, response will appear in the mesh log.")
|
||||
}
|
||||
.confirmationDialog(
|
||||
"This conversation will be deleted.",
|
||||
isPresented: $isPresentingDeleteUserMessagesConfirm,
|
||||
|
|
|
|||
|
|
@ -15,22 +15,24 @@ struct DetectionSensorLog: View {
|
|||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "messageTimestamp", ascending: false)],
|
||||
predicate: NSPredicate(format: "portNum == %d", Int32(PortNum.detectionSensorApp.rawValue)), animation: .none)
|
||||
private var detections: FetchedResults<MessageEntity>
|
||||
|
||||
var body: some View {
|
||||
let oneDayAgo = Calendar.current.date(byAdding: .day, value: -1, to: Date())
|
||||
let detections = getDetectionSensorMessages(nodeNum: node.num, context: context)
|
||||
let chartData = detections
|
||||
.filter { $0.timestamp >= oneDayAgo! }
|
||||
.filter { $0.timestamp >= oneDayAgo! && $0.fromUser?.num ?? -1 == node.user?.num ?? 0 }
|
||||
.sorted { $0.timestamp < $1.timestamp }
|
||||
|
||||
VStack {
|
||||
if chartData.count > 0 {
|
||||
GroupBox(label: Label("\(detections.count) Total Detection Events", systemImage: "sensor")) {
|
||||
GroupBox(label: Label("\(chartData.count) Total Detection Events", systemImage: "sensor")) {
|
||||
Chart {
|
||||
ForEach(chartData, id: \.self) { point in
|
||||
Plot {
|
||||
BarMark(
|
||||
x: .value("x", point.timestamp),
|
||||
x: .value("x", point.timestamp, unit: .hour),
|
||||
y: .value("y", 1)
|
||||
)
|
||||
}
|
||||
|
|
@ -49,12 +51,8 @@ struct DetectionSensorLog: View {
|
|||
}
|
||||
.chartXAxis(content: {
|
||||
AxisMarks(position: .top)
|
||||
// AxisMarks(position: .top, values: .stride(by: .hour)) { date in
|
||||
// AxisValueLabel(format: .dateTime.hour())
|
||||
// }
|
||||
})
|
||||
.chartXAxis(.automatic)
|
||||
.chartYScale(domain: 0...20)
|
||||
.chartForegroundStyleScale([
|
||||
"Detection events": .green
|
||||
])
|
||||
|
|
@ -91,9 +89,10 @@ struct DetectionSensorLog: View {
|
|||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
ForEach(detections) { d in
|
||||
ForEach(detections.filter( {$0.fromUser?.num ?? -1 == node.user?.num ?? 0})) { d in
|
||||
GridRow {
|
||||
Text(d.messagePayload ?? "Detected")
|
||||
.font(.caption)
|
||||
Text(d.timestamp.formattedDate(format: dateFormatString))
|
||||
.font(.caption)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ struct NodeMapSwiftUI: View {
|
|||
let positionArray = node.positions?.array as? [PositionEntity] ?? []
|
||||
var mostRecent = node.positions?.lastObject as? PositionEntity
|
||||
let lineCoords = positionArray.compactMap({(position) -> CLLocationCoordinate2D in
|
||||
return position.nodeCoordinate ?? LocationHelper.DefaultLocation
|
||||
return position.nodeCoordinate ?? LocationsHandler.DefaultLocation
|
||||
})
|
||||
|
||||
if node.hasPositions {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ struct PositionAltitudeChart: View {
|
|||
@State private var lineWidth = 2.0
|
||||
|
||||
var body: some View {
|
||||
let fiveYearsAgo = Calendar.current.date(byAdding: .year, value: -5, to: Date())
|
||||
let nodePositions = Array(node.positions!) as! [PositionEntity]
|
||||
let data = nodePositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
|
||||
let filteredPositions = nodePositions.filter({$0.time != nil && ($0.time ?? fiveYearsAgo!) > fiveYearsAgo!})
|
||||
let data = filteredPositions.map { PositionAltitude(time: $0.time ?? Date(), altitude: Measurement(value: Double($0.altitude), unit: .meters) ) }
|
||||
GroupBox(label: Label("Altitude", systemImage: "mountain.2")) {
|
||||
|
||||
Chart(data, id: \.time) {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@
|
|||
import SwiftUI
|
||||
import MapKit
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct PositionPopover: View {
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
|
@ -132,8 +134,8 @@ struct PositionPopover: View {
|
|||
.padding(.bottom, 5)
|
||||
|
||||
/// Distance
|
||||
if LocationHelper.currentLocation.distance(from: LocationHelper.DefaultLocation) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from: LocationHelper.currentLocation)
|
||||
if locationsHandler.lastLocation.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = position.coordinate.distance(from:CLLocationCoordinate2D(latitude: locationsHandler.lastLocation.coordinate.latitude, longitude: locationsHandler.lastLocation.coordinate.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
|
||||
.foregroundColor(.primary)
|
||||
|
|
|
|||
|
|
@ -93,9 +93,21 @@ struct NodeDetail: View {
|
|||
}
|
||||
.disabled(!node.hasDetectionSensorMetrics)
|
||||
Divider()
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
TraceRouteLog(node: node)
|
||||
} label: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.font(.title)
|
||||
|
||||
Text("Trace Route Log")
|
||||
.font(.title3)
|
||||
}
|
||||
.disabled(node.traceRoutes?.count ?? 0 == 0)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if self.bleManager.connectedPeripheral != nil && node.metadata != nil {
|
||||
HStack {
|
||||
if node.metadata?.canShutdown ?? false {
|
||||
|
|
|
|||
|
|
@ -55,14 +55,28 @@ struct NodeListItem: View {
|
|||
if node.positions?.count ?? 0 > 0 && connectedNode != node.num {
|
||||
HStack {
|
||||
let lastPostion = node.positions!.reversed()[0] as! PositionEntity
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.callout)
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
let myCoord = CLLocation(latitude: LocationsHandler.shared.lastLocation.coordinate.latitude, longitude: LocationsHandler.shared.lastLocation.coordinate.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationsHandler.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationsHandler.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.callout)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
let myCoord = CLLocation(latitude: LocationHelper.currentLocation.latitude, longitude: LocationHelper.currentLocation.longitude)
|
||||
if lastPostion.nodeCoordinate != nil && myCoord.coordinate.longitude != LocationHelper.DefaultLocation.longitude && myCoord.coordinate.latitude != LocationHelper.DefaultLocation.latitude {
|
||||
let nodeCoord = CLLocation(latitude: lastPostion.nodeCoordinate!.latitude, longitude: lastPostion.nodeCoordinate!.longitude)
|
||||
let metersAway = nodeCoord.distance(from: myCoord)
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.font(.callout)
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
DistanceText(meters: metersAway).font(.callout)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ struct NodeList: View {
|
|||
|
||||
@State private var columnVisibility = NavigationSplitViewVisibility.all
|
||||
@State private var selectedNode: NodeInfoEntity?
|
||||
@State private var isPresentingTraceRouteSentAlert = false
|
||||
|
||||
@SceneStorage("selectedDetailView") var selectedDetailView: String?
|
||||
|
||||
|
|
@ -72,13 +73,31 @@ struct NodeList: View {
|
|||
} label: {
|
||||
Label(node.user!.mute ? "Show Alerts" : "Hide Alerts", systemImage: node.user!.mute ? "bell" : "bell.slash")
|
||||
}
|
||||
if connectedNodeNum != node.num {
|
||||
Button {
|
||||
let success = bleManager.sendTraceRouteRequest(destNum: node.user?.num ?? 0, wantResponse: true)
|
||||
if success {
|
||||
isPresentingTraceRouteSentAlert = true
|
||||
}
|
||||
} label: {
|
||||
Label("Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.alert(
|
||||
"Trace Route Sent",
|
||||
isPresented: $isPresentingTraceRouteSentAlert
|
||||
) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text("This could take a while, response will appear in the trace route log for the node it was sent to.")
|
||||
}
|
||||
}
|
||||
.searchable(text: nodesQuery, prompt: "Find a node")
|
||||
.navigationTitle(String.localizedStringWithFormat("nodes %@".localized, String(nodes.count)))
|
||||
.listStyle(.plain)
|
||||
|
||||
.navigationSplitViewColumnWidth(min: 100, ideal: 250, max: 500)
|
||||
.navigationBarItems(leading:
|
||||
MeshtasticLogo(),
|
||||
|
|
|
|||
161
Meshtastic/Views/Nodes/TraceRouteLog.swift
Normal file
161
Meshtastic/Views/Nodes/TraceRouteLog.swift
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
//
|
||||
// TraceRouteLog.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 12/7/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
#if canImport(MapKit)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct TraceRouteLog: View {
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
|
||||
@State private var isPresentingClearLogConfirm: Bool = false
|
||||
@State var isExporting = false
|
||||
@State var exportString = ""
|
||||
@ObservedObject var node: NodeInfoEntity
|
||||
@State private var selectedRoute: TraceRouteEntity?
|
||||
// Map Configuration
|
||||
@Namespace var mapScope
|
||||
@State var mapStyle: MapStyle = MapStyle.standard(elevation: .realistic, emphasis: MapStyle.StandardEmphasis.muted ,pointsOfInterest: .all, showsTraffic: true)
|
||||
@State var position = MapCameraPosition.automatic
|
||||
let distanceFormatter = MKDistanceFormatter()
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
VStack {
|
||||
VStack {
|
||||
List(node.traceRoutes?.reversed() as? [TraceRouteEntity] ?? [], id: \.self, selection: $selectedRoute) { route in
|
||||
|
||||
Label {
|
||||
Text("\(route.time?.formatted() ?? "unknown".localized) - \(route.response ? (route.hops?.count == 0 && route.response ? "Direct" : "\(route.hops?.count == 0) Hops") : "No Response")")
|
||||
} icon: {
|
||||
Image(systemName: route.response ? (route.hops?.count == 0 && route.response ? "person.line.dotted.person" : "point.3.connected.trianglepath.dotted") : "person.slash")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
.frame(minHeight: 200, maxHeight: 230)
|
||||
VStack {
|
||||
if selectedRoute != nil {
|
||||
if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 > 0 {
|
||||
Text("Received by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)")
|
||||
Text("Route: \(selectedRoute?.routeText ?? "unknown".localized)")
|
||||
.font(.title3)
|
||||
} else if selectedRoute?.response ?? false {
|
||||
Label {
|
||||
Text("Trace route received directly by \(selectedRoute?.node?.user?.longName ?? "unknown".localized)")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.font(.title3)
|
||||
}
|
||||
|
||||
let hopsArray = selectedRoute?.hops?.array as? [TraceRouteHopEntity] ?? []
|
||||
let lineCoords = hopsArray.compactMap({(hop) -> CLLocationCoordinate2D in
|
||||
return hop.coordinate ?? LocationHelper.DefaultLocation
|
||||
})
|
||||
if selectedRoute?.response ?? false {
|
||||
if selectedRoute?.coordinate != nil && (selectedRoute?.node?.positions?.count ?? 0 > 0 || false ) {
|
||||
Map(position: $position, bounds: MapCameraBounds(minimumDistance: 1, maximumDistance: .infinity), scope: mapScope) {
|
||||
Annotation("You", coordinate: selectedRoute?.coordinate ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.green))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
.annotationTitles(.automatic)
|
||||
// Direct Trace Route
|
||||
if selectedRoute?.response ?? false && selectedRoute?.hops?.count ?? 0 == 0 {
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0 {
|
||||
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
|
||||
var traceRouteCoords: [CLLocationCoordinate2D] = [selectedRoute?.coordinate ?? LocationsHandler.DefaultLocation, mostRecent.coordinate]
|
||||
Annotation(selectedRoute?.node?.user?.shortName ?? "???", coordinate: mostRecent.nodeCoordinate ?? LocationHelper.DefaultLocation) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(.black))
|
||||
.strokeBorder(.white, lineWidth: 3)
|
||||
.frame(width: 15, height: 15)
|
||||
}
|
||||
}
|
||||
let dashed = StrokeStyle(
|
||||
lineWidth: 2,
|
||||
lineCap: .round, lineJoin: .round, dash: [7, 10]
|
||||
)
|
||||
MapPolyline(coordinates: traceRouteCoords)
|
||||
.stroke(.blue, style: dashed)
|
||||
}
|
||||
} else if selectedRoute?.hops?.count ?? 0 == 0 {
|
||||
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
VStack {
|
||||
/// Distance
|
||||
if selectedRoute?.node?.positions?.count ?? 0 > 0 && selectedRoute?.coordinate != nil {
|
||||
let mostRecent = selectedRoute?.node?.positions?.lastObject as! PositionEntity
|
||||
let startPoint = CLLocation(latitude: selectedRoute?.coordinate?.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: selectedRoute?.coordinate?.longitude ?? LocationsHandler.DefaultLocation.longitude)
|
||||
|
||||
if startPoint.distance(from: CLLocation(latitude: LocationsHandler.DefaultLocation.latitude, longitude: LocationsHandler.DefaultLocation.longitude)) > 0.0 {
|
||||
let metersAway = selectedRoute?.coordinate?.distance(from:CLLocationCoordinate2D(latitude: mostRecent.latitude ?? LocationsHandler.DefaultLocation.latitude, longitude: mostRecent.longitude ?? LocationsHandler.DefaultLocation.longitude))
|
||||
Label {
|
||||
Text("distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway ?? 0)))")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "lines.measurement.horizontal")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
VStack {
|
||||
Label {
|
||||
Text("Trace route sent to \(selectedRoute?.node?.user?.longName ?? "unknown".localized)")
|
||||
} icon: {
|
||||
Image(systemName: "signpost.right.and.left")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.font(.title3)
|
||||
Divider()
|
||||
Label {
|
||||
Text("\(selectedRoute?.time?.formatted() ?? "") - No response")
|
||||
|
||||
} icon: {
|
||||
Image(systemName: "person.slash")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
}
|
||||
.font(.callout)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ContentUnavailableView("Select a Trace Route", systemImage: "signpost.right.and.left")
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.navigationTitle("Trace Route Log")
|
||||
}
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
.onAppear {
|
||||
if self.bleManager.context == nil {
|
||||
self.bleManager.context = context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// AmbientLightingConfig.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Copyright(c) Garth Vander Houwen 11/26/23
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct AmbientLightingConfig: View {
|
||||
@Environment(\.self) var environment
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@EnvironmentObject var bleManager: BLEManager
|
||||
@Environment(\.dismiss) private var goBack
|
||||
|
||||
var node: NodeInfoEntity?
|
||||
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
@State var hasChanges = false
|
||||
@State var ledState: Bool = false
|
||||
@State var current = 10
|
||||
@State var red = 0
|
||||
@State var green = 0
|
||||
@State var blue = 0
|
||||
@State private var color = Color(red: 51, green: 199, blue: 88) // Color(.sRGB, red: 0.98, green: 0.9, blue: 0.2)
|
||||
@State private var components: Color.Resolved?
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.rtttlConfig == nil {
|
||||
Text("Ambient Lighting config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setAmbientLightingConfigValue()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $ledState) {
|
||||
Label("LED State", systemImage: ledState ? "lightbulb.led.fill" : "lightbulb.led")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.hidden)
|
||||
Text("The state of the LED (on/off)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
HStack {
|
||||
Image(systemName: "eyedropper")
|
||||
.foregroundColor(.accentColor)
|
||||
ColorPicker("Color", selection: $color, supportsOpacity: false)
|
||||
.padding(5)
|
||||
}
|
||||
HStack {
|
||||
Image(systemName: "directcurrent")
|
||||
.foregroundColor(.accentColor)
|
||||
Stepper("Current: \(current)", value: $current, in: 0...31, step: 1)
|
||||
.padding(5)
|
||||
}
|
||||
.onChange(of: color, initial: true) {
|
||||
components = color.resolve(in: environment)
|
||||
hasChanges = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.ambientLightingConfig == nil)
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
Label("save", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || !hasChanges)
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.capsule)
|
||||
.controlSize(.large)
|
||||
.padding()
|
||||
.confirmationDialog(
|
||||
"are.you.sure",
|
||||
isPresented: $isPresentingSaveConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
let nodeName = node?.user?.longName ?? "unknown".localized
|
||||
let buttonText = String.localizedStringWithFormat("save.config %@".localized, nodeName)
|
||||
Button(buttonText) {
|
||||
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if connectedNode != nil {
|
||||
var al = ModuleConfig.AmbientLightingConfig()
|
||||
al.ledState = ledState
|
||||
al.current = UInt32(current)
|
||||
if let components {
|
||||
al.red = UInt32(components.red * 255)
|
||||
al.green = UInt32(components.green * 255)
|
||||
al.blue = UInt32(components.blue * 255)
|
||||
}
|
||||
|
||||
let adminMessageId = bleManager.saveAmbientLightingModuleConfig(config: al, fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
if adminMessageId > 0 {
|
||||
// Should show a saved successfully alert once I know that to be true
|
||||
// for now just disable the button after a successful save
|
||||
hasChanges = false
|
||||
goBack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
message: {
|
||||
Text("config.save.confirm")
|
||||
}
|
||||
.navigationTitle("ambient.lighting.config")
|
||||
.navigationBarItems(trailing:
|
||||
ZStack {
|
||||
ConnectedDevice(bluetoothOn: bleManager.isSwitchedOn, deviceConnected: bleManager.connectedPeripheral != nil, name: (bleManager.connectedPeripheral != nil) ? bleManager.connectedPeripheral.shortName : "?")
|
||||
})
|
||||
.onAppear {
|
||||
self.bleManager.context = context
|
||||
setAmbientLightingConfigValue()
|
||||
// Need to request a Ambient Lighting Config from the remote node before allowing changes
|
||||
if bleManager.connectedPeripheral != nil && node?.ambientLightingConfig == nil {
|
||||
let connectedNode = getNodeInfo(id: bleManager.connectedPeripheral.num, context: context)
|
||||
if node != nil && connectedNode != nil {
|
||||
_ = bleManager.requestAmbientLightingConfig(fromUser: connectedNode!.user!, toUser: node!.user!, adminIndex: connectedNode?.myInfo?.adminIndex ?? 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: ledState) { newLedState in
|
||||
if node != nil && node!.ambientLightingConfig != nil {
|
||||
if newLedState != node!.ambientLightingConfig!.ledState { hasChanges = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func setAmbientLightingConfigValue() {
|
||||
self.ledState = node?.ambientLightingConfig?.ledState ?? false
|
||||
self.current = Int(node?.ambientLightingConfig?.current ?? 10)
|
||||
let red = Double(node?.ambientLightingConfig?.red ?? 255)
|
||||
let green = Double(node?.ambientLightingConfig?.green ?? 255)
|
||||
let blue = Double(node?.ambientLightingConfig?.blue ?? 255)
|
||||
color = Color(red: red / 255.0, green: green / 255.0, blue: blue / 255.0)
|
||||
self.hasChanges = false
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ struct CannedMessagesConfig: View {
|
|||
@State var messages = ""
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,20 @@
|
|||
//
|
||||
import SwiftUI
|
||||
|
||||
enum DetectionSensorRole: String, CaseIterable, Equatable {
|
||||
case sensor
|
||||
case client
|
||||
var description: String {
|
||||
switch self {
|
||||
case .sensor:
|
||||
return "Sensor"
|
||||
case .client:
|
||||
return "Client"
|
||||
}
|
||||
}
|
||||
var localized: String { self.rawValue.localized }
|
||||
}
|
||||
|
||||
struct DetectionSensorConfig: View {
|
||||
|
||||
@Environment(\.managedObjectContext) var context
|
||||
|
|
@ -14,8 +28,10 @@ struct DetectionSensorConfig: View {
|
|||
var node: NodeInfoEntity?
|
||||
@State private var isPresentingSaveConfirm: Bool = false
|
||||
@State var hasChanges: Bool = false
|
||||
@AppStorage("detectionSensorRole") private var role: DetectionSensorRole = .sensor
|
||||
@AppStorage("enableDetectionNotifications") private var detectionNotificationsEnabled = false
|
||||
/// Module Config Settings
|
||||
@State var enabled = false
|
||||
/// DetectionSensorModule will sends a bell character with the messages.
|
||||
@State var sendBell: Bool = false
|
||||
@State var name: String = ""
|
||||
@State var detectionTriggeredHigh: Bool = true
|
||||
|
|
@ -25,99 +41,152 @@ struct DetectionSensorConfig: View {
|
|||
@State var monitorPin = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.detectionSensorConfig == nil {
|
||||
Text("Detection Sensor config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
Text("Enables the detection sensor module, it needs to be enabled on both the node with the sensor, and any nodes that you want to receive detection sensor text messages or view the detection sensor log and chart.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
if enabled {
|
||||
HStack {
|
||||
Picker(selection: $role, label: Text("Role")) {
|
||||
ForEach(DetectionSensorRole.allCases, id: \.self) { r in
|
||||
Text(r.description)
|
||||
.tag(r)
|
||||
}
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
.padding(.top, 5)
|
||||
.padding(.bottom, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
if enabled && role == .client {
|
||||
Section(header: Text("Client options")) {
|
||||
Toggle(isOn: $detectionNotificationsEnabled) {
|
||||
Label("Enable Notifications", systemImage: "bell.badge")
|
||||
Text("Detection sensor messages are received as text messages. If you enable notifications you will recieve a notification for each detection message received and a corresponding unread message badge.")
|
||||
.font(.caption)
|
||||
}
|
||||
.listRowSeparator(.visible)
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
}
|
||||
TextField("Friendly name (sent for detection alerts text messages)", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
if enabled && role == .sensor {
|
||||
Section(header: Text("Sensor options")) {
|
||||
Toggle(isOn: $sendBell) {
|
||||
Label("Send Bell", systemImage: "bell")
|
||||
Text("Send ASCII bell with alert message. Useful for triggering external notification on bell.")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
.listRowSeparator(.visible)
|
||||
HStack {
|
||||
Label("Name", systemImage: "signature")
|
||||
TextField("Friendly name", text: $name, axis: .vertical)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: name, perform: { _ in
|
||||
|
||||
let totalBytes = name.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 20 {
|
||||
|
||||
let firstNBytes = Data(name.utf8.prefix(20))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
name = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Friendly name used to format message sent to mesh. Example: A name \"Motion\" would result in a message \"Motion detected\"")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
.offset(y: -10)
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
Section(header: Text("Sensor option")) {
|
||||
Picker("GPIO Pin to monitor", selection: $monitorPin) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
Text("Whether or not the GPIO pin state detection is triggered on HIGH (1) or LOW (0)")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
Text(" Whether or not use INPUT_PULLUP mode for GPIO pin. Only applicable if the board uses pull-up resistors on the pin")
|
||||
.font(.caption)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
.listRowSeparator(.visible)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
.listRowSeparator(.hidden)
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Toggle(isOn: $detectionTriggeredHigh) {
|
||||
Label("Detection trigger High", systemImage: "dial.high")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $usePullup) {
|
||||
Label("Uses pullup resistor", systemImage: "arrow.up.to.line")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
}
|
||||
Section(header: Text("update.interval")) {
|
||||
Picker("Minimum time between detection broadcasts", selection: $minimumBroadcastSecs) {
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Mininum time between detection broadcasts. Default is 45 seconds.")
|
||||
.font(.caption)
|
||||
Picker("State Broadcast Interval", selection: $stateBroadcastSecs) {
|
||||
Text("Never").tag(0)
|
||||
ForEach(UpdateIntervals.allCases) { ui in
|
||||
Text(ui.description).tag(ui.rawValue)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("How often to send detection sensor state to mesh regardless of detection. Default is Never.")
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
|
|
@ -223,6 +292,9 @@ struct DetectionSensorConfig: View {
|
|||
if newStateBroadcastSecs != node!.detectionSensorConfig!.stateBroadcastSecs { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: detectionNotificationsEnabled) { newDetectionNotificationsEnabled in
|
||||
UserDefaults.enableDetectionNotifications = newDetectionNotificationsEnabled
|
||||
}
|
||||
}
|
||||
func setDetectionSensorValues() {
|
||||
self.enabled = (node?.detectionSensorConfig?.enabled ?? false)
|
||||
|
|
|
|||
|
|
@ -32,137 +32,138 @@ struct ExternalNotificationConfig: View {
|
|||
@State var nagTimeout = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.externalNotificationConfig == nil {
|
||||
Text("External notification config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setExternalNotificationValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "megaphone")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
Toggle(isOn: $alertBell) {
|
||||
Label("Alert when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessage) {
|
||||
Label("Alert when receiving a message", systemImage: "message")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $usePWM) {
|
||||
Label("Use PWM Buzzer", systemImage: "light.beacon.max.fill")
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Use a PWM output (like the RAK Buzzer) for tunes instead of an on/off output. This will ignore the output, output duration and active settings and use the device config buzzer GPIO option instead.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
Section(header: Text("Advanced GPIO Options")) {
|
||||
Section(header: Text("Primary GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $active) {
|
||||
Label("Active", systemImage: "togglepower")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If enabled, the 'output' Pin will be pulled active high, disabled means active low.")
|
||||
.font(.caption)
|
||||
Picker("Output pin GPIO", selection: $output) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("GPIO Output Duration", selection: $outputMilliseconds ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("When using in GPIO mode, keep the output on for this long. ")
|
||||
.font(.caption)
|
||||
Picker("Nag timeout", selection: $nagTimeout ) {
|
||||
ForEach(OutputIntervals.allCases) { oi in
|
||||
Text(oi.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Text("Specifies how long the monitored GPIO should output.")
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
Section(header: Text("Optional GPIO")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.textCase(.uppercase)) {
|
||||
Toggle(isOn: $alertBellBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertBellVibra) {
|
||||
Label("Alert GPIO vibra motor when receiving a bell", systemImage: "bell")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO buzzer when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $alertMessageBuzzer) {
|
||||
Label("Alert GPIO vibra motor when receiving a message", systemImage: "message")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Picker("Output pin buzzer GPIO ", selection: $outputBuzzer) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("Output pin vibra GPIO", selection: $outputVibra) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
Text("unset")
|
||||
} else {
|
||||
Text("Pin \($0)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
}
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.externalNotificationConfig == nil)
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -25,176 +25,177 @@ struct MQTTConfig: View {
|
|||
@State var root = "msh"
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.mqttConfig == nil {
|
||||
Text("MQTT config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setMqttValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
|
||||
Label("enabled", systemImage: "dot.radiowaves.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Toggle(isOn: $proxyToClientEnabled) {
|
||||
|
||||
Label("mqtt.clientproxy", systemImage: "iphone.radiowaves.left.and.right")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("If both MQTT and the client proxy are enabled your mobile device will utalize an available network connection to connect to the specified MQTT server.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $encryptionEnabled) {
|
||||
|
||||
Label("Encryption Enabled", systemImage: "lock.icloud")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
|
||||
Toggle(isOn: $jsonEnabled) {
|
||||
|
||||
Label("JSON Enabled", systemImage: "ellipsis.curlybraces")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("JSON mode is a limited, unencrypted MQTT output.")
|
||||
.font(.caption2)
|
||||
|
||||
Toggle(isOn: $tlsEnabled) {
|
||||
|
||||
Label("TLS Enabled", systemImage: "checkmark.shield.fill")
|
||||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
Text("Your MQTT Server must support TLS.")
|
||||
.font(.caption2)
|
||||
}
|
||||
Section(header: Text("Custom Server")) {
|
||||
HStack {
|
||||
Label("Address", systemImage: "server.rack")
|
||||
TextField("Server Address", text: $address)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: address, perform: { _ in
|
||||
let totalBytes = address.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
address = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
.keyboardType(.default)
|
||||
}
|
||||
.autocorrectionDisabled()
|
||||
|
||||
HStack {
|
||||
Label("mqtt.username", systemImage: "person.text.rectangle")
|
||||
TextField("mqtt.username", text: $username)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: username, perform: { _ in
|
||||
|
||||
let totalBytes = username.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(username.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
username = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("password", systemImage: "wallet.pass")
|
||||
TextField("password", text: $password)
|
||||
.foregroundColor(.gray)
|
||||
.autocapitalization(.none)
|
||||
.disableAutocorrection(true)
|
||||
.onChange(of: password, perform: { _ in
|
||||
|
||||
let totalBytes = password.utf8.count
|
||||
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 62 {
|
||||
|
||||
let firstNBytes = Data(password.utf8.prefix(62))
|
||||
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
password = maxBytesString
|
||||
}
|
||||
}
|
||||
hasChanges = true
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.default)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
HStack {
|
||||
Label("Root Topic", systemImage: "tree")
|
||||
TextField("Root Topic", text: $root)
|
||||
.foregroundColor(.gray)
|
||||
.onChange(of: root, perform: { _ in
|
||||
let totalBytes = root.utf8.count
|
||||
// Only mess with the value if it is too big
|
||||
if totalBytes > 14 {
|
||||
let firstNBytes = Data(root.utf8.prefix(14))
|
||||
if let maxBytesString = String(data: firstNBytes, encoding: String.Encoding.utf8) {
|
||||
// Set the shortName back to the last place where it was the right size
|
||||
root = maxBytesString
|
||||
}
|
||||
}
|
||||
})
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.keyboardType(.asciiCapable)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disableAutocorrection(true)
|
||||
Text("The root topic to use for MQTT messages. Default is \"msh\". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs")
|
||||
.font(.caption2)
|
||||
}
|
||||
Text("You can set uplink and downlink for each channel.")
|
||||
.font(.callout)
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.mqttConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -26,9 +26,7 @@ struct SerialConfig: View {
|
|||
@State var mode = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
|
|
|
|||
|
|
@ -27,72 +27,72 @@ struct StoreForwardConfig: View {
|
|||
@State var historyReturnWindow = 0
|
||||
|
||||
var body: some View {
|
||||
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("There has been no response to a request for device metadata over the admin channel for this node.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
|
||||
} else if node != nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
// Let users know what is going on if they are using remote admin and don't have the config yet
|
||||
if node?.storeForwardConfig == nil {
|
||||
Text("Store and forward config data was requested over the admin channel but no response has been returned from the remote node. You can check the status of admin message requests in the admin message log.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
} else {
|
||||
Text("Remote administration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
.onAppear {
|
||||
setDetectionSensorValues()
|
||||
}
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
} else if node != nil && node?.num ?? 0 == bleManager.connectedPeripheral?.num ?? 0 {
|
||||
Text("Configuration for: \(node?.user?.longName ?? "Unknown")")
|
||||
.font(.title3)
|
||||
} else {
|
||||
Text("Please connect to a radio to configure settings.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.orange)
|
||||
}
|
||||
Section(header: Text("options")) {
|
||||
Toggle(isOn: $enabled) {
|
||||
Label("enabled", systemImage: "envelope.arrow.triangle.branch")
|
||||
}
|
||||
Toggle(isOn: $heartbeat) {
|
||||
Label("storeforward.heartbeat", systemImage: "waveform.path.ecg")
|
||||
}
|
||||
Picker("Number of records", selection: $records) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Max", selection: $historyReturnMax ) {
|
||||
Text("unset").tag(0)
|
||||
Text("25").tag(25)
|
||||
Text("50").tag(50)
|
||||
Text("75").tag(75)
|
||||
Text("100").tag(100)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
Picker("History Return Window", selection: $historyReturnWindow ) {
|
||||
Text("unset").tag(0)
|
||||
Text("One Minute").tag(60)
|
||||
Text("Five Minutes").tag(300)
|
||||
Text("Ten Minutes").tag(600)
|
||||
Text("Fifteen Minutes").tag(900)
|
||||
Text("Thirty Minutes").tag(1800)
|
||||
Text("One Hour").tag(3600)
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.disabled(self.bleManager.connectedPeripheral == nil || node?.storeForwardConfig == nil)
|
||||
|
||||
Button {
|
||||
isPresentingSaveConfirm = true
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ struct TelemetryConfig: View {
|
|||
@State var environmentDisplayFahrenheit = false
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack {
|
||||
Form {
|
||||
if node != nil && node?.metadata == nil && node?.num ?? 0 != bleManager.connectedPeripheral?.num ?? 0 {
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@ struct PositionConfig: View {
|
|||
@State var rxGpio = 0
|
||||
@State var txGpio = 0
|
||||
@State var fixedPosition = false
|
||||
@State var gpsUpdateInterval = 0
|
||||
@State var gpsAttemptTime = 0
|
||||
@State var positionBroadcastSeconds = 0
|
||||
@State var broadcastSmartMinimumDistance = 0
|
||||
@State var broadcastSmartMinimumIntervalSecs = 0
|
||||
|
|
@ -212,19 +210,7 @@ struct PositionConfig: View {
|
|||
}
|
||||
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
|
||||
if deviceGpsEnabled {
|
||||
Picker("Update Interval", selection: $gpsUpdateInterval) {
|
||||
ForEach(GpsUpdateIntervals.allCases) { ui in
|
||||
Text(ui.description)
|
||||
}
|
||||
}
|
||||
Text("How often should we try to get a GPS position.")
|
||||
.font(.caption)
|
||||
Picker("Attempt Time", selection: $gpsAttemptTime) {
|
||||
ForEach(GpsAttemptTimes.allCases) { at in
|
||||
Text(at.description)
|
||||
}
|
||||
}
|
||||
.pickerStyle(DefaultPickerStyle())
|
||||
|
||||
Picker("GPS Receive GPIO", selection: $rxGpio) {
|
||||
ForEach(0..<46) {
|
||||
if $0 == 0 {
|
||||
|
|
@ -286,8 +272,6 @@ struct PositionConfig: View {
|
|||
pc.positionBroadcastSmartEnabled = smartPositionEnabled
|
||||
pc.gpsEnabled = deviceGpsEnabled
|
||||
pc.fixedPosition = fixedPosition
|
||||
pc.gpsUpdateInterval = UInt32(gpsUpdateInterval)
|
||||
pc.gpsAttemptTime = UInt32(gpsAttemptTime)
|
||||
pc.positionBroadcastSecs = UInt32(positionBroadcastSeconds)
|
||||
pc.broadcastSmartMinimumIntervalSecs = UInt32(broadcastSmartMinimumIntervalSecs)
|
||||
pc.broadcastSmartMinimumDistance = UInt32(broadcastSmartMinimumDistance)
|
||||
|
|
@ -354,16 +338,6 @@ struct PositionConfig: View {
|
|||
if newTxGpio != node!.positionConfig!.txGpio { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsAttemptTime) { newGpsAttemptTime in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newGpsAttemptTime != node!.positionConfig!.gpsAttemptTime { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: gpsUpdateInterval) { newGpsUpdateInterval in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newGpsUpdateInterval != node!.positionConfig!.gpsUpdateInterval { hasChanges = true }
|
||||
}
|
||||
}
|
||||
.onChange(of: smartPositionEnabled) { newSmartPositionEnabled in
|
||||
if node != nil && node!.positionConfig != nil {
|
||||
if newSmartPositionEnabled != node!.positionConfig!.smartPositionEnabled { hasChanges = true }
|
||||
|
|
@ -451,8 +425,6 @@ struct PositionConfig: View {
|
|||
self.rxGpio = Int(node?.positionConfig?.rxGpio ?? 0)
|
||||
self.txGpio = Int(node?.positionConfig?.txGpio ?? 0)
|
||||
self.fixedPosition = node?.positionConfig?.fixedPosition ?? false
|
||||
self.gpsUpdateInterval = Int(node?.positionConfig?.gpsUpdateInterval ?? 30)
|
||||
self.gpsAttemptTime = Int(node?.positionConfig?.gpsAttemptTime ?? 30)
|
||||
self.positionBroadcastSeconds = Int(node?.positionConfig?.positionBroadcastSeconds ?? 900)
|
||||
self.broadcastSmartMinimumIntervalSecs = Int(node?.positionConfig?.broadcastSmartMinimumIntervalSecs ?? 30)
|
||||
self.broadcastSmartMinimumDistance = Int(node?.positionConfig?.broadcastSmartMinimumDistance ?? 50)
|
||||
|
|
|
|||
169
Meshtastic/Views/Settings/RouteRecorder.swift
Normal file
169
Meshtastic/Views/Settings/RouteRecorder.swift
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
//
|
||||
// Routes.swift
|
||||
// Meshtastic
|
||||
//
|
||||
// Created by Garth Vander Houwen on 11/21/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreData
|
||||
import MapKit
|
||||
import CoreLocation
|
||||
import CoreMotion
|
||||
|
||||
struct TimerDisplayObject {
|
||||
var seconds: Int = 0
|
||||
var minutes: Int = 0
|
||||
var hours: Int = 0
|
||||
|
||||
var display: String {
|
||||
if self.seconds == 0 {
|
||||
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):00"
|
||||
} else {
|
||||
"\(String(format: "%02d", self.hours)):\(String(format: "%02d", self.minutes)):\(String(format: "%02d", self.seconds))"
|
||||
}
|
||||
}
|
||||
|
||||
var timeMinuteCalculator: Float { Float(hours*60+seconds/60+minutes) }
|
||||
}
|
||||
|
||||
@available(iOS 17.0, macOS 14.0, *)
|
||||
struct RouteRecorder: View {
|
||||
|
||||
@ObservedObject var locationsHandler = LocationsHandler.shared
|
||||
@Environment(\.managedObjectContext) var context
|
||||
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
|
||||
@State var isTimerRunning = false
|
||||
@State var isShowingDetails = false
|
||||
@State var timer: Timer?
|
||||
@Namespace var namespace
|
||||
@Namespace var routerecorderscope
|
||||
@State var timeElapsed: TimerDisplayObject = TimerDisplayObject()
|
||||
@State var timerDisplay = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
VStack {
|
||||
Map(position: $position, scope: routerecorderscope) {
|
||||
UserAnnotation()
|
||||
// ForEach(locations, id: \.id) { location in
|
||||
// Marker(location.name, systemImage: location.icon, coordinate: location.location)
|
||||
// .tint(location.colour)
|
||||
// }
|
||||
}
|
||||
}
|
||||
.mapScope(routerecorderscope)
|
||||
.mapControls {
|
||||
MapUserLocationButton()
|
||||
MapCompass()
|
||||
MapScaleView()
|
||||
MapPitchToggle()
|
||||
}
|
||||
.mapStyle(.hybrid(elevation: .realistic, showsTraffic: true))
|
||||
.transition(.slide)
|
||||
.mapControlVisibility(.visible)
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
ZStack {
|
||||
VStack {
|
||||
HStack(spacing: 10) {
|
||||
Spacer()
|
||||
if isTimerRunning {
|
||||
Button {
|
||||
isShowingDetails = true
|
||||
isTimerRunning = false
|
||||
} label: {
|
||||
Image(systemName: "pause.fill")
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Pause Button", in: namespace)
|
||||
} else {
|
||||
Button {
|
||||
isShowingDetails = true
|
||||
isTimerRunning = true
|
||||
timeElapsed.seconds -= 1
|
||||
} label: {
|
||||
Image(systemName: "play.fill")
|
||||
.frame(width: 60, height: 60)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.circle)
|
||||
.matchedGeometryEffect(id: "Play Button", in: namespace)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.onReceive(timerDisplay) { _ in
|
||||
if isTimerRunning {
|
||||
timeElapsed.seconds += 1
|
||||
if timeElapsed.seconds == 60 {
|
||||
timeElapsed.seconds = 0
|
||||
timeElapsed.minutes += 1
|
||||
if timeElapsed.minutes == 60 {
|
||||
timeElapsed.minutes = 0
|
||||
timeElapsed.hours += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.sheet(isPresented: $isShowingDetails) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(timeElapsed.display)
|
||||
.font(.largeTitle)
|
||||
Text("Time Elapseed")
|
||||
.font(.callout)
|
||||
}
|
||||
.padding()
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
let horizontalAccuracy = Measurement(value: locationsHandler.lastLocation.horizontalAccuracy, unit: UnitLength.meters)
|
||||
let verticalAccuracy = Measurement(value: locationsHandler.lastLocation.verticalAccuracy, unit: UnitLength.meters)
|
||||
let altitiude = Measurement(value: locationsHandler.lastLocation.altitude, unit: UnitLength.meters)
|
||||
let speed = Measurement(value: locationsHandler.lastLocation.speed, unit: UnitSpeed.kilometersPerHour)
|
||||
List {
|
||||
Label("Coordinate \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.latitude)), \(String(format: "%.5f", locationsHandler.lastLocation.coordinate.longitude))", systemImage: "mappin")
|
||||
.textSelection(.enabled)
|
||||
Label("Horizontal Accuracy \(horizontalAccuracy.formatted())", systemImage: "scope")
|
||||
if locationsHandler.lastLocation.verticalAccuracy > 0 {
|
||||
Label("Altitude \(altitiude.formatted())", systemImage: "mountain.2")
|
||||
}
|
||||
Label("Vertical Accuracy \(verticalAccuracy.formatted())", systemImage: "lines.measurement.vertical")
|
||||
Label("Satellites Estimate \(LocationHelper.satsInView)", systemImage: "sparkles")
|
||||
Label("\(locationsHandler.isStationary ? "Moving" : "Stationary")", systemImage: locationsHandler.isStationary ? "figure.walk.motion" : "figure.stand")
|
||||
if locationsHandler.lastLocation.speedAccuracy > 0 {
|
||||
Label("Speed \(speed.formatted())", systemImage: "speedometer")
|
||||
}
|
||||
if locationsHandler.lastLocation.courseAccuracy > 0 {
|
||||
/// Heading
|
||||
let degrees = Angle.degrees(Double(locationsHandler.lastLocation.course))
|
||||
Label {
|
||||
let heading = Measurement(value: degrees.degrees, unit: UnitAngle.degrees)
|
||||
/// Text("Heading: \(heading.formatted())")
|
||||
Text("Heading \(String(format: "%.2f", locationsHandler.lastLocation.course))°")
|
||||
.foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: "location.circle")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
.frame(width: 35)
|
||||
.rotationEffect(degrees)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.presentationDetents([.fraction(0.6)])
|
||||
.presentationDragIndicator(.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,7 @@ struct Routes: View {
|
|||
|
||||
var routes: FetchedResults<RouteEntity>
|
||||
var body: some View {
|
||||
//NavigationSplitView(columnVisibility: $columnVisibility) {
|
||||
NavigationStack {
|
||||
VStack {
|
||||
Button("Import Route") {
|
||||
importing = true
|
||||
}
|
||||
|
|
@ -152,8 +151,6 @@ struct Routes: View {
|
|||
.listStyle(.plain)
|
||||
}
|
||||
.navigationTitle("Route List")
|
||||
// } detail: {
|
||||
|
||||
VStack {
|
||||
if selectedRoute != nil {
|
||||
let locationArray = selectedRoute?.locations?.array as? [LocationEntity] ?? []
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ struct Settings: View {
|
|||
enum SettingsSidebar {
|
||||
case appSettings
|
||||
case routes
|
||||
case routeRecorder
|
||||
case shareChannels
|
||||
case userConfig
|
||||
case loraConfig
|
||||
|
|
@ -27,6 +28,7 @@ struct Settings: View {
|
|||
case displayConfig
|
||||
case networkConfig
|
||||
case positionConfig
|
||||
case ambientLightingConfig
|
||||
case cannedMessagesConfig
|
||||
case detectionSensorConfig
|
||||
case externalNotificationConfig
|
||||
|
|
@ -67,6 +69,15 @@ struct Settings: View {
|
|||
Text("routes")
|
||||
}
|
||||
.tag(SettingsSidebar.routes)
|
||||
|
||||
// NavigationLink {
|
||||
// RouteRecorder()
|
||||
// } label: {
|
||||
// Image(systemName: "record.circle")
|
||||
// .symbolRenderingMode(.hierarchical)
|
||||
// Text("route.recorder")
|
||||
// }
|
||||
// .tag(SettingsSidebar.routeRecorder)
|
||||
}
|
||||
|
||||
let node = nodes.first(where: { $0.num == preferredNodeNum })
|
||||
|
|
@ -187,6 +198,16 @@ struct Settings: View {
|
|||
.tag(SettingsSidebar.positionConfig)
|
||||
}
|
||||
Section("module.configuration") {
|
||||
if #available(iOS 17.0, macOS 14.0, *) {
|
||||
NavigationLink {
|
||||
AmbientLightingConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
Image(systemName: "light.max")
|
||||
.symbolRenderingMode(.hierarchical)
|
||||
Text("ambient.lighting")
|
||||
}
|
||||
.tag(SettingsSidebar.ambientLightingConfig)
|
||||
}
|
||||
NavigationLink {
|
||||
CannedMessagesConfig(node: nodes.first(where: { $0.num == selectedNode }))
|
||||
} label: {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"ago"="her";
|
||||
"airtime"="Airtime";
|
||||
"always.on"="Immer an";
|
||||
"ambient.lighting"="Ambient Lighting";
|
||||
"ambient.lighting.config"="Ambient Lighting Config";
|
||||
"app.settings"="App Einstellungen";
|
||||
"are.you.sure"="Bist Du sicher?";
|
||||
"ascii.capable"="ASCII fähig";
|
||||
|
|
@ -64,6 +66,8 @@
|
|||
"device.metrics.log"="Device Metrics Log";
|
||||
"device.role.client"="Client (Standard) - Mit App verbundener Client.";
|
||||
"device.role.clientmute"="Client Leise - Das selbe wie Client, außer das die Pakete nicht über diesen Node weitergeleitet werden. Nimmt nicht am Mesh-Routing teil.";
|
||||
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
|
||||
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
|
||||
"device.role.router"="Router - Mesh Pakete werden bevorzugt über diesen Node gerouted. Dieser Node wird nicht von einer Client App benutzt. WLAN, Bluetooth und Display sind aus.";
|
||||
"device.role.routerclient"="Router Client - Mesh Pakete werden bevorzugt über diesen Node gerouted. Der Router Client kann parallel auch von einer Client-App genutzt werden.";
|
||||
"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role.";
|
||||
|
|
@ -149,6 +153,7 @@
|
|||
"map.usertrackingmode.followwithheading"="Follow with heading";
|
||||
"mesh.live.activity"="Mesh Live Activity";
|
||||
"mesh.log"="Mesh Log";
|
||||
"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@";
|
||||
"mesh.log.bluetooth.config %@"="Bluetooth Konfiguration empfangen: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Canned Message module config received: %@";
|
||||
"mesh.log.cannedmessages.messages.get %@"="Requested Canned Messages Module Messages for node: %@";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"ago"="ago";
|
||||
"airtime"="Airtime";
|
||||
"always.on"="Always On";
|
||||
"ambient.lighting"="Ambient Lighting";
|
||||
"ambient.lighting.config"="Ambient Lighting Config";
|
||||
"app.settings"="App Settings";
|
||||
"are.you.sure"="Are you sure?";
|
||||
"ascii.capable"="ASCII Capable";
|
||||
|
|
@ -66,11 +68,14 @@
|
|||
"device.metrics.delete"="Delete all device metrics?";
|
||||
"device.metrics.log"="Device Metrics Log";
|
||||
"device.role.client"="Client (default) - App connected client.";
|
||||
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
|
||||
"device.role.clientmute"="Client Mute - Same as a client except packets will not hop over this node, does not contribute to routing packets for mesh.";
|
||||
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
|
||||
"device.role.router"="Router - Mesh packets will prefer to be routed over this node. Assumes device will operate in a standalone manner while placed in a location with a coverage advantage. WARNING: The BLE/Wi-Fi radios and the OLED screen will be put to sleep.";
|
||||
"device.role.routerclient"="Router Client - Hybrid of the Client and Router roles. Similar to Router, except the Router Client can be used as both a Router and an app connected Client. BLE/Wi-Fi and OLED screen will not be put to sleep.";
|
||||
"device.role.repeater"="Repeater - Mesh packets will prefer to be routed over this node. This role eliminates unnecessary overhead such as NodeInfo, DeviceTelemetry, and any other mesh packet, resulting in the device not appearing as part of the network. Please see Rebroadcast Mode for additional settings specific to this role.";
|
||||
"device.role.tracker"="Tracker - For use with devices intended as a GPS tracker. Position packets sent from this device will be higher priority, with position broadcasting every two minutes. Smart Position Broadcast will default to off.";
|
||||
"device.role.tak"="Used for nodes dedicated for connection to an ATAK EUD. Turns off many of the routine broadcasts to favor CoT packet stream from the Meshtastic ATAK plugin -> IMeshService -> Node";
|
||||
"direct.messages"="Direct Messages";
|
||||
"dismiss.keyboard"="Dismiss";
|
||||
"display"="Display (Device Screen)";
|
||||
|
|
@ -152,6 +157,7 @@
|
|||
"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: %@";
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
"ago"="temu";
|
||||
"airtime"="Czas nadawania";
|
||||
"always.on"="Zawsze włączone";
|
||||
"ambient.lighting"="Ambient Lighting";
|
||||
"ambient.lighting.config"="Ambient Lighting Config";
|
||||
"app.settings"="Ustawienia aplikacji";
|
||||
"are.you.sure"="Jesteś pewny?";
|
||||
"ascii.capable"="Zgodny z ASCII";
|
||||
|
|
@ -66,6 +68,8 @@
|
|||
"device.metrics.log"="Dziennik metryk urządzenia";
|
||||
"device.role.client"="Klient (domyślnie) - Klient połączony z aplikacją.";
|
||||
"device.role.clientmute"="Wyciszenie klienta - To samo, co klient, z wyjątkiem pakietów, które nie przeskakują przez ten węzeł, nie przyczynia się do routingu pakietów dla siatki.";
|
||||
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
|
||||
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
|
||||
"device.role.router"="Router - Pakiety siatki będą preferować trasowanie przez ten węzeł. Zakłada, że urządzenie będzie działać samodzielnie, umieszczone w miejscu z przewagą zasięgu. UWAGA: Radia BLE/Wi-Fi i ekran OLED zostaną uśpione.";
|
||||
"device.role.routerclient"="Router Client - Hybryda ról klienta i routera. Podobnie jak w przypadku routera, z tym że Router Client może być używany zarówno jako router, jak i klient połączony z aplikacją. Radia BLE/Wi-Fi i ekran OLED nie zostaną uśpione.";
|
||||
"device.role.repeater"="Przekaźnik - Pakiety siatki będą preferować trasowanie przez ten węzeł. Ta rola eliminuje niepotrzebny nadmiar, taki jak NodeInfo, DeviceTelemetry i inne pakiety siatki, skutkując tym, że urządzenie nie będzie widoczne jako część sieci. Proszę zobaczyć tryb Rebroadcast dla dodatkowych ustawień specyficznych dla tej roli.";
|
||||
|
|
@ -151,6 +155,7 @@
|
|||
"map.usertrackingmode.none"="Brak";
|
||||
"mesh.live.activity"="Aktywność na Żywo";
|
||||
"mesh.log"="Dziennik Sieci";
|
||||
"mesh.log.ambientlighting.config %@"="Ambient Lighting module config received: %@";
|
||||
"mesh.log.bluetooth.config %@"="Otrzymano konfigurację Bluetooth: %@";
|
||||
"mesh.log.cannedmessage.config %@"="Otrzymano konfigurację modułu wiadomości gotowych: %@";
|
||||
"mesh.log.cannedmessages.messages.get %@"="Zażądano Wiadomości z Modułu Wiadomości Gotowych dla węzła: %@";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@
|
|||
"ago"="ago";
|
||||
"airtime"="广播时间";
|
||||
"always.on"="常亮";
|
||||
"ambient.lighting"="Ambient Lighting";
|
||||
"ambient.lighting.config"="Ambient Lighting Config";
|
||||
"app.settings"="通用设置";
|
||||
"are.you.sure"="是否确认?";
|
||||
"ascii.capable"="ASCII Capable";
|
||||
|
|
@ -64,6 +66,8 @@
|
|||
"device.metrics.log"="电台指标日志";
|
||||
"device.role.client"="标准模式 - App 可以连接到电台进行收发操作,并且会自动转发 Mesh 网络中其他节点的消息。";
|
||||
"device.role.clientmute"="静默模式 - 与标准模式类似,App 可以连接到电台进行收发操作,但不会转发 Mesh 网络中其他节点的消息。";
|
||||
"device.role.clienthidden"=" Used for nodes that \"only speak when spoken to\" Turns all of the routine broadcasts but allows for ad-hoc communication. Still rebroadcasts, but with local only rebroadcast mode (known meshes only). Can be used for private operation or to dramatically reduce airtime / power consumption.";
|
||||
"device.role.lostandfound"="Used to automatically send a text message to the mesh with the current position of the device on a frequent interval: \"I'm lost! Position: lat / long\"";
|
||||
"device.role.router"="纯路由模式 - 自动转发 Mesh 网络中其他节点的消息,中继模式下屏幕会熄灭,Wi-Fi 和蓝牙将会进入睡眠模式,App 将无法连接到电台进行收发操作。";
|
||||
"device.role.routerclient"="路由客户端模式 - 优先转发 Mesh 网络中其他节点的消息,App 也可以连接到电台进行收发操作。";
|
||||
"device.role.repeater"="中继模式 - Mesh 网络数据包将优先通过此节点路由。此模式可消除不必要的开销,如 NodeInfo、DeviceTelemetry 和任何其他 Mesh 数据包,从而使设备不显示为 Mesh 网络的一部分。有关此角色的其他特定设置,请参阅转播模式。";
|
||||
|
|
@ -149,6 +153,7 @@
|
|||
"map.usertrackingmode.followwithheading"="Follow with heading";
|
||||
"mesh.live.activity"="Mesh 实时活动";
|
||||
"mesh.log"="Mesh 日志";
|
||||
"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: %@";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue